Decouple request and response via WebSocket
This commit is contained in:
parent
08be6e68bc
commit
2093bf2429
12 changed files with 369 additions and 146 deletions
|
@ -14,6 +14,14 @@ other = "Dein Schlüsselmaterial ist bereit zum Herunterladen. Die herunterladba
|
||||||
hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8"
|
hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8"
|
||||||
other = "Herunterladen"
|
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"]
|
["JavaScript.KeyGen.Generated"]
|
||||||
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
|
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
|
||||||
other = "Schlüssel in __seconds__ Sekunden erzeugt"
|
other = "Schlüssel in __seconds__ Sekunden erzeugt"
|
||||||
|
|
|
@ -14,6 +14,14 @@ other = "Your key material is ready for download. The downloadable file contains
|
||||||
hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8"
|
hash = "sha1-a479c9c34e878d07b4d67a73a48f432ad7dc53c8"
|
||||||
other = "Download"
|
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"]
|
["JavaScript.KeyGen.Generated"]
|
||||||
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
|
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
|
||||||
other = "key generated in __seconds__ seconds"
|
other = "key generated in __seconds__ seconds"
|
||||||
|
|
|
@ -2,6 +2,8 @@ CSRButtonLabel = "Generate signing request"
|
||||||
CSRGenTitle = "CSR generation in browser"
|
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."
|
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"
|
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.Generated" = "key generated in __seconds__ seconds"
|
||||||
"JavaScript.KeyGen.Running" = "key generation running for __seconds__ seconds"
|
"JavaScript.KeyGen.Running" = "key generation running for __seconds__ seconds"
|
||||||
"JavaScript.KeyGen.Started" = "started key generation"
|
"JavaScript.KeyGen.Started" = "started key generation"
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -4,6 +4,10 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
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/gorilla/csrf v1.7.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.1.1
|
github.com/nicksnyder/go-i18n/v2 v2.1.1
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.7.0
|
||||||
|
|
8
go.sum
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
|
||||||
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
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=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
|
|
@ -18,6 +18,15 @@ func NewIndexHandler(bundle *i18n.Bundle) *IndexHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
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"))
|
localizer := i18n.NewLocalizer(i.bundle, r.Header.Get("Accept-Language"))
|
||||||
csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{
|
csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{
|
||||||
ID: "CSRGenTitle",
|
ID: "CSRGenTitle",
|
||||||
|
|
|
@ -32,6 +32,10 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
Running string `json:"running"`
|
Running string `json:"running"`
|
||||||
Generated string `json:"generated"`
|
Generated string `json:"generated"`
|
||||||
} `json:"keygen"`
|
} `json:"keygen"`
|
||||||
|
Certificate struct {
|
||||||
|
Waiting string `json:"waiting"`
|
||||||
|
Received string `json:"received"`
|
||||||
|
} `json:"certificate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
translations := &translationData{}
|
translations := &translationData{}
|
||||||
|
@ -47,6 +51,14 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
ID: "JavaScript.KeyGen.Generated",
|
ID: "JavaScript.KeyGen.Generated",
|
||||||
Other: "key generated in __seconds__ seconds",
|
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)
|
encoder := json.NewEncoder(w)
|
||||||
if err := encoder.Encode(translations); err != nil {
|
if err := encoder.Encode(translations); err != nil {
|
||||||
|
|
172
handlers/registry.go
Normal file
172
handlers/registry.go
Normal 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++
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,83 +1,18 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CertificateSigningHandler struct {
|
type CertificateSigningHandler struct {
|
||||||
caCertificates []*x509.Certificate
|
requestRegistry *SigningRequestRegistry
|
||||||
caChainMap map[string][]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertificateSigningHandler(caCertificates []*x509.Certificate) *CertificateSigningHandler {
|
func NewCertificateSigningHandler(requestRegistry *SigningRequestRegistry) *CertificateSigningHandler {
|
||||||
return &CertificateSigningHandler{caCertificates: caCertificates, caChainMap: make(map[string][]string)}
|
return &CertificateSigningHandler{requestRegistry: requestRegistry}
|
||||||
}
|
|
||||||
|
|
||||||
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 (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
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 err error
|
||||||
var requestBody requestData
|
var requestBody requestData
|
||||||
var responseData responseData
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
log.Print(err)
|
log.Error(err)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonBytes []byte
|
w.WriteHeader(http.StatusAccepted)
|
||||||
if jsonBytes, err = json.Marshal(&responseData); err != nil {
|
response := &acceptedResponse{RequestId: taskUuid}
|
||||||
log.Print(err)
|
if err = json.NewEncoder(w).Encode(response); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = w.Write(jsonBytes); err != nil {
|
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,48 +60,3 @@ type responseData struct {
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
CAChain []string `json:"ca_chain"`
|
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
79
handlers/websocket.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
4
main.go
4
main.go
|
@ -41,12 +41,14 @@ func main() {
|
||||||
|
|
||||||
csrfKey := initCSRFKey()
|
csrfKey := initCSRFKey()
|
||||||
|
|
||||||
mux.Handle("/sign/", handlers.NewCertificateSigningHandler(loadCACertificates()))
|
signingRequestRegistry := handlers.NewSigningRequestRegistry(loadCACertificates())
|
||||||
|
mux.Handle("/sign/", handlers.NewCertificateSigningHandler(signingRequestRegistry))
|
||||||
mux.Handle("/", handlers.NewIndexHandler(bundle))
|
mux.Handle("/", handlers.NewIndexHandler(bundle))
|
||||||
fileServer := http.FileServer(http.Dir("./public"))
|
fileServer := http.FileServer(http.Dir("./public"))
|
||||||
mux.Handle("/css/", fileServer)
|
mux.Handle("/css/", fileServer)
|
||||||
mux.Handle("/js/", fileServer)
|
mux.Handle("/js/", fileServer)
|
||||||
mux.Handle("/locales/", handlers.NewJSLocalesHandler(bundle))
|
mux.Handle("/locales/", handlers.NewJSLocalesHandler(bundle))
|
||||||
|
mux.Handle("/ws/", handlers.NewWebSocketHandler(signingRequestRegistry))
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
Addr: ":8000",
|
Addr: ":8000",
|
||||||
Handler: csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux),
|
Handler: csrf.Protect(csrfKey, csrf.FieldName("csrfToken"), csrf.RequestHeader("X-CSRF-Token"))(mux),
|
||||||
|
|
|
@ -148,32 +148,60 @@
|
||||||
document.getElementById("csr").innerHTML = csrPem;
|
document.getElementById("csr").innerHTML = csrPem;
|
||||||
progressBar.style.width = "75%";
|
progressBar.style.width = "75%";
|
||||||
progressBar.setAttribute("aria-valuenow", "3");
|
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)
|
postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById("crt").innerHTML = data["certificate"];
|
const request_id = data["request_id"]
|
||||||
let certificates = []
|
const webSocket = new WebSocket(
|
||||||
certificates.push(forge.pki.certificateFromPem(data["certificate"]));
|
"wss://" + window.location.toString().substring(
|
||||||
|
"https://".length
|
||||||
for (let certificatePemData of data["ca_chain"]) {
|
).split("/")[0] + "/ws/")
|
||||||
certificates.push(forge.pki.certificateFromPem(certificatePemData));
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue