From b1babf5b77848f22cf9b17267a3c735e3130b302 Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Wed, 24 Feb 2021 20:01:48 +0100 Subject: [PATCH] Update TLS certificates, generate certificate if not found. We now notice that a TLS certificate on disk has changed, and we generate a self-signed certificate if none is found. --- README | 16 +++++-- webserver/certificate.go | 98 ++++++++++++++++++++++++++++++++++++++++ webserver/webserver.go | 19 +++++--- 3 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 webserver/certificate.go diff --git a/README b/README index b7129b5..9160f5f 100644 --- a/README +++ b/README @@ -4,15 +4,11 @@ CGO_ENABLED=0 go build -ldflags='-s -w' -## Create a server certificate - - mkdir data - openssl req -newkey rsa:2048 -nodes -keyout data/key.pem -x509 -days 365 -out data/cert.pem - ## Set the server administrator credentials This step is optional. + mkdir data echo 'god:topsecret' > data/passwd ## Set up a group @@ -110,6 +106,16 @@ For a 32-bit MIPS board with no hardware floating point (WNDR3800, etc.): Set up a user *galene* on your server, then do: rsync -a galene static data groups galene@server.example.org: + +If you don't have a TLS certificate, Galène will generate a self-signed +certificate automatically (and print a warning to the logs). If you have +a certificate, install it in the files `data/cert.pem` and `data/key.pem`: + + ssh galene@server.example.org + sudo cp /etc/letsencrypt/live/server.example.org/fullchain.pem data/cert.pem + sudo cp /etc/letsencrypt/live/server.example.org/key.pem data/key.pem + sudo chown galene:galene data/*.pem + sudo chmod go-rw data/key.pem Now run the binary on the server: diff --git a/webserver/certificate.go b/webserver/certificate.go new file mode 100644 index 0000000..9fc9da8 --- /dev/null +++ b/webserver/certificate.go @@ -0,0 +1,98 @@ +package webserver + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "log" + "math/big" + "os" + "path/filepath" + "sync/atomic" + "time" +) + +type certInfo struct { + certificate *tls.Certificate + keyTime time.Time + certTime time.Time +} + +var certificate atomic.Value + +func generateCertificate(dataDir string) (tls.Certificate, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, err + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + now := time.Now() + + template := x509.Certificate{ + SerialNumber: serialNumber, + NotBefore: now, + NotAfter: now.Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + bytes, err := x509.CreateCertificate( + rand.Reader, &template, &template, &priv.PublicKey, priv, + ) + if err != nil { + return tls.Certificate{}, err + } + + return tls.Certificate{ + Certificate: [][]byte{bytes}, + PrivateKey: priv, + }, nil +} + +func fileTime(filename string) time.Time { + fi, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("%v: %v", filename, err) + } + return time.Time{} + } + return fi.ModTime() +} + +func getCertificate(dataDir string) (*tls.Certificate, error) { + info, ok := certificate.Load().(*certInfo) + + certFile := filepath.Join(dataDir, "cert.pem") + keyFile := filepath.Join(dataDir, "key.pem") + certTime := fileTime(certFile) + keyTime := fileTime(keyFile) + + if !ok || !info.certTime.Equal(certTime) || !info.keyTime.Equal(keyTime) { + var cert tls.Certificate + if certTime.Equal(time.Time{}) || keyTime.Equal(time.Time{}) { + log.Printf("Generating self-signed certificate") + var err error + cert, err = generateCertificate(dataDir) + if err != nil { + return nil, err + } + } else { + var err error + cert, err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + } + info = &certInfo{ + certificate: &cert, + certTime: certTime, + keyTime: keyTime, + } + certificate.Store(info) + } + return info.certificate, nil +} diff --git a/webserver/webserver.go b/webserver/webserver.go index b0de1e1..e336f88 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -3,6 +3,7 @@ package webserver import ( "bufio" "context" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -55,6 +56,13 @@ func Serve(address string, dataDir string) error { ReadHeaderTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } + if !Insecure { + s.TLSConfig = &tls.Config{ + GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return getCertificate(dataDir) + }, + } + } s.RegisterOnShutdown(func() { group.Range(func(g *group.Group) bool { go g.Shutdown("server is shutting down") @@ -67,10 +75,7 @@ func Serve(address string, dataDir string) error { var err error if !Insecure { - err = s.ListenAndServeTLS( - filepath.Join(dataDir, "cert.pem"), - filepath.Join(dataDir, "key.pem"), - ) + err = s.ListenAndServeTLS("", "") } else { err = s.ListenAndServe() } @@ -273,8 +278,8 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { return } - if r.URL.Path != "/group/" + name { - http.Redirect(w, r, "/group/" + name, + if r.URL.Path != "/group/"+name { + http.Redirect(w, r, "/group/"+name, http.StatusPermanentRedirect) return } @@ -433,7 +438,7 @@ func statsHandler(w http.ResponseWriter, r *http.Request, dataDir string) { fmt.Fprintf(w, "\n") } -var wsUpgrader = websocket.Upgrader { +var wsUpgrader = websocket.Upgrader{ HandshakeTimeout: 30 * time.Second, }