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" hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d"
other = "CSR-Erzeugung im Browser" 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"] ["JavaScript.KeyGen.Generated"]
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
other = "Schlüssel in __seconds__ Sekunden erzeugt" 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" hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa"
other = "RSA-Schlüssellänge" other = "RSA-Schlüssellänge"
[SendCSRButtonLabel]
hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477"
other = "Signieranfrage abschicken"
[StatusLoading] [StatusLoading]
hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9"
other = "Lade ..." other = "Lade ..."

View File

@ -6,6 +6,14 @@ other = "Generate signing request"
hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d" hash = "sha1-f1a8f21b12fe51250da4a11f1c6ab28eab69b69d"
other = "CSR generation in browser" 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"] ["JavaScript.KeyGen.Generated"]
hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e" hash = "sha1-34cdfcdc837e3fc052733a3588cc3923b793103e"
other = "key generated in __seconds__ seconds" 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" hash = "sha1-bd446df78ad62000d6516a95594a24b98688e1fa"
other = "RSA Key Size" other = "RSA Key Size"
[SendCSRButtonLabel]
hash = "sha1-376b8bd1617b2c9d54272604677b1d75d3e6f477"
other = "Send signing request"
[StatusLoading] [StatusLoading]
hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9" hash = "sha1-530afa5bce434b05e3a10e83ff2567f7f8622af9"
other = "Loading ..." other = "Loading ..."

View File

@ -1,5 +1,7 @@
CSRButtonLabel = "Generate signing request" 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."
DownloadLabel = "Download"
"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"
@ -11,5 +13,4 @@ RSA3072Label = "3072 Bit"
RSA4096Label = "4096 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." 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" RSAKeySizeLabel = "RSA Key Size"
SendCSRButtonLabel = "Send signing request"
StatusLoading = "Loading ..." StatusLoading = "Loading ..."

View File

