making headway
This commit is contained in:
		
							parent
							
								
									bfc4ab6795
								
							
						
					
					
						commit
						692301e37d
					
				
							
								
								
									
										2
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								app.js
									
									
									
									
									
								
							| @ -41,7 +41,7 @@ function run() { | |||||||
|     , namedCurve: $('input[name="ec-crv"]:checked').value |     , namedCurve: $('input[name="ec-crv"]:checked').value | ||||||
|     , modulusLength: $('input[name="rsa-len"]:checked').value |     , modulusLength: $('input[name="rsa-len"]:checked').value | ||||||
|     }; |     }; | ||||||
|     console.log(opts); |     console.log('opts', opts); | ||||||
|     Keypairs.generate(opts).then(function (results) { |     Keypairs.generate(opts).then(function (results) { | ||||||
|       $('.js-jwk').innerText = JSON.stringify(results, null, 2); |       $('.js-jwk').innerText = JSON.stringify(results, null, 2); | ||||||
|       //
 |       //
 | ||||||
|  | |||||||
| @ -48,6 +48,8 @@ | |||||||
|     <div class="js-loading" hidden>Loading</div> |     <div class="js-loading" hidden>Loading</div> | ||||||
|     <pre><code class="js-jwk"> </code></pre> |     <pre><code class="js-jwk"> </code></pre> | ||||||
| 
 | 
 | ||||||
|  |     <script src="./lib/ecdsa.js"></script> | ||||||
|  |     <script src="./lib/rsa.js"></script> | ||||||
|     <script src="./lib/keypairs.js"></script> |     <script src="./lib/keypairs.js"></script> | ||||||
|     <script src="./app.js"></script> |     <script src="./app.js"></script> | ||||||
|   </body> |   </body> | ||||||
|  | |||||||
							
								
								
									
										699
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										699
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,699 @@ | |||||||
|  | /*global CSR*/ | ||||||
|  | // CSR takes a while to load after the page load
 | ||||||
|  | (function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var BACME = exports.ACME = {}; | ||||||
|  | var webFetch = exports.fetch; | ||||||
|  | var Keypairs = exports.Keypairs; | ||||||
|  | var Promise = exports.Promise; | ||||||
|  | 
 | ||||||
|  | var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||||
|  | var directory; | ||||||
|  | 
 | ||||||
|  | var nonceUrl; | ||||||
|  | var nonce; | ||||||
|  | 
 | ||||||
|  | var accountKeypair; | ||||||
|  | var accountJwk; | ||||||
|  | 
 | ||||||
|  | var accountUrl; | ||||||
|  | 
 | ||||||
|  | BACME.challengePrefixes = { | ||||||
|  |   'http-01': '/.well-known/acme-challenge' | ||||||
|  | , 'dns-01': '_acme-challenge' | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME._logHeaders = function (resp) { | ||||||
|  |   console.log('Headers:'); | ||||||
|  |   Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME._logBody = function (body) { | ||||||
|  |   console.log('Body:'); | ||||||
|  |   console.log(JSON.stringify(body, null, 2)); | ||||||
|  |   console.log(''); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.directory = function (opts) { | ||||||
|  |   return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  |     return resp.json().then(function (reply) { | ||||||
|  |       if (/error/.test(reply.type)) { | ||||||
|  |         return Promise.reject(new Error(reply.detail || reply.type)); | ||||||
|  |       } | ||||||
|  |       directory = reply; | ||||||
|  |       nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; | ||||||
|  |       accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; | ||||||
|  |       orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; | ||||||
|  |       BACME._logBody(reply); | ||||||
|  |       return reply; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.nonce = function () { | ||||||
|  |   return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  |     nonce = resp.headers.get('replay-nonce'); | ||||||
|  |     console.log('Nonce:', nonce); | ||||||
|  |     // resp.body is empty
 | ||||||
|  |     return resp.headers.get('replay-nonce'); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.accounts = {}; | ||||||
|  | 
 | ||||||
|  | // type = ECDSA
 | ||||||
|  | // bitlength = 256
 | ||||||
|  | BACME.accounts.generateKeypair = function (opts) { | ||||||
|  |   return BACME.generateKeypair(opts).then(function (result) { | ||||||
|  |     accountKeypair = result; | ||||||
|  | 
 | ||||||
|  |     return webCrypto.subtle.exportKey( | ||||||
|  |       "jwk" | ||||||
|  |     , result.privateKey | ||||||
|  |     ).then(function (privJwk) { | ||||||
|  | 
 | ||||||
|  |       accountJwk = privJwk; | ||||||
|  |       console.log('private jwk:'); | ||||||
|  |       console.log(JSON.stringify(privJwk, null, 2)); | ||||||
|  | 
 | ||||||
|  |       return privJwk; | ||||||
|  |       /* | ||||||
|  |       return webCrypto.subtle.exportKey( | ||||||
|  |         "pkcs8" | ||||||
|  |       , result.privateKey | ||||||
|  |       ).then(function (keydata) { | ||||||
|  |         console.log('pkcs8:'); | ||||||
|  |         console.log(Array.from(new Uint8Array(keydata))); | ||||||
|  | 
 | ||||||
|  |         return privJwk; | ||||||
|  |         //return accountKeypair;
 | ||||||
|  |       }); | ||||||
|  |       */ | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // json to url-safe base64
 | ||||||
|  | BACME._jsto64 = function (json) { | ||||||
|  |   return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var textEncoder = new TextEncoder(); | ||||||
|  | 
 | ||||||
|  | BACME._importKey = function (jwk) { | ||||||
|  |   var alg; // I think the 256 refers to the hash
 | ||||||
|  |   var wcOpts = {}; | ||||||
|  |   var extractable = true; // TODO make optionally false?
 | ||||||
|  |   var priv = jwk; | ||||||
|  |   var pub; | ||||||
|  | 
 | ||||||
|  |   // ECDSA
 | ||||||
|  |   if (/^EC/i.test(jwk.kty)) { | ||||||
|  |     wcOpts.name = 'ECDSA'; | ||||||
|  |     wcOpts.namedCurve = jwk.crv; | ||||||
|  |     alg = 'ES256'; | ||||||
|  |     pub = { | ||||||
|  |       crv: priv.crv | ||||||
|  |     , kty: priv.kty | ||||||
|  |     , x: priv.x | ||||||
|  |     , y: priv.y | ||||||
|  |     }; | ||||||
|  |     if (!priv.d) { | ||||||
|  |       priv = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // RSA
 | ||||||
|  |   if (/^RS/i.test(jwk.kty)) { | ||||||
|  |     wcOpts.name = 'RSASSA-PKCS1-v1_5'; | ||||||
|  |     wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |     alg = 'RS256'; | ||||||
|  |     pub = { | ||||||
|  |       e: priv.e | ||||||
|  |     , kty: priv.kty | ||||||
|  |     , n: priv.n | ||||||
|  |     }; | ||||||
|  |     if (!priv.p) { | ||||||
|  |       priv = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return window.crypto.subtle.importKey( | ||||||
|  |     "jwk" | ||||||
|  |   , pub | ||||||
|  |   , wcOpts | ||||||
|  |   , extractable | ||||||
|  |   , [ "verify" ] | ||||||
|  |   ).then(function (publicKey) { | ||||||
|  |     function give(privateKey) { | ||||||
|  |       return { | ||||||
|  |         wcPub: publicKey | ||||||
|  |       , wcKey: privateKey | ||||||
|  |       , wcKeypair: { publicKey: publicKey, privateKey: privateKey } | ||||||
|  |       , meta: { | ||||||
|  |           alg: alg | ||||||
|  |         , name: wcOpts.name | ||||||
|  |         , hash: wcOpts.hash | ||||||
|  |         } | ||||||
|  |       , jwk: jwk | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (!priv) { | ||||||
|  |       return give(); | ||||||
|  |     } | ||||||
|  |     return window.crypto.subtle.importKey( | ||||||
|  |       "jwk" | ||||||
|  |     , priv | ||||||
|  |     , wcOpts | ||||||
|  |     , extractable | ||||||
|  |     , [ "sign"/*, "verify"*/ ] | ||||||
|  |     ).then(give); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | BACME._sign = function (opts) { | ||||||
|  |   var wcPrivKey = opts.abstractKey.wcKeypair.privateKey; | ||||||
|  |   var wcOpts = opts.abstractKey.meta; | ||||||
|  |   var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash
 | ||||||
|  |   var signHash; | ||||||
|  | 
 | ||||||
|  |   console.log('kty', opts.abstractKey.jwk.kty); | ||||||
|  |   signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') }; | ||||||
|  | 
 | ||||||
|  |   var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64); | ||||||
|  |   console.log('msg:', msg); | ||||||
|  |   return window.crypto.subtle.sign( | ||||||
|  |     { name: wcOpts.name, hash: signHash } | ||||||
|  |   , wcPrivKey | ||||||
|  |   , msg | ||||||
|  |   ).then(function (signature) { | ||||||
|  |     //console.log('sig1:', signature);
 | ||||||
|  |     //console.log('sig2:', new Uint8Array(signature));
 | ||||||
|  |     //console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature)));
 | ||||||
|  |     // convert buffer to urlsafe base64
 | ||||||
|  |     var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { | ||||||
|  |       return String.fromCharCode(ch); | ||||||
|  |     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||||
|  | 
 | ||||||
|  |     console.log('[1] URL-safe Base64 Signature:'); | ||||||
|  |     console.log(sig64); | ||||||
|  | 
 | ||||||
|  |     var signedMsg = { | ||||||
|  |       protected: opts.protected64 | ||||||
|  |     , payload: opts.payload64 | ||||||
|  |     , signature: sig64 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     console.log('Signed Base64 Msg:'); | ||||||
|  |     console.log(JSON.stringify(signedMsg, null, 2)); | ||||||
|  | 
 | ||||||
|  |     return signedMsg; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | // email = john.doe@gmail.com
 | ||||||
|  | // jwk = { ... }
 | ||||||
|  | // agree = true
 | ||||||
|  | BACME.accounts.sign = function (opts) { | ||||||
|  | 
 | ||||||
|  |   return BACME._importKey(opts.jwk).then(function (abstractKey) { | ||||||
|  | 
 | ||||||
|  |     var payloadJson = | ||||||
|  |       { termsOfServiceAgreed: opts.agree | ||||||
|  |       , onlyReturnExisting: false | ||||||
|  |       , contact: opts.contacts || [ 'mailto:' + opts.email ] | ||||||
|  |       }; | ||||||
|  |     console.log('payload:'); | ||||||
|  |     console.log(payloadJson); | ||||||
|  |     var payload64 = BACME._jsto64( | ||||||
|  |       payloadJson | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     var protectedJson = | ||||||
|  |       { nonce: opts.nonce | ||||||
|  |       , url: accountUrl | ||||||
|  |       , alg: abstractKey.meta.alg | ||||||
|  |       , jwk: null | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |     if (/EC/i.test(opts.jwk.kty)) { | ||||||
|  |       protectedJson.jwk = { | ||||||
|  |         crv: opts.jwk.crv | ||||||
|  |       , kty: opts.jwk.kty | ||||||
|  |       , x: opts.jwk.x | ||||||
|  |       , y: opts.jwk.y | ||||||
|  |       }; | ||||||
|  |     } else if (/RS/i.test(opts.jwk.kty)) { | ||||||
|  |       protectedJson.jwk = { | ||||||
|  |         e: opts.jwk.e | ||||||
|  |       , kty: opts.jwk.kty | ||||||
|  |       , n: opts.jwk.n | ||||||
|  |       }; | ||||||
|  |     } else { | ||||||
|  |       return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log('protected:'); | ||||||
|  |     console.log(protectedJson); | ||||||
|  |     var protected64 = BACME._jsto64( | ||||||
|  |       protectedJson | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // Note: this function hashes before signing so send data, not the hash
 | ||||||
|  |     return BACME._sign({ | ||||||
|  |       abstractKey: abstractKey | ||||||
|  |     , payload64: payload64 | ||||||
|  |     , protected64: protected64 | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var accountId; | ||||||
|  | 
 | ||||||
|  | BACME.accounts.set = function (opts) { | ||||||
|  |   nonce = null; | ||||||
|  |   return window.fetch(accountUrl, { | ||||||
|  |     mode: 'cors' | ||||||
|  |   , method: 'POST' | ||||||
|  |   , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |   , body: JSON.stringify(opts.signedAccount) | ||||||
|  |   }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  |     nonce = resp.headers.get('replay-nonce'); | ||||||
|  |     accountId = resp.headers.get('location'); | ||||||
|  |     console.log('Next nonce:', nonce); | ||||||
|  |     console.log('Location/kid:', accountId); | ||||||
|  | 
 | ||||||
|  |     if (!resp.headers.get('content-type')) { | ||||||
|  |      console.log('Body: <none>'); | ||||||
|  | 
 | ||||||
|  |      return { kid: accountId }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return resp.json().then(function (result) { | ||||||
|  |       if (/^Error/i.test(result.detail)) { | ||||||
|  |         return Promise.reject(new Error(result.detail)); | ||||||
|  |       } | ||||||
|  |       result.kid = accountId; | ||||||
|  |       BACME._logBody(result); | ||||||
|  | 
 | ||||||
|  |       return result; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var orderUrl; | ||||||
|  | 
 | ||||||
|  | BACME.orders = {}; | ||||||
|  | 
 | ||||||
|  | // identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
 | ||||||
|  | // signedAccount
 | ||||||
|  | BACME.orders.sign = function (opts) { | ||||||
|  |   var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); | ||||||
|  | 
 | ||||||
|  |   return BACME._importKey(opts.jwk).then(function (abstractKey) { | ||||||
|  |     var protected64 = BACME._jsto64( | ||||||
|  |       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } | ||||||
|  |     ); | ||||||
|  |     console.log('abstractKey:'); | ||||||
|  |     console.log(abstractKey); | ||||||
|  |     return BACME._sign({ | ||||||
|  |       abstractKey: abstractKey | ||||||
|  |     , payload64: payload64 | ||||||
|  |     , protected64: protected64 | ||||||
|  |     }).then(function (sig) { | ||||||
|  |       if (!sig) { | ||||||
|  |         throw new Error('sig is undefined... nonsense!'); | ||||||
|  |       } | ||||||
|  |       console.log('newsig', sig); | ||||||
|  |       return sig; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var currentOrderUrl; | ||||||
|  | var authorizationUrls; | ||||||
|  | var finalizeUrl; | ||||||
|  | 
 | ||||||
|  | BACME.orders.create = function (opts) { | ||||||
|  |   nonce = null; | ||||||
|  |   return window.fetch(orderUrl, { | ||||||
|  |     mode: 'cors' | ||||||
|  |   , method: 'POST' | ||||||
|  |   , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |   , body: JSON.stringify(opts.signedOrder) | ||||||
|  |   }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  |     currentOrderUrl = resp.headers.get('location'); | ||||||
|  |     nonce = resp.headers.get('replay-nonce'); | ||||||
|  |     console.log('Next nonce:', nonce); | ||||||
|  | 
 | ||||||
|  |     return resp.json().then(function (result) { | ||||||
|  |       if (/^Error/i.test(result.detail)) { | ||||||
|  |         return Promise.reject(new Error(result.detail)); | ||||||
|  |       } | ||||||
|  |       authorizationUrls = result.authorizations; | ||||||
|  |       finalizeUrl = result.finalize; | ||||||
|  |       BACME._logBody(result); | ||||||
|  | 
 | ||||||
|  |       result.url = currentOrderUrl; | ||||||
|  |       return result; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.challenges = {}; | ||||||
|  | BACME.challenges.all = function () { | ||||||
|  |   var challenges = []; | ||||||
|  | 
 | ||||||
|  |   function next() { | ||||||
|  |     if (!authorizationUrls.length) { | ||||||
|  |       return challenges; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return BACME.challenges.view().then(function (challenge) { | ||||||
|  |       challenges.push(challenge); | ||||||
|  |       return next(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return next(); | ||||||
|  | }; | ||||||
|  | BACME.challenges.view = function () { | ||||||
|  |   var authzUrl = authorizationUrls.pop(); | ||||||
|  |   var token; | ||||||
|  |   var challengeDomain; | ||||||
|  |   var challengeUrl; | ||||||
|  | 
 | ||||||
|  |   return window.fetch(authzUrl, { | ||||||
|  |     mode: 'cors' | ||||||
|  |   }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  | 
 | ||||||
|  |     return resp.json().then(function (result) { | ||||||
|  |       // Note: select the challenge you wish to use
 | ||||||
|  |       var challenge = result.challenges.slice(0).pop(); | ||||||
|  |       token = challenge.token; | ||||||
|  |       challengeUrl = challenge.url; | ||||||
|  |       challengeDomain = result.identifier.value; | ||||||
|  | 
 | ||||||
|  |       BACME._logBody(result); | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         challenges: result.challenges | ||||||
|  |       , expires: result.expires | ||||||
|  |       , identifier: result.identifier | ||||||
|  |       , status: result.status | ||||||
|  |       , wildcard: result.wildcard | ||||||
|  |       //, token: challenge.token
 | ||||||
|  |       //, url: challenge.url
 | ||||||
|  |       //, domain: result.identifier.value,
 | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var thumbprint; | ||||||
|  | var keyAuth; | ||||||
|  | var httpPath; | ||||||
|  | var dnsAuth; | ||||||
|  | var dnsRecord; | ||||||
|  | 
 | ||||||
|  | BACME.thumbprint = function (opts) { | ||||||
|  |   // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | ||||||
|  | 
 | ||||||
|  |   var accountJwk = opts.jwk; | ||||||
|  |   var keys; | ||||||
|  | 
 | ||||||
|  |   if (/^EC/i.test(opts.jwk.kty)) { | ||||||
|  |     keys = [ 'crv', 'kty', 'x', 'y' ]; | ||||||
|  |   } else if (/^RS/i.test(opts.jwk.kty)) { | ||||||
|  |     keys = [ 'e', 'kty', 'n' ]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var accountPublicStr = '{' + keys.map(function (key) { | ||||||
|  |     return '"' + key + '":"' + accountJwk[key] + '"'; | ||||||
|  |   }).join(',') + '}'; | ||||||
|  | 
 | ||||||
|  |   return window.crypto.subtle.digest( | ||||||
|  |     { name: "SHA-256" } // SHA-256 is spec'd, non-optional
 | ||||||
|  |   , textEncoder.encode(accountPublicStr) | ||||||
|  |   ).then(function (hash) { | ||||||
|  |     thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { | ||||||
|  |       return String.fromCharCode(ch); | ||||||
|  |     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||||
|  | 
 | ||||||
|  |     console.log('Thumbprint:'); | ||||||
|  |     console.log(opts); | ||||||
|  |     console.log(accountPublicStr); | ||||||
|  |     console.log(thumbprint); | ||||||
|  | 
 | ||||||
|  |     return thumbprint; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // { token, thumbprint, challengeDomain }
 | ||||||
|  | BACME.challenges['http-01'] = function (opts) { | ||||||
|  |   // The contents of the key authorization file
 | ||||||
|  |   keyAuth = opts.token + '.' + opts.thumbprint; | ||||||
|  | 
 | ||||||
|  |   // Where the key authorization file goes
 | ||||||
|  |   httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; | ||||||
|  | 
 | ||||||
|  |   console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     path: httpPath | ||||||
|  |   , value: keyAuth | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // { keyAuth }
 | ||||||
|  | BACME.challenges['dns-01'] = function (opts) { | ||||||
|  |   console.log('opts.keyAuth for DNS:'); | ||||||
|  |   console.log(opts.keyAuth); | ||||||
|  |   return window.crypto.subtle.digest( | ||||||
|  |     { name: "SHA-256", } | ||||||
|  |   , textEncoder.encode(opts.keyAuth) | ||||||
|  |   ).then(function (hash) { | ||||||
|  |     dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { | ||||||
|  |       return String.fromCharCode(ch); | ||||||
|  |     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||||
|  | 
 | ||||||
|  |     dnsRecord = '_acme-challenge.' + opts.challengeDomain; | ||||||
|  | 
 | ||||||
|  |     console.log('DNS TXT Auth:'); | ||||||
|  |     // The name of the record
 | ||||||
|  |     console.log(dnsRecord); | ||||||
|  |     // The TXT record value
 | ||||||
|  |     console.log(dnsAuth); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       type: 'TXT' | ||||||
|  |     , host: dnsRecord | ||||||
|  |     , answer: dnsAuth | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var challengePollUrl; | ||||||
|  | 
 | ||||||
|  | // { jwk, challengeUrl, accountId (kid) }
 | ||||||
|  | BACME.challenges.accept = function (opts) { | ||||||
|  |   var payload64 = BACME._jsto64({}); | ||||||
|  | 
 | ||||||
|  |   return BACME._importKey(opts.jwk).then(function (abstractKey) { | ||||||
|  |     var protected64 = BACME._jsto64( | ||||||
|  |       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } | ||||||
|  |     ); | ||||||
|  |     return BACME._sign({ | ||||||
|  |       abstractKey: abstractKey | ||||||
|  |     , payload64: payload64 | ||||||
|  |     , protected64: protected64 | ||||||
|  |     }); | ||||||
|  |   }).then(function (signedAccept) { | ||||||
|  | 
 | ||||||
|  |     nonce = null; | ||||||
|  |     return window.fetch( | ||||||
|  |       opts.challengeUrl | ||||||
|  |     , { mode: 'cors' | ||||||
|  |       , method: 'POST' | ||||||
|  |       , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |       , body: JSON.stringify(signedAccept) | ||||||
|  |       } | ||||||
|  |     ).then(function (resp) { | ||||||
|  |       BACME._logHeaders(resp); | ||||||
|  |       nonce = resp.headers.get('replay-nonce'); | ||||||
|  |       console.log("ACCEPT NONCE:", nonce); | ||||||
|  | 
 | ||||||
|  |       return resp.json().then(function (reply) { | ||||||
|  |         challengePollUrl = reply.url; | ||||||
|  | 
 | ||||||
|  |         console.log('Challenge ACK:'); | ||||||
|  |         console.log(JSON.stringify(reply)); | ||||||
|  |         return reply; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.challenges.check = function (opts) { | ||||||
|  |   return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  | 
 | ||||||
|  |     return resp.json().then(function (reply) { | ||||||
|  |       if (/error/.test(reply.type)) { | ||||||
|  |         return Promise.reject(new Error(reply.detail || reply.type)); | ||||||
|  |       } | ||||||
|  |       challengePollUrl = reply.url; | ||||||
|  | 
 | ||||||
|  |       BACME._logBody(reply); | ||||||
|  | 
 | ||||||
|  |       return reply; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var domainKeypair; | ||||||
|  | var domainJwk; | ||||||
|  | 
 | ||||||
|  | BACME.generateKeypair = function (opts) { | ||||||
|  |   var wcOpts = {}; | ||||||
|  | 
 | ||||||
|  |   // ECDSA has only the P curves and an associated bitlength
 | ||||||
|  |   if (/^EC/i.test(opts.type)) { | ||||||
|  |     wcOpts.name = 'ECDSA'; | ||||||
|  |     if (/256/.test(opts.bitlength)) { | ||||||
|  |       wcOpts.namedCurve = 'P-256'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // RSA-PSS is another option, but I don't think it's used for Let's Encrypt
 | ||||||
|  |   // I think the hash is only necessary for signing, not generation or import
 | ||||||
|  |   if (/^RS/i.test(opts.type)) { | ||||||
|  |     wcOpts.name = 'RSASSA-PKCS1-v1_5'; | ||||||
|  |     wcOpts.modulusLength = opts.bitlength; | ||||||
|  |     if (opts.bitlength < 2048) { | ||||||
|  |       wcOpts.modulusLength = opts.bitlength * 8; | ||||||
|  |     } | ||||||
|  |     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); | ||||||
|  |     wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |   } | ||||||
|  |   var extractable = true; | ||||||
|  |   return window.crypto.subtle.generateKey( | ||||||
|  |     wcOpts | ||||||
|  |   , extractable | ||||||
|  |   , [ 'sign', 'verify' ] | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | BACME.domains = {}; | ||||||
|  | // TODO factor out from BACME.accounts.generateKeypair even more
 | ||||||
|  | BACME.domains.generateKeypair = function (opts) { | ||||||
|  |   return BACME.generateKeypair(opts).then(function (result) { | ||||||
|  |     domainKeypair = result; | ||||||
|  | 
 | ||||||
|  |     return window.crypto.subtle.exportKey( | ||||||
|  |       "jwk" | ||||||
|  |     , result.privateKey | ||||||
|  |     ).then(function (privJwk) { | ||||||
|  | 
 | ||||||
|  |       domainJwk = privJwk; | ||||||
|  |       console.log('private jwk:'); | ||||||
|  |       console.log(JSON.stringify(privJwk, null, 2)); | ||||||
|  | 
 | ||||||
|  |       return privJwk; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // { serverJwk, domains }
 | ||||||
|  | BACME.orders.generateCsr = function (opts) { | ||||||
|  |   return BACME._importKey(opts.serverJwk).then(function (abstractKey) { | ||||||
|  |     return Promise.resolve(CSR.generate({ keypair: abstractKey.wcKeypair, domains: opts.domains })); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | var certificateUrl; | ||||||
|  | 
 | ||||||
|  | // { csr, jwk, finalizeUrl, accountId }
 | ||||||
|  | BACME.orders.finalize = function (opts) { | ||||||
|  |   var payload64 = BACME._jsto64( | ||||||
|  |     { csr: opts.csr } | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return BACME._importKey(opts.jwk).then(function (abstractKey) { | ||||||
|  |     var protected64 = BACME._jsto64( | ||||||
|  |       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } | ||||||
|  |     ); | ||||||
|  |     return BACME._sign({ | ||||||
|  |       abstractKey: abstractKey | ||||||
|  |     , payload64: payload64 | ||||||
|  |     , protected64: protected64 | ||||||
|  |     }); | ||||||
|  |   }).then(function (signedFinal) { | ||||||
|  | 
 | ||||||
|  |     nonce = null; | ||||||
|  |     return window.fetch( | ||||||
|  |       opts.finalizeUrl | ||||||
|  |     , { mode: 'cors' | ||||||
|  |       , method: 'POST' | ||||||
|  |       , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |       , body: JSON.stringify(signedFinal) | ||||||
|  |       } | ||||||
|  |     ).then(function (resp) { | ||||||
|  |       BACME._logHeaders(resp); | ||||||
|  |       nonce = resp.headers.get('replay-nonce'); | ||||||
|  | 
 | ||||||
|  |       return resp.json().then(function (reply) { | ||||||
|  |         if (/error/.test(reply.type)) { | ||||||
|  |           return Promise.reject(new Error(reply.detail || reply.type)); | ||||||
|  |         } | ||||||
|  |         certificateUrl = reply.certificate; | ||||||
|  |         BACME._logBody(reply); | ||||||
|  | 
 | ||||||
|  |         return reply; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.orders.receive = function (opts) { | ||||||
|  |   return window.fetch( | ||||||
|  |     opts.certificateUrl | ||||||
|  |   , { mode: 'cors' | ||||||
|  |     , method: 'GET' | ||||||
|  |     } | ||||||
|  |   ).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  |     nonce = resp.headers.get('replay-nonce'); | ||||||
|  | 
 | ||||||
|  |     return resp.text().then(function (reply) { | ||||||
|  |       BACME._logBody(reply); | ||||||
|  | 
 | ||||||
|  |       return reply; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | BACME.orders.check = function (opts) { | ||||||
|  |   return window.fetch( | ||||||
|  |     opts.orderUrl | ||||||
|  |   , { mode: 'cors' | ||||||
|  |     , method: 'GET' | ||||||
|  |     } | ||||||
|  |   ).then(function (resp) { | ||||||
|  |     BACME._logHeaders(resp); | ||||||
|  | 
 | ||||||
|  |     return resp.json().then(function (reply) { | ||||||
|  |       if (/error/.test(reply.type)) { | ||||||
|  |         return Promise.reject(new Error(reply.detail || reply.type)); | ||||||
|  |       } | ||||||
|  |       BACME._logBody(reply); | ||||||
|  | 
 | ||||||
|  |       return reply; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }(window)); | ||||||
							
								
								
									
										112
									
								
								lib/ecdsa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								lib/ecdsa.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | |||||||
|  | /*global Promise*/ | ||||||
|  | (function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var EC = exports.Eckles = {}; | ||||||
|  | if ('undefined' !== typeof module) { module.exports = EC; } | ||||||
|  | var Enc = {}; | ||||||
|  | var textEncoder = new TextEncoder(); | ||||||
|  | 
 | ||||||
|  | EC._stance = "We take the stance that if you're knowledgeable enough to" | ||||||
|  |   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; | ||||||
|  | EC._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; | ||||||
|  | EC.generate = function (opts) { | ||||||
|  |   var wcOpts = {}; | ||||||
|  |   if (!opts) { opts = {}; } | ||||||
|  |   if (!opts.kty) { opts.kty = 'EC'; } | ||||||
|  | 
 | ||||||
|  |   // ECDSA has only the P curves and an associated bitlength
 | ||||||
|  |   wcOpts.name = 'ECDSA'; | ||||||
|  |   if (!opts.namedCurve) { | ||||||
|  |     opts.namedCurve = 'P-256'; | ||||||
|  |   } | ||||||
|  |   wcOpts.namedCurve = opts.namedCurve; // true for supported curves
 | ||||||
|  |   if (/256/.test(wcOpts.namedCurve)) { | ||||||
|  |     wcOpts.namedCurve = 'P-256'; | ||||||
|  |     wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |   } else if (/384/.test(wcOpts.namedCurve)) { | ||||||
|  |     wcOpts.namedCurve = 'P-384'; | ||||||
|  |     wcOpts.hash = { name: "SHA-384" }; | ||||||
|  |   } else { | ||||||
|  |     return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " | ||||||
|  |       + " Please choose either 'P-256' or 'P-384'. " | ||||||
|  |       + EC._stance)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var extractable = true; | ||||||
|  |   return window.crypto.subtle.generateKey( | ||||||
|  |     wcOpts | ||||||
|  |   , extractable | ||||||
|  |   , [ 'sign', 'verify' ] | ||||||
|  |   ).then(function (result) { | ||||||
|  |     return window.crypto.subtle.exportKey( | ||||||
|  |       "jwk" | ||||||
|  |     , result.privateKey | ||||||
|  |     ).then(function (privJwk) { | ||||||
|  |       return { | ||||||
|  |         private: privJwk | ||||||
|  |       , public: EC.neuter({ jwk: privJwk }) | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Chopping off the private parts is now part of the public API.
 | ||||||
|  | // I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | ||||||
|  | EC.neuter = function (opts) { | ||||||
|  |   // trying to find the best balance of an immutable copy with custom attributes
 | ||||||
|  |   var jwk = {}; | ||||||
|  |   Object.keys(opts.jwk).forEach(function (k) { | ||||||
|  |     if ('undefined' === typeof opts.jwk[k]) { return; } | ||||||
|  |     // ignore EC private parts
 | ||||||
|  |     if ('d' === k) { return; } | ||||||
|  |     jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); | ||||||
|  |   }); | ||||||
|  |   return jwk; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | ||||||
|  | EC.__thumbprint = function (jwk) { | ||||||
|  |   // Use the same entropy for SHA as for key
 | ||||||
|  |   var alg = 'SHA-256'; | ||||||
|  |   if (/384/.test(jwk.crv)) { | ||||||
|  |     alg = 'SHA-384'; | ||||||
|  |   } | ||||||
|  |   return window.crypto.subtle.digest( | ||||||
|  |     { name: alg } | ||||||
|  |   , textEncoder.encode('{"crv":"' + jwk.crv + '","kty":"EC","x":"' + jwk.x + '","y":"' + jwk.y + '"}') | ||||||
|  |   ).then(function (hash) { | ||||||
|  |     return Enc.bufToUrlBase64(new Uint8Array(hash)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | EC.thumbprint = function (opts) { | ||||||
|  |   return Promise.resolve().then(function () { | ||||||
|  |     var jwk; | ||||||
|  |     if ('EC' === opts.kty) { | ||||||
|  |       jwk = opts; | ||||||
|  |     } else if (opts.jwk) { | ||||||
|  |       jwk = opts.jwk; | ||||||
|  |     } else { | ||||||
|  |       return EC.import(opts).then(function (jwk) { | ||||||
|  |         return EC.__thumbprint(jwk); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return EC.__thumbprint(jwk); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Enc.bufToUrlBase64 = function (u8) { | ||||||
|  |   return Enc.bufToBase64(u8) | ||||||
|  |     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Enc.bufToBase64 = function (u8) { | ||||||
|  |   var bin = ''; | ||||||
|  |   u8.forEach(function (i) { | ||||||
|  |     bin += String.fromCharCode(i); | ||||||
|  |   }); | ||||||
|  |   return btoa(bin); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }('undefined' !== typeof module ? module.exports : window)); | ||||||
							
								
								
									
										155
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								lib/keypairs.js
									
									
									
									
									
								
							| @ -3,84 +3,107 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var Keypairs = exports.Keypairs = {}; | var Keypairs = exports.Keypairs = {}; | ||||||
|  | var Rasha = exports.Rasha || require('rasha'); | ||||||
|  | var Eckles = exports.Eckles || require('eckles'); | ||||||
| 
 | 
 | ||||||
| Keypairs._stance = "We take the stance that if you're knowledgeable enough to" | Keypairs._stance = "We take the stance that if you're knowledgeable enough to" | ||||||
|   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; |   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; | ||||||
| Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; | Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; | ||||||
| Keypairs.generate = function (opts) { | Keypairs.generate = function (opts) { | ||||||
|   var wcOpts = {}; |   opts = opts || {}; | ||||||
|   if (!opts) { |   var p; | ||||||
|     opts = {}; |   if (!opts.kty) { opts.kty = opts.type; } | ||||||
|   } |   if (!opts.kty) { opts.kty = 'EC'; } | ||||||
|   if (!opts.kty) { |  | ||||||
|     opts.kty = 'EC'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // ECDSA has only the P curves and an associated bitlength
 |  | ||||||
|   if (/^EC/i.test(opts.kty)) { |   if (/^EC/i.test(opts.kty)) { | ||||||
|     wcOpts.name = 'ECDSA'; |     p = Eckles.generate(opts); | ||||||
|     if (!opts.namedCurve) { |  | ||||||
|       opts.namedCurve = 'P-256'; |  | ||||||
|     } |  | ||||||
|     wcOpts.namedCurve = opts.namedCurve; // true for supported curves
 |  | ||||||
|     if (/256/.test(wcOpts.namedCurve)) { |  | ||||||
|       wcOpts.namedCurve = 'P-256'; |  | ||||||
|       wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     } else if (/384/.test(wcOpts.namedCurve)) { |  | ||||||
|       wcOpts.namedCurve = 'P-384'; |  | ||||||
|       wcOpts.hash = { name: "SHA-384" }; |  | ||||||
|     } else { |  | ||||||
|       return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " |  | ||||||
|         + " Please choose either 'P-256' or 'P-384'. " |  | ||||||
|         + Keypairs._stance)); |  | ||||||
|     } |  | ||||||
|   } else if (/^RSA$/i.test(opts.kty)) { |   } else if (/^RSA$/i.test(opts.kty)) { | ||||||
|     // Support PSS? I don't think it's used for Let's Encrypt
 |     p = Rasha.generate(opts); | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     if (!opts.modulusLength) { |  | ||||||
|       opts.modulusLength = 2048; |  | ||||||
|     } |  | ||||||
|     wcOpts.modulusLength = opts.modulusLength; |  | ||||||
|     if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { |  | ||||||
|       // erring on the small side... for no good reason
 |  | ||||||
|       wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { |  | ||||||
|       wcOpts.hash = { name: "SHA-384" }; |  | ||||||
|     } else if (wcOpts.modulusLength < 4097) { |  | ||||||
|       wcOpts.hash = { name: "SHA-512" }; |  | ||||||
|     } else { |  | ||||||
|       // Public key thumbprints should be paired with a hash of similar length,
 |  | ||||||
|       // so anything above SHA-512's keyspace would be left under-represented anyway.
 |  | ||||||
|       return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" |  | ||||||
|         + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" |  | ||||||
|         + " divisible by 8 are allowed. " + Keypairs._stance)); |  | ||||||
|     } |  | ||||||
|     // TODO maybe allow this to be set to any of the standard values?
 |  | ||||||
|     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |  | ||||||
|   } else { |   } else { | ||||||
|     return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." |     return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." | ||||||
|       + Keypairs._universal |       + Keypairs._universal | ||||||
|       + " Please choose either 'EC' or 'RSA' keys.")); |       + " Please choose 'EC', or 'RSA' if you have good reason to.")); | ||||||
|   } |   } | ||||||
| 
 |   return p.then(function (pair) { | ||||||
|   var extractable = true; |     return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { | ||||||
|   return window.crypto.subtle.generateKey( |       pair.private.kid = thumb; // maybe not the same id on the private key?
 | ||||||
|     wcOpts |       pair.public.kid = thumb; | ||||||
|   , extractable |       return pair; | ||||||
|   , [ 'sign', 'verify' ] |  | ||||||
|   ).then(function (result) { |  | ||||||
|     return window.crypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
|       // TODO remove
 |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
|       return { |  | ||||||
|         privateKey: privJwk |  | ||||||
|       }; |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| }(window)); | 
 | ||||||
|  | // Chopping off the private parts is now part of the public API.
 | ||||||
|  | // I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | ||||||
|  | Keypairs.neuter = Keypairs._neuter = function (opts) { | ||||||
|  |   // trying to find the best balance of an immutable copy with custom attributes
 | ||||||
|  |   var jwk = {}; | ||||||
|  |   Object.keys(opts.jwk).forEach(function (k) { | ||||||
|  |     if ('undefined' === typeof opts.jwk[k]) { return; } | ||||||
|  |     // ignore RSA and EC private parts
 | ||||||
|  |     if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } | ||||||
|  |     jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); | ||||||
|  |   }); | ||||||
|  |   return jwk; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Keypairs.thumbprint = function (opts) { | ||||||
|  |   return Promise.resolve().then(function () { | ||||||
|  |     if (/EC/i.test(opts.jwk.kty)) { | ||||||
|  |       return Eckles.thumbprint(opts); | ||||||
|  |     } else { | ||||||
|  |       return Rasha.thumbprint(opts); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Keypairs.publish = function (opts) { | ||||||
|  |   if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } | ||||||
|  | 
 | ||||||
|  |   // returns a copy
 | ||||||
|  |   var jwk = Keypairs.neuter(opts); | ||||||
|  | 
 | ||||||
|  |   if (jwk.exp) { | ||||||
|  |     jwk.exp = setTime(jwk.exp); | ||||||
|  |   } else { | ||||||
|  |     if (opts.exp) { jwk.exp = setTime(opts.exp); } | ||||||
|  |     else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; } | ||||||
|  |     else if (opts.expiresAt) { jwk.exp = opts.expiresAt; } | ||||||
|  |   } | ||||||
|  |   if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; } | ||||||
|  | 
 | ||||||
|  |   if (jwk.kid) { return Promise.resolve(jwk); } | ||||||
|  |   return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function setTime(time) { | ||||||
|  |   if ('number' === typeof time) { return time; } | ||||||
|  | 
 | ||||||
|  |   var t = time.match(/^(\-?\d+)([dhms])$/i); | ||||||
|  |   if (!t || !t[0]) { | ||||||
|  |     throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var now = Math.round(Date.now()/1000); | ||||||
|  |   var num = parseInt(t[1], 10); | ||||||
|  |   var unit = t[2]; | ||||||
|  |   var mult = 1; | ||||||
|  |   switch(unit) { | ||||||
|  |     // fancy fallthrough, what fun!
 | ||||||
|  |     case 'd': | ||||||
|  |       mult *= 24; | ||||||
|  |       /*falls through*/ | ||||||
|  |     case 'h': | ||||||
|  |       mult *= 60; | ||||||
|  |       /*falls through*/ | ||||||
|  |     case 'm': | ||||||
|  |       mult *= 60; | ||||||
|  |       /*falls through*/ | ||||||
|  |     case 's': | ||||||
|  |       mult *= 1; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return now + (mult * num); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | }('undefined' !== typeof module ? module.exports : window)); | ||||||
|  | |||||||
							
								
								
									
										86
									
								
								lib/keypairs.js.min2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/keypairs.js.min2
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | |||||||
|  | /*global Promise*/ | ||||||
|  | (function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var Keypairs = exports.Keypairs = {}; | ||||||
|  | 
 | ||||||
|  | Keypairs._stance = "We take the stance that if you're knowledgeable enough to" | ||||||
|  |   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; | ||||||
|  | Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; | ||||||
|  | Keypairs.generate = function (opts) { | ||||||
|  |   var wcOpts = {}; | ||||||
|  |   if (!opts) { | ||||||
|  |     opts = {}; | ||||||
|  |   } | ||||||
|  |   if (!opts.kty) { | ||||||
|  |     opts.kty = 'EC'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // ECDSA has only the P curves and an associated bitlength | ||||||
|  |   if (/^EC/i.test(opts.kty)) { | ||||||
|  |     wcOpts.name = 'ECDSA'; | ||||||
|  |     if (!opts.namedCurve) { | ||||||
|  |       opts.namedCurve = 'P-256'; | ||||||
|  |     } | ||||||
|  |     wcOpts.namedCurve = opts.namedCurve; // true for supported curves | ||||||
|  |     if (/256/.test(wcOpts.namedCurve)) { | ||||||
|  |       wcOpts.namedCurve = 'P-256'; | ||||||
|  |       wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |     } else if (/384/.test(wcOpts.namedCurve)) { | ||||||
|  |       wcOpts.namedCurve = 'P-384'; | ||||||
|  |       wcOpts.hash = { name: "SHA-384" }; | ||||||
|  |     } else { | ||||||
|  |       return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " | ||||||
|  |         + " Please choose either 'P-256' or 'P-384'. " | ||||||
|  |         + Keypairs._stance)); | ||||||
|  |     } | ||||||
|  |   } else if (/^RSA$/i.test(opts.kty)) { | ||||||
|  |     // Support PSS? I don't think it's used for Let's Encrypt | ||||||
|  |     wcOpts.name = 'RSASSA-PKCS1-v1_5'; | ||||||
|  |     if (!opts.modulusLength) { | ||||||
|  |       opts.modulusLength = 2048; | ||||||
|  |     } | ||||||
|  |     wcOpts.modulusLength = opts.modulusLength; | ||||||
|  |     if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { | ||||||
|  |       // erring on the small side... for no good reason | ||||||
|  |       wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |     } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { | ||||||
|  |       wcOpts.hash = { name: "SHA-384" }; | ||||||
|  |     } else if (wcOpts.modulusLength < 4097) { | ||||||
|  |       wcOpts.hash = { name: "SHA-512" }; | ||||||
|  |     } else { | ||||||
|  |       // Public key thumbprints should be paired with a hash of similar length, | ||||||
|  |       // so anything above SHA-512's keyspace would be left under-represented anyway. | ||||||
|  |       return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" | ||||||
|  |         + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" | ||||||
|  |         + " divisible by 8 are allowed. " + Keypairs._stance)); | ||||||
|  |     } | ||||||
|  |     // TODO maybe allow this to be set to any of the standard values? | ||||||
|  |     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); | ||||||
|  |   } else { | ||||||
|  |     return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." | ||||||
|  |       + Keypairs._universal | ||||||
|  |       + " Please choose either 'EC' or 'RSA' keys.")); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var extractable = true; | ||||||
|  |   return window.crypto.subtle.generateKey( | ||||||
|  |     wcOpts | ||||||
|  |   , extractable | ||||||
|  |   , [ 'sign', 'verify' ] | ||||||
|  |   ).then(function (result) { | ||||||
|  |     return window.crypto.subtle.exportKey( | ||||||
|  |       "jwk" | ||||||
|  |     , result.privateKey | ||||||
|  |     ).then(function (privJwk) { | ||||||
|  |       // TODO remove | ||||||
|  |       console.log('private jwk:'); | ||||||
|  |       console.log(JSON.stringify(privJwk, null, 2)); | ||||||
|  |       return { | ||||||
|  |         privateKey: privJwk | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }(window)); | ||||||
							
								
								
									
										122
									
								
								lib/rsa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/rsa.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | |||||||
|  | /*global Promise*/ | ||||||
|  | (function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var RSA = exports.Rasha = {}; | ||||||
|  | if ('undefined' !== typeof module) { module.exports = RSA; } | ||||||
|  | var Enc = {}; | ||||||
|  | var textEncoder = new TextEncoder(); | ||||||
|  | 
 | ||||||
|  | RSA._stance = "We take the stance that if you're knowledgeable enough to" | ||||||
|  |   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; | ||||||
|  | RSA._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; | ||||||
|  | RSA.generate = function (opts) { | ||||||
|  |   var wcOpts = {}; | ||||||
|  |   if (!opts) { opts = {}; } | ||||||
|  |   if (!opts.kty) { opts.kty = 'RSA'; } | ||||||
|  | 
 | ||||||
|  |   // Support PSS? I don't think it's used for Let's Encrypt
 | ||||||
|  |   wcOpts.name = 'RSASSA-PKCS1-v1_5'; | ||||||
|  |   if (!opts.modulusLength) { | ||||||
|  |     opts.modulusLength = 2048; | ||||||
|  |   } | ||||||
|  |   wcOpts.modulusLength = opts.modulusLength; | ||||||
|  |   if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { | ||||||
|  |     // erring on the small side... for no good reason
 | ||||||
|  |     wcOpts.hash = { name: "SHA-256" }; | ||||||
|  |   } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { | ||||||
|  |     wcOpts.hash = { name: "SHA-384" }; | ||||||
|  |   } else if (wcOpts.modulusLength < 4097) { | ||||||
|  |     wcOpts.hash = { name: "SHA-512" }; | ||||||
|  |   } else { | ||||||
|  |     // Public key thumbprints should be paired with a hash of similar length,
 | ||||||
|  |     // so anything above SHA-512's keyspace would be left under-represented anyway.
 | ||||||
|  |     return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" | ||||||
|  |       + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" | ||||||
|  |       + " divisible by 8 are allowed. " + RSA._stance)); | ||||||
|  |   } | ||||||
|  |   // TODO maybe allow this to be set to any of the standard values?
 | ||||||
|  |   wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); | ||||||
|  | 
 | ||||||
|  |   var extractable = true; | ||||||
|  |   return window.crypto.subtle.generateKey( | ||||||
|  |     wcOpts | ||||||
|  |   , extractable | ||||||
|  |   , [ 'sign', 'verify' ] | ||||||
|  |   ).then(function (result) { | ||||||
|  |     return window.crypto.subtle.exportKey( | ||||||
|  |       "jwk" | ||||||
|  |     , result.privateKey | ||||||
|  |     ).then(function (privJwk) { | ||||||
|  |       return { | ||||||
|  |         private: privJwk | ||||||
|  |       , public: RSA.neuter({ jwk: privJwk }) | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Chopping off the private parts is now part of the public API.
 | ||||||
|  | // I thought it sounded a little too crude at first, but it really is the best name in every possible way.
 | ||||||
|  | RSA.neuter = function (opts) { | ||||||
|  |   // trying to find the best balance of an immutable copy with custom attributes
 | ||||||
|  |   var jwk = {}; | ||||||
|  |   Object.keys(opts.jwk).forEach(function (k) { | ||||||
|  |     if ('undefined' === typeof opts.jwk[k]) { return; } | ||||||
|  |     // ignore RSA private parts
 | ||||||
|  |     if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } | ||||||
|  |     jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); | ||||||
|  |   }); | ||||||
|  |   return jwk; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | ||||||
|  | RSA.__thumbprint = function (jwk) { | ||||||
|  |   // Use the same entropy for SHA as for key
 | ||||||
|  |   var len = Math.floor(jwk.n.length * 0.75); | ||||||
|  |   var alg = 'SHA-256'; | ||||||
|  |   // TODO this may be a bug
 | ||||||
|  |   // need to confirm that the padding is no more or less than 1 byte
 | ||||||
|  |   if (len >= 511) { | ||||||
|  |     alg = 'SHA-512'; | ||||||
|  |   } else if (len >= 383) { | ||||||
|  |     alg = 'SHA-384'; | ||||||
|  |   } | ||||||
|  |   return window.crypto.subtle.digest( | ||||||
|  |     { name: alg } | ||||||
|  |   , textEncoder.encode('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}') | ||||||
|  |   ).then(function (hash) { | ||||||
|  |     return Enc.bufToUrlBase64(new Uint8Array(hash)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | RSA.thumbprint = function (opts) { | ||||||
|  |   return Promise.resolve().then(function () { | ||||||
|  |     var jwk; | ||||||
|  |     if ('EC' === opts.kty) { | ||||||
|  |       jwk = opts; | ||||||
|  |     } else if (opts.jwk) { | ||||||
|  |       jwk = opts.jwk; | ||||||
|  |     } else { | ||||||
|  |       return RSA.import(opts).then(function (jwk) { | ||||||
|  |         return RSA.__thumbprint(jwk); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return RSA.__thumbprint(jwk); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Enc.bufToUrlBase64 = function (u8) { | ||||||
|  |   return Enc.bufToBase64(u8) | ||||||
|  |     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Enc.bufToBase64 = function (u8) { | ||||||
|  |   var bin = ''; | ||||||
|  |   u8.forEach(function (i) { | ||||||
|  |     bin += String.fromCharCode(i); | ||||||
|  |   }); | ||||||
|  |   return btoa(bin); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }('undefined' !== typeof module ? module.exports : window)); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user