From ca01e3112f5ec880a9af25910aab7d4f4bdd9388 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 2 May 2019 23:05:14 -0600 Subject: [PATCH 01/17] don't break telebit, duh --- server.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 34938da..33e6e40 100644 --- a/server.js +++ b/server.js @@ -118,8 +118,11 @@ app.get('/api/dns/:domain', function (req, res, next) { dig.resolveJson(query, opts); }); -// curl -L http://localhost:3000/api/dns/example.com?type=A -console.log("Listening on localhost:3000"); -app.listen(3000); -console.log("Try this:"); -console.log("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); +module.exports = app; +if (require.main === module) { + // curl -L http://localhost:3000/api/dns/example.com?type=A + console.log("Listening on localhost:3000"); + app.listen(3000); + console.log("Try this:"); + console.log("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); +} -- 2.38.5 From 45800a42ec01f3cf174c890396ed809f82a20632 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 00:01:06 -0600 Subject: [PATCH 02/17] dns and http relay with api check --- package-lock.json | 6 ++++++ package.json | 1 + server.js | 19 +++++++++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2060678..7eaa7b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@root/request": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.10.tgz", + "integrity": "sha512-GSn8dfsGp0juJyXS9k7B/DjYm7Axe85wiCHfPs30eQ+/V6p2aqey45e1czb3ZwP+iPmzWCKXahhWnZhSDIil6w==", + "dev": true + }, "accepts": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.6.tgz", diff --git a/package.json b/package.json index a29af32..e122eeb 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0", "devDependencies": { + "@root/request": "^1.3.10", "dig.js": "^1.3.9", "dns-suite": "^1.2.12", "express": "^4.16.4" diff --git a/server.js b/server.js index 33e6e40..553ab31 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ var crypto = require('crypto'); //var dnsjs = require('dns-suite'); var dig = require('dig.js/dns-request'); +var request = require('util').promisify(require('@root/request')); var express = require('express'); var app = express(); @@ -13,7 +14,6 @@ var nameserver = nameservers[index]; app.use('/', express.static('./')); app.use('/api', express.json()); app.get('/api/dns/:domain', function (req, res, next) { - console.log(req.params); var domain = req.params.domain; var casedDomain = domain.toLowerCase().split('').map(function (ch) { // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is @@ -117,12 +117,23 @@ app.get('/api/dns/:domain', function (req, res, next) { dig.resolveJson(query, opts); }); +app.get('/api/http', function (req, res) { + var url = req.query.url; + return request({ method: 'GET', url: url }).then(function (resp) { + res.send(resp.body); + }); +}); +app.get('/api/_acme_api_', function (req, res) { + res.send({ success: true }); +}); module.exports = app; if (require.main === module) { // curl -L http://localhost:3000/api/dns/example.com?type=A - console.log("Listening on localhost:3000"); + console.info("Listening on localhost:3000"); app.listen(3000); - console.log("Try this:"); - console.log("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); + console.info("Try this:"); + console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); + console.info("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); + console.info("\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"); } -- 2.38.5 From dcfa093e0304157adf01efe19075e636285566bb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 00:11:50 -0600 Subject: [PATCH 03/17] use package dir, not current dir --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 553ab31..0eadbf5 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ var nameservers = require('dns').getServers(); var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; var nameserver = nameservers[index]; -app.use('/', express.static('./')); +app.use('/', express.static(__dirname)); app.use('/api', express.json()); app.get('/api/dns/:domain', function (req, res, next) { var domain = req.params.domain; -- 2.38.5 From f6d26c8b8eba7723a2c3332dad2620aeb73f8cf7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 00:28:55 -0600 Subject: [PATCH 04/17] dns01 challenge test works again --- app.js | 4 +-- lib/acme.js | 91 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/app.js b/app.js index 3531ee2..43eb90c 100644 --- a/app.js +++ b/app.js @@ -114,7 +114,7 @@ acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { console.log('acme result', result); var privJwk = JSON.parse($('.js-jwk').innerText).private; - var email = $('.js-email').innerText; + var email = $('.js-email').value; function checkTos(tos) { console.log("TODO checkbox for agree to terms"); return tos; @@ -130,7 +130,7 @@ , modulusLength: 2048 }).then(function (pair) { console.log('domain keypair:', pair); - var domains = ($('.js-domains').innerText||'example.com').split(/[, ]+/g); + var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); return acme.certificates.create({ accountKeypair: { privateKeyJwk: privJwk } , account: account diff --git a/lib/acme.js b/lib/acme.js index d5b3438..467e53c 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -29,20 +29,19 @@ ACME.challengePrefixes = { }; ACME.challengeTests = { 'http-01': function (me, auth) { - var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; - return me.request({ method: 'GET', url: url }).then(function (resp) { + return me.http01(auth).then(function (keyAuth) { var err; // TODO limit the number of bytes that are allowed to be downloaded - if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + if (auth.keyAuthorization === (keyAuth||'').trim()) { return true; } err = new Error( "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" - + "curl '" + url + "'\n" + + "curl '" + auth.challengeUrl + "'\n" + "Expected: '" + auth.keyAuthorization + "'\n" - + "Got: '" + resp.body + "'\n" + + "Got: '" + keyAuth + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); err.code = 'E_FAIL_DRY_CHALLENGE'; @@ -51,10 +50,7 @@ ACME.challengeTests = { } , 'dns-01': function (me, auth) { // remove leading *. on wildcard domains - return me.dig({ - type: 'TXT' - , name: auth.dnsHost - }).then(function (ans) { + return me.dns01(auth).then(function (ans) { var err; if (ans.answer.some(function (txt) { @@ -154,7 +150,7 @@ ACME._registerAccount = function (me, options) { , kid: options.externalAccount.id , url: me._directoryUrls.newAccount } - , payload: Enc.strToBuf(JSON.stringify(pair.public)) + , payload: Enc.binToBuf(JSON.stringify(pair.public)) }).then(function (jws) { body.externalAccountBinding = jws; return body; @@ -390,6 +386,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead + // TODO auth.http01Url ? auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); @@ -440,7 +437,7 @@ ACME._postChallenge = function (me, options, auth) { options: options , url: auth.url , protected: { kid: options._kid } - , payload: Enc.strToBuf(JSON.stringify({ "status": "deactivated" })) + , payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" })) }).then(function (resp) { if (me.debug) { console.debug('deactivate challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } @@ -515,7 +512,7 @@ ACME._postChallenge = function (me, options, auth) { options: options , url: auth.url , protected: { kid: options._kid } - , payload: Enc.strToBuf(JSON.stringify({})) + , payload: Enc.binToBuf(JSON.stringify({})) }).then(function (resp) { if (me.debug) { console.debug('respond to challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } @@ -576,7 +573,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { options: options , url: options._finalize , protected: { kid: options._kid } - , payload: Enc.strToBuf(payload) + , payload: Enc.binToBuf(payload) }).then(function (resp) { if (me.debug) { console.debug('order finalized: resp.body:'); } if (me.debug) { console.debug(resp.body); } @@ -717,7 +714,7 @@ ACME._getCertificate = function (me, options) { options: options , url: me._directoryUrls.newOrder , protected: { kid: options._kid } - , payload: Enc.strToBuf(payload) + , payload: Enc.binToBuf(payload) }).then(function (resp) { var location = resp.headers.location; var setAuths; @@ -818,21 +815,21 @@ ACME.create = function create(me) { me.challengePrefixes = ACME.challengePrefixes; me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; me._nonces = []; + if (!me._baseUrl) { + me._baseUrl = ""; + } //me.Keypairs = me.Keypairs || require('keypairs'); //me.request = me.request || require('@root/request'); - if (!me.dig) { - me.dig = function (query) { - // TODO use digd.js - return new me.request({ url: "/api/dns/" + query.name + "?type=" + query.type }).then(function (resp) { - if (!resp.body || !Array.isArray(resp.body.answer)) { - throw new Error("failed to get DNS response"); - } - return { - answer: resp.body.answer.map(function (ans) { - return { data: ans.data, ttl: ans.ttl }; - }) - }; - }); + if (!me.dns01) { + me.dns01 = function (auth) { + return ACME._dns01(me, auth); + }; + } + // backwards compat + if (!me.dig) { me.dig = me.dns01; } + if (!me.http01) { + me.http01 = function (auth) { + return ACME._http01(me, auth); }; } @@ -853,8 +850,21 @@ ACME.create = function create(me) { if ('string' !== typeof me.directoryUrl) { throw new Error("you must supply either the ACME directory url as a string or an object of the ACME urls"); } - return ACME._directory(me).then(function (resp) { - return fin(resp.body); + var p = Promise.resolve(); + if (!me.skipChallengeTest) { + p = me.request({ url: me._baseUrl + "/api/_acme_api_/" }).then(function (resp) { + if (resp.body.success) { + me._canCheckHttp01 = true; + me._canCheckDns01 = true; + } + }).catch(function () { + // ignore + }); + } + return p.then(function () { + return ACME._directory(me).then(function (resp) { + return fin(resp.body); + }); }); }; me.accounts = { @@ -992,6 +1002,29 @@ ACME._prnd = function (n) { ACME._toHex = function (pair) { return parseInt(pair, 10).toString(16); }; +ACME._dns01 = function (me, auth) { + return new me.request({ url: me._baseUrl + "/api/dns/" + auth.dnsHost + "?type=TXT" }).then(function (resp) { + var err; + if (!resp.body || !Array.isArray(resp.body.answer)) { + err = new Error("failed to get DNS response"); + console.error(err); + throw err; + } + var result = { + answer: resp.body.answer.map(function (ans) { + return { data: ans.data, ttl: ans.ttl }; + }) + }; + console.log(result); + return result; + }); +}; +ACME._http01 = function (me, auth) { + var url = encodeURIComponent(auth.challengeUrl); + return new me.request({ url: me._baseUrl + "/api/http?url=" + url }).then(function (resp) { + return resp.body; + }); +}; Enc.bufToUrlBase64 = function (u8) { return Enc.bufToBase64(u8) -- 2.38.5 From e479d79c158a480bc0b032d068b67cb58c76e9b1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 00:51:46 -0600 Subject: [PATCH 05/17] http-01 challenge pre-flight works --- app.js | 29 ++++++++++++++++++++++++++--- index.html | 35 +++++++++++++++++------------------ lib/acme.js | 8 ++++---- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/app.js b/app.js index 43eb90c..28c9e63 100644 --- a/app.js +++ b/app.js @@ -141,8 +141,9 @@ , challenges: { 'dns-01': { set: function (opts) { - console.log('dns-01 set challenge:'); - console.log(JSON.stringify(opts, null, 2)); + console.info('dns-01 set challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); return new Promise(function (resolve) { while (!window.confirm("Did you set the challenge?")) {} resolve(); @@ -150,7 +151,28 @@ } , remove: function (opts) { console.log('dns-01 remove challenge:'); - console.log(JSON.stringify(opts, null, 2)); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } + } + , 'http-01': { + set: function (opts) { + console.info('http-01 set challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('http-01 remove challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); return new Promise(function (resolve) { while (!window.confirm("Did you delete the challenge?")) {} resolve(); @@ -158,6 +180,7 @@ } } } + , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] }); }); }).catch(function (err) { diff --git a/index.html b/index.html index b4d91c8..f984d04 100644 --- a/index.html +++ b/index.html @@ -34,27 +34,21 @@

EC Options:

- - - - - + + +
@@ -67,6 +61,11 @@
+ + +
diff --git a/lib/acme.js b/lib/acme.js index 467e53c..8bfba66 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -433,7 +433,7 @@ ACME._postChallenge = function (me, options, auth) { */ function deactivate() { if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } - return ACME._jwsRequest({ + return ACME._jwsRequest(me, { options: options , url: auth.url , protected: { kid: options._kid } @@ -508,7 +508,7 @@ ACME._postChallenge = function (me, options, auth) { function respondToChallenge() { if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } - return ACME._jwsRequest({ + return ACME._jwsRequest(me, { options: options , url: auth.url , protected: { kid: options._kid } @@ -569,7 +569,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { function pollCert() { if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } - return ACME._jwsRequest({ + return ACME._jwsRequest(me, { options: options , url: options._finalize , protected: { kid: options._kid } @@ -710,7 +710,7 @@ ACME._getCertificate = function (me, options) { var payload = JSON.stringify(body); if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } - return ACME._jwsRequest({ + return ACME._jwsRequest(me, { options: options , url: me._directoryUrls.newOrder , protected: { kid: options._kid } -- 2.38.5 From ad81b6c3394ab1cae814de06772816b081823aa4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 01:30:05 -0600 Subject: [PATCH 06/17] http-01 and dns-01 challenges can pass --- lib/acme.js | 53 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/acme.js b/lib/acme.js index 8bfba66..5bb6da6 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -322,6 +322,9 @@ ACME._testChallenges = function (me, options) { , expires: new Date(Date.now() + (60 * 1000)).toISOString() , wildcard: identifierValue.includes('*.') || undefined }; + + // The dry-run comes first in the spirit of "fail fast" + // (and protecting against challenge failure rate limits) var dryrun = true; return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { return ACME._setChallenge(me, options, auth).then(function () { @@ -332,7 +335,11 @@ ACME._testChallenges = function (me, options) { })).then(function (auths) { return ACME._wait(CHECK_DELAY).then(function () { return Promise.all(auths.map(function (auth) { - return ACME.challengeTests[auth.type](me, auth); + return ACME.challengeTests[auth.type](me, auth).then(function (result) { + // not a blocker + ACME._removeChallenge(me, options, auth); + return result; + }); })); }); }); @@ -475,18 +482,7 @@ ACME._postChallenge = function (me, options, auth) { if (me.debug) { console.debug('poll: valid'); } try { - if (1 === options.removeChallenge.length) { - options.removeChallenge(auth).then(function () {}, function () {}); - } else if (2 === options.removeChallenge.length) { - options.removeChallenge(auth, function (err) { return err; }); - } else { - if (!ACME._removeChallengeWarn) { - console.warn("Please update to acme-v2 removeChallenge(options) or removeChallenge(options, cb)."); - console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); - ACME._removeChallengeWarn = true; - } - options.removeChallenge(auth.request.identifier, auth.token, function () {}); - } + ACME._removeChallenge(me, options, auth); } catch(e) {} return resp.body; } @@ -523,8 +519,6 @@ ACME._postChallenge = function (me, options, auth) { return respondToChallenge(); }; ACME._setChallenge = function (me, options, auth) { - console.log('challenge auth:', auth); - console.log('challenges:', options.challenges); return new Promise(function (resolve, reject) { var challengers = options.challenges || {}; var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge; @@ -886,6 +880,10 @@ ACME._jwsRequest = function (me, bigopts) { bigopts.protected.nonce = nonce; bigopts.protected.url = bigopts.url; // protected.alg: added by Keypairs.signJws + if (!bigopts.protected.jwk) { + // protected.kid must be overwritten due to ACME's interpretation of the spec + if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; } + } return me.Keypairs.signJws( { jwk: bigopts.options.accountKeypair.privateKeyJwk , protected: bigopts.protected @@ -1010,13 +1008,16 @@ ACME._dns01 = function (me, auth) { console.error(err); throw err; } - var result = { + if (!resp.body.answer.length) { + err = new Error("failed to get DNS answer record in response"); + console.error(err); + throw err; + } + return { answer: resp.body.answer.map(function (ans) { return { data: ans.data, ttl: ans.ttl }; }) }; - console.log(result); - return result; }); }; ACME._http01 = function (me, auth) { @@ -1025,6 +1026,22 @@ ACME._http01 = function (me, auth) { return resp.body; }); }; +ACME._removeChallenge = function (me, options, auth) { + var challengers = options.challenges || {}; + var removeChallenge = (challengers[auth.type] && challengers[auth.type].remove) || options.removeChallenge; + if (1 === removeChallenge.length) { + removeChallenge(auth).then(function () {}, function () {}); + } else if (2 === removeChallenge.length) { + removeChallenge(auth, function (err) { return err; }); + } else { + if (!ACME._removeChallengeWarn) { + console.warn("Please update to acme-v2 removeChallenge(options) or removeChallenge(options, cb)."); + console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); + ACME._removeChallengeWarn = true; + } + removeChallenge(auth.request.identifier, auth.token, function () {}); + } +}; Enc.bufToUrlBase64 = function (u8) { return Enc.bufToBase64(u8) -- 2.38.5 From c974cb703907f8547a2a45c16a36aa436f8d1fed Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 01:32:31 -0600 Subject: [PATCH 07/17] whitespace fix --- server.js | 104 +++++++++++++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/server.js b/server.js index 0eadbf5..5ae0a55 100644 --- a/server.js +++ b/server.js @@ -16,10 +16,10 @@ app.use('/api', express.json()); app.get('/api/dns/:domain', function (req, res, next) { var domain = req.params.domain; var casedDomain = domain.toLowerCase().split('').map(function (ch) { - // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is - // ch = ch | 0x20; - return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); - }).join(''); + // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is + // ch = ch | 0x20; + return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); + }).join(''); var typ = req.query.type; var query = { header: { @@ -40,60 +40,60 @@ app.get('/api/dns/:domain', function (req, res, next) { } ] }; - var opts = { + var opts = { onError: function (err) { - next(err); - } + next(err); + } , onMessage: function (packet) { - var fail0x20; + var fail0x20; - if (packet.id !== query.id) { - console.error('[SECURITY] ignoring packet for \'' + packet.question[0].name + '\' due to mismatched id'); - console.error(packet); - return; - } + if (packet.id !== query.id) { + console.error('[SECURITY] ignoring packet for \'' + packet.question[0].name + '\' due to mismatched id'); + console.error(packet); + return; + } - packet.question.forEach(function (q) { - // if (-1 === q.name.lastIndexOf(cli.casedQuery)) - if (q.name !== casedDomain) { - fail0x20 = q.name; - } - }); + packet.question.forEach(function (q) { + // if (-1 === q.name.lastIndexOf(cli.casedQuery)) + if (q.name !== casedDomain) { + fail0x20 = q.name; + } + }); - [ 'question', 'answer', 'authority', 'additional' ].forEach(function (group) { - (packet[group]||[]).forEach(function (a) { - var an = a.name; - var i = domain.toLowerCase().lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM - var j = a.name.toLowerCase().lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM + [ 'question', 'answer', 'authority', 'additional' ].forEach(function (group) { + (packet[group]||[]).forEach(function (a) { + var an = a.name; + var i = domain.toLowerCase().lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM + var j = a.name.toLowerCase().lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM - // it's important to note that these should only relpace changes in casing that we expected - // any abnormalities should be left intact to go "huh?" about - // TODO detect abnormalities? - if (-1 !== i) { - // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) - a.name = a.name.replace(casedDomain.substr(i), domain.substr(i)); - } else if (-1 !== j) { - // "www.example.com".replace("EXamPLE.cOm", "example.com") - a.name = a.name.substr(0, j) + a.name.substr(j).replace(casedDomain, domain); - } + // it's important to note that these should only relpace changes in casing that we expected + // any abnormalities should be left intact to go "huh?" about + // TODO detect abnormalities? + if (-1 !== i) { + // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) + a.name = a.name.replace(casedDomain.substr(i), domain.substr(i)); + } else if (-1 !== j) { + // "www.example.com".replace("EXamPLE.cOm", "example.com") + a.name = a.name.substr(0, j) + a.name.substr(j).replace(casedDomain, domain); + } - // NOTE: right now this assumes that anything matching the query matches all the way to the end - // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly - // (but I don't think it should need to) - if (a.name.length !== an.length) { - console.error("[ERROR] question / answer mismatch: '" + an + "' != '" + a.length + "'"); - console.error(a); - } - }); - }); + // NOTE: right now this assumes that anything matching the query matches all the way to the end + // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly + // (but I don't think it should need to) + if (a.name.length !== an.length) { + console.error("[ERROR] question / answer mismatch: '" + an + "' != '" + a.length + "'"); + console.error(a); + } + }); + }); - if (fail0x20) { - console.warn(";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" - + casedDomain + "' but got response for '" + fail0x20 + "'."); - return; - } + if (fail0x20) { + console.warn(";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" + + casedDomain + "' but got response for '" + fail0x20 + "'."); + return; + } - res.send({ + res.send({ header: packet.header , question: packet.question , answer: packet.answer @@ -101,12 +101,12 @@ app.get('/api/dns/:domain', function (req, res, next) { , additional: packet.additional , edns_options: packet.edns_options }); - } + } , onListening: function () {} , onSent: function (/*res*/) { } , onTimeout: function (res) { - console.error('dns timeout:', res); - next(new Error("DNS timeout - no response")); + console.error('dns timeout:', res); + next(new Error("DNS timeout - no response")); } , onClose: function () { } //, mdns: cli.mdns -- 2.38.5 From 11ca0051422dbba64f786728f0e2d8e9382ab3a1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 23:49:32 -0600 Subject: [PATCH 08/17] WIP make checks more optional --- lib/acme.js | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/acme.js b/lib/acme.js index 5bb6da6..87668c4 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -284,10 +284,6 @@ ACME._testChallengeOptions = function () { ]; }; ACME._testChallenges = function (me, options) { - if (me.skipChallengeTest) { - return Promise.resolve(); - } - var CHECK_DELAY = 0; return Promise.all(options.domains.map(function (identifierValue) { // TODO we really only need one to pass, not all to pass @@ -307,6 +303,12 @@ ACME._testChallenges = function (me, options) { + " You must enable one of ( " + suitable + " )." )); } + + // TODO remove skipChallengeTest + if (me.skipDryRun || me.skipChallengeTest) { + return null; + } + if ('dns-01' === challenge.type) { // Give the nameservers a moment to propagate CHECK_DELAY = 1.5 * 1000; @@ -327,12 +329,15 @@ ACME._testChallenges = function (me, options) { // (and protecting against challenge failure rate limits) var dryrun = true; return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { + if (!me._canUse[auth.type]) { return; } return ACME._setChallenge(me, options, auth).then(function () { return auth; }); }); }); })).then(function (auths) { + auths = auths.filter(Boolean); + if (!auths.length) { /*skip actual test*/ return; } return ACME._wait(CHECK_DELAY).then(function () { return Promise.all(auths.map(function (auth) { return ACME.challengeTests[auth.type](me, auth).then(function (result) { @@ -712,6 +717,7 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { var location = resp.headers.location; var setAuths; + var validAuths = []; var auths = []; if (me.debug) { console.debug('[ordered]', location); } // the account id url if (me.debug) { console.debug(resp); } @@ -756,16 +762,32 @@ ACME._getCertificate = function (me, options) { }); } - function challengeNext() { + function checkNext() { var auth = auths.shift(); if (!auth) { return; } + + if (!me._canUse[auth.type] || me.skipChallengeTest) { + // not so much "valid" as "not invalid" + // but in this case we can't confirm either way + validAuths.push(auth); + return Promise.resolve(); + } + + return ACME.challengeTests[auth.type](me, auth).then(function () { + validAuths.push(auth); + }).then(checkNext); + } + + function challengeNext() { + var auth = validAuths.shift(); + if (!auth) { return; } return ACME._postChallenge(me, options, auth).then(challengeNext); } // First we set every challenge // Then we ask for each challenge to be checked // Doing otherwise would potentially cause us to poison our own DNS cache with misses - return setNext().then(challengeNext).then(function () { + return setNext().then(checkNext).then(challengeNext).then(function () { if (me.debug) { console.debug("[getCertificate] next.then"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; @@ -809,6 +831,7 @@ ACME.create = function create(me) { me.challengePrefixes = ACME.challengePrefixes; me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; me._nonces = []; + me._canCheck = {}; if (!me._baseUrl) { me._baseUrl = ""; } @@ -848,8 +871,8 @@ ACME.create = function create(me) { if (!me.skipChallengeTest) { p = me.request({ url: me._baseUrl + "/api/_acme_api_/" }).then(function (resp) { if (resp.body.success) { - me._canCheckHttp01 = true; - me._canCheckDns01 = true; + me._canCheck['http-01'] = true; + me._canCheck['dns-01'] = true; } }).catch(function () { // ignore -- 2.38.5 From 48507da7f4567e292420977639c7899e43ddff79 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 3 May 2019 23:58:35 -0600 Subject: [PATCH 09/17] WIP copy over node csr gen --- lib/csr-ec.js | 157 +++++++++++++++++++++++++++++++++++++ lib/csr.js | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 lib/csr-ec.js create mode 100644 lib/csr.js diff --git a/lib/csr-ec.js b/lib/csr-ec.js new file mode 100644 index 0000000..72ee072 --- /dev/null +++ b/lib/csr-ec.js @@ -0,0 +1,157 @@ +// 1.2.840.10045.3.1.7 +// prime256v1 (ANSI X9.62 named elliptic curve) +var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); +// 1.3.132.0.34 +// secp384r1 (SECG (Certicom) named elliptic curve) +var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); + +var ECDSACSR = {}; +var ECDSA = {}; +var DER = {}; +var PEM = {}; +var ASN1; +var Hex = {}; +var AB = {}; + +// +// CSR - the main event +// + +ECDSACSR.create = function createEcCsr(keypem, domains) { + var pemblock = PEM.parseBlock(keypem); + var ecpub = PEM.parseEcPubkey(pemblock.der); + var request = ECDSACSR.request(ecpub, domains); + return AB.fromHex(ECDSACSR.sign(keypem, request)); +}; + +ECDSACSR.request = function createCsrBodyEc(xy, domains) { + var publen = xy.x.byteLength; + var compression = '04'; + var hxy = ''; + // 04 == x+y, 02 == x-only + if (xy.y) { + publen += xy.y.byteLength; + } else { + // Note: I don't intend to support compression - it isn't used by most + // libraries and it requir more dependencies for bigint ops to deflate. + // This is more just a placeholder. It won't work right now anyway + // because compression requires an exta bit stored (odd vs even), which + // I haven't learned yet, and I'm not sure if it's allowed at all + compression = '02'; + } + hxy += Hex.fromAB(xy.x); + if (xy.y) { hxy += Hex.fromAB(xy.y); } + + // Sorry for the mess, but it is what it is + return ASN1('30' + + // Version (0) + , ASN1.UInt('00') + + // CN / Subject + , ASN1('30' + , ASN1('31' + , ASN1('30' + // object id (commonName) + , ASN1('06', '55 04 03') + , ASN1('0C', Hex.fromString(domains[0]))))) + + // EC P-256 Public Key + , ASN1('30' + , ASN1('30' + // 1.2.840.10045.2.1 ecPublicKey + // (ANSI X9.62 public key type) + , ASN1('06', '2A 86 48 CE 3D 02 01') + // 1.2.840.10045.3.1.7 prime256v1 + // (ANSI X9.62 named elliptic curve) + , ASN1('06', '2A 86 48 CE 3D 03 01 07') + ) + , ASN1.BitStr(compression + hxy)) + + // CSR Extension Subject Alternative Names + , ASN1('A0' + , ASN1('30' + // (extensionRequest (PKCS #9 via CRMF)) + , ASN1('06', '2A 86 48 86 F7 0D 01 09 0E') + , ASN1('31' + , ASN1('30' + , ASN1('30' + // (subjectAltName (X.509 extension)) + , ASN1('06', '55 1D 11') + , ASN1('04' + , ASN1('30', domains.map(function (d) { + return ASN1('82', Hex.fromString(d)); + }).join('')))))))) + ); +}; + +ECDSACSR.sign = function csrEcSig(keypem, request) { + var sig = ECDSA.sign(keypem, AB.fromHex(request)); + var rLen = sig.r.byteLength; + var rc = ''; + var sLen = sig.s.byteLength; + var sc = ''; + + if (0x80 & new Uint8Array(sig.r)[0]) { rc = '00'; rLen += 1; } + if (0x80 & new Uint8Array(sig.s)[0]) { sc = '00'; sLen += 1; } + + return ASN1('30' + // The Full CSR Request Body + , request + + // The Signature Type + , ASN1('30' + // 1.2.840.10045.4.3.2 ecdsaWithSHA256 + // (ANSI X9.62 ECDSA algorithm with SHA256) + , ASN1('06', '2A 86 48 CE 3D 04 03 02') + ) + + // The Signature, embedded in a Bit Stream + , ASN1.BitStr( + // As far as I can tell this is a completely separate ASN.1 structure + // that just so happens to be embedded in a Bit String of another ASN.1 + ASN1('30' + , ASN1.UInt(Hex.fromAB(sig.r)) + , ASN1.UInt(Hex.fromAB(sig.s)))) + ); +}; + +// +// ECDSA +// + +// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a +ECDSA.sign = function signEc(keypem, ab) { + // Signer is a stream + var sign = crypto.createSign('SHA256'); + sign.write(new Uint8Array(ab)); + sign.end(); + + // The signature is ASN1 encoded + var sig = sign.sign(keypem); + + // Convert to a JavaScript ArrayBuffer just because + sig = new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); + + // The first two bytes '30 xx' signify SEQUENCE and LENGTH + // The sequence length byte will be a single byte because the signature is less that 128 bytes (0x80, 1024-bit) + // (this would not be true for P-521, but I'm not supporting that yet) + // The 3rd byte will be '02', signifying INTEGER + // The 4th byte will tell us the length of 'r' (which, on occassion, will be less than the full 255 bytes) + var rIndex = 3; + var rLen = sig[rIndex]; + var rEnd = rIndex + 1 + rLen; + var sIndex = rEnd + 1; + var sLen = sig[sIndex]; + var sEnd = sIndex + 1 + sLen; + var r = sig.slice(rIndex + 1, rEnd); + var s = sig.slice(sIndex + 1, sEnd); // this should be end-of-file + + // ASN1 INTEGER types use the high-order bit to signify a negative number, + // hence a leading '00' is used for numbers that begin with '80' or greater + // which is why r length is sometimes a byte longer than its bit length + if (0 === s[0]) { s = s.slice(1); } + if (0 === r[0]) { r = r.slice(1); } + + return { raw: sig.buffer, r: r.buffer, s: s.buffer }; +}; diff --git a/lib/csr.js b/lib/csr.js new file mode 100644 index 0000000..aab4763 --- /dev/null +++ b/lib/csr.js @@ -0,0 +1,213 @@ +'use strict'; + +var crypto = require('crypto'); +var ASN1 = require('./asn1.js'); +var Enc = require('./encoding.js'); +var PEM = require('./pem.js'); +var X509 = require('./x509.js'); +var RSA = {}; + +/*global Promise*/ +var CSR = module.exports = function rsacsr(opts) { + // We're using a Promise here to be compatible with the browser version + // which will probably use the webcrypto API for some of the conversions + opts = CSR._prepare(opts); + + return CSR.create(opts).then(function (bytes) { + return CSR._encode(opts, bytes); + }); +}; + +CSR._prepare = function (opts) { + var Rasha; + opts = JSON.parse(JSON.stringify(opts)); + var pem, jwk; + + // We do a bit of extra error checking for user convenience + if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); } + if (!Array.isArray(opts.domains) || 0 === opts.domains.length) { + new Error("You must pass options.domains as a non-empty array"); + } + + // I need to check that 例.中国 is a valid domain name + if (!opts.domains.every(function (d) { + // allow punycode? xn-- + if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) { + return true; + } + })) { + throw new Error("You must pass options.domains as strings"); + } + + if (opts.pem) { + pem = opts.pem; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + if (!opts.key) { + throw new Error("You must pass options.key as a JSON web key"); + } else if (opts.key.kty) { + jwk = opts.key; + } else { + pem = opts.key; + } + } + + if (pem) { + try { + Rasha = require('rasha'); + } catch(e) { + throw new Error("Rasha.js is an optional dependency for PEM-to-JWK.\n" + + "Install it if you'd like to use it:\n" + + "\tnpm install --save rasha\n" + + "Otherwise supply a jwk as the private key." + ); + } + jwk = Rasha.importSync({ pem: pem }); + } + + opts.jwk = jwk; + return opts; +}; +CSR.sync = function (opts) { + opts = CSR._prepare(opts); + var bytes = CSR.createSync(opts); + return CSR._encode(opts, bytes); +}; +CSR._encode = function (opts, bytes) { + if ('der' === (opts.encoding||'').toLowerCase()) { + return bytes; + } + return PEM.packBlock({ + type: "CERTIFICATE REQUEST" + , bytes: bytes /* { jwk: jwk, domains: opts.domains } */ + }); +}; + +CSR.createSync = function createCsr(opts) { + var hex = CSR.request(opts.jwk, opts.domains); + var csr = CSR.signSync(opts.jwk, hex); + return Enc.hexToBuf(csr); +}; +CSR.create = function createCsr(opts) { + var hex = CSR.request(opts.jwk, opts.domains); + return CSR.sign(opts.jwk, hex).then(function (csr) { + return Enc.hexToBuf(csr); + }); +}; + +CSR.request = function createCsrBodyEc(jwk, domains) { + var asn1pub = X509.packCsrPublicKey(jwk); + return X509.packCsr(asn1pub, domains); +}; + +CSR.signSync = function csrEcSig(jwk, request) { + var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); + var sig = RSA.signSync(keypem, Enc.hexToBuf(request)); + return CSR.toDer({ request: request, signature: sig }); +}; +CSR.sign = function csrEcSig(jwk, request) { + var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); + return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) { + return CSR.toDer({ request: request, signature: sig }); + }); +}; +CSR.toDer = function encode(opts) { + var sty = ASN1('30' + // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) + , ASN1('06', '2a864886f70d01010b') + , ASN1('05') + ); + return ASN1('30' + // The Full CSR Request Body + , opts.request + // The Signature Type + , sty + // The Signature + , ASN1.BitStr(Enc.bufToHex(opts.signature)) + ); +}; + +// +// RSA +// + +// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a +RSA.signSync = function signRsaSync(keypem, ab) { + // Signer is a stream + var sign = crypto.createSign('SHA256'); + sign.write(new Uint8Array(ab)); + sign.end(); + + // The signature is ASN1 encoded, as it turns out + var sig = sign.sign(keypem); + + // Convert to a JavaScript ArrayBuffer just because + return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); +}; +RSA.sign = function signRsa(keypem, ab) { + return Promise.resolve().then(function () { + return RSA.signSync(keypem, ab); + }); +}; + + +X509.packCsrRsa = function (asn1pubkey, domains) { + return ASN1('30' + // Version (0) + , ASN1.UInt('00') + + // 2.5.4.3 commonName (X.520 DN component) + , ASN1('30', ASN1('31', ASN1('30', ASN1('06', '550403'), ASN1('0c', Enc.utf8ToHex(domains[0]))))) + + // Public Key (RSA or EC) + , asn1pubkey + + // Request Body + , ASN1('a0' + , ASN1('30' + // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) + , ASN1('06', '2a864886f70d01090e') + , ASN1('31' + , ASN1('30' + , ASN1('30' + // 2.5.29.17 subjectAltName (X.509 extension) + , ASN1('06', '551d11') + , ASN1('04' + , ASN1('30', domains.map(function (d) { + return ASN1('82', Enc.utf8ToHex(d)); + }).join('')))))))) + ); +}; + +X509.packPkcs1 = function (jwk) { + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + + if (!jwk.d) { + return Enc.hexToBuf(ASN1('30', n, e)); + } + + return Enc.hexToBuf(ASN1('30' + , ASN1.UInt('00') + , n + , e + , ASN1.UInt(Enc.base64ToHex(jwk.d)) + , ASN1.UInt(Enc.base64ToHex(jwk.p)) + , ASN1.UInt(Enc.base64ToHex(jwk.q)) + , ASN1.UInt(Enc.base64ToHex(jwk.dp)) + , ASN1.UInt(Enc.base64ToHex(jwk.dq)) + , ASN1.UInt(Enc.base64ToHex(jwk.qi)) + )); +}; + +X509.packCsrRsaPublicKey = function (jwk) { + // Sequence the key + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + var asn1pub = ASN1('30', n, e); + //var asn1pub = X509.packPkcs1({ kty: jwk.kty, n: jwk.n, e: jwk.e }); + + // Add the CSR pub key header + return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); +}; -- 2.38.5 From 07f75f8e430c4eec022f7bf8db4bb9af67bd838d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 4 May 2019 00:59:47 -0600 Subject: [PATCH 10/17] working RSA CSR! --- app.js | 158 +++++++++++++++++++++++--------------- index.html | 14 +++- lib/bluecrypt-encoding.js | 2 + lib/csr.js | 78 +++++++------------ 4 files changed, 137 insertions(+), 115 deletions(-) diff --git a/app.js b/app.js index 28c9e63..fc3d9da 100644 --- a/app.js +++ b/app.js @@ -6,7 +6,9 @@ var Rasha = window.Rasha; var Eckles = window.Eckles; var x509 = window.x509; + var CSR = window.CSR; var ACME = window.ACME; + var accountStuff = {}; function $(sel) { return document.querySelector(sel); @@ -15,6 +17,11 @@ return Array.prototype.slice.call(document.querySelectorAll(sel)); } + function checkTos(tos) { + console.log("TODO checkbox for agree to terms"); + return tos; + } + function run() { console.log('hello'); @@ -101,6 +108,9 @@ $$('input').map(function ($el) { $el.disabled = false; }); $$('button').map(function ($el) { $el.disabled = false; }); $('.js-toc-jwk').hidden = false; + + $('.js-create-account').hidden = false; + $('.js-create-csr').hidden = false; }); }); @@ -115,74 +125,17 @@ console.log('acme result', result); var privJwk = JSON.parse($('.js-jwk').innerText).private; var email = $('.js-email').value; - function checkTos(tos) { - console.log("TODO checkbox for agree to terms"); - return tos; - } return acme.accounts.create({ email: email , agreeToTerms: checkTos , accountKeypair: { privateKeyJwk: privJwk } }).then(function (account) { console.log("account created result:", account); - return Keypairs.generate({ - kty: 'RSA' - , modulusLength: 2048 - }).then(function (pair) { - console.log('domain keypair:', pair); - var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - return acme.certificates.create({ - accountKeypair: { privateKeyJwk: privJwk } - , account: account - , domainKeypair: { privateKeyJwk: pair.private } - , email: email - , domains: domains - , agreeToTerms: checkTos - , challenges: { - 'dns-01': { - set: function (opts) { - console.info('dns-01 set challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); - } - , remove: function (opts) { - console.log('dns-01 remove challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); - } - } - , 'http-01': { - set: function (opts) { - console.info('http-01 set challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); - } - , remove: function (opts) { - console.log('http-01 remove challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); - } - } - } - , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] - }); - }); + accountStuff.account = account; + accountStuff.privateJwk = privJwk; + accountStuff.email = email; + accountStuff.acme = acme; + $('.js-create-order').hidden = false; }).catch(function (err) { console.error("A bad thing happened:"); console.error(err); @@ -191,8 +144,87 @@ }); }); + $('form.js-csr').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); + var privJwk = JSON.parse($('.js-jwk').innerText).private; + return CSR({ jwk: privJwk, domains: domains }).then(function (web64) { + // Verify with https://www.sslshopper.com/csr-decoder.html + console.log('urlBase64 CSR:'); + console.log(web64); + }); + }); + + $('form.js-acme-order').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var account = accountStuff.account; + var privJwk = accountStuff.privateJwk; + var email = accountStuff.email; + var acme = accountStuff.acme; + + return Keypairs.generate({ + kty: 'RSA' + , modulusLength: 2048 + }).then(function (pair) { + console.log('domain keypair:', pair); + var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); + return acme.certificates.create({ + accountKeypair: { privateKeyJwk: privJwk } + , account: account + , domainKeypair: { privateKeyJwk: pair.private } + , email: email + , domains: domains + , agreeToTerms: checkTos + , challenges: { + 'dns-01': { + set: function (opts) { + console.info('dns-01 set challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('dns-01 remove challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } + } + , 'http-01': { + set: function (opts) { + console.info('http-01 set challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('http-01 remove challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } + } + } + , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] + }); + }); + }); + $('.js-generate').hidden = false; - $('.js-create-account').hidden = false; } window.addEventListener('load', run); diff --git a/index.html b/index.html index f984d04..4379cdb 100644 --- a/index.html +++ b/index.html @@ -58,15 +58,26 @@
+ + + +

Certificate Signing Request

+

+ +
+ +

ACME Certificate Order

+
+ Challenge type:
- +
@@ -117,6 +128,7 @@ + diff --git a/lib/bluecrypt-encoding.js b/lib/bluecrypt-encoding.js index 7dc1073..c2473a6 100644 --- a/lib/bluecrypt-encoding.js +++ b/lib/bluecrypt-encoding.js @@ -110,6 +110,8 @@ Enc.binToHex = function (bin) { return h; }).join(''); }; +// TODO are there any nuance differences here? +Enc.utf8ToHex = Enc.binToHex; Enc.hexToBase64 = function (hex) { return btoa(Enc.hexToBin(hex)); diff --git a/lib/csr.js b/lib/csr.js index aab4763..bd21fa3 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -1,14 +1,18 @@ +// Copyright 2018-present AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function (exports) { 'use strict'; -var crypto = require('crypto'); -var ASN1 = require('./asn1.js'); -var Enc = require('./encoding.js'); -var PEM = require('./pem.js'); -var X509 = require('./x509.js'); -var RSA = {}; +var ASN1 = exports.ASN1; +var Enc = exports.Enc; +var PEM = exports.PEM; +var X509 = exports.x509; +var Keypairs = exports.Keypairs; -/*global Promise*/ -var CSR = module.exports = function rsacsr(opts) { +// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken +var CSR = exports.CSR = function (opts) { // We're using a Promise here to be compatible with the browser version // which will probably use the webcrypto API for some of the conversions opts = CSR._prepare(opts); @@ -69,11 +73,7 @@ CSR._prepare = function (opts) { opts.jwk = jwk; return opts; }; -CSR.sync = function (opts) { - opts = CSR._prepare(opts); - var bytes = CSR.createSync(opts); - return CSR._encode(opts, bytes); -}; + CSR._encode = function (opts, bytes) { if ('der' === (opts.encoding||'').toLowerCase()) { return bytes; @@ -84,11 +84,6 @@ CSR._encode = function (opts, bytes) { }); }; -CSR.createSync = function createCsr(opts) { - var hex = CSR.request(opts.jwk, opts.domains); - var csr = CSR.signSync(opts.jwk, hex); - return Enc.hexToBuf(csr); -}; CSR.create = function createCsr(opts) { var hex = CSR.request(opts.jwk, opts.domains); return CSR.sign(opts.jwk, hex).then(function (csr) { @@ -96,22 +91,25 @@ CSR.create = function createCsr(opts) { }); }; +// +// RSA +// +// CSR.request = function createCsrBodyEc(jwk, domains) { - var asn1pub = X509.packCsrPublicKey(jwk); - return X509.packCsr(asn1pub, domains); + var asn1pub = X509.packCsrRsaPublicKey(jwk); + return X509.packCsrRsa(asn1pub, domains); }; -CSR.signSync = function csrEcSig(jwk, request) { - var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); - var sig = RSA.signSync(keypem, Enc.hexToBuf(request)); - return CSR.toDer({ request: request, signature: sig }); -}; CSR.sign = function csrEcSig(jwk, request) { - var keypem = PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: X509.packPkcs1(jwk) }); - return RSA.sign(keypem, Enc.hexToBuf(request)).then(function (sig) { + // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a + // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same) + // TODO have a consistent non-private way to sign + return Keypairs._sign({ jwk: jwk }, Enc.hexToBuf(request)).then(function (sig) { return CSR.toDer({ request: request, signature: sig }); }); }; + + CSR.toDer = function encode(opts) { var sty = ASN1('30' // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) @@ -128,30 +126,6 @@ CSR.toDer = function encode(opts) { ); }; -// -// RSA -// - -// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a -RSA.signSync = function signRsaSync(keypem, ab) { - // Signer is a stream - var sign = crypto.createSign('SHA256'); - sign.write(new Uint8Array(ab)); - sign.end(); - - // The signature is ASN1 encoded, as it turns out - var sig = sign.sign(keypem); - - // Convert to a JavaScript ArrayBuffer just because - return new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); -}; -RSA.sign = function signRsa(keypem, ab) { - return Promise.resolve().then(function () { - return RSA.signSync(keypem, ab); - }); -}; - - X509.packCsrRsa = function (asn1pubkey, domains) { return ASN1('30' // Version (0) @@ -211,3 +185,5 @@ X509.packCsrRsaPublicKey = function (jwk) { // Add the CSR pub key header return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); }; + +}('undefined' === typeof window ? module.exports : window)); -- 2.38.5 From 274e47b726a34a99bcf672efc0822b06d1f059f9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 03:35:22 -0600 Subject: [PATCH 11/17] generates seemingly-valid EC and RSA CSRs --- app.js | 7 +- lib/csr-ec.js | 157 -------------------------------------------- lib/csr.js | 168 +++++++++++++++++++++++++----------------------- lib/keypairs.js | 76 +++++++++------------- lib/x509.js | 2 +- 5 files changed, 125 insertions(+), 285 deletions(-) delete mode 100644 lib/csr-ec.js diff --git a/app.js b/app.js index fc3d9da..1185815 100644 --- a/app.js +++ b/app.js @@ -58,8 +58,10 @@ , namedCurve: $('input[name="ec-crv"]:checked').value , modulusLength: $('input[name="rsa-len"]:checked').value }; + var then = Date.now(); console.log('opts', opts); Keypairs.generate(opts).then(function (results) { + console.log("Key generation time:", (Date.now() - then) + "ms"); var pubDer; var privDer; if (/EC/i.test(opts.kty)) { @@ -165,8 +167,9 @@ var acme = accountStuff.acme; return Keypairs.generate({ - kty: 'RSA' - , modulusLength: 2048 + kty: $('input[name="kty"]:checked').value + , namedCurve: $('input[name="ec-crv"]:checked').value + , modulusLength: $('input[name="rsa-len"]:checked').value }).then(function (pair) { console.log('domain keypair:', pair); var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); diff --git a/lib/csr-ec.js b/lib/csr-ec.js deleted file mode 100644 index 72ee072..0000000 --- a/lib/csr-ec.js +++ /dev/null @@ -1,157 +0,0 @@ -// 1.2.840.10045.3.1.7 -// prime256v1 (ANSI X9.62 named elliptic curve) -var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); -// 1.3.132.0.34 -// secp384r1 (SECG (Certicom) named elliptic curve) -var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); - -var ECDSACSR = {}; -var ECDSA = {}; -var DER = {}; -var PEM = {}; -var ASN1; -var Hex = {}; -var AB = {}; - -// -// CSR - the main event -// - -ECDSACSR.create = function createEcCsr(keypem, domains) { - var pemblock = PEM.parseBlock(keypem); - var ecpub = PEM.parseEcPubkey(pemblock.der); - var request = ECDSACSR.request(ecpub, domains); - return AB.fromHex(ECDSACSR.sign(keypem, request)); -}; - -ECDSACSR.request = function createCsrBodyEc(xy, domains) { - var publen = xy.x.byteLength; - var compression = '04'; - var hxy = ''; - // 04 == x+y, 02 == x-only - if (xy.y) { - publen += xy.y.byteLength; - } else { - // Note: I don't intend to support compression - it isn't used by most - // libraries and it requir more dependencies for bigint ops to deflate. - // This is more just a placeholder. It won't work right now anyway - // because compression requires an exta bit stored (odd vs even), which - // I haven't learned yet, and I'm not sure if it's allowed at all - compression = '02'; - } - hxy += Hex.fromAB(xy.x); - if (xy.y) { hxy += Hex.fromAB(xy.y); } - - // Sorry for the mess, but it is what it is - return ASN1('30' - - // Version (0) - , ASN1.UInt('00') - - // CN / Subject - , ASN1('30' - , ASN1('31' - , ASN1('30' - // object id (commonName) - , ASN1('06', '55 04 03') - , ASN1('0C', Hex.fromString(domains[0]))))) - - // EC P-256 Public Key - , ASN1('30' - , ASN1('30' - // 1.2.840.10045.2.1 ecPublicKey - // (ANSI X9.62 public key type) - , ASN1('06', '2A 86 48 CE 3D 02 01') - // 1.2.840.10045.3.1.7 prime256v1 - // (ANSI X9.62 named elliptic curve) - , ASN1('06', '2A 86 48 CE 3D 03 01 07') - ) - , ASN1.BitStr(compression + hxy)) - - // CSR Extension Subject Alternative Names - , ASN1('A0' - , ASN1('30' - // (extensionRequest (PKCS #9 via CRMF)) - , ASN1('06', '2A 86 48 86 F7 0D 01 09 0E') - , ASN1('31' - , ASN1('30' - , ASN1('30' - // (subjectAltName (X.509 extension)) - , ASN1('06', '55 1D 11') - , ASN1('04' - , ASN1('30', domains.map(function (d) { - return ASN1('82', Hex.fromString(d)); - }).join('')))))))) - ); -}; - -ECDSACSR.sign = function csrEcSig(keypem, request) { - var sig = ECDSA.sign(keypem, AB.fromHex(request)); - var rLen = sig.r.byteLength; - var rc = ''; - var sLen = sig.s.byteLength; - var sc = ''; - - if (0x80 & new Uint8Array(sig.r)[0]) { rc = '00'; rLen += 1; } - if (0x80 & new Uint8Array(sig.s)[0]) { sc = '00'; sLen += 1; } - - return ASN1('30' - // The Full CSR Request Body - , request - - // The Signature Type - , ASN1('30' - // 1.2.840.10045.4.3.2 ecdsaWithSHA256 - // (ANSI X9.62 ECDSA algorithm with SHA256) - , ASN1('06', '2A 86 48 CE 3D 04 03 02') - ) - - // The Signature, embedded in a Bit Stream - , ASN1.BitStr( - // As far as I can tell this is a completely separate ASN.1 structure - // that just so happens to be embedded in a Bit String of another ASN.1 - ASN1('30' - , ASN1.UInt(Hex.fromAB(sig.r)) - , ASN1.UInt(Hex.fromAB(sig.s)))) - ); -}; - -// -// ECDSA -// - -// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a -ECDSA.sign = function signEc(keypem, ab) { - // Signer is a stream - var sign = crypto.createSign('SHA256'); - sign.write(new Uint8Array(ab)); - sign.end(); - - // The signature is ASN1 encoded - var sig = sign.sign(keypem); - - // Convert to a JavaScript ArrayBuffer just because - sig = new Uint8Array(sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength)); - - // The first two bytes '30 xx' signify SEQUENCE and LENGTH - // The sequence length byte will be a single byte because the signature is less that 128 bytes (0x80, 1024-bit) - // (this would not be true for P-521, but I'm not supporting that yet) - // The 3rd byte will be '02', signifying INTEGER - // The 4th byte will tell us the length of 'r' (which, on occassion, will be less than the full 255 bytes) - var rIndex = 3; - var rLen = sig[rIndex]; - var rEnd = rIndex + 1 + rLen; - var sIndex = rEnd + 1; - var sLen = sig[sIndex]; - var sEnd = sIndex + 1 + sLen; - var r = sig.slice(rIndex + 1, rEnd); - var s = sig.slice(sIndex + 1, sEnd); // this should be end-of-file - - // ASN1 INTEGER types use the high-order bit to signify a negative number, - // hence a leading '00' is used for numbers that begin with '80' or greater - // which is why r length is sometimes a byte longer than its bit length - if (0 === s[0]) { s = s.slice(1); } - if (0 === r[0]) { r = r.slice(1); } - - return { raw: sig.buffer, r: r.buffer, s: s.buffer }; -}; diff --git a/lib/csr.js b/lib/csr.js index bd21fa3..89609e2 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function (exports) { 'use strict'; +/*global Promise*/ var ASN1 = exports.ASN1; var Enc = exports.Enc; @@ -15,63 +16,57 @@ var Keypairs = exports.Keypairs; var CSR = exports.CSR = function (opts) { // We're using a Promise here to be compatible with the browser version // which will probably use the webcrypto API for some of the conversions - opts = CSR._prepare(opts); - - return CSR.create(opts).then(function (bytes) { - return CSR._encode(opts, bytes); + return CSR._prepare(opts).then(function (opts) { + return CSR.create(opts).then(function (bytes) { + return CSR._encode(opts, bytes); + }); }); }; CSR._prepare = function (opts) { - var Rasha; - opts = JSON.parse(JSON.stringify(opts)); - var pem, jwk; + return Promise.resolve().then(function () { + var Keypairs; + opts = JSON.parse(JSON.stringify(opts)); - // We do a bit of extra error checking for user convenience - if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); } - if (!Array.isArray(opts.domains) || 0 === opts.domains.length) { - new Error("You must pass options.domains as a non-empty array"); - } - - // I need to check that 例.中国 is a valid domain name - if (!opts.domains.every(function (d) { - // allow punycode? xn-- - if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) { - return true; + // We do a bit of extra error checking for user convenience + if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); } + if (!Array.isArray(opts.domains) || 0 === opts.domains.length) { + new Error("You must pass options.domains as a non-empty array"); } - })) { - throw new Error("You must pass options.domains as strings"); - } - if (opts.pem) { - pem = opts.pem; - } else if (opts.jwk) { - jwk = opts.jwk; - } else { - if (!opts.key) { + // I need to check that 例.中国 is a valid domain name + if (!opts.domains.every(function (d) { + // allow punycode? xn-- + if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) { + return true; + } + })) { + throw new Error("You must pass options.domains as strings"); + } + + if (opts.jwk) { return opts; } + if (opts.key && opts.key.kty) { + opts.jwk = opts.key; + return opts; + } + if (!opts.pem && !opts.key) { throw new Error("You must pass options.key as a JSON web key"); - } else if (opts.key.kty) { - jwk = opts.key; - } else { - pem = opts.key; } - } - if (pem) { - try { - Rasha = require('rasha'); - } catch(e) { - throw new Error("Rasha.js is an optional dependency for PEM-to-JWK.\n" + Keypairs = exports.Keypairs; + if (!exports.Keypairs) { + throw new Error("Keypairs.js is an optional dependency for PEM-to-JWK.\n" + "Install it if you'd like to use it:\n" + "\tnpm install --save rasha\n" + "Otherwise supply a jwk as the private key." ); } - jwk = Rasha.importSync({ pem: pem }); - } - opts.jwk = jwk; - return opts; + return Keypairs.import({ pem: opts.pem || opts.key }).then(function (pair) { + opts.jwk = pair.private; + return opts; + }); + }); }; CSR._encode = function (opts, bytes) { @@ -86,47 +81,56 @@ CSR._encode = function (opts, bytes) { CSR.create = function createCsr(opts) { var hex = CSR.request(opts.jwk, opts.domains); - return CSR.sign(opts.jwk, hex).then(function (csr) { + return CSR._sign(opts.jwk, hex).then(function (csr) { return Enc.hexToBuf(csr); }); }; // -// RSA -// +// EC / RSA // CSR.request = function createCsrBodyEc(jwk, domains) { - var asn1pub = X509.packCsrRsaPublicKey(jwk); - return X509.packCsrRsa(asn1pub, domains); + var asn1pub; + if (/^EC/i.test(jwk.kty)) { + asn1pub = X509.packCsrEcPublicKey(jwk); + } else { + asn1pub = X509.packCsrRsaPublicKey(jwk); + } + return X509.packCsr(asn1pub, domains); }; -CSR.sign = function csrEcSig(jwk, request) { +CSR._sign = function csrEcSig(jwk, request) { // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same) // TODO have a consistent non-private way to sign - return Keypairs._sign({ jwk: jwk }, Enc.hexToBuf(request)).then(function (sig) { - return CSR.toDer({ request: request, signature: sig }); + return Keypairs._sign({ jwk: jwk, format: 'x509' }, Enc.hexToBuf(request)).then(function (sig) { + return CSR._toDer({ request: request, signature: sig, kty: jwk.kty }); }); }; - -CSR.toDer = function encode(opts) { - var sty = ASN1('30' +CSR._toDer = function encode(opts) { + var sty; + var sig; + if (/^EC/i.test(opts.kty)) { + // 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256) + sty = ASN1('30', ASN1('06', '2a8648ce3d040302')); + sig = ASN1.BitStr(ASN1('30', Enc.bufToHex(opts.signature))); + } else { // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) - , ASN1('06', '2a864886f70d01010b') - , ASN1('05') - ); + sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05')); + sig = ASN1.BitStr(Enc.bufToHex(opts.signature)); + } return ASN1('30' // The Full CSR Request Body , opts.request // The Signature Type , sty // The Signature - , ASN1.BitStr(Enc.bufToHex(opts.signature)) + , sig ); }; -X509.packCsrRsa = function (asn1pubkey, domains) { +X509.packCsr = function (asn1pubkey, domains) { return ASN1('30' // Version (0) , ASN1.UInt('00') @@ -154,36 +158,42 @@ X509.packCsrRsa = function (asn1pubkey, domains) { ); }; -X509.packPkcs1 = function (jwk) { - var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); - var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); - - if (!jwk.d) { - return Enc.hexToBuf(ASN1('30', n, e)); - } - - return Enc.hexToBuf(ASN1('30' - , ASN1.UInt('00') - , n - , e - , ASN1.UInt(Enc.base64ToHex(jwk.d)) - , ASN1.UInt(Enc.base64ToHex(jwk.p)) - , ASN1.UInt(Enc.base64ToHex(jwk.q)) - , ASN1.UInt(Enc.base64ToHex(jwk.dp)) - , ASN1.UInt(Enc.base64ToHex(jwk.dq)) - , ASN1.UInt(Enc.base64ToHex(jwk.qi)) - )); -}; - X509.packCsrRsaPublicKey = function (jwk) { // Sequence the key var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); var asn1pub = ASN1('30', n, e); - //var asn1pub = X509.packPkcs1({ kty: jwk.kty, n: jwk.n, e: jwk.e }); // Add the CSR pub key header return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); }; +X509.packCsrEcPublicKey = function (jwk) { + var ecOid = X509._oids[jwk.crv]; + if (!ecOid) { + throw new Error("Unsupported namedCurve '" + jwk.crv + "'. Supported types are " + Object.keys(X509._oids)); + } + var cmp = '04'; // 04 == x+y, 02 == x-only + var hxy = ''; + // Placeholder. I'm not even sure if compression should be supported. + if (!jwk.y) { cmp = '02'; } + hxy += Enc.base64ToHex(jwk.x); + if (jwk.y) { hxy += Enc.base64ToHex(jwk.y); } + + // 1.2.840.10045.2.1 ecPublicKey + return ASN1('30', ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)), ASN1.BitStr(cmp + hxy)); +}; +X509._oids = { + // 1.2.840.10045.3.1.7 prime256v1 + // (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07) + 'P-256': '2a8648ce3d030107' + // 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22) + // (SEC 2 recommended EC domain secp256r1) +, 'P-384': '2b81040022' + // requires more logic and isn't a recommended standard + // 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23) + // (SEC 2 alternate P-521) +//, 'P-521': '2B 81 04 00 23' +}; + }('undefined' === typeof window ? module.exports : window)); diff --git a/lib/keypairs.js b/lib/keypairs.js index 9d6cf3d..2e423f9 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -180,14 +180,6 @@ Keypairs.signJws = function (opts) { var msg = protected64 + '.' + payload64; return Keypairs._sign(opts, msg).then(function (buf) { - /* - * This will come back into play for CSRs, but not for JOSE - if ('EC' === opts.jwk.kty) { - // ECDSA JWT signatures differ from "normal" ECDSA signatures - // https://tools.ietf.org/html/rfc7518#section-3.4 - binsig = convertIfEcdsa(binsig); - } - */ var signedMsg = { protected: protected64 , payload: payload64 @@ -212,40 +204,6 @@ Keypairs.signJws = function (opts) { } }); }; -Keypairs._convertIfEcdsa = function (binsig) { - // should have asn1 sequence header of 0x30 - if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } - var index = 2; // first ecdsa "R" header byte - var len = binsig[1]; - var lenlen = 0; - // Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values) - if (0x80 & len) { - lenlen = len - 0x80; // should be exactly 1 - len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding) - index += lenlen; - } - // should be of BigInt type - if (0x02 !== binsig[index]) { throw new Error("Impossible EC SHA R marker"); } - index += 1; - - var rlen = binsig[index]; - var bits = 32; - if (rlen > 49) { - bits = 64; - } else if (rlen > 33) { - bits = 48; - } - var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex'); - var slen = binsig[index + 1 + rlen + 1]; // skip header and read length - var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex'); - if (2 *slen !== s.length) { throw new Error("Impossible EC SHA S length"); } - // There may be one byte of padding on either - while (r.length < 2*bits) { r = '00' + r; } - while (s.length < 2*bits) { s = '00' + s; } - if (2*(bits+1) === r.length) { r = r.slice(2); } - if (2*(bits+1) === s.length) { s = s.slice(2); } - return Enc.hexToBuf(r + s); -}; Keypairs._sign = function (opts, payload) { return Keypairs._import(opts).then(function (privkey) { @@ -259,9 +217,12 @@ Keypairs._sign = function (opts, payload) { , privkey , payload ).then(function (signature) { - // convert buffer to urlsafe base64 - //return Enc.bufToUrlBase64(new Uint8Array(signature)); - return new Uint8Array(signature); + signature = new Uint8Array(signature); // ArrayBuffer -> u8 + // This will come back into play for CSRs, but not for JOSE + if ('EC' === opts.jwk.kty && /x509/i.test(opts.format)) { + signature = Keypairs._ecdsaJoseSigToAsn1Sig(signature); + } + return signature; }); }); }; @@ -287,7 +248,6 @@ Keypairs._getName = function (opts) { return 'RSASSA-PKCS1-v1_5'; } }; - Keypairs._import = function (opts) { return Promise.resolve().then(function () { var ops; @@ -316,6 +276,30 @@ Keypairs._import = function (opts) { }); }); }; +// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures +// https://tools.ietf.org/html/rfc7518#section-3.4 +Keypairs._ecdsaJoseSigToAsn1Sig = function (bufsig) { + // it's easier to do the manipulation in the browser with an array + bufsig = Array.from(bufsig); + var hlen = bufsig.length / 2; // should be even + var r = bufsig.slice(0, hlen); + var s = bufsig.slice(hlen); + // unpad positive ints less than 32 bytes wide + while (!r[0]) { r = r.slice(1); } + while (!s[0]) { s = s.slice(1); } + // pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide + if (0x80 & r[0]) { r.unshift(0); } + if (0x80 & s[0]) { s.unshift(0); } + + var len = 2 + r.length + 2 + s.length; + var head = [0x30]; + // hard code 0x80 + 1 because it won't be longer than + // two SHA512 plus two pad bytes (130 bytes <= 256) + if (len >= 0x80) { head.push(0x81); } + head.push(len); + + return Uint8Array.from(head.concat([0x02, r.length], r, [0x02, s.byteLength], s)); +}; function setTime(time) { if ('number' === typeof time) { return time; } diff --git a/lib/x509.js b/lib/x509.js index 901bb36..575b8c9 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -1,6 +1,6 @@ -'use strict'; (function (exports) { 'use strict'; + var x509 = exports.x509 = {}; var ASN1 = exports.ASN1; var Enc = exports.Enc; -- 2.38.5 From 9a89e432634268cd7a23043470679506b59eb61d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 11:27:00 -0600 Subject: [PATCH 12/17] typo and other fixes --- lib/csr.js | 5 +---- lib/keypairs.js | 10 ++++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/csr.js b/lib/csr.js index 89609e2..3eb75cd 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -110,15 +110,12 @@ CSR._sign = function csrEcSig(jwk, request) { CSR._toDer = function encode(opts) { var sty; - var sig; if (/^EC/i.test(opts.kty)) { // 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256) sty = ASN1('30', ASN1('06', '2a8648ce3d040302')); - sig = ASN1.BitStr(ASN1('30', Enc.bufToHex(opts.signature))); } else { // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05')); - sig = ASN1.BitStr(Enc.bufToHex(opts.signature)); } return ASN1('30' // The Full CSR Request Body @@ -126,7 +123,7 @@ CSR._toDer = function encode(opts) { // The Signature Type , sty // The Signature - , sig + , ASN1.BitStr(Enc.bufToHex(opts.signature)) ); }; diff --git a/lib/keypairs.js b/lib/keypairs.js index 2e423f9..f81bc14 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -219,10 +219,12 @@ Keypairs._sign = function (opts, payload) { ).then(function (signature) { signature = new Uint8Array(signature); // ArrayBuffer -> u8 // This will come back into play for CSRs, but not for JOSE - if ('EC' === opts.jwk.kty && /x509/i.test(opts.format)) { - signature = Keypairs._ecdsaJoseSigToAsn1Sig(signature); + if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { + return Keypairs._ecdsaJoseSigToAsn1Sig(signature); + } else { + // jose/jws/jwt + return signature; } - return signature; }); }); }; @@ -298,7 +300,7 @@ Keypairs._ecdsaJoseSigToAsn1Sig = function (bufsig) { if (len >= 0x80) { head.push(0x81); } head.push(len); - return Uint8Array.from(head.concat([0x02, r.length], r, [0x02, s.byteLength], s)); + return Uint8Array.from(head.concat([0x02, r.length], r, [0x02, s.length], s)); }; function setTime(time) { -- 2.38.5 From 266d8a0ba02d07fb6d97a7ed564f3727a56f5858 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 15:25:15 -0600 Subject: [PATCH 13/17] also parse CSR for info --- app.js | 9 ++-- index.html | 1 + lib/acme.js | 14 ++++++- lib/asn1-parser.js | 2 +- lib/csr.js | 102 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index 1185815..c861b5b 100644 --- a/app.js +++ b/app.js @@ -151,10 +151,13 @@ ev.stopPropagation(); var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); var privJwk = JSON.parse($('.js-jwk').innerText).private; - return CSR({ jwk: privJwk, domains: domains }).then(function (web64) { + return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { // Verify with https://www.sslshopper.com/csr-decoder.html - console.log('urlBase64 CSR:'); - console.log(web64); + console.log('CSR:'); + console.log(pem); + + console.log('CSR info:'); + console.log(CSR._info(pem)); }); }); diff --git a/index.html b/index.html index 4379cdb..5978ffd 100644 --- a/index.html +++ b/index.html @@ -128,6 +128,7 @@ + diff --git a/lib/acme.js b/lib/acme.js index 87668c4..ea7113a 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -8,6 +8,7 @@ var ACME = exports.ACME = {}; //var Keypairs = exports.Keypairs || {}; +//var CSR = exports.CSR; var Enc = exports.Enc || {}; var Crypto = exports.Crypto || {}; @@ -670,6 +671,14 @@ ACME._getCertificate = function (me, options) { return Promise.reject(new Error("options.challengeTypes (string array) must be specified" + " (and in order of preferential priority).")); } + if (options.csr) { + // TODO validate csr signature + options._csr = me.CSR._info(options.csr); + options.domains = options._csr.altnames; + if (options._csr.subject !== options.domains[0]) { + return Promise.reject(new Error("certificate subject (commonName) does not match first altname (SAN)")); + } + } if (!(options.domains && options.domains.length)) { return Promise.reject(new Error("options.domains must be a list of string domain names," + " with the first being the subject of the certificate (or options.subject must specified).")); @@ -818,8 +827,8 @@ ACME._getCertificate = function (me, options) { }); }; ACME._generateCsrWeb64 = function (me, options, validatedDomains) { - return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) { - return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) { + return ACME._importKeypair(me, options.domainKeypair).then(function (pair) { + return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { return Enc.bufToUrlBase64(der); }); }); @@ -830,6 +839,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; + me.CSR = me.CSR || require('CSR').CSR; me._nonces = []; me._canCheck = {}; if (!me._baseUrl) { diff --git a/lib/asn1-parser.js b/lib/asn1-parser.js index 82f7cd0..9314aa3 100644 --- a/lib/asn1-parser.js +++ b/lib/asn1-parser.js @@ -125,7 +125,7 @@ PEM.parseBlock = PEM.parseBlock || function (str) { var der = str.split(/\n/).filter(function (line) { return !/-----/.test(line); }).join(''); - return { der: Enc.base64ToBuf(der) }; + return { bytes: Enc.base64ToBuf(der) }; }; Enc.base64ToBuf = function (b64) { diff --git a/lib/csr.js b/lib/csr.js index 3eb75cd..4f6d61b 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -155,6 +155,100 @@ X509.packCsr = function (asn1pubkey, domains) { ); }; +// TODO finish this later +// we want to parse the domains, the public key, and verify the signature +CSR._info = function (der) { + // standard base64 PEM + if ('string' === typeof der && '-' === der[0]) { + der = PEM.parseBlock(der).bytes; + } + // jose urlBase64 not-PEM + if ('string' === typeof der) { + der = Enc.base64ToBuf(der); + } + // not supporting binary-encoded bas64 + var c = ASN1.parse(der); + var kty; + // A cert has 3 parts: cert, signature meta, signature + if (c.children.length !== 3) { + throw new Error("doesn't look like a certificate request: expected 3 parts of header"); + } + var sig = c.children[2]; + if (sig.children.length) { + // ASN1/X509 EC + sig = sig.children[0]; + sig = ASN1('30', ASN1.UInt(Enc.bufToHex(sig.children[0].value)), ASN1.UInt(Enc.bufToHex(sig.children[1].value))); + sig = Enc.hexToBuf(sig); + kty = 'EC'; + } else { + // Raw RSA Sig + sig = sig.value; + kty = 'RSA'; + } + //c.children[1]; // signature type + var req = c.children[0]; + // TODO utf8 + if (4 !== req.children.length) { + throw new Error("doesn't look like a certificate request: expected 4 parts to request"); + } + // 0 null + // 1 commonName / subject + var sub = Enc.bufToBin(req.children[1].children[0].children[0].children[1].value); + // 3 public key (type, key) + //console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value)); + var pub; + // TODO reuse ASN1 parser for these? + if ('EC' === kty) { + // throw away compression byte + pub = req.children[2].children[1].value.slice(1); + pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) }; + while (0 === pub.x[0]) { pub.x = pub.x.slice(1); } + while (0 === pub.y[0]) { pub.y = pub.y.slice(1); } + if ((pub.x.length || pub.x.byteLength) > 48) { + pub.crv = 'P-521'; + } else if ((pub.x.length || pub.x.byteLength) > 32) { + pub.crv = 'P-384'; + } else { + pub.crv = 'P-256'; + } + pub.x = Enc.bufToUrlBase64(pub.x); + pub.y = Enc.bufToUrlBase64(pub.y); + } else { + pub = req.children[2].children[1].children[0]; + pub = { kty: kty, n: pub.children[0].value, e: pub.children[1].value }; + while (0 === pub.n[0]) { pub.n = pub.n.slice(1); } + while (0 === pub.e[0]) { pub.e = pub.e.slice(1); } + pub.n = Enc.bufToUrlBase64(pub.n); + pub.e = Enc.bufToUrlBase64(pub.e); + } + // 4 extensions + var domains = req.children[3].children.filter(function (seq) { + // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) + if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) { + return true; + } + }).map(function (seq) { + return seq.children[1].children[0].children.filter(function (seq2) { + // subjectAltName (X.509 extension) + if ('551d11' === Enc.bufToHex(seq2.children[0].value)) { + return true; + } + }).map(function (seq2) { + return seq2.children[1].children[0].children.map(function (name) { + // TODO utf8 + return Enc.bufToBin(name.value); + }); + }); + })[0]; + + return { + subject: sub + , altnames: domains + , jwk: pub + , signature: sig + }; +}; + X509.packCsrRsaPublicKey = function (jwk) { // Sequence the key var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); @@ -193,4 +287,12 @@ X509._oids = { //, 'P-521': '2B 81 04 00 23' }; +// don't replace the full parseBlock, if it exists +PEM.parseBlock = PEM.parseBlock || function (str) { + var der = str.split(/\n/).filter(function (line) { + return !/-----/.test(line); + }).join(''); + return { bytes: Enc.base64ToBuf(der) }; +}; + }('undefined' === typeof window ? module.exports : window)); -- 2.38.5 From 914ec5a516f19b1aa767240124da4534ec237b06 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 15:46:33 -0600 Subject: [PATCH 14/17] use existing CSR, if any --- lib/acme.js | 10 ++++++++++ lib/bluecrypt-encoding.js | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/acme.js b/lib/acme.js index ea7113a..b48f2c9 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -827,6 +827,16 @@ ACME._getCertificate = function (me, options) { }); }; ACME._generateCsrWeb64 = function (me, options, validatedDomains) { + var csr; + if (options.csr) { + csr = options.csr; + // if der, convert to base64 + if ('string' !== typeof csr) { csr = Enc.bufToUrlBase64(csr); } + // nix PEM headers, if any + if ('-' === csr[0]) { csr = csr.split(/\n+/).slice(1, -1).join(''); } + csr = Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, '')); + return Promise.resolve(csr); + } return ACME._importKeypair(me, options.domainKeypair).then(function (pair) { return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { return Enc.bufToUrlBase64(der); diff --git a/lib/bluecrypt-encoding.js b/lib/bluecrypt-encoding.js index c2473a6..a9609e5 100644 --- a/lib/bluecrypt-encoding.js +++ b/lib/bluecrypt-encoding.js @@ -66,8 +66,11 @@ Enc.numToHex = function (d) { }; Enc.bufToUrlBase64 = function (u8) { - return Enc.bufToBase64(u8) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); +}; + +Enc.base64ToUrlBase64 = function (str) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; Enc.bufToBase64 = function (u8) { -- 2.38.5 From 001667bfe0df902e466c3953daaae35b9650124f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 19:21:37 -0600 Subject: [PATCH 15/17] tested with existing CSR --- app.js | 133 +++++++++++++++++++++++++----------------------- lib/acme.js | 6 +-- lib/csr.js | 2 +- lib/keypairs.js | 5 -- 4 files changed, 74 insertions(+), 72 deletions(-) diff --git a/app.js b/app.js index c861b5b..3ea96c5 100644 --- a/app.js +++ b/app.js @@ -122,6 +122,7 @@ $('.js-loading').hidden = false; var acme = ACME.create({ Keypairs: Keypairs + , CSR: CSR }); acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { console.log('acme result', result); @@ -137,7 +138,6 @@ accountStuff.privateJwk = privJwk; accountStuff.email = email; accountStuff.acme = acme; - $('.js-create-order').hidden = false; }).catch(function (err) { console.error("A bad thing happened:"); console.error(err); @@ -150,14 +150,24 @@ ev.preventDefault(); ev.stopPropagation(); var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - var privJwk = JSON.parse($('.js-jwk').innerText).private; - return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { - // Verify with https://www.sslshopper.com/csr-decoder.html - console.log('CSR:'); - console.log(pem); + //var privJwk = JSON.parse($('.js-jwk').innerText).private; + return Keypairs.generate({ + kty: $('input[name="kty"]:checked').value + , namedCurve: $('input[name="ec-crv"]:checked').value + , modulusLength: $('input[name="rsa-len"]:checked').value + }).then(function (pair) { + console.log('domain keypair:', pair); + accountStuff.domainPrivateJwk = pair.private; + return CSR({ jwk: pair.private, domains: domains }).then(function (pem) { + // Verify with https://www.sslshopper.com/csr-decoder.html + accountStuff.csr = pem; + console.log('CSR:'); + console.log(pem); - console.log('CSR info:'); - console.log(CSR._info(pem)); + console.log('CSR info:'); + console.log(CSR._info(pem)); + $('.js-create-order').hidden = false; + }); }); }); @@ -169,64 +179,61 @@ var email = accountStuff.email; var acme = accountStuff.acme; - return Keypairs.generate({ - kty: $('input[name="kty"]:checked').value - , namedCurve: $('input[name="ec-crv"]:checked').value - , modulusLength: $('input[name="rsa-len"]:checked').value - }).then(function (pair) { - console.log('domain keypair:', pair); - var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - return acme.certificates.create({ - accountKeypair: { privateKeyJwk: privJwk } - , account: account - , domainKeypair: { privateKeyJwk: pair.private } - , email: email - , domains: domains - , agreeToTerms: checkTos - , challenges: { - 'dns-01': { - set: function (opts) { - console.info('dns-01 set challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); - } - , remove: function (opts) { - console.log('dns-01 remove challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); - } + + var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); + return acme.certificates.create({ + accountKeypair: { privateKeyJwk: privJwk } + , account: account + //, domainKeypair: { privateKeyJwk: accountStuff.domainPrivateJwk } + , csr: accountStuff.csr + , email: email + , domains: domains + , agreeToTerms: checkTos + , challenges: { + 'dns-01': { + set: function (opts) { + console.info('dns-01 set challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); } - , 'http-01': { - set: function (opts) { - console.info('http-01 set challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); - } - , remove: function (opts) { - console.log('http-01 remove challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); - } + , remove: function (opts) { + console.log('dns-01 remove challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); } } - , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] - }); + , 'http-01': { + set: function (opts) { + console.info('http-01 set challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('http-01 remove challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } + } + } + , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] + }).catch(function (err) { + window.alert("failed! " + err.message || JSON.stringify(err)); }); }); diff --git a/lib/acme.js b/lib/acme.js index b48f2c9..a5f95d9 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -848,10 +848,10 @@ ACME.create = function create(me) { if (!me) { me = {}; } // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; - me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; - me.CSR = me.CSR || require('CSR').CSR; + me.Keypairs = me.Keypairs || exports.Keypairs || require('keypairs').Keypairs; + me.CSR = me.CSR || exports.cSR || require('CSR').CSR; me._nonces = []; - me._canCheck = {}; + me._canUse = {}; if (!me._baseUrl) { me._baseUrl = ""; } diff --git a/lib/csr.js b/lib/csr.js index 4f6d61b..12834e0 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -238,7 +238,7 @@ CSR._info = function (der) { // TODO utf8 return Enc.bufToBin(name.value); }); - }); + })[0]; })[0]; return { diff --git a/lib/keypairs.js b/lib/keypairs.js index f81bc14..932bc65 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -186,10 +186,6 @@ Keypairs.signJws = function (opts) { , signature: Enc.bufToUrlBase64(buf) }; - console.log('Signed Base64 Msg:'); - console.log(JSON.stringify(signedMsg, null, 2)); - - console.log('msg:', msg); return signedMsg; }); } @@ -263,7 +259,6 @@ Keypairs._import = function (opts) { opts.jwk.ext = true; opts.jwk.key_ops = ops; - console.log('jwk', opts.jwk); return window.crypto.subtle.importKey( "jwk" , opts.jwk -- 2.38.5 From 009e0dc1fb949b470cca865291db381348eaf06d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 19:34:17 -0600 Subject: [PATCH 16/17] use pre-gen or jit-CSR --- app.js | 156 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 86 insertions(+), 70 deletions(-) diff --git a/app.js b/app.js index 3ea96c5..45a5024 100644 --- a/app.js +++ b/app.js @@ -138,6 +138,7 @@ accountStuff.privateJwk = privJwk; accountStuff.email = email; accountStuff.acme = acme; + $('.js-create-order').hidden = false; }).catch(function (err) { console.error("A bad thing happened:"); console.error(err); @@ -149,26 +150,7 @@ $('form.js-csr').addEventListener('submit', function (ev) { ev.preventDefault(); ev.stopPropagation(); - var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - //var privJwk = JSON.parse($('.js-jwk').innerText).private; - return Keypairs.generate({ - kty: $('input[name="kty"]:checked').value - , namedCurve: $('input[name="ec-crv"]:checked').value - , modulusLength: $('input[name="rsa-len"]:checked').value - }).then(function (pair) { - console.log('domain keypair:', pair); - accountStuff.domainPrivateJwk = pair.private; - return CSR({ jwk: pair.private, domains: domains }).then(function (pem) { - // Verify with https://www.sslshopper.com/csr-decoder.html - accountStuff.csr = pem; - console.log('CSR:'); - console.log(pem); - - console.log('CSR info:'); - console.log(CSR._info(pem)); - $('.js-create-order').hidden = false; - }); - }); + generateCsr(); }); $('form.js-acme-order').addEventListener('submit', function (ev) { @@ -181,64 +163,98 @@ var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - return acme.certificates.create({ - accountKeypair: { privateKeyJwk: privJwk } - , account: account - //, domainKeypair: { privateKeyJwk: accountStuff.domainPrivateJwk } - , csr: accountStuff.csr - , email: email - , domains: domains - , agreeToTerms: checkTos - , challenges: { - 'dns-01': { - set: function (opts) { - console.info('dns-01 set challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); + return getDomainPrivkey().then(function () { + return acme.certificates.create({ + accountKeypair: { privateKeyJwk: privJwk } + , account: account + //, domainKeypair: { privateKeyJwk: accountStuff.domainPrivateJwk } + , csr: accountStuff.csr + , email: email + , domains: domains + , agreeToTerms: checkTos + , challenges: { + 'dns-01': { + set: function (opts) { + console.info('dns-01 set challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('dns-01 remove challenge:'); + console.info('TXT', opts.dnsHost); + console.info(opts.dnsAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } } - , remove: function (opts) { - console.log('dns-01 remove challenge:'); - console.info('TXT', opts.dnsHost); - console.info(opts.dnsAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); + , 'http-01': { + set: function (opts) { + console.info('http-01 set challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you set the challenge?")) {} + resolve(); + }); + } + , remove: function (opts) { + console.log('http-01 remove challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + return new Promise(function (resolve) { + while (!window.confirm("Did you delete the challenge?")) {} + resolve(); + }); + } } } - , 'http-01': { - set: function (opts) { - console.info('http-01 set challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you set the challenge?")) {} - resolve(); - }); - } - , remove: function (opts) { - console.log('http-01 remove challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - return new Promise(function (resolve) { - while (!window.confirm("Did you delete the challenge?")) {} - resolve(); - }); - } - } - } - , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] - }).catch(function (err) { - window.alert("failed! " + err.message || JSON.stringify(err)); + , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] + }).catch(function (err) { + window.alert("failed! " + err.message || JSON.stringify(err)); + }); }); }); $('.js-generate').hidden = false; } + function getDomainPrivkey() { + if (accountStuff.domainPrivateJwk) { return Promise.resolve(accountStuff.domainPrivateJwk); } + return Keypairs.generate({ + kty: $('input[name="kty"]:checked').value + , namedCurve: $('input[name="ec-crv"]:checked').value + , modulusLength: $('input[name="rsa-len"]:checked').value + }).then(function (pair) { + console.log('domain keypair:', pair); + accountStuff.domainPrivateJwk = pair.private; + return pair.private; + }); + } + + function generateCsr() { + var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); + //var privJwk = JSON.parse($('.js-jwk').innerText).private; + return getDomainPrivkey().then(function (privJwk) { + accountStuff.domainPrivateJwk = privJwk; + return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { + // Verify with https://www.sslshopper.com/csr-decoder.html + accountStuff.csr = pem; + console.log('CSR:'); + console.log(pem); + + console.log('CSR info:'); + console.log(CSR._info(pem)); + + return pem; + }); + }); + } + window.addEventListener('load', run); }()); -- 2.38.5 From 4ae3b19ed733776dadaaeb94665b50124863545d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 May 2019 23:05:25 -0600 Subject: [PATCH 17/17] ACME fully tested and working --- app.js | 25 +- index.html | 20 +- lib/acme.js | 1 + lib/browser-acme.js | 699 ------------------------------------------- lib/keypairs.js.min2 | 86 ------ 5 files changed, 34 insertions(+), 797 deletions(-) delete mode 100644 lib/browser-acme.js delete mode 100644 lib/keypairs.js.min2 diff --git a/app.js b/app.js index 45a5024..bbac95c 100644 --- a/app.js +++ b/app.js @@ -18,8 +18,11 @@ } function checkTos(tos) { - console.log("TODO checkbox for agree to terms"); - return tos; + if ($('input[name="tos"]:checked')) { + return tos; + } else { + return ''; + } } function run() { @@ -139,6 +142,8 @@ accountStuff.email = email; accountStuff.acme = acme; $('.js-create-order').hidden = false; + $('.js-toc-acme-account-response').hidden = false; + $('.js-acme-account-response').innerText = JSON.stringify(account, null, 2); }).catch(function (err) { console.error("A bad thing happened:"); console.error(err); @@ -163,14 +168,17 @@ var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); - return getDomainPrivkey().then(function () { + return getDomainPrivkey().then(function (domainPrivJwk) { + console.log('Has CSR already?'); + console.log(accountStuff.csr); return acme.certificates.create({ accountKeypair: { privateKeyJwk: privJwk } , account: account - //, domainKeypair: { privateKeyJwk: accountStuff.domainPrivateJwk } + , domainKeypair: { privateKeyJwk: domainPrivJwk } , csr: accountStuff.csr , email: email , domains: domains + , skipDryRun: $('input[name="skip-dryrun"]:checked') && true , agreeToTerms: checkTos , challenges: { 'dns-01': { @@ -215,7 +223,14 @@ } } , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] + }).then(function (results) { + console.log('Got Certificates:'); + console.log(results); + $('.js-toc-acme-order-response').hidden = false; + $('.js-acme-order-response').innerText = JSON.stringify(results, null, 2); }).catch(function (err) { + console.error("challenge failed:"); + console.error(err); window.alert("failed! " + err.message || JSON.stringify(err)); }); }); @@ -245,7 +260,7 @@ return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { // Verify with https://www.sslshopper.com/csr-decoder.html accountStuff.csr = pem; - console.log('CSR:'); + console.log('Created CSR:'); console.log(pem); console.log('CSR info:'); diff --git a/index.html b/index.html index 5978ffd..27f0aa5 100644 --- a/index.html +++ b/index.html @@ -56,7 +56,10 @@

ACME Account

@@ -64,7 +67,7 @@

Certificate Signing Request

- +
@@ -77,6 +80,9 @@
+ +
@@ -114,14 +120,14 @@ PEM Public (base64-encoded SPKI/PKIX DER)
- + diff --git a/lib/acme.js b/lib/acme.js index a5f95d9..ad966ec 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -837,6 +837,7 @@ ACME._generateCsrWeb64 = function (me, options, validatedDomains) { csr = Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, '')); return Promise.resolve(csr); } + return ACME._importKeypair(me, options.domainKeypair).then(function (pair) { return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { return Enc.bufToUrlBase64(der); diff --git a/lib/browser-acme.js b/lib/browser-acme.js deleted file mode 100644 index 4fba0fe..0000000 --- a/lib/browser-acme.js +++ /dev/null @@ -1,699 +0,0 @@ -/*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: '); - - 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)); diff --git a/lib/keypairs.js.min2 b/lib/keypairs.js.min2 deleted file mode 100644 index bf530b8..0000000 --- a/lib/keypairs.js.min2 +++ /dev/null @@ -1,86 +0,0 @@ -/*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)); -- 2.38.5