diff --git a/active.de-DE.toml b/active.de-DE.toml index 6d1e079..00d957b 100644 --- a/active.de-DE.toml +++ b/active.de-DE.toml @@ -14,6 +14,14 @@ other = "Dein Schlüsselmaterial ist bereit zum Herunterladen. Die herunterladba hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8" other = "Herunterladen" +["JavaScript.Certificate.Received"] +hash = "sha1-217622c21b50fcfb864802155080be482c285456" +other = "Zertifikat von der CA erhalten" + +["JavaScript.Certificate.Waiting"] +hash = "sha1-0a528daa78d850d2c9360cdec82f6f849ffb6bcf" +other = "Warte auf Zertifikat ..." + ["JavaScript.KeyGen.Generated"] hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" other = "Schlüssel in __seconds__ Sekunden erzeugt" diff --git a/active.en-US.toml b/active.en-US.toml index e14a12b..95cf49e 100644 --- a/active.en-US.toml +++ b/active.en-US.toml @@ -14,6 +14,14 @@ other = "Your key material is ready for download. The downloadable file contains hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8" other = "Download" +["JavaScript.Certificate.Received"] +hash = "sha1-217622c21b50fcfb864802155080be482c285456" +other = "received certificate from CA" + +["JavaScript.Certificate.Waiting"] +hash = "sha1-0a528daa78d850d2c9360cdec82f6f849ffb6bcf" +other = "waiting for certificate ..." + ["JavaScript.KeyGen.Generated"] hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" other = "key generated in __seconds__ seconds" diff --git a/active.en.toml b/active.en.toml index 639d053..dc52590 100644 --- a/active.en.toml +++ b/active.en.toml @@ -2,6 +2,8 @@ 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.Certificate.Received" = "received certificate from CA" +"JavaScript.Certificate.Waiting" = "waiting for certificate ..." "JavaScript.KeyGen.Generated" = "key generated in __seconds__ seconds" "JavaScript.KeyGen.Running" = "key generation running for __seconds__ seconds" "JavaScript.KeyGen.Started" = "started key generation" diff --git a/go.mod b/go.mod index 15095c9..da19a77 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,10 @@ go 1.13 require ( github.com/BurntSushi/toml v0.3.1 + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.0.4 + github.com/google/uuid v1.1.2 github.com/gorilla/csrf v1.7.0 github.com/nicksnyder/go-i18n/v2 v2.1.1 github.com/sirupsen/logrus v1.7.0 diff --git a/go.sum b/go.sum index 410eca2..5ddbba0 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,14 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs= +github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y= github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= diff --git a/handlers/index.go b/handlers/index.go index 61044a6..0f3c083 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -18,6 +18,15 @@ func NewIndexHandler(bundle *i18n.Bundle) *IndexHandler { } func (i *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + localizer := i18n.NewLocalizer(i.bundle, r.Header.Get("Accept-Language")) csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ ID: "CSRGenTitle", diff --git a/handlers/jslocales.go b/handlers/jslocales.go index 1377625..84799ef 100644 --- a/handlers/jslocales.go +++ b/handlers/jslocales.go @@ -32,6 +32,10 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Running string `json:"running"` Generated string `json:"generated"` } `json:"keygen"` + Certificate struct { + Waiting string `json:"waiting"` + Received string `json:"received"` + } `json:"certificate"` } translations := &translationData{} @@ -47,6 +51,14 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ID: "JavaScript.KeyGen.Generated", Other: "key generated in __seconds__ seconds", }}) + translations.Certificate.Waiting = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ + ID: "JavaScript.Certificate.Waiting", + Other: "waiting for certificate ...", + }}) + translations.Certificate.Received = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ + ID: "JavaScript.Certificate.Received", + Other: "received certificate from CA", + }}) encoder := json.NewEncoder(w) if err := encoder.Encode(translations); err != nil { diff --git a/handlers/registry.go b/handlers/registry.go new file mode 100644 index 0000000..9e92307 --- /dev/null +++ b/handlers/registry.go @@ -0,0 +1,172 @@ +package handlers + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "time" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +type SigningRequestRegistry struct { + caCertificates []*x509.Certificate + caChainMap map[string][]string + requests map[string]chan *responseData +} + +func NewSigningRequestRegistry(caCertificates []*x509.Certificate) *SigningRequestRegistry { + return &SigningRequestRegistry{ + caCertificates: caCertificates, + caChainMap: make(map[string][]string), + requests: make(map[string]chan *responseData), + } +} + +func (registry *SigningRequestRegistry) AddSigningRequest(request *requestData) (string, error) { + requestUuid, err := uuid.NewRandom() + if err != nil { + return "", err + } + go func() { + responseChannel := make(chan *responseData, 1) + registry.requests[requestUuid.String()] = responseChannel + registry.signCertificate(responseChannel, request) + }() + return requestUuid.String(), nil +} + +func (registry *SigningRequestRegistry) signCertificate(channel chan *responseData, request *requestData) { + responseData, err := registry.sign(request) + if err != nil { + log.Error(err) + close(channel) + return + } + channel <- responseData +} + +func (registry *SigningRequestRegistry) sign(request *requestData) (response *responseData, err error) { + log.Debugf("received CSR for %s:\n\n%s", request.CommonName, request.Csr) + subjectDN := fmt.Sprintf("/CN=%s", request.CommonName) + var csrFile *os.File + if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { + log.Errorf("could not open temporary file: %s", err) + return + } + if _, err = csrFile.Write([]byte(request.Csr)); err != nil { + log.Errorf("could not write CSR to file: %s", err) + return + } + if err = csrFile.Close(); err != nil { + log.Errorf("could not close CSR file: %s", err) + return + } + defer func(file *os.File) { + err = os.Remove(file.Name()) + if err != nil { + log.Errorf("could not remove temporary file: %s", err) + } + }(csrFile) + + // simulate a delay during certificate creation + time.Sleep(5 * time.Second) + + opensslCommand := exec.Command( + "openssl", "ca", "-config", "ca.cnf", + "-policy", "policy_match", "-extensions", "client_ext", + "-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") + var out, cmdErr bytes.Buffer + opensslCommand.Stdout = &out + opensslCommand.Stderr = &cmdErr + err = opensslCommand.Run() + if err != nil { + log.Error(err) + log.Error(cmdErr.String()) + return + } + + 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 + } + + var caChain []string + if caChain, err = registry.getCAChain(certificate); err != nil { + return + } + + response = &responseData{ + Certificate: string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + })), + CAChain: caChain, + } + return +} + +func (registry *SigningRequestRegistry) GetResponseChannel(requestUuid string) (chan *responseData, error) { + if responseChannel, exists := registry.requests[requestUuid]; exists { + delete(registry.requests, requestUuid) + return responseChannel, nil + } else { + return nil, errors.New("no request found") + } +} + +func (registry *SigningRequestRegistry) getCAChain(certificate *x509.Certificate) ([]string, error) { + issuerString := string(certificate.RawIssuer) + + if value, exists := registry.caChainMap[issuerString]; exists { + return value, nil + } + + 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 + var count = 0 + for { + if len(registry.caCertificates) == 0 { + return nil, errors.New("no CA certificates loaded") + } + if count > len(registry.caCertificates) { + return nil, errors.New("could not construct certificate chain") + } + for _, caCert := range registry.caCertificates { + if previous == nil { + if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { + previous = caCert + appendCert(caCert) + } + } else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { + registry.caChainMap[issuerString] = result + return result, nil + } else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { + previous = caCert + appendCert(caCert) + } else { + log.Debugf("skipped certificate %s", caCert.Subject) + } + } + count++ + } +} diff --git a/handlers/signing.go b/handlers/signing.go index b20b7c5..33144a1 100644 --- a/handlers/signing.go +++ b/handlers/signing.go @@ -1,83 +1,18 @@ package handlers import ( - "bytes" - "crypto/x509" "encoding/json" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" "net/http" - "os" - "os/exec" log "github.com/sirupsen/logrus" ) type CertificateSigningHandler struct { - caCertificates []*x509.Certificate - caChainMap map[string][]string + requestRegistry *SigningRequestRegistry } -func NewCertificateSigningHandler(caCertificates []*x509.Certificate) *CertificateSigningHandler { - return &CertificateSigningHandler{caCertificates: caCertificates, caChainMap: make(map[string][]string)} -} - -func (h *CertificateSigningHandler) 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 - if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { - log.Errorf("could not open temporary file: %s", err) - return - } - if _, err = csrFile.Write([]byte(csrPem)); err != nil { - log.Errorf("could not write CSR to file: %s", err) - return - } - if err = csrFile.Close(); err != nil { - log.Errorf("could not close CSR file: %s", err) - return - } - defer func(file *os.File) { - err = os.Remove(file.Name()) - if err != nil { - log.Errorf("could not remove temporary file: %s", err) - } - }(csrFile) - - opensslCommand := exec.Command( - "openssl", "ca", "-config", "ca.cnf", - "-policy", "policy_match", "-extensions", "client_ext", - "-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") - var out, cmdErr bytes.Buffer - opensslCommand.Stdout = &out - opensslCommand.Stderr = &cmdErr - err = opensslCommand.Run() - if err != nil { - log.Print(err) - log.Print(cmdErr.String()) - return - } - - 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 +func NewCertificateSigningHandler(requestRegistry *SigningRequestRegistry) *CertificateSigningHandler { + return &CertificateSigningHandler{requestRegistry: requestRegistry} } func (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -91,26 +26,27 @@ func (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Req } var err error var requestBody requestData - var responseData responseData if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { - log.Print(err) + log.Error(err) http.Error(w, err.Error(), http.StatusBadRequest) return } - responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) - if err != nil { - http.Error(w, "Could not sign certificate", http.StatusInternalServerError) - return + type acceptedResponse struct { + RequestId string `json:"request_id"` } - var jsonBytes []byte - if jsonBytes, err = json.Marshal(&responseData); err != nil { - log.Print(err) + taskUuid, err := h.requestRegistry.AddSigningRequest(&requestBody) + if err != nil { + log.Error(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } - if _, err = w.Write(jsonBytes); err != nil { + w.WriteHeader(http.StatusAccepted) + response := &acceptedResponse{RequestId: taskUuid} + if err = json.NewEncoder(w).Encode(response); err != nil { log.Print(err) } } @@ -124,48 +60,3 @@ type responseData struct { Certificate string `json:"certificate"` CAChain []string `json:"ca_chain"` } - -func (h *CertificateSigningHandler) getCAChain(certificate *x509.Certificate) ([]string, error) { - issuerString := string(certificate.RawIssuer) - - if value, exists := h.caChainMap[issuerString]; exists { - return value, nil - } - - 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 - var count = 0 - for { - if len(h.caCertificates) == 0 { - return nil, errors.New("no CA certificates loaded") - } - if count > len(h.caCertificates) { - return nil, errors.New("could not construct certificate chain") - } - for _, caCert := range h.caCertificates { - if previous == nil { - if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { - previous = caCert - appendCert(caCert) - } - } else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { - h.caChainMap[issuerString] = result - return result, nil - } else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { - previous = caCert - appendCert(caCert) - } else { - log.Debugf("skipped certificate %s", caCert.Subject) - } - } - count++ - } -} diff --git a/handlers/websocket.go b/handlers/websocket.go new file mode 100644 index 0000000..6543da1 --- /dev/null +++ b/handlers/websocket.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + log "github.com/sirupsen/logrus" +) + +type WebSocketHandler struct { + requestRegistry *SigningRequestRegistry +} + +func NewWebSocketHandler(registry *SigningRequestRegistry) *WebSocketHandler { + return &WebSocketHandler{requestRegistry: registry} +} + +func (w *WebSocketHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + conn, _, _, err := ws.UpgradeHTTP(request, writer) + if err != nil { + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + go func() { + defer func() { _ = conn.Close() }() + + var ( + reader = wsutil.NewReader(conn, ws.StateServerSide) + writer = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + jsonDecoder = json.NewDecoder(reader) + jsonEncoder = json.NewEncoder(writer) + ) + + for { + header, err := reader.NextFrame() + if err != nil { + log.Error(err) + break + } + if header.OpCode == ws.OpClose { + log.Debug("channel closed") + break + } + + type requestType struct { + RequestId string `json:"request_id"` + } + + request := &requestType{} + err = jsonDecoder.Decode(request) + if err != nil { + log.Error(err) + break + } + + channel, err := w.requestRegistry.GetResponseChannel(request.RequestId) + if err != nil { + log.Error(err) + break + } + + var response *responseData + response = <-channel + if err = jsonEncoder.Encode(response); err != nil { + log.Error(err) + break + } + close(channel) + + if err = writer.Flush(); err != nil { + log.Error(err) + break + } + } + }() +} diff --git a/main.go b/main.go index a8f75fd..31b07d1 100644 --- a/main.go +++ b/main.go @@ -41,12 +41,14 @@ func main() { csrfKey := initCSRFKey() - mux.Handle("/sign/", handlers.NewCertificateSigningHandler(loadCACertificates())) + signingRequestRegistry := handlers.NewSigningRequestRegistry(loadCACertificates()) + mux.Handle("/sign/", handlers.NewCertificateSigningHandler(signingRequestRegistry)) mux.Handle("/", handlers.NewIndexHandler(bundle)) fileServer := http.FileServer(http.Dir("./public")) mux.Handle("/css/", fileServer) mux.Handle("/js/", fileServer) mux.Handle("/locales/", handlers.NewJSLocalesHandler(bundle)) + mux.Handle("/ws/", handlers.NewWebSocketHandler(signingRequestRegistry)) server := http.Server{ Addr: ":8000", Handler: csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux), diff --git a/templates/index.html b/templates/index.html index 8fdee19..f215765 100644 --- a/templates/index.html +++ b/templates/index.html @@ -148,32 +148,60 @@ document.getElementById("csr").innerHTML = csrPem; progressBar.style.width = "75%"; progressBar.setAttribute("aria-valuenow", "3"); + progressBar.classList.add('progress-bar-striped', 'progress-bar-animated'); + progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds}) + ', ' + i18n.t('certificate.waiting'); postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken) .then(data => { - document.getElementById("crt").innerHTML = data["certificate"]; - let certificates = [] - certificates.push(forge.pki.certificateFromPem(data["certificate"])); - - for (let certificatePemData of data["ca_chain"]) { - certificates.push(forge.pki.certificateFromPem(certificatePemData)); + const request_id = data["request_id"] + const webSocket = new WebSocket( + "wss://" + window.location.toString().substring( + "https://".length + ).split("/")[0] + "/ws/") + webSocket.onopen = function () { + webSocket.send(JSON.stringify({"request_id": request_id})) + } + webSocket.onmessage = function (event) { + handleCertificateResponse(JSON.parse(event.data)); + } + webSocket.onclose = function (event) { + if (event.wasClean) { + console.debug("websocket closed cleanly"); + } else { + console.error("websocket connection died"); + } + } + webSocket.onerror = function (error) { + console.error(error.message); } + }); - // browsers have trouble importing anything but 3des encrypted PKCS#12 - const p12asn1 = forge.pkcs12.toPkcs12Asn1( - keys.privateKey, certificates, password, - {algorithm: '3des'} - ); - const p12Der = forge.asn1.toDer(p12asn1).getBytes(); - const p12B64 = forge.util.encode64(p12Der); + function handleCertificateResponse(data) { + document.getElementById("crt").innerHTML = data["certificate"]; + let certificates = [] + certificates.push(forge.pki.certificateFromPem(data["certificate"])); - const downloadLink = document.getElementById('download-link'); - downloadLink.download = 'client_certificate.p12'; - downloadLink.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64); + for (let certificatePemData of data["ca_chain"]) { + certificates.push(forge.pki.certificateFromPem(certificatePemData)); + } - document.getElementById('download-wrapper').classList.remove("d-none"); - progressBar.style.width = "100%"; - progressBar.setAttribute("aria-valuenow", "4"); - }); + // browsers have trouble importing anything but 3des encrypted PKCS#12 + const p12asn1 = forge.pkcs12.toPkcs12Asn1( + keys.privateKey, certificates, password, + {algorithm: '3des'} + ); + const p12Der = forge.asn1.toDer(p12asn1).getBytes(); + const p12B64 = forge.util.encode64(p12Der); + + const downloadLink = document.getElementById('download-link'); + downloadLink.download = 'client_certificate.p12'; + downloadLink.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64); + + document.getElementById('download-wrapper').classList.remove("d-none"); + progressBar.classList.remove("progress-bar-animated", 'progress-bar-striped'); + progressBar.style.width = "100%"; + progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds}) + ', ' + i18n.t('certificate.received'); + progressBar.setAttribute("aria-valuenow", "4"); + } } } }