Browse Source

Decouple request and response via WebSocket

master
Jan Dittberner 11 months ago
parent
commit
2093bf2429
  1. 8
      active.de-DE.toml
  2. 8
      active.en-US.toml
  3. 2
      active.en.toml
  4. 4
      go.mod
  5. 8
      go.sum
  6. 9
      handlers/index.go
  7. 12
      handlers/jslocales.go
  8. 172
      handlers/registry.go
  9. 137
      handlers/signing.go
  10. 79
      handlers/websocket.go
  11. 4
      main.go
  12. 68
      templates/index.html

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

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

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

4
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

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

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

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

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

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

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

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

68
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");
}
}
}
}

Loading…
Cancel
Save