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 | ||||||
							
								
								
									
										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 |                     <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