Add signer backend
This commit adds a simple go backend calling openssl ca to sign CRS coming from the client. The JavaScript code in src/index.html has been extended to send requests to the sign endpoint and display the resulting certificate in a separate div element. A script setup_example_ca.sh and an openssl configuration file ca.cnf has been added to allow quick setup of a simple example CA.
This commit is contained in:
parent
c751c51713
commit
5c3f0ea942
6 changed files with 233 additions and 43 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
|||
*.pem
|
||||
.*.swp
|
||||
/.idea/
|
||||
/exampleca/
|
||||
/node_modules/
|
||||
/public/
|
||||
|
|
31
ca.cnf
Normal file
31
ca.cnf
Normal file
|
@ -0,0 +1,31 @@
|
|||
extensions = v3_ext
|
||||
|
||||
[ca]
|
||||
default_ca = EXAMPLECA
|
||||
|
||||
[EXAMPLECA]
|
||||
dir = ./exampleca
|
||||
certs = $dir/certs
|
||||
crl_dir = $dir/crl
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir/newcerts
|
||||
serial = $dir/serial
|
||||
crl = $dir/crl.pem
|
||||
certificate = $dir/ca.crt.pem
|
||||
serial = $dir/serial
|
||||
crl = $dir/crl.pem
|
||||
private_key = $dir/private/ca.key.pem
|
||||
RANDFILE = $dir/private/.rand
|
||||
unique_subject = no
|
||||
email_in_dn = no
|
||||
default_md = sha256
|
||||
|
||||
[policy_match]
|
||||
commonName = supplied
|
||||
|
||||
[client_ext]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = keyEncipherment,digitalSignature,nonRepudiation
|
||||
extendedKeyUsage = clientAuth,emailProtection
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer:always
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module git.dittberner.info/jan/browser_csr_generation
|
||||
|
||||
go 1.13
|
112
main.go
Normal file
112
main.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
type signCertificate struct{}
|
||||
|
||||
type requestData struct {
|
||||
Csr string `json:"csr"`
|
||||
CommonName string `json:"commonName"`
|
||||
}
|
||||
|
||||
type responseData struct {
|
||||
Certificate string `json:"certificate"`
|
||||
}
|
||||
|
||||
func (h *signCertificate) sign(csrPem string, commonName string) (certPem string, err error) {
|
||||
log.Printf("received CSR for %s:\n\n%s", commonName, csrPem)
|
||||
subjectDN := fmt.Sprintf("/CN=%s", commonName)
|
||||
err = ioutil.WriteFile("in.pem", []byte(csrPem), 0644)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
opensslCommand := exec.Command(
|
||||
"openssl", "ca", "-config", "ca.cnf", "-days", "365",
|
||||
"-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
|
||||
}
|
||||
certPem = out.String()
|
||||
return
|
||||
}
|
||||
|
||||
func (h *signCertificate) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Only POST requests support", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.Header.Get("content-type") != "application/json" {
|
||||
http.Error(w, "Only JSON content is accepted", http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var requestBody requestData
|
||||
var certificate string
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
log.Print(err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
certificate, 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 {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
if _, err = w.Write(jsonBytes); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
tlsConfig := &tls.Config{
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
NextProtos: []string{"h2"},
|
||||
PreferServerCipherSuites: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.FileServer(http.Dir("public")))
|
||||
mux.Handle("/sign/", &signCertificate{})
|
||||
server := http.Server{
|
||||
Addr: ":8000",
|
||||
Handler: mux,
|
||||
TLSConfig: tlsConfig,
|
||||
ReadTimeout: 20 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 30 * time.Second,
|
||||
}
|
||||
err := server.ListenAndServeTLS("server.crt.pem", "server.key.pem")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
11
setup_example_ca.sh
Executable file
11
setup_example_ca.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ ! -d "exampleca" ]; then
|
||||
mkdir -p exampleca/newcerts
|
||||
touch exampleca/index.txt
|
||||
umask 077
|
||||
mkdir exampleca/private
|
||||
openssl req -new -x509 -keyout exampleca/private/ca.key.pem -out exampleca/ca.crt.pem -days 3650 \
|
||||
-subj "/CN=Example CA" -nodes -newkey rsa:3072 -addext "basicConstraints=critical,CA:true,pathlen:0"
|
||||
chmod +r exampleca/ca.crt.pem
|
||||
fi
|
115
src/index.html
115
src/index.html
|
@ -41,7 +41,7 @@
|
|||
<small id="keySizeHelp" class="form-text text-muted">An RSA key pair will be generated in your
|
||||
browser. Longer key sizes provide better security but take longer to generate.</small>
|
||||
</fieldset>
|
||||
<button type="submit" id="gen-csr-button" class="btn btn-primary">Generate Signing Request</button>
|
||||
<button type="submit" id="gen-csr-button" class="btn btn-primary">Generate signing request</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,58 +55,89 @@
|
|||
</div>
|
||||
<pre id="key"></pre>
|
||||
<pre id="csr"></pre>
|
||||
<pre id="crt"></pre>
|
||||
<button type="button" disabled id="send-button" class="btn btn-default disabled">Send signing request</button>
|
||||
</div>
|
||||
<script src="../public/js/jquery.slim.min.js"></script>
|
||||
<script src="../public/js/forge.all.min.js"></script>
|
||||
<script src="../public/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const keyElement = document.getElementById('key');
|
||||
document.getElementById('csr-form').onsubmit = function (event) {
|
||||
const subject = event.target["nameInput"].value;
|
||||
const keySize = parseInt(event.target["keySize"].value);
|
||||
if (isNaN(keySize)) {
|
||||
return false;
|
||||
}
|
||||
const spinner = document.getElementById('status-spinner');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const statusBlock = document.getElementById('status-block');
|
||||
statusBlock.classList.remove('d-none');
|
||||
spinner.classList.remove('d-none');
|
||||
async function postData(url = '', data = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: "error",
|
||||
referrerPolicy: "no-referrer",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001);
|
||||
statusText.innerHTML = 'started key generation';
|
||||
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 = `key generation running for ${seconds} seconds`;
|
||||
} else {
|
||||
statusText.innerHTML = `key generated in ${seconds} seconds`
|
||||
spinner.classList.add('d-none');
|
||||
const keys = state.keys;
|
||||
keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey);
|
||||
const csr = forge.pki.createCertificationRequest();
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const keyElement = document.getElementById('key');
|
||||
document.getElementById('csr-form').onsubmit = function (event) {
|
||||
const subject = event.target["nameInput"].value;
|
||||
const keySize = parseInt(event.target["keySize"].value);
|
||||
if (isNaN(keySize)) {
|
||||
return false;
|
||||
}
|
||||
const spinner = document.getElementById('status-spinner');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const statusBlock = document.getElementById('status-block');
|
||||
statusBlock.classList.remove('d-none');
|
||||
spinner.classList.remove('d-none');
|
||||
|
||||
csr.publicKey = keys.publicKey;
|
||||
csr.setSubject([{
|
||||
name: 'commonName',
|
||||
value: subject,
|
||||
valueTagClass: forge.asn1.Type.UTF8,
|
||||
}]);
|
||||
csr.sign(keys.privateKey, forge.md.sha256.create());
|
||||
const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001);
|
||||
statusText.innerHTML = 'started key generation';
|
||||
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 = `key generation running for ${seconds} seconds`;
|
||||
} else {
|
||||
statusText.innerHTML = `key generated in ${seconds} seconds`
|
||||
spinner.classList.add('d-none');
|
||||
const keys = state.keys;
|
||||
keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey);
|
||||
const csr = forge.pki.createCertificationRequest();
|
||||
|
||||
const verified = csr.verify();
|
||||
if (verified) {
|
||||
document.getElementById("csr").innerHTML = forge.pki.certificationRequestToPem(csr);
|
||||
csr.publicKey = keys.publicKey;
|
||||
csr.setSubject([{
|
||||
name: 'commonName',
|
||||
value: subject,
|
||||
valueTagClass: forge.asn1.Type.UTF8,
|
||||
}]);
|
||||
csr.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
const verified = csr.verify();
|
||||
if (verified) {
|
||||
let csrPem = forge.pki.certificationRequestToPem(csr);
|
||||
document.getElementById("csr").innerHTML = csrPem;
|
||||
const sendButton =
|
||||
document.getElementById("send-button");
|
||||
sendButton.addEventListener("click", function () {
|
||||
postData("/sign/", {"csr": csrPem, "commonName": subject})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
document.getElementById("crt").innerHTML = data["certificate"];
|
||||
});
|
||||
})
|
||||
sendButton.removeAttribute("disabled");
|
||||
sendButton.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(step);
|
||||
return false;
|
||||
};
|
||||
setTimeout(step);
|
||||
return false;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
Reference in a new issue