Decouple request and response via WebSocket

This commit is contained in:
Jan Dittberner 2020-12-12 09:59:06 +01:00
parent 08be6e68bc
commit 2093bf2429
12 changed files with 369 additions and 146 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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"

4
go.mod
View file

@ -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

8
go.sum
View file

@ -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=

View file

@ -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",

View file

@ -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 {

172
handlers/registry.go Normal file
View file

@ -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++
}
}

View file

@ -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)
type acceptedResponse struct {
RequestId string `json:"request_id"`
}
taskUuid, err := h.requestRegistry.AddSigningRequest(&requestBody)
if err != nil {
http.Error(w, "Could not sign certificate", http.StatusInternalServerError)
log.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
var jsonBytes []byte
if jsonBytes, err = json.Marshal(&responseData); err != nil {
log.Print(err)
}
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++
}
}

79
handlers/websocket.go Normal file
View file

@ -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
}
}
}()
}

View file

@ -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),

View file

@ -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);
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.style.width = "100%";
progressBar.setAttribute("aria-valuenow", "4");
});
function handleCertificateResponse(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));
}
// 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");
}
}
}
}