From 2cc5a41268f4d3fb80bff50b01482205c63ac2cf Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 9 Nov 2018 21:05:53 -0700 Subject: [PATCH] lint and fix and use domains.generateKeypair --- app/js/app.js | 2 +- app/js/bacme.js | 434 ++++++++++++++++++++++++------------------------ 2 files changed, 215 insertions(+), 221 deletions(-) diff --git a/app/js/app.js b/app/js/app.js index d465814..ae18ee3 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -444,7 +444,7 @@ }; } - return BACME.accounts.generateKeypair(opts).then(function (serverJwk) { + return BACME.domains.generateKeypair(opts).then(function (serverJwk) { localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); return serverJwk; }); diff --git a/app/js/bacme.js b/app/js/bacme.js index 00e7c45..705af40 100644 --- a/app/js/bacme.js +++ b/app/js/bacme.js @@ -4,6 +4,8 @@ var BACME = exports.BACME = {}; var webFetch = exports.fetch; var webCrypto = exports.crypto; +var Promise = window.Promise; +var CSR = window.CSR; var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; var directory; @@ -15,7 +17,6 @@ var accountKeypair; var accountJwk; var accountUrl; -var signedAccount; BACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' @@ -23,38 +24,38 @@ BACME.challengePrefixes = { }; BACME._logHeaders = function (resp) { - console.log('Headers:'); - Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); + 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(''); + 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 (body) { - directory = body; + return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { + BACME._logHeaders(resp); + return resp.json().then(function (body) { + directory = body; 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(body); return body; - }); - }); + }); + }); }; BACME.nonce = function () { - return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { + 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'); - }); + nonce = resp.headers.get('replay-nonce'); + console.log('Nonce:', nonce); + // resp.body is empty + return resp.headers.get('replay-nonce'); + }); }; BACME.accounts = {}; @@ -62,66 +63,38 @@ BACME.accounts = {}; // type = ECDSA // bitlength = 256 BACME.accounts.generateKeypair = function (opts) { - var wcOpts = {}; + return BACME.generateKeypair(opts).then(function (result) { + accountKeypair = result; - // 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'; - } - } + return webCrypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { - // 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" }; - } - - // https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey - var extractable = true; - return webCrypto.subtle.generateKey( - wcOpts - , extractable - , [ 'sign', 'verify' ] - ).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)); + 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 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, ''); + return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); }; var textEncoder = new TextEncoder(); @@ -158,7 +131,7 @@ BACME._importKey = function (jwk) { e: priv.e , kty: priv.kty , n: priv.n - } + }; if (!priv.p) { priv = null; } @@ -167,7 +140,7 @@ BACME._importKey = function (jwk) { return window.crypto.subtle.importKey( "jwk" , pub - , wcOpts + , wcOpts , extractable , [ "verify" ] ).then(function (publicKey) { @@ -271,8 +244,8 @@ BACME.accounts.sign = function (opts) { protectedJson ); - // Note: this function hashes before signing so send data, not the hash - return BACME._sign({ + // Note: this function hashes before signing so send data, not the hash + return BACME._sign({ abstractKey: abstractKey , payload64: payload64 , protected64: protected64 @@ -280,30 +253,29 @@ BACME.accounts.sign = function (opts) { }); }; -var account; 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); + 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: '); + if (!resp.headers.get('content-type')) { + console.log('Body: '); - return { kid: accountId }; - } + return { kid: accountId }; + } - return resp.json().then(function (result) { + return resp.json().then(function (result) { if (/^Error/i.test(result.detail)) { return Promise.reject(new Error(result.detail)); } @@ -311,21 +283,20 @@ BACME.accounts.set = function (opts) { BACME._logBody(result); return result; - }); - }); + }); + }); }; var orderUrl; -var signedOrder; 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 }); + var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); - return BACME._importKey(opts.jwk).then(function (abstractKey) { + return BACME._importKey(opts.jwk).then(function (abstractKey) { var protected64 = BACME._jsto64( { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } ); @@ -345,36 +316,35 @@ BACME.orders.sign = function (opts) { }); }; -var order; 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) { + 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); + currentOrderUrl = resp.headers.get('location'); + nonce = resp.headers.get('replay-nonce'); + console.log('Next nonce:', nonce); - return resp.json().then(function (result) { + 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; + authorizationUrls = result.authorizations; + finalizeUrl = result.finalize; BACME._logBody(result); result.url = currentOrderUrl; return result; - }); - }); + }); + }); }; BACME.challenges = {}; @@ -395,22 +365,22 @@ BACME.challenges.all = function () { return next(); }; BACME.challenges.view = function () { - var authzUrl = authorizationUrls.pop(); - var token; - var challengeDomain; - var challengeUrl; + var authzUrl = authorizationUrls.pop(); + var token; + var challengeDomain; + var challengeUrl; - return window.fetch(authzUrl, { - mode: 'cors' - }).then(function (resp) { + 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; + 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); @@ -424,8 +394,8 @@ BACME.challenges.view = function () { //, url: challenge.url //, domain: result.identifier.value, }; - }); - }); + }); + }); }; var thumbprint; @@ -435,7 +405,7 @@ var dnsAuth; var dnsRecord; BACME.thumbprint = function (opts) { - // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk + // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk var accountJwk = opts.jwk; var keys; @@ -446,34 +416,34 @@ BACME.thumbprint = function (opts) { keys = [ 'e', 'kty', 'n' ]; } - var accountPublicStr = '{' + keys.map(function (key) { - return '"' + key + '":"' + accountJwk[key] + '"'; - }).join(',') + '}'; + 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, ''); + 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); + 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; + // 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; + // Where the key authorization file goes + httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); @@ -487,28 +457,28 @@ BACME.challenges['http-01'] = function (opts) { 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, ''); + 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; + 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); + 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; @@ -516,84 +486,108 @@ 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({ + 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) { + 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'); + nonce = resp.headers.get('replay-nonce'); console.log("ACCEPT NONCE:", nonce); - return resp.json().then(function (reply) { + return resp.json().then(function (reply) { challengePollUrl = reply.url; - console.log('Challenge ACK:'); - console.log(JSON.stringify(reply)); + 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) { + return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { BACME._logHeaders(resp); - return resp.json().then(function (reply) { - challengePollUrl = reply.url; + return resp.json().then(function (reply) { + challengePollUrl = reply.url; BACME._logBody(reply); - return 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( + { name: "ECDSA", namedCurve: "P-256" } + , extractable + , [ 'sign', 'verify' ] + ); +}; BACME.domains = {}; -// TODO factor out from BACME.accounts.generateKeypair -BACME.domains.generateKeypair = function () { - var extractable = true; - return window.crypto.subtle.generateKey( - { name: "ECDSA", namedCurve: "P-256" } - , extractable - , [ 'sign', 'verify' ] - ).then(function (result) { - domainKeypair = result; +// 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 (jwk) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { - domainJwk = jwk; - console.log('private jwk:'); - console.log(JSON.stringify(jwk, null, 2)); + domainJwk = privJwk; + console.log('private jwk:'); + console.log(JSON.stringify(privJwk, null, 2)); - return domainKeypair; - }) - }); + return privJwk; + }); + }); }; // { serverJwk, domains } @@ -607,41 +601,41 @@ var certificateUrl; // { csr, jwk, finalizeUrl, accountId } BACME.orders.finalize = function (opts) { - var payload64 = BACME._jsto64( - { csr: opts.csr } - ); + 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({ + 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) { + 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'); + nonce = resp.headers.get('replay-nonce'); - return resp.json().then(function (reply) { - certificateUrl = reply.certificate; + return resp.json().then(function (reply) { + certificateUrl = reply.certificate; BACME._logBody(reply); return reply; - }); - }); - }); + }); + }); + }); }; BACME.orders.receive = function (opts) {