@ -5,7 +5,6 @@ const rename = require('gulp-rename');
const replace = require('gulp-replace'); const replace = require('gulp-replace');
const sass = require('gulp-sass'); const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
const sriHash = require('gulp-sri-hash');
const uglify = require('gulp-uglify'); const uglify = require('gulp-uglify');
sass.compiler = require('node-sass'); sass.compiler = require('node-sass');
@ -48,7 +47,7 @@ function publishAssets() {
} }
function publish() { 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( exports.default = series(

165
main.go
View File

@ -4,14 +4,19 @@ import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strings" "strings"
"syscall"
"time" "time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
@ -29,10 +34,13 @@ type requestData struct {
} }
type responseData struct { type responseData struct {
Certificate string `json:"certificate"` 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) log.Printf("received CSR for %s:\n\n%s", commonName, csrPem)
subjectDN := fmt.Sprintf("/CN=%s", commonName) subjectDN := fmt.Sprintf("/CN=%s", commonName)
var csrFile *os.File var csrFile *os.File
@ -68,7 +76,23 @@ func (h *signCertificate) sign(csrPem string, commonName string) (certPem string
log.Print(cmdErr.String()) log.Print(cmdErr.String())
return 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 return
} }
@ -83,7 +107,8 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
var err error var err error
var requestBody requestData var requestBody requestData
var certificate string
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.Print(err)
@ -91,14 +116,14 @@ func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
certificate, err = h.sign(requestBody.Csr, requestBody.CommonName) responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName)
if err != nil { if err != nil {
http.Error(w, "Could not sign certificate", http.StatusInternalServerError) http.Error(w, "Could not sign certificate", http.StatusInternalServerError)
return return
} }
var jsonBytes []byte var jsonBytes []byte
if jsonBytes, err = json.Marshal(&responseData{Certificate: certificate}); err != nil { if jsonBytes, err = json.Marshal(&responseData); err != nil {
log.Print(err) 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 { type indexHandler struct {
Bundle *i18n.Bundle Bundle *i18n.Bundle
} }
@ -158,26 +216,33 @@ func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ID: "StatusLoading", ID: "StatusLoading",
Other: "Loading ...", Other: "Loading ...",
}}) }})
sendCSRButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ downloadLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{
ID: "SendCSRButtonLabel", ID: "DownloadLabel",
Other: "Send signing request", 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")) t := template.Must(template.ParseFiles("templates/index.html"))
err := t.Execute(w, map[string]interface{}{ err := t.Execute(w, map[string]interface{}{
"Title": csrGenTitle, "Title": csrGenTitle,
"NameLabel": nameLabel, "NameLabel": nameLabel,
"NameHelpText": nameHelpText, "NameHelpText": nameHelpText,
"PasswordLabel": passwordLabel, "PasswordLabel": passwordLabel,
"RSAKeySizeLegend": rsaKeySizeLegend, "RSAKeySizeLegend": rsaKeySizeLegend,
"RSA3072Label": rsa3072Label, "RSA3072Label": rsa3072Label,
"RSA2048Label": rsa2048Label, "RSA2048Label": rsa2048Label,
"RSA4096Label": rsa4096Label, "RSA4096Label": rsa4096Label,
"RSAHelpText": rsaHelpText, "RSAHelpText": rsaHelpText,
"CSRButtonLabel": csrButtonLabel, "CSRButtonLabel": csrButtonLabel,
"StatusLoading": statusLoading, "StatusLoading": statusLoading,
"SendCSRButtonLabel": sendCSRButtonLabel, "DownloadDescription": downloadDescription,
csrf.TemplateTag: csrf.TemplateField(r), "DownloadLabel": downloadLabel,
csrf.TemplateTag: csrf.TemplateField(r),
}) })
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
@ -238,6 +303,26 @@ func generateRandomBytes(count int) []byte {
return randomBytes 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() { func main() {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
CipherSuites: []uint16{ CipherSuites: []uint16{
@ -258,7 +343,21 @@ func main() {
} }
mux := http.NewServeMux() 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("/sign/", &signCertificate{})
mux.Handle("/", &indexHandler{Bundle: bundle}) mux.Handle("/", &indexHandler{Bundle: bundle})
fileServer := http.FileServer(http.Dir("./public")) fileServer := http.FileServer(http.Dir("./public"))
@ -274,8 +373,22 @@ func main() {
WriteTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second,
IdleTimeout: 30 * time.Second, IdleTimeout: 30 * time.Second,
} }
err := server.ListenAndServeTLS("server.crt.pem", "server.key.pem") go func() {
if err != nil { err := server.ListenAndServeTLS("server.crt.pem", "server.key.pem")
log.Fatal(err) 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"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/styles.min.css" <link rel="stylesheet" href="css/styles.min.css">
integrity="sha384-vKuz4xd0kXa+x9wRdibDAVE8gXC/1up2T9QVSas8Rk07AZhzOzbwFdj00XUjOO4i" crossorigin="anonymous">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
@ -45,40 +44,39 @@
</div> </div>
<small id="keySizeHelp" class="form-text text-muted">{{ .RSAHelpText }}</small> <small id="keySizeHelp" class="form-text text-muted">{{ .RSAHelpText }}</small>
</fieldset> </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> </form>
</div> </div>
</div> </div>
<div id="status-block" class="d-none row"> <div class="row d-none" id="status-block">
<div class="col-12"> <div class="col-12 py-3">
<div class="d-flex align-items-center"> <div class="progress" style="height: 2rem">
<strong id="status-text">{{ .StatusLoading }}</strong> <div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0"
<div class="spinner-border ml-auto" id="status-spinner" role="status" aria-hidden="true"></div> aria-valuemax="4">{{ .StatusLoading }}
</div>
</div> </div>
</div> </div>
</div> <div class="col-12 d-none" id="download-wrapper">
<div class="row"> <p class="text-info">{{ .DownloadDescription }}</p>
<div class="col-12"> <a href="#" class="btn btn-success" id="download-link">
<div id="result"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor"
<button type="button" disabled id="send-button" class="btn btn-default disabled"> xmlns="http://www.w3.org/2000/svg">
{{ .SendCSRButtonLabel }} <path fill-rule="evenodd"
</button> 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"/>
</div> <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>
</div> </div>
<pre id="key"></pre> <pre id="key" class="d-none"></pre>
<pre id="csr"></pre> <pre id="csr" class="d-none"></pre>
<pre id="crt"></pre> <pre id="crt" class="d-none"></pre>
</div> </div>
<script src="js/jquery.min.js" integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2" <script src="js/jquery.min.js"></script>
crossorigin="anonymous"></script> <script src="js/forge.all.min.js"></script>
<script src="js/forge.all.min.js" integrity="sha384-VfWVy4csHnuL0Tq/vQkZtIpDf4yhSLNf3aBffGj3wKUmyn1UPNx4v0Pzo9chiHu1" <script src="js/bootstrap.bundle.min.js"></script>
crossorigin="anonymous"></script> <script src="js/i18next.min.js"></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> <script>
async function postData(url = '', data = {}, csrfToken) { async function postData(url = '', data = {}, csrfToken) {
const response = await fetch(url, { const response = await fetch(url, {
@ -98,7 +96,7 @@
} }
document.addEventListener("DOMContentLoaded", function () { 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); if (err) return console.log('something went wrong loading', err);
}); });
@ -111,24 +109,27 @@
if (isNaN(keySize)) { if (isNaN(keySize)) {
return false; return false;
} }
const spinner = document.getElementById('status-spinner');
const statusText = document.getElementById('status-text');
const statusBlock = document.getElementById('status-block'); const statusBlock = document.getElementById('status-block');
const progressBar = document.getElementById('progress-bar');
statusBlock.classList.remove('d-none'); 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); 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 startDate = new Date();
const step = function () { const step = function () {
let duration = (new Date()).getTime() - startDate.getTime(); let duration = (new Date()).getTime() - startDate.getTime();
let seconds = Math.floor(duration / 100) / 10; let seconds = Math.floor(duration / 100) / 10;
if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) { if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) {
setTimeout(step, 1); 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 { } else {
statusText.innerHTML = i18n.t('keygen.generated', {seconds: seconds}); // `` progressBar.classList.remove("progress-bar-animated", 'progress-bar-striped');
spinner.classList.add('d-none'); progressBar.style.width = "50%";
progressBar.setAttribute("aria-valuenow", "2");
progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds});
const keys = state.keys; const keys = state.keys;
keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey); keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey);
const csr = forge.pki.createCertificationRequest(); const csr = forge.pki.createCertificationRequest();
@ -145,31 +146,34 @@
if (verified) { if (verified) {
let csrPem = forge.pki.certificationRequestToPem(csr); let csrPem = forge.pki.certificationRequestToPem(csr);
document.getElementById("csr").innerHTML = csrPem; document.getElementById("csr").innerHTML = csrPem;
const sendButton = progressBar.style.width = "75%";
document.getElementById("send-button"); progressBar.setAttribute("aria-valuenow", "3");
sendButton.addEventListener("click", function () { postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken)
postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken) .then(data => {
.then(data => { document.getElementById("crt").innerHTML = data["certificate"];
console.log(data); let certificates = []
document.getElementById("crt").innerHTML = data["certificate"]; certificates.push(forge.pki.certificateFromPem(data["certificate"]));
const certificate = forge.pki.certificateFromPem(data["certificate"]);
// browsers have trouble importing anything but 3des encrypted PKCS#12
const p12asn1 = forge.pkcs12.toPkcs12Asn1(
keys.privateKey, certificate, password,
{algorithm: '3des'}
);
const p12Der = forge.asn1.toDer(p12asn1).getBytes();
const p12B64 = forge.util.encode64(p12Der);
const a = document.createElement('a'); for (let certificatePemData of data["ca_chain"]) {
a.download = 'client_certificate.p12'; certificates.push(forge.pki.certificateFromPem(certificatePemData));
a.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64); }
a.appendChild(document.createTextNode("Download"));
document.getElementById('result').appendChild(a); // browsers have trouble importing anything but 3des encrypted PKCS#12
}); const p12asn1 = forge.pkcs12.toPkcs12Asn1(
}); keys.privateKey, certificates, password,
sendButton.removeAttribute("disabled"); {algorithm: '3des'}
sendButton.classList.remove("disabled"); );
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");
});
} }
} }
} }