WIP Building out all features necessary for Let's Encrypt #6
							
								
								
									
										158
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								app.js
									
									
									
									
									
								
							| @ -6,7 +6,9 @@ | |||||||
|   var Rasha = window.Rasha; |   var Rasha = window.Rasha; | ||||||
|   var Eckles = window.Eckles; |   var Eckles = window.Eckles; | ||||||
|   var x509 = window.x509; |   var x509 = window.x509; | ||||||
|  |   var CSR = window.CSR; | ||||||
|   var ACME = window.ACME; |   var ACME = window.ACME; | ||||||
|  |   var accountStuff = {}; | ||||||
| 
 | 
 | ||||||
|   function $(sel) { |   function $(sel) { | ||||||
|     return document.querySelector(sel); |     return document.querySelector(sel); | ||||||
| @ -15,6 +17,11 @@ | |||||||
|     return Array.prototype.slice.call(document.querySelectorAll(sel)); |     return Array.prototype.slice.call(document.querySelectorAll(sel)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   function checkTos(tos) { | ||||||
|  |     console.log("TODO checkbox for agree to terms"); | ||||||
|  |     return tos; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   function run() { |   function run() { | ||||||
|     console.log('hello'); |     console.log('hello'); | ||||||
| 
 | 
 | ||||||
| @ -101,6 +108,9 @@ | |||||||
|         $$('input').map(function ($el) { $el.disabled = false; }); |         $$('input').map(function ($el) { $el.disabled = false; }); | ||||||
|         $$('button').map(function ($el) { $el.disabled = false; }); |         $$('button').map(function ($el) { $el.disabled = false; }); | ||||||
|         $('.js-toc-jwk').hidden = false; |         $('.js-toc-jwk').hidden = false; | ||||||
|  | 
 | ||||||
|  |         $('.js-create-account').hidden = false; | ||||||
|  |         $('.js-create-csr').hidden = false; | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -115,74 +125,17 @@ | |||||||
|         console.log('acme result', result); |         console.log('acme result', result); | ||||||
|         var privJwk = JSON.parse($('.js-jwk').innerText).private; |         var privJwk = JSON.parse($('.js-jwk').innerText).private; | ||||||
|         var email = $('.js-email').value; |         var email = $('.js-email').value; | ||||||
|         function checkTos(tos) { |  | ||||||
|           console.log("TODO checkbox for agree to terms"); |  | ||||||
|           return tos; |  | ||||||
|         } |  | ||||||
|         return acme.accounts.create({ |         return acme.accounts.create({ | ||||||
|           email: email |           email: email | ||||||
|         , agreeToTerms: checkTos |         , agreeToTerms: checkTos | ||||||
|         , accountKeypair: { privateKeyJwk: privJwk } |         , accountKeypair: { privateKeyJwk: privJwk } | ||||||
|         }).then(function (account) { |         }).then(function (account) { | ||||||
|           console.log("account created result:", account); |           console.log("account created result:", account); | ||||||
|           return Keypairs.generate({ |           accountStuff.account = account; | ||||||
|             kty: 'RSA' |           accountStuff.privateJwk = privJwk; | ||||||
|           , modulusLength: 2048 |           accountStuff.email = email; | ||||||
|           }).then(function (pair) { |           accountStuff.acme = acme; | ||||||
|             console.log('domain keypair:', pair); |           $('.js-create-order').hidden = false; | ||||||
|             var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); |  | ||||||
|             return acme.certificates.create({ |  | ||||||
|               accountKeypair: { privateKeyJwk: privJwk } |  | ||||||
|             , account: account |  | ||||||
|             , domainKeypair: { privateKeyJwk: pair.private } |  | ||||||
|             , email: email |  | ||||||
|             , domains: domains |  | ||||||
|             , agreeToTerms: checkTos |  | ||||||
|             , challenges: { |  | ||||||
|                 'dns-01': { |  | ||||||
|                   set: function (opts) { |  | ||||||
|                     console.info('dns-01 set challenge:'); |  | ||||||
|                     console.info('TXT', opts.dnsHost); |  | ||||||
|                     console.info(opts.dnsAuthorization); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you set the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 , remove: function (opts) { |  | ||||||
|                     console.log('dns-01 remove challenge:'); |  | ||||||
|                     console.info('TXT', opts.dnsHost); |  | ||||||
|                     console.info(opts.dnsAuthorization); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you delete the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|               , 'http-01': { |  | ||||||
|                   set: function (opts) { |  | ||||||
|                     console.info('http-01 set challenge:'); |  | ||||||
|                     console.info(opts.challengeUrl); |  | ||||||
|                     console.info(opts.keyAuthorization); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you set the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 , remove: function (opts) { |  | ||||||
|                     console.log('http-01 remove challenge:'); |  | ||||||
|                     console.info(opts.challengeUrl); |  | ||||||
|                     console.info(opts.keyAuthorization); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you delete the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }).catch(function (err) { |         }).catch(function (err) { | ||||||
|           console.error("A bad thing happened:"); |           console.error("A bad thing happened:"); | ||||||
|           console.error(err); |           console.error(err); | ||||||
| @ -191,8 +144,87 @@ | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     $('form.js-csr').addEventListener('submit', function (ev) { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |       var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); | ||||||
|  |       var privJwk = JSON.parse($('.js-jwk').innerText).private; | ||||||
|  |       return CSR({ jwk: privJwk, domains: domains }).then(function (web64) { | ||||||
|  |         // Verify with https://www.sslshopper.com/csr-decoder.html
 | ||||||
|  |         console.log('urlBase64 CSR:'); | ||||||
|  |         console.log(web64); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     $('form.js-acme-order').addEventListener('submit', function (ev) { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |       var account = accountStuff.account; | ||||||
|  |       var privJwk = accountStuff.privateJwk; | ||||||
|  |       var email = accountStuff.email; | ||||||
|  |       var acme = accountStuff.acme; | ||||||
|  | 
 | ||||||
|  |       return Keypairs.generate({ | ||||||
|  |         kty: 'RSA' | ||||||
|  |       , modulusLength: 2048 | ||||||
|  |       }).then(function (pair) { | ||||||
|  |         console.log('domain keypair:', pair); | ||||||
|  |         var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); | ||||||
|  |         return acme.certificates.create({ | ||||||
|  |           accountKeypair: { privateKeyJwk: privJwk } | ||||||
|  |         , account: account | ||||||
|  |         , domainKeypair: { privateKeyJwk: pair.private } | ||||||
|  |         , email: email | ||||||
|  |         , domains: domains | ||||||
|  |         , agreeToTerms: checkTos | ||||||
|  |         , challenges: { | ||||||
|  |             'dns-01': { | ||||||
|  |               set: function (opts) { | ||||||
|  |                 console.info('dns-01 set challenge:'); | ||||||
|  |                 console.info('TXT', opts.dnsHost); | ||||||
|  |                 console.info(opts.dnsAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you set the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             , remove: function (opts) { | ||||||
|  |                 console.log('dns-01 remove challenge:'); | ||||||
|  |                 console.info('TXT', opts.dnsHost); | ||||||
|  |                 console.info(opts.dnsAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you delete the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           , 'http-01': { | ||||||
|  |               set: function (opts) { | ||||||
|  |                 console.info('http-01 set challenge:'); | ||||||
|  |                 console.info(opts.challengeUrl); | ||||||
|  |                 console.info(opts.keyAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you set the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             , remove: function (opts) { | ||||||
|  |                 console.log('http-01 remove challenge:'); | ||||||
|  |                 console.info(opts.challengeUrl); | ||||||
|  |                 console.info(opts.keyAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you delete the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     $('.js-generate').hidden = false; |     $('.js-generate').hidden = false; | ||||||
|     $('.js-create-account').hidden = false; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   window.addEventListener('load', run); |   window.addEventListener('load', run); | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								index.html
									
									
									
									
									
								
							| @ -58,15 +58,26 @@ | |||||||
|       <label for="-acmeEmail">Email:</label> |       <label for="-acmeEmail">Email:</label> | ||||||
|       <input class="js-email" type="email" id="-acmeEmail"> |       <input class="js-email" type="email" id="-acmeEmail"> | ||||||
|       <br> |       <br> | ||||||
|  |       <button class="js-create-account" hidden>Create Account</button> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|  |     <h2>Certificate Signing Request</h2> | ||||||
|  |     <form class="js-csr"> | ||||||
|       <label for="-acmeDomains">Domains:</label> |       <label for="-acmeDomains">Domains:</label> | ||||||
|       <input class="js-domains" type="text" id="-acmeDomains"> |       <input class="js-domains" type="text" id="-acmeDomains"> | ||||||
|       <br> |       <br> | ||||||
|  |       <button class="js-create-csr" hidden>Create CSR</button> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|  |     <h2>ACME Certificate Order</h2> | ||||||
|  |     <form class="js-acme-order"> | ||||||
|  |       Challenge type: | ||||||
|       <label for="-http01"><input type="radio" id="-http01" |       <label for="-http01"><input type="radio" id="-http01" | ||||||
|        name="acme-challenge-type" value="http-01" checked>http-01</label> |        name="acme-challenge-type" value="http-01" checked>http-01</label> | ||||||
|       <label for="-dns01"><input type="radio" id="-dns01" |       <label for="-dns01"><input type="radio" id="-dns01" | ||||||
|        name="acme-challenge-type" value="dns-01">dns-01</label> |        name="acme-challenge-type" value="dns-01">dns-01</label> | ||||||
|       <br> |       <br> | ||||||
|       <button class="js-create-account" hidden>Create Account</button> |       <button class="js-create-order" hidden>Create Order</button> | ||||||
|     </form> |     </form> | ||||||
| 
 | 
 | ||||||
|     <div class="js-loading" hidden>Loading</div> |     <div class="js-loading" hidden>Loading</div> | ||||||
| @ -117,6 +128,7 @@ | |||||||
|     <script src="./lib/ecdsa.js"></script> |     <script src="./lib/ecdsa.js"></script> | ||||||
|     <script src="./lib/rsa.js"></script> |     <script src="./lib/rsa.js"></script> | ||||||
|     <script src="./lib/keypairs.js"></script> |     <script src="./lib/keypairs.js"></script> | ||||||
|  |     <script src="./lib/csr.js"></script> | ||||||
|     <script src="./lib/acme.js"></script> |     <script src="./lib/acme.js"></script> | ||||||
|     <script src="./app.js"></script> |     <script src="./app.js"></script> | ||||||
|   </body> |   </body> | ||||||
|  | |||||||
| @ -110,6 +110,8 @@ Enc.binToHex = function (bin) { | |||||||
|     return h; |     return h; | ||||||
|   }).join(''); |   }).join(''); | ||||||
| }; | }; | ||||||
|  | // TODO are there any nuance differences here?
 | ||||||
|  | Enc.utf8ToHex = Enc.binToHex; | ||||||
| 
 | 
 | ||||||
| Enc.hexToBase64 = function (hex) { | Enc.hexToBase64 = function (hex) { | ||||||
|   return btoa(Enc.hexToBin(hex)); |   return btoa(Enc.hexToBin(hex)); | ||||||
|  | |||||||
							
								
								
									
										78
									
								
								lib/csr.js
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								lib/csr.js
									
									
									
									
									
								
							| @ -1,14 +1,18 @@ | |||||||
|  | // Copyright 2018-present AJ ONeal. All rights reserved
 | ||||||
|  | /* This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||||
|  | (function (exports) { | ||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var crypto = require('crypto'); | var ASN1 = exports.ASN1; | ||||||
| var ASN1 = require('./asn1.js'); | var Enc = exports.Enc; | ||||||
| var Enc = require('./encoding.js'); | var PEM = exports.PEM; | ||||||
| var PEM = require('./pem.js'); | var X509 = exports.x509; | ||||||
| var X509 = require('./x509.js'); | var Keypairs = exports.Keypairs; | ||||||
| var RSA = {}; |  | ||||||
| 
 | 
 | ||||||
| /*global Promise*/ | // TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
 | ||||||
| var CSR = module.exports = function rsacsr(opts) { | var CSR = exports.CSR = function (opts) { | ||||||
|   // We're using a Promise here to be compatible with the browser version
 |   // We're using a Promise here to be compatible with the browser version
 | ||||||
|   // which will probably use the webcrypto API for some of the conversions
 |   // which will probably use the webcrypto API for some of the conversions
 | ||||||
|   opts = CSR._prepare(opts); |   opts = CSR._prepare(opts); | ||||||
| @ -69,11 +73,7 @@ CSR._prepare = function (opts) { | |||||||
|   opts.jwk = jwk; |   opts.jwk = jwk; | ||||||
|   return opts; |   return opts; | ||||||
| }; | }; | ||||||
| CSR.sync = function (opts) { | 
 | ||||||
|   opts = CSR._prepare(opts); |  | ||||||
|   var bytes = CSR.createSync(opts); |  | ||||||
|   return CSR._encode(opts, bytes); |  | ||||||
| }; |  | ||||||
| CSR._encode = function (opts, bytes) { | CSR._encode = function (opts, bytes) { | ||||||
|   if ('der' === (opts.encoding||'').toLowerCase()) { |   if ('der' === (opts.encoding||'').toLowerCase()) { | ||||||
|     return bytes; |     return bytes; | ||||||
| @ -84,11 +84,6 @@ CSR._encode = function (opts, bytes) { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| CSR.createSync = function createCsr(opts) { |  | ||||||
|   var hex = CSR.request(opts.jwk, opts.domains); |  | ||||||
|   var csr = CSR.signSync(opts.jwk, hex); |  | ||||||
|   return Enc.hexToBuf(csr); |  | ||||||
| }; |  | ||||||
| CSR.create = function createCsr(opts) { | CSR.create = function createCsr(opts) { | ||||||
|   var hex = CSR.request(opts.jwk, opts.domains); |   var hex = CSR.request(opts.jwk, opts.domains); | ||||||
|   return CSR.sign(opts.jwk, hex).then(function (csr) { |   return CSR.sign(opts.jwk, hex).then(function (csr) { | ||||||
| @ -96,22 +91,25 @@ CSR.create = function createCsr(opts) { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | //
 | ||||||
|  | // RSA
 | ||||||
|  | //
 | ||||||
|  | //
 | ||||||
| CSR.request = function createCsrBodyEc(jwk, domains) { | CSR.request = function createCsrBodyEc(jwk, domains) { | ||||||
|   var asn1pub = X509.packCsrPublicKey(jwk); |   var asn1pub = X509.packCsrRsaPublicKey(jwk); | ||||||
|   return X509.packCsr(asn1pub, domains); |   return X509.packCsrRsa(asn1pub, domains); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| CSR.signSync = function csrEcSig(jwk, request) { |  | ||||||
|   var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); |  | ||||||
|   var sig = RSA.signSync(keypem, Enc.hexToBuf(request)); |  | ||||||
|   return CSR.toDer({ request: request, signature: sig }); |  | ||||||
| }; |  | ||||||
| CSR.sign = function csrEcSig(jwk, request) { | CSR.sign = function csrEcSig(jwk, request) { | ||||||
|   var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); |   // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
 | ||||||
|   return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) { |   // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
 | ||||||
|  |   // TODO have a consistent non-private way to sign
 | ||||||
|  |   return Keypairs._sign({ jwk: jwk }, Enc.hexToBuf(request)).then(function (sig) { | ||||||
|     return CSR.toDer({ request: request, signature: sig }); |     return CSR.toDer({ request: request, signature: sig }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| CSR.toDer = function encode(opts) { | CSR.toDer = function encode(opts) { | ||||||
|   var sty = ASN1('30' |   var sty = ASN1('30' | ||||||
|     // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
 |     // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
 | ||||||
| @ -128,30 +126,6 @@ CSR.toDer = function encode(opts) { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| //
 |  | ||||||
| // RSA
 |  | ||||||
| //
 |  | ||||||
| 
 |  | ||||||
| // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
 |  | ||||||
| RSA.signSync = function signRsaSync(keypem, ab) { |  | ||||||
|   // Signer is a stream
 |  | ||||||
|   var sign = crypto.createSign('SHA256'); |  | ||||||
|   sign.write(new Uint8Array(ab)); |  | ||||||
|   sign.end(); |  | ||||||
| 
 |  | ||||||
|   // The signature is ASN1 encoded, as it turns out
 |  | ||||||
|   var sig = sign.sign(keypem); |  | ||||||
| 
 |  | ||||||
|   // Convert to a JavaScript ArrayBuffer just because
 |  | ||||||
|   return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); |  | ||||||
| }; |  | ||||||
| RSA.sign = function signRsa(keypem, ab) { |  | ||||||
|   return Promise.resolve().then(function () { |  | ||||||
|     return RSA.signSync(keypem, ab); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| X509.packCsrRsa = function (asn1pubkey, domains) { | X509.packCsrRsa = function (asn1pubkey, domains) { | ||||||
|   return ASN1('30' |   return ASN1('30' | ||||||
|     // Version (0)
 |     // Version (0)
 | ||||||
| @ -211,3 +185,5 @@ X509.packCsrRsaPublicKey = function (jwk) { | |||||||
|   // Add the CSR pub key header
 |   // Add the CSR pub key header
 | ||||||
|   return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); |   return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | }('undefined' === typeof window ? module.exports : window)); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user