Add CA chain to download, improve UI

This commit is contained in:
Jan Dittberner 2020-12-11 22:05:27 +01:00
parent a960a60ecd
commit b748050de3
6 changed files with 222 additions and 97 deletions

View file

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

View file

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

View file

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

View file

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

133
main.go
View file

@ -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"
@ -30,9 +35,12 @@ type requestData struct {
type responseData struct {
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,9 +216,15 @@ 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"))
@ -176,7 +240,8 @@ func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"RSAHelpText": rsaHelpText,
"CSRButtonLabel": csrButtonLabel,
"StatusLoading": statusLoading,
"SendCSRButtonLabel": sendCSRButtonLabel,
"DownloadDescription": downloadDescription,
"DownloadLabel": downloadLabel,
csrf.TemplateTag: csrf.TemplateField(r),
})
if err != nil {
@ -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,
}
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()
}

View file

@ -5,8 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/styles.min.css"
integrity="sha384-vKuz4xd0kXa+x9wRdibDAVE8gXC/1up2T9QVSas8Rk07AZhzOzbwFdj00XUjOO4i" crossorigin="anonymous">
<link rel="stylesheet" href="css/styles.min.css">
<meta name="theme-color" content="#ffffff">
<title>{{ .Title }}</title>
@ -45,40 +44,39 @@
</div>
<small id="keySizeHelp" class="form-text text-muted">{{ .RSAHelpText }}</small>
</fieldset>
<button type="submit" id="gen-csr-button" class="btn btn-primary">{{ .CSRButtonLabel }}</button>
<button type="submit" id="action-button" class="btn btn-primary">{{ .CSRButtonLabel }}</button>
</form>
</div>
</div>
<div id="status-block" class="d-none row">
<div class="col-12">
<div class="d-flex align-items-center">
<strong id="status-text">{{ .StatusLoading }}</strong>
<div class="spinner-border ml-auto" id="status-spinner" role="status" aria-hidden="true"></div>
<div class="row d-none" id="status-block">
<div class="col-12 py-3">
<div class="progress" style="height: 2rem">
<div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="4">{{ .StatusLoading }}
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="result">
<button type="button" disabled id="send-button" class="btn btn-default disabled">
{{ .SendCSRButtonLabel }}
</button>
<div class="col-12 d-none" id="download-wrapper">
<p class="text-info">{{ .DownloadDescription }}</p>
<a href="#" class="btn btn-success" id="download-link">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd"
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
{{ .DownloadLabel }}</a>
</div>
</div>
<pre id="key" class="d-none"></pre>
<pre id="csr" class="d-none"></pre>
<pre id="crt" class="d-none"></pre>
</div>
<pre id="key"></pre>
<pre id="csr"></pre>
<pre id="crt"></pre>
</div>
<script src="js/jquery.min.js" integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2"
crossorigin="anonymous"></script>
<script src="js/forge.all.min.js" integrity="sha384-VfWVy4csHnuL0Tq/vQkZtIpDf4yhSLNf3aBffGj3wKUmyn1UPNx4v0Pzo9chiHu1"
crossorigin="anonymous"></script>
<script src="js/bootstrap.bundle.min.js"
integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx"
crossorigin="anonymous"></script>
<script src="js/i18next.min.js" integrity="sha384-Juj1kpjwKBUTV6Yp9WHG4GdeoMxCmx0zBN9SkwlyrAh5QYWb3l4WrfG7oTv/b00a"
crossorigin="anonymous"></script>
<script src="js/jquery.min.js"></script>
<script src="js/forge.all.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/i18next.min.js"></script>
<script>
async function postData(url = '', data = {}, csrfToken) {
const response = await fetch(url, {
@ -98,7 +96,7 @@
}
document.addEventListener("DOMContentLoaded", function () {
i18n.init({fallbackLng: 'en', debug: true}, (err) => {
i18n.init({fallbackLng: 'en', debug: true, useCookie: false}, (err) => {
if (err) return console.log('something went wrong loading', err);
});
@ -111,24 +109,27 @@
if (isNaN(keySize)) {
return false;
}
const spinner = document.getElementById('status-spinner');
const statusText = document.getElementById('status-text');
const statusBlock = document.getElementById('status-block');
const progressBar = document.getElementById('progress-bar');
statusBlock.classList.remove('d-none');
spinner.classList.remove('d-none');
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
progressBar.style.width = "25%";
progressBar.setAttribute("aria-valuenow", "1");
const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001);
statusText.innerHTML = i18n.t('keygen.started');
progressBar.innerHTML = i18n.t('keygen.started');
const startDate = new Date();
const step = function () {
let duration = (new Date()).getTime() - startDate.getTime();
let seconds = Math.floor(duration / 100) / 10;
if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) {
setTimeout(step, 1);
statusText.innerHTML = i18n.t('keygen.running', {seconds: seconds}); // `key generation running for ${seconds} seconds`;
progressBar.innerHTML = i18n.t('keygen.running', {seconds: seconds});
} else {
statusText.innerHTML = i18n.t('keygen.generated', {seconds: seconds}); // ``
spinner.classList.add('d-none');
progressBar.classList.remove("progress-bar-animated", 'progress-bar-striped');
progressBar.style.width = "50%";
progressBar.setAttribute("aria-valuenow", "2");
progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds});
const keys = state.keys;
keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey);
const csr = forge.pki.createCertificationRequest();
@ -145,31 +146,34 @@
if (verified) {
let csrPem = forge.pki.certificationRequestToPem(csr);
document.getElementById("csr").innerHTML = csrPem;
const sendButton =
document.getElementById("send-button");
sendButton.addEventListener("click", function () {
progressBar.style.width = "75%";
progressBar.setAttribute("aria-valuenow", "3");
postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken)
.then(data => {
console.log(data);
document.getElementById("crt").innerHTML = data["certificate"];
const certificate = forge.pki.certificateFromPem(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, certificate, password,
keys.privateKey, certificates, password,
{algorithm: '3des'}
);
const p12Der = forge.asn1.toDer(p12asn1).getBytes();
const p12B64 = forge.util.encode64(p12Der);
const a = document.createElement('a');
a.download = 'client_certificate.p12';
a.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64);
a.appendChild(document.createTextNode("Download"));
document.getElementById('result').appendChild(a);
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");
});
});
sendButton.removeAttribute("disabled");
sendButton.classList.remove("disabled");
}
}
}