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:
Jan Dittberner 2020-11-30 00:08:05 +01:00
parent c751c51713
commit 5c3f0ea942
6 changed files with 233 additions and 43 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
*.pem
.*.swp .*.swp
/.idea/ /.idea/
/exampleca/
/node_modules/ /node_modules/
/public/ /public/

31
ca.cnf Normal file
View 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
View file

@ -0,0 +1,3 @@
module git.dittberner.info/jan/browser_csr_generation
go 1.13

112
main.go Normal file
View 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
View 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

View file

@ -41,7 +41,7 @@
<small id="keySizeHelp" class="form-text text-muted">An RSA key pair will be generated in your <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> browser. Longer key sizes provide better security but take longer to generate.</small>
</fieldset> </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> </form>
</div> </div>
</div> </div>
@ -55,58 +55,89 @@
</div> </div>
<pre id="key"></pre> <pre id="key"></pre>
<pre id="csr"></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> </div>
<script src="../public/js/jquery.slim.min.js"></script> <script src="../public/js/jquery.slim.min.js"></script>
<script src="../public/js/forge.all.min.js"></script> <script src="../public/js/forge.all.min.js"></script>
<script src="../public/js/bootstrap.bundle.min.js"></script> <script src="../public/js/bootstrap.bundle.min.js"></script>
<script> <script>
const keyElement = document.getElementById('key'); async function postData(url = '', data = {}) {
document.getElementById('csr-form').onsubmit = function (event) { const response = await fetch(url, {
const subject = event.target["nameInput"].value; method: 'POST',
const keySize = parseInt(event.target["keySize"].value); mode: 'cors',
if (isNaN(keySize)) { cache: 'no-cache',
return false; credentials: 'same-origin',
} headers: {
const spinner = document.getElementById('status-spinner'); 'Content-Type': 'application/json',
const statusText = document.getElementById('status-text'); },
const statusBlock = document.getElementById('status-block'); redirect: "error",
statusBlock.classList.remove('d-none'); referrerPolicy: "no-referrer",
spinner.classList.remove('d-none'); body: JSON.stringify(data),
});
return response.json()
}
const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001); document.addEventListener("DOMContentLoaded", function () {
statusText.innerHTML = 'started key generation'; const keyElement = document.getElementById('key');
const startDate = new Date(); document.getElementById('csr-form').onsubmit = function (event) {
const step = function () { const subject = event.target["nameInput"].value;
let duration = (new Date()).getTime() - startDate.getTime(); const keySize = parseInt(event.target["keySize"].value);
let seconds = Math.floor(duration / 100) / 10; if (isNaN(keySize)) {
if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) { return false;
setTimeout(step, 1); }
statusText.innerHTML = `key generation running for ${seconds} seconds`; const spinner = document.getElementById('status-spinner');
} else { const statusText = document.getElementById('status-text');
statusText.innerHTML = `key generated in ${seconds} seconds` const statusBlock = document.getElementById('status-block');
spinner.classList.add('d-none'); statusBlock.classList.remove('d-none');
const keys = state.keys; spinner.classList.remove('d-none');
keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey);
const csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey; const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001);
csr.setSubject([{ statusText.innerHTML = 'started key generation';
name: 'commonName', const startDate = new Date();
value: subject, const step = function () {
valueTagClass: forge.asn1.Type.UTF8, let duration = (new Date()).getTime() - startDate.getTime();
}]); let seconds = Math.floor(duration / 100) / 10;
csr.sign(keys.privateKey, forge.md.sha256.create()); 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(); csr.publicKey = keys.publicKey;
if (verified) { csr.setSubject([{
document.getElementById("csr").innerHTML = forge.pki.certificationRequestToPem(csr); 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> </script>
</body> </body>
</html> </html>