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