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
|
.*.swp
|
||||||
/.idea/
|
/.idea/
|
||||||
|
/exampleca/
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/public/
|
/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
|
117
src/index.html
117
src/index.html
|
@ -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>
|
||||||
|
|
Reference in a new issue