Add CA chain to download, improve UI
This commit is contained in:
parent
a960a60ecd
commit
b748050de3
6 changed files with 222 additions and 97 deletions
|
@ -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 ..."
|
||||
|
|
|
@ -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 ..."
|
||||
|
|
|
@ -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 ..."
|
||||
|
|
|
@ -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
133
main.go
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue