diff --git a/active.de-DE.toml b/active.de-DE.toml index 514b20c..6d1e079 100644 --- a/active.de-DE.toml +++ b/active.de-DE.toml @@ -6,6 +6,14 @@ other = "Zertifikats-Signier-Anfrage erzeugen" hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d" other = "CSR-Erzeugung im Browser" +[DownloadDescription] +hash = "sha1-f4a7826398e5c57c7feb4709ee939ea655f05469" +other = "Dein Schlüsselmaterial ist bereit zum Herunterladen. Die herunterladbare Datei enthält deinen privaten Schlüssel und dein Zertifikat verschlüsselt mit deinem Passwort. Du kannst die Datei jetzt verwenden, um dein Zertifikat in deinem Browser oder anderen Anwendungen zu installieren." + +[DownloadLabel] +hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8" +other = "Herunterladen" + ["JavaScript.KeyGen.Generated"] hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" other = "Schlüssel in __seconds__ Sekunden erzeugt" @@ -50,10 +58,6 @@ other = "In Deinem Browser wird ein RSA-Schlüsselpaar erzeugt. Größere Schlü hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa" other = "RSA-Schlüssellänge" -[SendCSRButtonLabel] -hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477" -other = "Signieranfrage abschicken" - [StatusLoading] hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" other = "Lade ..." diff --git a/active.en-US.toml b/active.en-US.toml index a6c5e29..e14a12b 100644 --- a/active.en-US.toml +++ b/active.en-US.toml @@ -6,6 +6,14 @@ other = "Generate signing request" hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d" other = "CSR generation in browser" +[DownloadDescription] +hash = "sha1-f4a7826398e5c57c7feb4709ee939ea655f05469" +other = "Your key material is ready for download. The downloadable file contains your private key and your certificate encrypted with your password. You can now use the file to install your certificate in your browser or other applications." + +[DownloadLabel] +hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8" +other = "Download" + ["JavaScript.KeyGen.Generated"] hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" other = "key generated in __seconds__ seconds" @@ -50,10 +58,6 @@ other = "An RSA key pair will be generated in your browser. Longer key sizes pro hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa" other = "RSA Key Size" -[SendCSRButtonLabel] -hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477" -other = "Send signing request" - [StatusLoading] hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" other = "Loading ..." diff --git a/active.en.toml b/active.en.toml index 5333d0b..639d053 100644 --- a/active.en.toml +++ b/active.en.toml @@ -1,5 +1,7 @@ CSRButtonLabel = "Generate signing request" CSRGenTitle = "CSR generation in browser" +DownloadDescription = "Your key material is ready for download. The downloadable file contains your private key and your certificate encrypted with your password. You can now use the file to install your certificate in your browser or other applications." +DownloadLabel = "Download" "JavaScript.KeyGen.Generated" = "key generated in __seconds__ seconds" "JavaScript.KeyGen.Running" = "key generation running for __seconds__ seconds" "JavaScript.KeyGen.Started" = "started key generation" @@ -11,5 +13,4 @@ RSA3072Label = "3072 Bit" RSA4096Label = "4096 Bit" RSAHelpText = "An RSA key pair will be generated in your browser. Longer key sizes provide better security but take longer to generate." RSAKeySizeLabel = "RSA Key Size" -SendCSRButtonLabel = "Send signing request" StatusLoading = "Loading ..." diff --git a/gulpfile.js b/gulpfile.js index 0c52e7b..a0eebb1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,7 +5,6 @@ const rename = require('gulp-rename'); const replace = require('gulp-replace'); const sass = require('gulp-sass'); const sourcemaps = require('gulp-sourcemaps'); -const sriHash = require('gulp-sri-hash'); const uglify = require('gulp-uglify'); sass.compiler = require('node-sass'); @@ -48,7 +47,7 @@ function publishAssets() { } function publish() { - return src('src/*.html').pipe(sriHash()).pipe(replace('../public/', '')).pipe(dest('public')); + return src('src/*.html').pipe(replace('../public/', '')).pipe(dest('public')); } exports.default = series( diff --git a/main.go b/main.go index 7074364..b901d2d 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,19 @@ import ( "bytes" "crypto/rand" "crypto/tls" + "crypto/x509" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "html/template" "io/ioutil" "net/http" "os" "os/exec" + "os/signal" "strings" + "syscall" "time" "github.com/BurntSushi/toml" @@ -29,10 +34,13 @@ type requestData struct { } type responseData struct { - Certificate string `json:"certificate"` + Certificate string `json:"certificate"` + CAChain []string `json:"ca_chain"` } -func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, err error) { +var caCertificates []*x509.Certificate + +func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, caChain []string, err error) { log.Printf("received CSR for %s:\n\n%s", commonName, csrPem) subjectDN := fmt.Sprintf("/CN=%s", commonName) var csrFile *os.File @@ -68,7 +76,23 @@ func (h *signCertificate) sign(csrPem string, commonName string) (certPem string log.Print(cmdErr.String()) return } - certPem = out.String() + + var block *pem.Block + if block, _ = pem.Decode(out.Bytes()); block == nil { + err = fmt.Errorf("could not decode pem") + return + } + var certificate *x509.Certificate + if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { + return + } + + certPem = string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + })) + + caChain, err = h.getCAChain(certificate) return } @@ -83,7 +107,8 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) { } var err error var requestBody requestData - var certificate string + + var responseData responseData if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { log.Print(err) @@ -91,14 +116,14 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - certificate, err = h.sign(requestBody.Csr, requestBody.CommonName) + responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) if err != nil { http.Error(w, "Could not sign certificate", http.StatusInternalServerError) return } var jsonBytes []byte - if jsonBytes, err = json.Marshal(&responseData{Certificate: certificate}); err != nil { + if jsonBytes, err = json.Marshal(&responseData); err != nil { log.Print(err) } @@ -107,6 +132,39 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (*signCertificate) getCAChain(certificate *x509.Certificate) ([]string, error) { + result := make([]string, 0) + + appendCert := func(cert *x509.Certificate) { + result = append( + result, + string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) + log.Debugf("added %s to cachain", result[len(result)-1]) + } + + var previous *x509.Certificate + for { + if len(caCertificates) == 0 { + return result, nil + } + for _, caCert := range caCertificates { + if previous == nil { + if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { + previous = caCert + appendCert(caCert) + } + } else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { + return result, nil + } else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { + previous = caCert + appendCert(caCert) + } else { + log.Debugf("skipped certificate %s", caCert.Subject) + } + } + } +} + type indexHandler struct { Bundle *i18n.Bundle } @@ -158,26 +216,33 @@ func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ID: "StatusLoading", Other: "Loading ...", }}) - sendCSRButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ - ID: "SendCSRButtonLabel", - Other: "Send signing request", + downloadLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ + ID: "DownloadLabel", + Other: "Download", + }}) + downloadDescription := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ + ID: "DownloadDescription", + Other: "Your key material is ready for download. The downloadable file contains your private key and your" + + " certificate encrypted with your password. You can now use the file to install your certificate in your" + + " browser or other applications.", }}) t := template.Must(template.ParseFiles("templates/index.html")) err := t.Execute(w, map[string]interface{}{ - "Title": csrGenTitle, - "NameLabel": nameLabel, - "NameHelpText": nameHelpText, - "PasswordLabel": passwordLabel, - "RSAKeySizeLegend": rsaKeySizeLegend, - "RSA3072Label": rsa3072Label, - "RSA2048Label": rsa2048Label, - "RSA4096Label": rsa4096Label, - "RSAHelpText": rsaHelpText, - "CSRButtonLabel": csrButtonLabel, - "StatusLoading": statusLoading, - "SendCSRButtonLabel": sendCSRButtonLabel, - csrf.TemplateTag: csrf.TemplateField(r), + "Title": csrGenTitle, + "NameLabel": nameLabel, + "NameHelpText": nameHelpText, + "PasswordLabel": passwordLabel, + "RSAKeySizeLegend": rsaKeySizeLegend, + "RSA3072Label": rsa3072Label, + "RSA2048Label": rsa2048Label, + "RSA4096Label": rsa4096Label, + "RSAHelpText": rsaHelpText, + "CSRButtonLabel": csrButtonLabel, + "StatusLoading": statusLoading, + "DownloadDescription": downloadDescription, + "DownloadLabel": downloadLabel, + csrf.TemplateTag: csrf.TemplateField(r), }) if err != nil { log.Panic(err) @@ -238,6 +303,26 @@ func generateRandomBytes(count int) []byte { return randomBytes } +func init() { + var err error + caCertificates = make([]*x509.Certificate, 2) + for index, certFile := range []string{"example_ca/sub/ca.crt.pem", "example_ca/root/ca.crt.pem"} { + var certBytes []byte + if certBytes, err = ioutil.ReadFile(certFile); err != nil { + log.Panic(err) + } + var block *pem.Block + if block, _ = pem.Decode(certBytes); block == nil { + log.Panicf("no PEM data found in %s", certFile) + return + } + if caCertificates[index], err = x509.ParseCertificate(block.Bytes); err != nil { + log.Panic(err) + } + } + log.Infof("read %d CA certificates", len(caCertificates)) +} + func main() { tlsConfig := &tls.Config{ CipherSuites: []uint16{ @@ -258,7 +343,21 @@ func main() { } mux := http.NewServeMux() - csrfKey := generateRandomBytes(32) + + var csrfKey []byte = nil + + if csrfB64, exists := os.LookupEnv("CSRF_KEY"); exists { + csrfKey, _ = base64.RawStdEncoding.DecodeString(csrfB64) + log.Info("read CSRF key from environment variable") + } + if csrfKey == nil { + csrfKey = generateRandomBytes(32) + log.Infof( + "generated new random CSRF key, set environment variable CSRF_KEY to %s to "+ + "keep the same key for new sessions", + base64.RawStdEncoding.EncodeToString(csrfKey)) + } + mux.Handle("/sign/", &signCertificate{}) mux.Handle("/", &indexHandler{Bundle: bundle}) fileServer := http.FileServer(http.Dir("./public")) @@ -274,8 +373,22 @@ func main() { WriteTimeout: 30 * time.Second, IdleTimeout: 30 * time.Second, } - err := server.ListenAndServeTLS("server.crt.pem", "server.key.pem") - if err != nil { - log.Fatal(err) + go func() { + err := server.ListenAndServeTLS("server.crt.pem", "server.key.pem") + if err != nil { + log.Fatal(err) + } + }() + var hostPort string + if strings.HasPrefix(server.Addr, ":") { + hostPort = fmt.Sprintf("localhost%s", server.Addr) + } else { + hostPort = server.Addr } + log.Infof("started web server on https://%s/", hostPort) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + s := <-c + log.Infof("received %s, shutting down", s) + _ = server.Close() } diff --git a/templates/index.html b/templates/index.html index 6df2aef..8fdee19 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,8 +5,7 @@ - + {{ .Title }} @@ -45,40 +44,39 @@ {{ .RSAHelpText }} - + -
-
-
- {{ .StatusLoading }} - +
+
+
+
{{ .StatusLoading }} +
-
-
-
-
- -
+
+

{{ .DownloadDescription }}

+ + + + + + {{ .DownloadLabel }}
-

-    

-    

+    

+    

+    

 
- - - - + + + +