700 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			700 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | /*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)); |