From df34cbf6c1083febd5fce8199de0793cb2b872e6 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 15 Mar 2018 00:41:00 -0600 Subject: [PATCH 001/252] initial commit --- README.md | 10 +++ node.js | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 28 ++++++++ 3 files changed, 225 insertions(+) create mode 100644 README.md create mode 100644 node.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..0196434 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +acme-v2.js +========== + +A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. + +In progress + +* get directory +* get nonce +* create account diff --git a/node.js b/node.js new file mode 100644 index 0000000..f3d1db4 --- /dev/null +++ b/node.js @@ -0,0 +1,187 @@ +/*! + * acme-v2.js + * Copyright(c) 2018 AJ ONeal https://ppl.family + * Apache-2.0 OR MIT (and hence also MPL 2.0) +*/ +'use strict'; + +var defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' +, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, acmeChallengePrefix: '/.well-known/acme-challenge/' +, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] +, challengeType: 'http-01' // dns-01 +, keyType: 'rsa' // ecdsa +, keySize: 2048 // 256 +}; + +function create(deps) { + if (!deps) { deps = {}; } + deps.LeCore = {}; + deps.pkg = deps.pkg || require('./package.json'); + deps.os = deps.os || require('os'); + deps.process = deps.process || require('process'); + + var uaDefaults = { + pkg: "Greenlock/" + deps.pkg.version + , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" + , node: " Node.js/" + deps.process.version + , user: '' + }; + //var currentUAProps; + + function getUaString() { + var userAgent = ''; + + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function (key) { + userAgent += uaDefaults[key]; + //userAgent += currentUAProps[key]; + }); + + return userAgent.trim(); + } + + function getRequest(opts) { + if (!opts) { opts = {}; } + + return deps.request.defaults({ + headers: { + 'User-Agent': opts.userAgent || getUaString() + } + }); + } + + deps.request = deps.request || require('request'); + deps.promisify = deps.promisify || require('util').promisify; + + var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; + var request = deps.promisify(getRequest({})); + + var acme2 = { + getAcmeUrls: function () { + var me = this; + return request({ url: directoryUrl }).then(function (resp) { + me._directoryUrls = JSON.parse(resp.body); + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + } + , getNonce: function () { + var me = this; + return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); + } + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ + , registerNewAccount: function () { + var me = this; + var RSA = require('rsa-compat').RSA; + var crypto = require('crypto'); + RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = + function (keypair, payload, nonce) { + var prot = {}; + if (nonce) { + if ('string' === typeof nonce) { + prot.nonce = nonce; + } else { + prot = nonce; + } + } + keypair = RSA._internal.import(keypair); + keypair = RSA._internal.importForge(keypair); + keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); + + // Compute JWS signature + var protectedHeader = ""; + if (Object.keys(prot).length) { + protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url }); + } + var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); + var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); + var raw = protected64 + "." + payload64; + var sha256Buf = crypto.createHash('sha256').update(raw).digest(); + var sig64; + + if (RSA._URSA) { + sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); + } else { + sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); + } + + return { + /* + header: { + alg: "RS256" + , jwk: keypair.publicKeyJwk + } + */ + protected: protected64 + , payload: payload64 + , signature: sig64 + }; + }; + + var options = { + email: 'coolaj86@gmail.com' + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + }; + var body = { + termsOfServiceAgreed: true + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] + }; + var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } + ); + + console.log('jws:'); + console.log(jws); + return request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log(resp.toJSON()); + return resp.body; + }); + } + }; + return acme2; +} + +var acme2 = create(); +acme2.getAcmeUrls().then(function (body) { + console.log(body); + acme2.getNonce().then(function (nonce) { + console.log(nonce); + acme2.registerNewAccount().then(function (account) { + console.log(account); + }); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..477caca --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "acme-v2", + "version": "1.0.0", + "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "main": "node.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" + }, + "keywords": [ + "acmev2", + "acme-v2", + "acme", + "letsencrypt-v2", + "letsencryptv2", + "greenlock", + "greenlock2" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "request": "^2.85.0", + "rsa-compat": "^1.2.7" + } +} -- 2.38.5 From 4c4eaa83b76441a27dac24f68b59202ccf656586 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 15 Mar 2018 00:41:00 -0600 Subject: [PATCH 002/252] initial commit --- README.md | 10 +++ node.js | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 28 ++++++++ 3 files changed, 225 insertions(+) create mode 100644 README.md create mode 100644 node.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..0196434 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +acme-v2.js +========== + +A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. + +In progress + +* get directory +* get nonce +* create account diff --git a/node.js b/node.js new file mode 100644 index 0000000..f3d1db4 --- /dev/null +++ b/node.js @@ -0,0 +1,187 @@ +/*! + * acme-v2.js + * Copyright(c) 2018 AJ ONeal https://ppl.family + * Apache-2.0 OR MIT (and hence also MPL 2.0) +*/ +'use strict'; + +var defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' +, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, acmeChallengePrefix: '/.well-known/acme-challenge/' +, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] +, challengeType: 'http-01' // dns-01 +, keyType: 'rsa' // ecdsa +, keySize: 2048 // 256 +}; + +function create(deps) { + if (!deps) { deps = {}; } + deps.LeCore = {}; + deps.pkg = deps.pkg || require('./package.json'); + deps.os = deps.os || require('os'); + deps.process = deps.process || require('process'); + + var uaDefaults = { + pkg: "Greenlock/" + deps.pkg.version + , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" + , node: " Node.js/" + deps.process.version + , user: '' + }; + //var currentUAProps; + + function getUaString() { + var userAgent = ''; + + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function (key) { + userAgent += uaDefaults[key]; + //userAgent += currentUAProps[key]; + }); + + return userAgent.trim(); + } + + function getRequest(opts) { + if (!opts) { opts = {}; } + + return deps.request.defaults({ + headers: { + 'User-Agent': opts.userAgent || getUaString() + } + }); + } + + deps.request = deps.request || require('request'); + deps.promisify = deps.promisify || require('util').promisify; + + var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; + var request = deps.promisify(getRequest({})); + + var acme2 = { + getAcmeUrls: function () { + var me = this; + return request({ url: directoryUrl }).then(function (resp) { + me._directoryUrls = JSON.parse(resp.body); + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + } + , getNonce: function () { + var me = this; + return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); + } + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ + , registerNewAccount: function () { + var me = this; + var RSA = require('rsa-compat').RSA; + var crypto = require('crypto'); + RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = + function (keypair, payload, nonce) { + var prot = {}; + if (nonce) { + if ('string' === typeof nonce) { + prot.nonce = nonce; + } else { + prot = nonce; + } + } + keypair = RSA._internal.import(keypair); + keypair = RSA._internal.importForge(keypair); + keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); + + // Compute JWS signature + var protectedHeader = ""; + if (Object.keys(prot).length) { + protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url }); + } + var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); + var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); + var raw = protected64 + "." + payload64; + var sha256Buf = crypto.createHash('sha256').update(raw).digest(); + var sig64; + + if (RSA._URSA) { + sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); + } else { + sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); + } + + return { + /* + header: { + alg: "RS256" + , jwk: keypair.publicKeyJwk + } + */ + protected: protected64 + , payload: payload64 + , signature: sig64 + }; + }; + + var options = { + email: 'coolaj86@gmail.com' + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + }; + var body = { + termsOfServiceAgreed: true + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] + }; + var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } + ); + + console.log('jws:'); + console.log(jws); + return request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log(resp.toJSON()); + return resp.body; + }); + } + }; + return acme2; +} + +var acme2 = create(); +acme2.getAcmeUrls().then(function (body) { + console.log(body); + acme2.getNonce().then(function (nonce) { + console.log(nonce); + acme2.registerNewAccount().then(function (account) { + console.log(account); + }); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..477caca --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "acme-v2", + "version": "1.0.0", + "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "main": "node.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" + }, + "keywords": [ + "acmev2", + "acme-v2", + "acme", + "letsencrypt-v2", + "letsencryptv2", + "greenlock", + "greenlock2" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "request": "^2.85.0", + "rsa-compat": "^1.2.7" + } +} -- 2.38.5 From 01b42af5af83220638acfc40591c91e3e92f3e4b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 15 Mar 2018 00:43:41 -0600 Subject: [PATCH 003/252] add note on spec, junk version --- README.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0196434..4d3fb32 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ acme-v2.js A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. +Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 + In progress * get directory diff --git a/package.json b/package.json index 477caca..766e9eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.0", + "version": "0.0.1", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From 2a3849cf1ba7167a7aef7a17a3f88b64f0baad4c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 15 Mar 2018 00:43:41 -0600 Subject: [PATCH 004/252] add note on spec, junk version --- README.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0196434..4d3fb32 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ acme-v2.js A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. +Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 + In progress * get directory diff --git a/package.json b/package.json index 477caca..766e9eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.0", + "version": "0.0.1", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From 5f7db2845caa60ef16e783b0eb01d8006ffa656d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 16 Mar 2018 00:59:40 -0600 Subject: [PATCH 005/252] hard code more test functionality --- README.md | 9 +++ node.js | 173 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 138 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4d3fb32..3c11b5b 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,12 @@ In progress * get directory * get nonce * create account +* new order +* get challenges + +Not yet tried + +* respond to challenges +* finalize order +* poll for status +* download certificates diff --git a/node.js b/node.js index f3d1db4..7d58cd2 100644 --- a/node.js +++ b/node.js @@ -58,45 +58,6 @@ function create(deps) { var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; var request = deps.promisify(getRequest({})); - var acme2 = { - getAcmeUrls: function () { - var me = this; - return request({ url: directoryUrl }).then(function (resp) { - me._directoryUrls = JSON.parse(resp.body); - me._tos = me._directoryUrls.meta.termsOfService; - return me._directoryUrls; - }); - } - , getNonce: function () { - var me = this; - return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - return me._nonce; - }); - } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ - , registerNewAccount: function () { - var me = this; - var RSA = require('rsa-compat').RSA; var crypto = require('crypto'); RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = function (keypair, payload, nonce) { @@ -142,14 +103,60 @@ function create(deps) { }; }; - var options = { - email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - }; + var acme2 = { + getAcmeUrls: function () { + var me = this; + return request({ url: directoryUrl }).then(function (resp) { + me._directoryUrls = JSON.parse(resp.body); + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + } + , getNonce: function () { + var me = this; + return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); + } + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ + , registerNewAccount: function (options) { + var me = this; + var body = { termsOfServiceAgreed: true , onlyReturnExisting: false , contact: [ 'mailto:' + options.email ] + /* + "externalAccountBinding": { + "protected": base64url({ + "alg": "HS256", + "kid": /* key identifier from CA *//*, + "url": "https://example.com/acme/new-account" + }), + "payload": base64url(/* same as in "jwk" above *//*), + "signature": /* MAC using MAC key from CA *//* + } + */ }; var payload = JSON.stringify(body, null, 2); var jws = RSA.signJws( @@ -167,21 +174,99 @@ function create(deps) { , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers['location']; + console.log(location); // the account id url console.log(resp.toJSON()); + me._kid = location; return resp.body; }); } + /* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } + */ + , getCertificate: function (options, cb) { + var me = this; + + var body = { + identifiers: [ + { type: "dns" , value: "www.ppl.family" } + /* + , { type: "dns" , value: "example.net" } + */ + ] + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + ); + + return request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers['location']; + console.log(location); // the account id url + console.log(resp.toJSON()); + //var body = JSON.parse(resp.body); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + + //return resp.body; + return Promise.all(me._authorizations.map(function (auth) { + return request({ method: 'GET', url: auth, json: true }).then(function (resp) { + console.log('Authorization:'); + console.log(resp.body.challenges); + }); + })); + }); + } }; return acme2; } +var RSA = require('rsa-compat').RSA; var acme2 = create(); acme2.getAcmeUrls().then(function (body) { console.log(body); acme2.getNonce().then(function (nonce) { console.log(nonce); - acme2.registerNewAccount().then(function (account) { + + var options = { + email: 'coolaj86@gmail.com' + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + }; + acme2.registerNewAccount(options).then(function (account) { console.log(account); + acme2.getCertificate(options, function () { + console.log('got cert'); + }); }); }); }); -- 2.38.5 From df022959e445cce711ffa29d7c5448846444283d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 16 Mar 2018 00:59:40 -0600 Subject: [PATCH 006/252] hard code more test functionality --- README.md | 9 +++ node.js | 173 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 138 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4d3fb32..3c11b5b 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,12 @@ In progress * get directory * get nonce * create account +* new order +* get challenges + +Not yet tried + +* respond to challenges +* finalize order +* poll for status +* download certificates diff --git a/node.js b/node.js index f3d1db4..7d58cd2 100644 --- a/node.js +++ b/node.js @@ -58,45 +58,6 @@ function create(deps) { var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; var request = deps.promisify(getRequest({})); - var acme2 = { - getAcmeUrls: function () { - var me = this; - return request({ url: directoryUrl }).then(function (resp) { - me._directoryUrls = JSON.parse(resp.body); - me._tos = me._directoryUrls.meta.termsOfService; - return me._directoryUrls; - }); - } - , getNonce: function () { - var me = this; - return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - return me._nonce; - }); - } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ - , registerNewAccount: function () { - var me = this; - var RSA = require('rsa-compat').RSA; var crypto = require('crypto'); RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = function (keypair, payload, nonce) { @@ -142,14 +103,60 @@ function create(deps) { }; }; - var options = { - email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - }; + var acme2 = { + getAcmeUrls: function () { + var me = this; + return request({ url: directoryUrl }).then(function (resp) { + me._directoryUrls = JSON.parse(resp.body); + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + } + , getNonce: function () { + var me = this; + return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); + } + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ + , registerNewAccount: function (options) { + var me = this; + var body = { termsOfServiceAgreed: true , onlyReturnExisting: false , contact: [ 'mailto:' + options.email ] + /* + "externalAccountBinding": { + "protected": base64url({ + "alg": "HS256", + "kid": /* key identifier from CA *//*, + "url": "https://example.com/acme/new-account" + }), + "payload": base64url(/* same as in "jwk" above *//*), + "signature": /* MAC using MAC key from CA *//* + } + */ }; var payload = JSON.stringify(body, null, 2); var jws = RSA.signJws( @@ -167,21 +174,99 @@ function create(deps) { , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers['location']; + console.log(location); // the account id url console.log(resp.toJSON()); + me._kid = location; return resp.body; }); } + /* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } + */ + , getCertificate: function (options, cb) { + var me = this; + + var body = { + identifiers: [ + { type: "dns" , value: "www.ppl.family" } + /* + , { type: "dns" , value: "example.net" } + */ + ] + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + ); + + return request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers['location']; + console.log(location); // the account id url + console.log(resp.toJSON()); + //var body = JSON.parse(resp.body); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + + //return resp.body; + return Promise.all(me._authorizations.map(function (auth) { + return request({ method: 'GET', url: auth, json: true }).then(function (resp) { + console.log('Authorization:'); + console.log(resp.body.challenges); + }); + })); + }); + } }; return acme2; } +var RSA = require('rsa-compat').RSA; var acme2 = create(); acme2.getAcmeUrls().then(function (body) { console.log(body); acme2.getNonce().then(function (nonce) { console.log(nonce); - acme2.registerNewAccount().then(function (account) { + + var options = { + email: 'coolaj86@gmail.com' + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + }; + acme2.registerNewAccount(options).then(function (account) { console.log(account); + acme2.getCertificate(options, function () { + console.log('got cert'); + }); }); }); }); -- 2.38.5 From e6268c61198a78170a3d87e36bf8a7b0db16fc7f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 20 Mar 2018 01:24:36 -0600 Subject: [PATCH 007/252] successful test! yay! --- README.md | 26 ++++--- genkeypair.js | 18 +++++ node.js | 187 ++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- 4 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 genkeypair.js diff --git a/README.md b/README.md index 3c11b5b..04b20fd 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,21 @@ Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/ In progress -* get directory -* get nonce -* create account -* new order -* get challenges +* Mar 15, 2018 - get directory +* Mar 15, 2018 - get nonce +* Mar 15, 2018 - generate account keypair +* Mar 15, 2018 - create account +* Mar 16, 2018 - new order +* Mar 16, 2018 - get challenges +* Mar 20, 2018 - respond to challenges +* Mar 20, 2018 - generate domain keypair +* Mar 20, 2018 - finalize order (submit csr) +* Mar 20, 2018 - poll for status +* Mar 20, 2018 - download certificate -Not yet tried +* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -* respond to challenges -* finalize order -* poll for status -* download certificates +Todo + +* match api for acme v1 (le-acme-core.js) +* make not hard-coded diff --git a/genkeypair.js b/genkeypair.js new file mode 100644 index 0000000..f029ade --- /dev/null +++ b/genkeypair.js @@ -0,0 +1,18 @@ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem); +}); + +RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem); +}); diff --git a/node.js b/node.js index 7d58cd2..83c183a 100644 --- a/node.js +++ b/node.js @@ -201,12 +201,167 @@ function create(deps) { "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" } */ + , _getChallenges: function (options, auth) { + console.log('\n[DEBUG] getChallenges\n'); + return request({ method: 'GET', url: auth, json: true }).then(function (resp) { + console.log('Authorization:'); + console.log(resp.body.challenges); + return resp.body.challenges; + }); + } + // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 + , _postChallenge: function (options, ch) { + var me = this; + + var body = { }; + + var payload = JSON.stringify(body); + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + ); + + var thumbprint = RSA.thumbprint(options.keypair); + var keyAuthorization = ch.token + '.' + thumbprint; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + // /.well-known/acme-challenge/:token + console.log('type:'); + console.log(ch.type); + console.log('ch.token:'); + console.log(ch.token); + console.log('thumbprint:'); + console.log(thumbprint); + console.log('keyAuthorization:'); + console.log(keyAuthorization); + /* + options.setChallenge(ch.token, thumbprint, keyAuthorization, function (err) { + }); + */ + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return wait().then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + return request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return wait().then(pollStatus); + }); + } + + return wait(20 * 1000).then(post); + } + , _finalizeOrder: function (options, validatedDomains) { + console.log('finalizeOrder:'); + var me = this; + + var csr = RSA.generateCsrWeb64(options.certificateKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + + function pollCert() { + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + ); + + console.log('finalize:', me._finalize); + return request({ + method: 'POST' + , url: me._finalize + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + + console.log('order finalized: resp.body:'); + console.log(resp.body); + + if ('processing' === resp.body.status) { + return wait().then(pollCert); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; + } + + if ('invalid' === resp.body.status) { + console.error('cannot finalize: badness'); + return; + } + + console.error('(x) cannot finalize: badness'); + return; + }); + } + + return pollCert(); + } + , _getCertificate: function (auth) { + var me = this; + return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('Certificate:'); + console.log(resp.body); + return resp.body; + }); + } , getCertificate: function (options, cb) { var me = this; var body = { identifiers: [ - { type: "dns" , value: "www.ppl.family" } + { type: "dns" , value: "test.ppl.family" } /* , { type: "dns" , value: "example.net" } */ @@ -223,6 +378,7 @@ function create(deps) { , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } ); + console.log('\n[DEBUG] newOrder\n'); return request({ method: 'POST' , url: me._directoryUrls.newOrder @@ -237,14 +393,32 @@ function create(deps) { me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; //return resp.body; return Promise.all(me._authorizations.map(function (auth) { - return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - console.log('Authorization:'); - console.log(resp.body.challenges); + console.log('authz', auth); + return me._getChallenges(options, auth).then(function (challenges) { + var chp; + + challenges.forEach(function (ch) { + if ('http-01' !== ch.type) { + return; + } + chp = me._postChallenge(options, ch); + }); + + return chp; }); - })); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); + + return me._finalizeOrder(options, validatedDomains); + }).then(function () { + return me._getCertificate(); + }); }); } }; @@ -260,7 +434,8 @@ acme2.getAcmeUrls().then(function (body) { var options = { email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , certificateKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) }; acme2.registerNewAccount(options).then(function (account) { console.log(account); diff --git a/package.json b/package.json index 766e9eb..79af90f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.0.1", + "version": "0.0.2", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From afbcef688f2471b140972803a5b91f1c0979cafb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 20 Mar 2018 01:24:36 -0600 Subject: [PATCH 008/252] successful test! yay! --- README.md | 26 ++++--- genkeypair.js | 18 +++++ node.js | 187 ++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- 4 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 genkeypair.js diff --git a/README.md b/README.md index 3c11b5b..04b20fd 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,21 @@ Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/ In progress -* get directory -* get nonce -* create account -* new order -* get challenges +* Mar 15, 2018 - get directory +* Mar 15, 2018 - get nonce +* Mar 15, 2018 - generate account keypair +* Mar 15, 2018 - create account +* Mar 16, 2018 - new order +* Mar 16, 2018 - get challenges +* Mar 20, 2018 - respond to challenges +* Mar 20, 2018 - generate domain keypair +* Mar 20, 2018 - finalize order (submit csr) +* Mar 20, 2018 - poll for status +* Mar 20, 2018 - download certificate -Not yet tried +* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -* respond to challenges -* finalize order -* poll for status -* download certificates +Todo + +* match api for acme v1 (le-acme-core.js) +* make not hard-coded diff --git a/genkeypair.js b/genkeypair.js new file mode 100644 index 0000000..f029ade --- /dev/null +++ b/genkeypair.js @@ -0,0 +1,18 @@ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem); +}); + +RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem); +}); diff --git a/node.js b/node.js index 7d58cd2..83c183a 100644 --- a/node.js +++ b/node.js @@ -201,12 +201,167 @@ function create(deps) { "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" } */ + , _getChallenges: function (options, auth) { + console.log('\n[DEBUG] getChallenges\n'); + return request({ method: 'GET', url: auth, json: true }).then(function (resp) { + console.log('Authorization:'); + console.log(resp.body.challenges); + return resp.body.challenges; + }); + } + // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 + , _postChallenge: function (options, ch) { + var me = this; + + var body = { }; + + var payload = JSON.stringify(body); + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + ); + + var thumbprint = RSA.thumbprint(options.keypair); + var keyAuthorization = ch.token + '.' + thumbprint; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + // /.well-known/acme-challenge/:token + console.log('type:'); + console.log(ch.type); + console.log('ch.token:'); + console.log(ch.token); + console.log('thumbprint:'); + console.log(thumbprint); + console.log('keyAuthorization:'); + console.log(keyAuthorization); + /* + options.setChallenge(ch.token, thumbprint, keyAuthorization, function (err) { + }); + */ + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return wait().then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + return request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return wait().then(pollStatus); + }); + } + + return wait(20 * 1000).then(post); + } + , _finalizeOrder: function (options, validatedDomains) { + console.log('finalizeOrder:'); + var me = this; + + var csr = RSA.generateCsrWeb64(options.certificateKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + + function pollCert() { + //var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + ); + + console.log('finalize:', me._finalize); + return request({ + method: 'POST' + , url: me._finalize + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + + console.log('order finalized: resp.body:'); + console.log(resp.body); + + if ('processing' === resp.body.status) { + return wait().then(pollCert); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; + } + + if ('invalid' === resp.body.status) { + console.error('cannot finalize: badness'); + return; + } + + console.error('(x) cannot finalize: badness'); + return; + }); + } + + return pollCert(); + } + , _getCertificate: function (auth) { + var me = this; + return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('Certificate:'); + console.log(resp.body); + return resp.body; + }); + } , getCertificate: function (options, cb) { var me = this; var body = { identifiers: [ - { type: "dns" , value: "www.ppl.family" } + { type: "dns" , value: "test.ppl.family" } /* , { type: "dns" , value: "example.net" } */ @@ -223,6 +378,7 @@ function create(deps) { , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } ); + console.log('\n[DEBUG] newOrder\n'); return request({ method: 'POST' , url: me._directoryUrls.newOrder @@ -237,14 +393,32 @@ function create(deps) { me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; //return resp.body; return Promise.all(me._authorizations.map(function (auth) { - return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - console.log('Authorization:'); - console.log(resp.body.challenges); + console.log('authz', auth); + return me._getChallenges(options, auth).then(function (challenges) { + var chp; + + challenges.forEach(function (ch) { + if ('http-01' !== ch.type) { + return; + } + chp = me._postChallenge(options, ch); + }); + + return chp; }); - })); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); + + return me._finalizeOrder(options, validatedDomains); + }).then(function () { + return me._getCertificate(); + }); }); } }; @@ -260,7 +434,8 @@ acme2.getAcmeUrls().then(function (body) { var options = { email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , certificateKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) }; acme2.registerNewAccount(options).then(function (account) { console.log(account); diff --git a/package.json b/package.json index 766e9eb..79af90f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.0.1", + "version": "0.0.2", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From 92b494f53512043e8a97ce4c770ab87cbcb4bc50 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:26:23 -0600 Subject: [PATCH 009/252] not hard-coded, almost backwards compatible --- README.md | 60 +++++- node.js | 562 +++++++++++++++++++++++++++------------------------ package.json | 4 +- 3 files changed, 355 insertions(+), 271 deletions(-) diff --git a/README.md b/README.md index 04b20fd..bfd68d1 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,64 @@ In progress * Mar 20, 2018 - finalize order (submit csr) * Mar 20, 2018 - poll for status * Mar 20, 2018 - download certificate - * Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +* Mar 21, 2018 - can now accept values (not hard coded) +* Mar 21, 2018 - *mostly* matches le-acme-core.js API Todo -* match api for acme v1 (le-acme-core.js) -* make not hard-coded +* completely match api for acme v1 (le-acme-core.js) +* test http and dns challenges +* export http and dns challenge tests +* support ECDSA keys + +## API + +``` +var ACME = require('acme-v2.js').ACME.create({ + RSA: require('rsa-compat').RSA +}); +``` + +```javascript +// Accounts +ACME.registerNewAccount(options, cb) // returns "regr" registration data + + { email: '' // valid email (server checks MX records) + , accountKeypair: { // privateKeyPem or privateKeyJwt + privateKeyPem: '' + } + , agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end) + } + +// Registration +ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) }) + + { newAuthzUrl: '' // specify acmeUrls.newAuthz + , newCertUrl: '' // specify acmeUrls.newCert + + , domainKeypair: { + privateKeyPem: '' + } + , accountKeypair: { + privateKeyPem: '' + } + , domains: [ 'example.com' ] + + , setChallenge: fn (hostname, key, val, cb) + , removeChallenge: fn (hostname, key, cb) + } + +// Discovery URLs +ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) +``` + +Helpers & Stuff + +```javascript +// Constants +ACME.productionServerUrl // https://acme-v02.api.letsencrypt.org/directory +ACME.stagingServerUrl // https://acme-staging-v02.api.letsencrypt.org/directory +ACME.acmeChallengePrefix // /.well-known/acme-challenge/ +``` + diff --git a/node.js b/node.js index 83c183a..2069281 100644 --- a/node.js +++ b/node.js @@ -2,8 +2,9 @@ * acme-v2.js * Copyright(c) 2018 AJ ONeal https://ppl.family * Apache-2.0 OR MIT (and hence also MPL 2.0) -*/ + */ 'use strict'; +/* globals Promise */ var defaults = { productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' @@ -52,57 +53,13 @@ function create(deps) { }); } + var RSA = deps.RSA || require('rsa-compat').RSA; deps.request = deps.request || require('request'); deps.promisify = deps.promisify || require('util').promisify; var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; var request = deps.promisify(getRequest({})); - var crypto = require('crypto'); - RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = - function (keypair, payload, nonce) { - var prot = {}; - if (nonce) { - if ('string' === typeof nonce) { - prot.nonce = nonce; - } else { - prot = nonce; - } - } - keypair = RSA._internal.import(keypair); - keypair = RSA._internal.importForge(keypair); - keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); - - // Compute JWS signature - var protectedHeader = ""; - if (Object.keys(prot).length) { - protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url }); - } - var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); - var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); - var raw = protected64 + "." + payload64; - var sha256Buf = crypto.createHash('sha256').update(raw).digest(); - var sig64; - - if (RSA._URSA) { - sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); - } else { - sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); - } - - return { - /* - header: { - alg: "RS256" - , jwk: keypair.publicKeyJwk - } - */ - protected: protected64 - , payload: payload64 - , signature: sig64 - }; - }; - var acme2 = { getAcmeUrls: function () { var me = this; @@ -114,116 +71,138 @@ function create(deps) { } , getNonce: function () { var me = this; + if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; return me._nonce; }); } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ , registerNewAccount: function (options) { var me = this; - var body = { - termsOfServiceAgreed: true - , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] - /* - "externalAccountBinding": { - "protected": base64url({ - "alg": "HS256", - "kid": /* key identifier from CA *//*, - "url": "https://example.com/acme/new-account" - }), - "payload": base64url(/* same as in "jwk" above *//*), - "signature": /* MAC using MAC key from CA *//* - } - */ - }; - var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } - ); + console.log('[acme-v2] registerNewAccount'); - console.log('jws:'); - console.log(jws); - return request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers['location']; - console.log(location); // the account id url - console.log(resp.toJSON()); - me._kid = location; - return resp.body; + return me.getNonce().then(function () { + return new Promise(function (resolve, reject) { + + function agree(err, tosUrl) { + if (err) { reject(err); return; } + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + reject(err); + return; + } + + var jwk = RSA.exportPublicJwk(options.accountKeypair); + var body = { + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] + }; + if (options.externalAccount) { + body.externalAccountBinding = RSA.signJws( + options.externalAccount.secret + , undefined + , { alg: "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , new Buffer(JSON.stringify(jwk)) + ); + } + var payload = JSON.stringify(body); + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce + , alg: 'RS256' + , url: me._directoryUrls.newAccount + , jwk: jwk + } + , new Buffer(payload) + ); + + console.log('[acme-v2] registerNewAccount JSON body:'); + delete jws.header; + console.log(jws); + me._nonce = null; + return request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log('[DEBUG] new account location:'); // the account id url + console.log(location); // the account id url + console.log(resp.toJSON()); + me._kid = location; + return resp.body; + }).then(resolve); + } + + console.log('[acme-v2] agreeToTerms'); + options.agreeToTerms(me._tos, agree); + }); }); } - /* - POST /acme/new-order HTTP/1.1 - Host: example.com - Content-Type: application/jose+json + /* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "5XJ1L3lEkMG7tR6pA00clA", - "url": "https://example.com/acme/new-order" - }), - "payload": base64url({ - "identifiers": [{"type:"dns","value":"example.com"}], - "notBefore": "2016-01-01T00:00:00Z", - "notAfter": "2016-01-08T00:00:00Z" - }), - "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" - } - */ + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } + */ , _getChallenges: function (options, auth) { console.log('\n[DEBUG] getChallenges\n'); return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - console.log('Authorization:'); - console.log(resp.body.challenges); - return resp.body.challenges; + return resp.body; }); } // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 - , _postChallenge: function (options, ch) { - var me = this; + , _postChallenge: function (options, identifier, ch) { + var me = this; var body = { }; - var payload = JSON.stringify(body); - //var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - ); + var payload = JSON.stringify(body); - var thumbprint = RSA.thumbprint(options.keypair); + var thumbprint = RSA.thumbprint(options.accountKeypair); var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token @@ -235,67 +214,115 @@ function create(deps) { console.log(thumbprint); console.log('keyAuthorization:'); console.log(keyAuthorization); - /* - options.setChallenge(ch.token, thumbprint, keyAuthorization, function (err) { + + return new Promise(function (resolve, reject) { + if (options.setupChallenge) { + options.setupChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + , testChallenge + ); + } else { + options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge); + } + + function testChallenge(err) { + if (err) { reject(err); return; } + + // TODO put check dns / http checks here? + // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} + // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" + + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return wait().then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + try { + if (options.teardownChallenge) { + options.teardownChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + , function () {} + ); + } else { + options.removeChallenge(identifier.value, ch.token, function () {}); + } + } catch(e) {} + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(payload) + ); + me._nonce = null; + return request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return wait().then(pollStatus).then(resolve, reject); + }); + } + + return wait(20 * 1000).then(post); + } }); - */ - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); - return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - console.log('poll: again'); - return wait().then(pollStatus); - } - - if ('valid' === resp.body.status) { - console.log('poll: valid'); - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - }); - } - - console.log('\n[DEBUG] postChallenge\n'); - //console.log('\n[DEBUG] stop to fix things\n'); return; - - function post() { - return request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); - return wait().then(pollStatus); - }); - } - - return wait(20 * 1000).then(post); } , _finalizeOrder: function (options, validatedDomains) { console.log('finalizeOrder:'); - var me = this; + var me = this; - var csr = RSA.generateCsrWeb64(options.certificateKeypair, validatedDomains); + var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -306,14 +333,15 @@ function create(deps) { } function pollCert() { - //var payload = JSON.stringify(body, null, 2); var jws = RSA.signJws( - options.keypair - , new Buffer(payload) + options.accountKeypair + , undefined , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , new Buffer(payload) ); console.log('finalize:', me._finalize); + me._nonce = null; return request({ method: 'POST' , url: me._finalize @@ -348,7 +376,7 @@ function create(deps) { return pollCert(); } - , _getCertificate: function (auth) { + , _getCertificate: function () { var me = this; return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { console.log('Certificate:'); @@ -357,91 +385,93 @@ function create(deps) { }); } , getCertificate: function (options, cb) { - var me = this; + console.log('[acme-v2] DEBUG get cert 1'); + var me = this; - var body = { - identifiers: [ - { type: "dns" , value: "test.ppl.family" } - /* - , { type: "dns" , value: "example.net" } - */ - ] - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; + if (!options.challengeTypes) { + if (!options.challengeType) { + cb(new Error("challenge type must be specified")); + return Promise.reject(new Error("challenge type must be specified")); + } + options.challengeTypes = [ options.challengeType ]; + } - var payload = JSON.stringify(body); - //var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - ); + console.log('[acme-v2] getCertificate'); + return me.getNonce().then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; - console.log('\n[DEBUG] newOrder\n'); - return request({ - method: 'POST' - , url: me._directoryUrls.newOrder - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers['location']; - console.log(location); // the account id url - console.log(resp.toJSON()); - //var body = JSON.parse(resp.body); - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + var payload = JSON.stringify(body); + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , new Buffer(payload) + ); - //return resp.body; - return Promise.all(me._authorizations.map(function (auth) { - console.log('authz', auth); - return me._getChallenges(options, auth).then(function (challenges) { - var chp; + console.log('\n[DEBUG] newOrder\n'); + me._nonce = null; + return request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log(location); // the account id url + console.log(resp.toJSON()); + //var body = JSON.parse(resp.body); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; - challenges.forEach(function (ch) { - if ('http-01' !== ch.type) { - return; + //return resp.body; + return Promise.all(me._authorizations.map(function (authUrl) { + return me._getChallenges(options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); } - chp = me._postChallenge(options, ch); + + return me._postChallenge(options, results.identifier, challenge); + }); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; }); - return chp; + return me._finalizeOrder(options, validatedDomains); + }).then(function () { + return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb); }); - })).then(function () { - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); - - return me._finalizeOrder(options, validatedDomains); - }).then(function () { - return me._getCertificate(); }); }); - } + } }; return acme2; } -var RSA = require('rsa-compat').RSA; -var acme2 = create(); -acme2.getAcmeUrls().then(function (body) { - console.log(body); - acme2.getNonce().then(function (nonce) { - console.log(nonce); - - var options = { - email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , certificateKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - }; - acme2.registerNewAccount(options).then(function (account) { - console.log(account); - acme2.getCertificate(options, function () { - console.log('got cert'); - }); - }); - }); +module.exports.ACME = { + create: create +}; +Object.keys(defaults).forEach(function (key) { + module.exports.ACME[key] = defaults[key]; }); diff --git a/package.json b/package.json index 79af90f..0f57309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.0.2", + "version": "0.6.0", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { @@ -23,6 +23,6 @@ "license": "(MIT OR Apache-2.0)", "dependencies": { "request": "^2.85.0", - "rsa-compat": "^1.2.7" + "rsa-compat": "^1.3.0" } } -- 2.38.5 From 08e6fdc1a7426afc90aa966862f8f8c73655a200 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:26:23 -0600 Subject: [PATCH 010/252] not hard-coded, almost backwards compatible --- README.md | 60 +++++- node.js | 562 +++++++++++++++++++++++++++------------------------ package.json | 4 +- 3 files changed, 355 insertions(+), 271 deletions(-) diff --git a/README.md b/README.md index 04b20fd..bfd68d1 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,64 @@ In progress * Mar 20, 2018 - finalize order (submit csr) * Mar 20, 2018 - poll for status * Mar 20, 2018 - download certificate - * Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +* Mar 21, 2018 - can now accept values (not hard coded) +* Mar 21, 2018 - *mostly* matches le-acme-core.js API Todo -* match api for acme v1 (le-acme-core.js) -* make not hard-coded +* completely match api for acme v1 (le-acme-core.js) +* test http and dns challenges +* export http and dns challenge tests +* support ECDSA keys + +## API + +``` +var ACME = require('acme-v2.js').ACME.create({ + RSA: require('rsa-compat').RSA +}); +``` + +```javascript +// Accounts +ACME.registerNewAccount(options, cb) // returns "regr" registration data + + { email: '' // valid email (server checks MX records) + , accountKeypair: { // privateKeyPem or privateKeyJwt + privateKeyPem: '' + } + , agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end) + } + +// Registration +ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) }) + + { newAuthzUrl: '' // specify acmeUrls.newAuthz + , newCertUrl: '' // specify acmeUrls.newCert + + , domainKeypair: { + privateKeyPem: '' + } + , accountKeypair: { + privateKeyPem: '' + } + , domains: [ 'example.com' ] + + , setChallenge: fn (hostname, key, val, cb) + , removeChallenge: fn (hostname, key, cb) + } + +// Discovery URLs +ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) +``` + +Helpers & Stuff + +```javascript +// Constants +ACME.productionServerUrl // https://acme-v02.api.letsencrypt.org/directory +ACME.stagingServerUrl // https://acme-staging-v02.api.letsencrypt.org/directory +ACME.acmeChallengePrefix // /.well-known/acme-challenge/ +``` + diff --git a/node.js b/node.js index 83c183a..2069281 100644 --- a/node.js +++ b/node.js @@ -2,8 +2,9 @@ * acme-v2.js * Copyright(c) 2018 AJ ONeal https://ppl.family * Apache-2.0 OR MIT (and hence also MPL 2.0) -*/ + */ 'use strict'; +/* globals Promise */ var defaults = { productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' @@ -52,57 +53,13 @@ function create(deps) { }); } + var RSA = deps.RSA || require('rsa-compat').RSA; deps.request = deps.request || require('request'); deps.promisify = deps.promisify || require('util').promisify; var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; var request = deps.promisify(getRequest({})); - var crypto = require('crypto'); - RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = - function (keypair, payload, nonce) { - var prot = {}; - if (nonce) { - if ('string' === typeof nonce) { - prot.nonce = nonce; - } else { - prot = nonce; - } - } - keypair = RSA._internal.import(keypair); - keypair = RSA._internal.importForge(keypair); - keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); - - // Compute JWS signature - var protectedHeader = ""; - if (Object.keys(prot).length) { - protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url }); - } - var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); - var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); - var raw = protected64 + "." + payload64; - var sha256Buf = crypto.createHash('sha256').update(raw).digest(); - var sig64; - - if (RSA._URSA) { - sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); - } else { - sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); - } - - return { - /* - header: { - alg: "RS256" - , jwk: keypair.publicKeyJwk - } - */ - protected: protected64 - , payload: payload64 - , signature: sig64 - }; - }; - var acme2 = { getAcmeUrls: function () { var me = this; @@ -114,116 +71,138 @@ function create(deps) { } , getNonce: function () { var me = this; + if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; return me._nonce; }); } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ , registerNewAccount: function (options) { var me = this; - var body = { - termsOfServiceAgreed: true - , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] - /* - "externalAccountBinding": { - "protected": base64url({ - "alg": "HS256", - "kid": /* key identifier from CA *//*, - "url": "https://example.com/acme/new-account" - }), - "payload": base64url(/* same as in "jwk" above *//*), - "signature": /* MAC using MAC key from CA *//* - } - */ - }; - var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } - ); + console.log('[acme-v2] registerNewAccount'); - console.log('jws:'); - console.log(jws); - return request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers['location']; - console.log(location); // the account id url - console.log(resp.toJSON()); - me._kid = location; - return resp.body; + return me.getNonce().then(function () { + return new Promise(function (resolve, reject) { + + function agree(err, tosUrl) { + if (err) { reject(err); return; } + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + reject(err); + return; + } + + var jwk = RSA.exportPublicJwk(options.accountKeypair); + var body = { + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] + }; + if (options.externalAccount) { + body.externalAccountBinding = RSA.signJws( + options.externalAccount.secret + , undefined + , { alg: "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , new Buffer(JSON.stringify(jwk)) + ); + } + var payload = JSON.stringify(body); + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce + , alg: 'RS256' + , url: me._directoryUrls.newAccount + , jwk: jwk + } + , new Buffer(payload) + ); + + console.log('[acme-v2] registerNewAccount JSON body:'); + delete jws.header; + console.log(jws); + me._nonce = null; + return request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log('[DEBUG] new account location:'); // the account id url + console.log(location); // the account id url + console.log(resp.toJSON()); + me._kid = location; + return resp.body; + }).then(resolve); + } + + console.log('[acme-v2] agreeToTerms'); + options.agreeToTerms(me._tos, agree); + }); }); } - /* - POST /acme/new-order HTTP/1.1 - Host: example.com - Content-Type: application/jose+json + /* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "5XJ1L3lEkMG7tR6pA00clA", - "url": "https://example.com/acme/new-order" - }), - "payload": base64url({ - "identifiers": [{"type:"dns","value":"example.com"}], - "notBefore": "2016-01-01T00:00:00Z", - "notAfter": "2016-01-08T00:00:00Z" - }), - "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" - } - */ + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } + */ , _getChallenges: function (options, auth) { console.log('\n[DEBUG] getChallenges\n'); return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - console.log('Authorization:'); - console.log(resp.body.challenges); - return resp.body.challenges; + return resp.body; }); } // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 - , _postChallenge: function (options, ch) { - var me = this; + , _postChallenge: function (options, identifier, ch) { + var me = this; var body = { }; - var payload = JSON.stringify(body); - //var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - ); + var payload = JSON.stringify(body); - var thumbprint = RSA.thumbprint(options.keypair); + var thumbprint = RSA.thumbprint(options.accountKeypair); var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token @@ -235,67 +214,115 @@ function create(deps) { console.log(thumbprint); console.log('keyAuthorization:'); console.log(keyAuthorization); - /* - options.setChallenge(ch.token, thumbprint, keyAuthorization, function (err) { + + return new Promise(function (resolve, reject) { + if (options.setupChallenge) { + options.setupChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + , testChallenge + ); + } else { + options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge); + } + + function testChallenge(err) { + if (err) { reject(err); return; } + + // TODO put check dns / http checks here? + // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} + // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" + + function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); + } + + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return wait().then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + try { + if (options.teardownChallenge) { + options.teardownChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + , function () {} + ); + } else { + options.removeChallenge(identifier.value, ch.token, function () {}); + } + } catch(e) {} + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(payload) + ); + me._nonce = null; + return request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return wait().then(pollStatus).then(resolve, reject); + }); + } + + return wait(20 * 1000).then(post); + } }); - */ - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); - return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - console.log('poll: again'); - return wait().then(pollStatus); - } - - if ('valid' === resp.body.status) { - console.log('poll: valid'); - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - }); - } - - console.log('\n[DEBUG] postChallenge\n'); - //console.log('\n[DEBUG] stop to fix things\n'); return; - - function post() { - return request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); - return wait().then(pollStatus); - }); - } - - return wait(20 * 1000).then(post); } , _finalizeOrder: function (options, validatedDomains) { console.log('finalizeOrder:'); - var me = this; + var me = this; - var csr = RSA.generateCsrWeb64(options.certificateKeypair, validatedDomains); + var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -306,14 +333,15 @@ function create(deps) { } function pollCert() { - //var payload = JSON.stringify(body, null, 2); var jws = RSA.signJws( - options.keypair - , new Buffer(payload) + options.accountKeypair + , undefined , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , new Buffer(payload) ); console.log('finalize:', me._finalize); + me._nonce = null; return request({ method: 'POST' , url: me._finalize @@ -348,7 +376,7 @@ function create(deps) { return pollCert(); } - , _getCertificate: function (auth) { + , _getCertificate: function () { var me = this; return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { console.log('Certificate:'); @@ -357,91 +385,93 @@ function create(deps) { }); } , getCertificate: function (options, cb) { - var me = this; + console.log('[acme-v2] DEBUG get cert 1'); + var me = this; - var body = { - identifiers: [ - { type: "dns" , value: "test.ppl.family" } - /* - , { type: "dns" , value: "example.net" } - */ - ] - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; + if (!options.challengeTypes) { + if (!options.challengeType) { + cb(new Error("challenge type must be specified")); + return Promise.reject(new Error("challenge type must be specified")); + } + options.challengeTypes = [ options.challengeType ]; + } - var payload = JSON.stringify(body); - //var payload = JSON.stringify(body, null, 2); - var jws = RSA.signJws( - options.keypair - , new Buffer(payload) - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - ); + console.log('[acme-v2] getCertificate'); + return me.getNonce().then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; - console.log('\n[DEBUG] newOrder\n'); - return request({ - method: 'POST' - , url: me._directoryUrls.newOrder - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers['location']; - console.log(location); // the account id url - console.log(resp.toJSON()); - //var body = JSON.parse(resp.body); - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + var payload = JSON.stringify(body); + var jws = RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , new Buffer(payload) + ); - //return resp.body; - return Promise.all(me._authorizations.map(function (auth) { - console.log('authz', auth); - return me._getChallenges(options, auth).then(function (challenges) { - var chp; + console.log('\n[DEBUG] newOrder\n'); + me._nonce = null; + return request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log(location); // the account id url + console.log(resp.toJSON()); + //var body = JSON.parse(resp.body); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; - challenges.forEach(function (ch) { - if ('http-01' !== ch.type) { - return; + //return resp.body; + return Promise.all(me._authorizations.map(function (authUrl) { + return me._getChallenges(options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); } - chp = me._postChallenge(options, ch); + + return me._postChallenge(options, results.identifier, challenge); + }); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; }); - return chp; + return me._finalizeOrder(options, validatedDomains); + }).then(function () { + return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb); }); - })).then(function () { - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); - - return me._finalizeOrder(options, validatedDomains); - }).then(function () { - return me._getCertificate(); }); }); - } + } }; return acme2; } -var RSA = require('rsa-compat').RSA; -var acme2 = create(); -acme2.getAcmeUrls().then(function (body) { - console.log(body); - acme2.getNonce().then(function (nonce) { - console.log(nonce); - - var options = { - email: 'coolaj86@gmail.com' - , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , certificateKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - }; - acme2.registerNewAccount(options).then(function (account) { - console.log(account); - acme2.getCertificate(options, function () { - console.log('got cert'); - }); - }); - }); +module.exports.ACME = { + create: create +}; +Object.keys(defaults).forEach(function (key) { + module.exports.ACME[key] = defaults[key]; }); diff --git a/package.json b/package.json index 79af90f..0f57309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.0.2", + "version": "0.6.0", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { @@ -23,6 +23,6 @@ "license": "(MIT OR Apache-2.0)", "dependencies": { "request": "^2.85.0", - "rsa-compat": "^1.2.7" + "rsa-compat": "^1.3.0" } } -- 2.38.5 From e4beb3f04ed3a2493efedc060502ee64b598c78f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:33:54 -0600 Subject: [PATCH 011/252] use supplied directoryUrl --- node.js | 15 +++------------ test.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 test.js diff --git a/node.js b/node.js index 2069281..f1c01e4 100644 --- a/node.js +++ b/node.js @@ -61,10 +61,10 @@ function create(deps) { var request = deps.promisify(getRequest({})); var acme2 = { - getAcmeUrls: function () { + getAcmeUrls: function (_directoryUrl) { var me = this; - return request({ url: directoryUrl }).then(function (resp) { - me._directoryUrls = JSON.parse(resp.body); + return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) { + me._directoryUrls = resp.body; me._tos = me._directoryUrls.meta.termsOfService; return me._directoryUrls; }); @@ -206,14 +206,6 @@ function create(deps) { var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token - console.log('type:'); - console.log(ch.type); - console.log('ch.token:'); - console.log(ch.token); - console.log('thumbprint:'); - console.log(thumbprint); - console.log('keyAuthorization:'); - console.log(keyAuthorization); return new Promise(function (resolve, reject) { if (options.setupChallenge) { @@ -426,7 +418,6 @@ function create(deps) { var location = resp.toJSON().headers.location; console.log(location); // the account id url console.log(resp.toJSON()); - //var body = JSON.parse(resp.body); me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; diff --git a/test.js b/test.js new file mode 100644 index 0000000..7c1f373 --- /dev/null +++ b/test.js @@ -0,0 +1,56 @@ +'use strict'; + +var RSA = require('rsa-compat').RSA; +var acme2 = require('./').ACME.create({ RSA: RSA }); + +acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) { + console.log(body); + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + /* + , setupChallenge: function (opts) { + console.log('type:'); + console.log(ch.type); + console.log('ch.token:'); + console.log(ch.token); + console.log('thumbprint:'); + console.log(thumbprint); + console.log('keyAuthorization:'); + console.log(keyAuthorization); + console.log('dnsAuthorization:'); + console.log(dnsAuthorization); + } + */ + // teardownChallenge + , setChallenge: function (hostname, key, val, cb) { + console.log('[DEBUG] set challenge', hostname, key, val); + console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'"); + setTimeout(cb, 20 * 1000); + } + , removeChallenge: function (hostname, key, cb) { + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: 'http-01' + , email: 'coolaj86@gmail.com' + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: [ 'test.ppl.family' ] + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); +}); -- 2.38.5 From a38d751cfafe24b5ee311ff416fc73b127fb6247 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:33:54 -0600 Subject: [PATCH 012/252] use supplied directoryUrl --- node.js | 15 +++------------ test.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 test.js diff --git a/node.js b/node.js index 2069281..f1c01e4 100644 --- a/node.js +++ b/node.js @@ -61,10 +61,10 @@ function create(deps) { var request = deps.promisify(getRequest({})); var acme2 = { - getAcmeUrls: function () { + getAcmeUrls: function (_directoryUrl) { var me = this; - return request({ url: directoryUrl }).then(function (resp) { - me._directoryUrls = JSON.parse(resp.body); + return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) { + me._directoryUrls = resp.body; me._tos = me._directoryUrls.meta.termsOfService; return me._directoryUrls; }); @@ -206,14 +206,6 @@ function create(deps) { var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token - console.log('type:'); - console.log(ch.type); - console.log('ch.token:'); - console.log(ch.token); - console.log('thumbprint:'); - console.log(thumbprint); - console.log('keyAuthorization:'); - console.log(keyAuthorization); return new Promise(function (resolve, reject) { if (options.setupChallenge) { @@ -426,7 +418,6 @@ function create(deps) { var location = resp.toJSON().headers.location; console.log(location); // the account id url console.log(resp.toJSON()); - //var body = JSON.parse(resp.body); me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; diff --git a/test.js b/test.js new file mode 100644 index 0000000..7c1f373 --- /dev/null +++ b/test.js @@ -0,0 +1,56 @@ +'use strict'; + +var RSA = require('rsa-compat').RSA; +var acme2 = require('./').ACME.create({ RSA: RSA }); + +acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) { + console.log(body); + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + /* + , setupChallenge: function (opts) { + console.log('type:'); + console.log(ch.type); + console.log('ch.token:'); + console.log(ch.token); + console.log('thumbprint:'); + console.log(thumbprint); + console.log('keyAuthorization:'); + console.log(keyAuthorization); + console.log('dnsAuthorization:'); + console.log(dnsAuthorization); + } + */ + // teardownChallenge + , setChallenge: function (hostname, key, val, cb) { + console.log('[DEBUG] set challenge', hostname, key, val); + console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'"); + setTimeout(cb, 20 * 1000); + } + , removeChallenge: function (hostname, key, cb) { + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: 'http-01' + , email: 'coolaj86@gmail.com' + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: [ 'test.ppl.family' ] + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); +}); -- 2.38.5 From ea32bc6ae52cb5041a398a63025cb809fe4bd269 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:35:28 -0600 Subject: [PATCH 013/252] note sponsorship --- README.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfd68d1..c57a4df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ acme-v2.js ========== +| Sponsored by [ppl](https://ppl.family) + A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 diff --git a/package.json b/package.json index 0f57309..e1a7bb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.6.0", + "version": "0.6.1", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From 1f71a91979887d019901f044044e42f414a5ebb2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:35:28 -0600 Subject: [PATCH 014/252] note sponsorship --- README.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfd68d1..c57a4df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ acme-v2.js ========== +| Sponsored by [ppl](https://ppl.family) + A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 diff --git a/package.json b/package.json index 0f57309..e1a7bb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.6.0", + "version": "0.6.1", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "main": "node.js", "scripts": { -- 2.38.5 From 0c2f6a3aef3fa66cce191651877d13b80045adba Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:38:21 -0600 Subject: [PATCH 015/252] add homepage --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e1a7bb0..64aba1e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "acme-v2", - "version": "0.6.1", + "version": "0.6.2", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.38.5 From b70b1002b9eb717275a85ccf8d4ad81b2a4aa0fe Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:38:21 -0600 Subject: [PATCH 016/252] add homepage --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e1a7bb0..64aba1e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "acme-v2", - "version": "0.6.1", + "version": "0.6.2", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.38.5 From 2b09af6137ccc28786179e2d57257c11e1e3a9b9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:41:10 -0600 Subject: [PATCH 017/252] add keywords --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index 64aba1e..6ca8433 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,15 @@ "acmev2", "acme-v2", "acme", + "acme2", + "ssl", + "tls", + "https", + "Let's Encrypt", + "letsencrypt", "letsencrypt-v2", "letsencryptv2", + "letsencrypt2", "greenlock", "greenlock2" ], -- 2.38.5 From 27fb85ed9c209ffa2e1dfe02f897f7051548e766 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 21 Mar 2018 01:41:10 -0600 Subject: [PATCH 018/252] add keywords --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index 64aba1e..6ca8433 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,15 @@ "acmev2", "acme-v2", "acme", + "acme2", + "ssl", + "tls", + "https", + "Let's Encrypt", + "letsencrypt", "letsencrypt-v2", "letsencryptv2", + "letsencrypt2", "greenlock", "greenlock2" ], -- 2.38.5 From b12a1a7ea4c83372f3ebeb1cc0349e81374455a4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 01:31:57 -0600 Subject: [PATCH 019/252] yay for backwards compat tested and working --- README.md | 42 ++- compat.js | 53 +++ node.js | 860 +++++++++++++++++++++++++----------------------- test.cb.js | 75 +++++ test.compat.js | 57 ++++ test.js | 95 +++--- test.promise.js | 84 +++++ 7 files changed, 786 insertions(+), 480 deletions(-) create mode 100644 compat.js create mode 100644 test.cb.js create mode 100644 test.compat.js create mode 100644 test.promise.js diff --git a/README.md b/README.md index c57a4df..a5202be 100644 --- a/README.md +++ b/README.md @@ -31,27 +31,52 @@ Todo * export http and dns challenge tests * support ECDSA keys +## Let's Encrypt Directory URLs + +``` +# Production URL +https://acme-v02.api.letsencrypt.org/directory +``` + +``` +# Staging URL +https://acme-staging-v02.api.letsencrypt.org/directory +``` + ## API ``` -var ACME = require('acme-v2.js').ACME.create({ +var ACME = require('acme-v2').ACME.create({ RSA: require('rsa-compat').RSA + + // other overrides +, request: require('request') +, promisify: require('util').promisify + + // used for constructing user-agent +, os: require('os') +, process: require('process') + + // used for overriding the default user-agent +, userAgent: 'My custom UA String' +, getUserAgentString: function (deps) { return 'My custom UA String'; } }); ``` ```javascript // Accounts -ACME.registerNewAccount(options, cb) // returns "regr" registration data +ACME.accounts.create(options) // returns Promise registration data { email: '' // valid email (server checks MX records) , accountKeypair: { // privateKeyPem or privateKeyJwt privateKeyPem: '' } - , agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end) + , agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl } + // Registration -ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) }) +ACME.certificates.create(options) // returns Promise { newAuthzUrl: '' // specify acmeUrls.newAuthz , newCertUrl: '' // specify acmeUrls.newCert @@ -64,20 +89,19 @@ ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key } , domains: [ 'example.com' ] - , setChallenge: fn (hostname, key, val, cb) - , removeChallenge: fn (hostname, key, cb) + , setChallenge: fn (hostname, key, val) // return Promise + , removeChallenge: fn (hostname, key) // return Promise } + // Discovery URLs -ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) +ACME.init(acmeDirectoryUrl) // returns Promise ``` Helpers & Stuff ```javascript // Constants -ACME.productionServerUrl // https://acme-v02.api.letsencrypt.org/directory -ACME.stagingServerUrl // https://acme-staging-v02.api.letsencrypt.org/directory ACME.acmeChallengePrefix // /.well-known/acme-challenge/ ``` diff --git a/compat.js b/compat.js new file mode 100644 index 0000000..b9f6b62 --- /dev/null +++ b/compat.js @@ -0,0 +1,53 @@ +'use strict'; + +var ACME2 = require('./').ACME; + +function resolveFn(cb) { + return function (val) { + // nextTick to get out of Promise chain + process.nextTick(function () { cb(null, val); }); + }; +} +function rejectFn(cb) { + return function (err) { + console.log('reject something or other:'); + console.log(err.stack); + // nextTick to get out of Promise chain + process.nextTick(function () { cb(err); }); + }; +} + +function create(deps) { + deps.LeCore = {}; + var acme2 = ACME2.create(deps); + acme2.registerNewAccount = function (options, cb) { + acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getCertificate = function (options, cb) { + acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getAcmeUrls = function (options, cb) { + acme2.init(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; + acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + return acme2; +} + +module.exports.ACME = { }; +module.exports.defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' +, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] +, challengeTypes: [ 'http-01', 'dns-01' ] +, challengeType: 'http-01' +, keyType: 'rsa' // ecdsa +, keySize: 2048 // 256 +}; +Object.keys(module.exports.defaults).forEach(function (key) { + module.exports.ACME[key] = module.exports.defaults[key]; +}); +Object.keys(ACME2).forEach(function (key) { + module.exports.ACME[key] = ACME2[key]; + module.exports.ACME.create = create; +}); diff --git a/node.js b/node.js index f1c01e4..67544db 100644 --- a/node.js +++ b/node.js @@ -6,463 +6,487 @@ 'use strict'; /* globals Promise */ -var defaults = { - productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' -, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' -, acmeChallengePrefix: '/.well-known/acme-challenge/' -, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] -, challengeType: 'http-01' // dns-01 -, keyType: 'rsa' // ecdsa -, keySize: 2048 // 256 -}; +var ACME = module.exports.ACME = {}; -function create(deps) { - if (!deps) { deps = {}; } - deps.LeCore = {}; - deps.pkg = deps.pkg || require('./package.json'); - deps.os = deps.os || require('os'); - deps.process = deps.process || require('process'); +ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; +ACME._getUserAgentString = function (deps) { var uaDefaults = { pkg: "Greenlock/" + deps.pkg.version - , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" - , node: " Node.js/" + deps.process.version + , os: "(" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" + , node: "Node.js/" + deps.process.version , user: '' }; - //var currentUAProps; - function getUaString() { - var userAgent = ''; + var userAgent = []; - //Object.keys(currentUAProps) - Object.keys(uaDefaults).forEach(function (key) { - userAgent += uaDefaults[key]; - //userAgent += currentUAProps[key]; - }); - - return userAgent.trim(); - } - - function getRequest(opts) { - if (!opts) { opts = {}; } - - return deps.request.defaults({ - headers: { - 'User-Agent': opts.userAgent || getUaString() - } - }); - } - - var RSA = deps.RSA || require('rsa-compat').RSA; - deps.request = deps.request || require('request'); - deps.promisify = deps.promisify || require('util').promisify; - - var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; - var request = deps.promisify(getRequest({})); - - var acme2 = { - getAcmeUrls: function (_directoryUrl) { - var me = this; - return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) { - me._directoryUrls = resp.body; - me._tos = me._directoryUrls.meta.termsOfService; - return me._directoryUrls; - }); + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function (key) { + if (uaDefaults[key]) { + userAgent.push(uaDefaults[key]); } - , getNonce: function () { - var me = this; - if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } - return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - return me._nonce; - }); - } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ - , registerNewAccount: function (options) { - var me = this; + }); - console.log('[acme-v2] registerNewAccount'); + return userAgent.join(' ').trim(); +}; +ACME._directory = function (me) { + return me._request({ url: me.directoryUrl, json: true }); +}; +ACME._getNonce = function (me) { + if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } + return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); +}; +// ACME RFC Section 7.3 Account Creation +/* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } +*/ +ACME._registerAccount = function (me, options) { + console.log('[acme-v2] accounts.create'); - return me.getNonce().then(function () { - return new Promise(function (resolve, reject) { + return ACME._getNonce(me).then(function () { + return new Promise(function (resolve, reject) { - function agree(err, tosUrl) { - if (err) { reject(err); return; } - if (me._tos !== tosUrl) { - err = new Error("You must agree to the ToS at '" + me._tos + "'"); - err.code = "E_AGREE_TOS"; - reject(err); - return; - } - - var jwk = RSA.exportPublicJwk(options.accountKeypair); - var body = { - termsOfServiceAgreed: tosUrl === me._tos - , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] - }; - if (options.externalAccount) { - body.externalAccountBinding = RSA.signJws( - options.externalAccount.secret - , undefined - , { alg: "HS256" - , kid: options.externalAccount.id - , url: me._directoryUrls.newAccount - } - , new Buffer(JSON.stringify(jwk)) - ); - } - var payload = JSON.stringify(body); - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce - , alg: 'RS256' - , url: me._directoryUrls.newAccount - , jwk: jwk - } - , new Buffer(payload) - ); - - console.log('[acme-v2] registerNewAccount JSON body:'); - delete jws.header; - console.log(jws); - me._nonce = null; - return request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - console.log('[DEBUG] new account location:'); // the account id url - console.log(location); // the account id url - console.log(resp.toJSON()); - me._kid = location; - return resp.body; - }).then(resolve); - } - - console.log('[acme-v2] agreeToTerms'); - options.agreeToTerms(me._tos, agree); - }); - }); - } - /* - POST /acme/new-order HTTP/1.1 - Host: example.com - Content-Type: application/jose+json - - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "5XJ1L3lEkMG7tR6pA00clA", - "url": "https://example.com/acme/new-order" - }), - "payload": base64url({ - "identifiers": [{"type:"dns","value":"example.com"}], - "notBefore": "2016-01-01T00:00:00Z", - "notAfter": "2016-01-08T00:00:00Z" - }), - "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" - } - */ - , _getChallenges: function (options, auth) { - console.log('\n[DEBUG] getChallenges\n'); - return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - return resp.body; - }); - } - // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 - , _postChallenge: function (options, identifier, ch) { - var me = this; - - var body = { }; - - var payload = JSON.stringify(body); - - var thumbprint = RSA.thumbprint(options.accountKeypair); - var keyAuthorization = ch.token + '.' + thumbprint; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - // /.well-known/acme-challenge/:token - - return new Promise(function (resolve, reject) { - if (options.setupChallenge) { - options.setupChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , testChallenge - ); - } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge); - } - - function testChallenge(err) { - if (err) { reject(err); return; } - - // TODO put check dns / http checks here? - // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} - // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - - function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); - return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - console.log('poll: again'); - return wait().then(pollStatus); - } - - if ('valid' === resp.body.status) { - console.log('poll: valid'); - try { - if (options.teardownChallenge) { - options.teardownChallenge( - { identifier: identifier - , type: ch.type - , token: ch.token - } - , function () {} - ); - } else { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - - return Promise.reject(new Error("[acme-v2] bad challenge state")); - }); - } - - console.log('\n[DEBUG] postChallenge\n'); - //console.log('\n[DEBUG] stop to fix things\n'); return; - - function post() { - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(payload) - ); - me._nonce = null; - return request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); - return wait().then(pollStatus).then(resolve, reject); - }); - } - - return wait(20 * 1000).then(post); - } - }); - } - , _finalizeOrder: function (options, validatedDomains) { - console.log('finalizeOrder:'); - var me = this; - - var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); - var body = { csr: csr }; - var payload = JSON.stringify(body); - - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - - function pollCert() { - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } - , new Buffer(payload) - ); - - console.log('finalize:', me._finalize); - me._nonce = null; - return request({ - method: 'POST' - , url: me._finalize - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - - console.log('order finalized: resp.body:'); - console.log(resp.body); - - if ('processing' === resp.body.status) { - return wait().then(pollCert); - } - - if ('valid' === resp.body.status) { - me._expires = resp.body.expires; - me._certificate = resp.body.certificate; - - return resp.body; - } - - if ('invalid' === resp.body.status) { - console.error('cannot finalize: badness'); - return; - } - - console.error('(x) cannot finalize: badness'); + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + reject(err); return; - }); - } - - return pollCert(); - } - , _getCertificate: function () { - var me = this; - return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('Certificate:'); - console.log(resp.body); - return resp.body; - }); - } - , getCertificate: function (options, cb) { - console.log('[acme-v2] DEBUG get cert 1'); - var me = this; - - if (!options.challengeTypes) { - if (!options.challengeType) { - cb(new Error("challenge type must be specified")); - return Promise.reject(new Error("challenge type must be specified")); } - options.challengeTypes = [ options.challengeType ]; - } - console.log('[acme-v2] getCertificate'); - return me.getNonce().then(function () { + var jwk = me.RSA.exportPublicJwk(options.accountKeypair); var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] }; - + if (options.externalAccount) { + body.externalAccountBinding = me.RSA.signJws( + options.externalAccount.secret + , undefined + , { alg: "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , new Buffer(JSON.stringify(jwk)) + ); + } var payload = JSON.stringify(body); - var jws = RSA.signJws( + var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , { nonce: me._nonce + , alg: 'RS256' + , url: me._directoryUrls.newAccount + , jwk: jwk + } , new Buffer(payload) ); - console.log('\n[DEBUG] newOrder\n'); + console.log('[acme-v2] accounts.create JSON body:'); + delete jws.header; + console.log(jws); me._nonce = null; - return request({ + return me._request({ method: 'POST' - , url: me._directoryUrls.newOrder + , url: me._directoryUrls.newAccount , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; + console.log('[DEBUG] new account location:'); // the account id url console.log(location); // the account id url console.log(resp.toJSON()); - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + me._kid = location; + return resp.body; + }).then(resolve, reject); + } - //return resp.body; - return Promise.all(me._authorizations.map(function (authUrl) { - return me._getChallenges(options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - })[0]; - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; - } - })[0]; + console.log('[acme-v2] agreeToTerms'); + if (1 === options.agreeToTerms.length) { + return options.agreeToTerms(me._tos).then(agree, reject); + } + else if (2 === options.agreeToTerms.length) { + return options.agreeToTerms(me._tos, function (err, tosUrl) { + if (!err) { agree(tosUrl); return; } + reject(err); + }); + } + else { + reject(new Error('agreeToTerms has incorrect function signature.' + + ' Should be fn(tos) { return Promise; }')); + } + }); + }); +}; +/* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } +*/ +ACME._getChallenges = function (me, options, auth) { + console.log('\n[DEBUG] getChallenges\n'); + return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { + return resp.body; + }); +}; +ACME._wait = function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); +}; +// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 +ACME._postChallenge = function (me, options, identifier, ch) { + var body = { }; + + var payload = JSON.stringify(body); + + var thumbprint = me.RSA.thumbprint(options.accountKeypair); + var keyAuthorization = ch.token + '.' + thumbprint; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + // /.well-known/acme-challenge/:token + + return new Promise(function (resolve, reject) { + function failChallenge(err) { + if (err) { reject(err); return; } + testChallenge(); + } + + function testChallenge() { + // TODO put check dns / http checks here? + // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} + // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" + + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return ACME._wait(1 * 1000).then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + try { + if (1 === options.removeChallenge.length) { + options.removeChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + ).then(function () {}, function () {}); + } else if (2 === options.removeChallenge.length) { + options.removeChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + , function (err) { return err; } + ); + } else { + options.removeChallenge(identifier.value, ch.token, function () {}); } + } catch(e) {} + return resp.body; + } - return me._postChallenge(options, results.identifier, challenge); - }); - })).then(function () { - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } - return me._finalizeOrder(options, validatedDomains); - }).then(function () { - return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb); - }); + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(payload) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + }); + } + + return ACME._wait(1 * 1000).then(post); + } + + try { + if (1 === options.setChallenge.length) { + options.setChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + ).then(testChallenge, reject); + } else if (2 === options.setChallenge.length) { + options.setChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + , failChallenge + ); + } else { + options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); + } + } catch(e) { + reject(e); + } + }); +}; +ACME._finalizeOrder = function (me, options, validatedDomains) { + console.log('finalizeOrder:'); + var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , new Buffer(payload) + ); + + console.log('finalize:', me._finalize); + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._finalize + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + + console.log('order finalized: resp.body:'); + console.log(resp.body); + + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; + } + + if ('invalid' === resp.body.status) { + console.error('cannot finalize: badness'); + return; + } + + console.error('(x) cannot finalize: badness'); + return; + }); + } + + return pollCert(); +}; +ACME._getCertificate = function (me, options) { + console.log('[acme-v2] DEBUG get cert 1'); + + if (!options.challengeTypes) { + if (!options.challengeType) { + return Promise.reject(new Error("challenge type must be specified")); + } + options.challengeTypes = [ options.challengeType ]; + } + + console.log('[acme-v2] certificates.create'); + return ACME._getNonce(me).then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , new Buffer(payload) + ); + + console.log('\n[DEBUG] newOrder\n'); + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log(location); // the account id url + console.log(resp.toJSON()); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; + + //return resp.body; + return Promise.all(me._authorizations.map(function (authUrl, i) { + console.log("Authorizations map #" + i); + return ACME._getChallenges(me, options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; + + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + } + + return ACME._postChallenge(me, options, results.identifier, challenge); + }); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); + + return ACME._finalizeOrder(me, options, validatedDomains); + }).then(function () { + return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('Certificate:'); + console.log(resp.body); + return resp.body; }); }); + }); + }); +}; + +ACME.create = function create(me) { + if (!me) { me = {}; } + me.acmeChallengePrefix = ACME.acmeChallengePrefix; + me.RSA = me.RSA || require('rsa-compat').RSA; + me.request = me.request || require('request'); + me.promisify = me.promisify || require('util').promisify; + + + if ('function' !== typeof me.getUserAgentString) { + me.pkg = me.pkg || require('./package.json'); + me.os = me.os || require('os'); + me.process = me.process || require('process'); + me.userAgent = ACME._getUserAgentString(me); + } + + function getRequest(opts) { + if (!opts) { opts = {}; } + + return me.request.defaults({ + headers: { + 'User-Agent': opts.userAgent || me.userAgent || me.getUserAgentString(me) + } + }); + } + + if ('function' !== typeof me._request) { + me._request = me.promisify(getRequest({})); + } + + me.init = function (_directoryUrl) { + me.directoryUrl = me.directoryUrl || _directoryUrl; + return ACME._directory(me).then(function (resp) { + me._directoryUrls = resp.body; + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + }; + me.accounts = { + create: function (options) { + return ACME._registerAccount(me, options); } }; - return acme2; -} - -module.exports.ACME = { - create: create + me.certificates = { + create: function (options) { + return ACME._getCertificate(me, options); + } + }; + return me; }; -Object.keys(defaults).forEach(function (key) { - module.exports.ACME[key] = defaults[key]; -}); diff --git a/test.cb.js b/test.cb.js new file mode 100644 index 0000000..7cccb73 --- /dev/null +++ b/test.cb.js @@ -0,0 +1,75 @@ +'use strict'; + +module.exports.run = function run(web, chType, email) { + var RSA = require('rsa-compat').RSA; + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + var acme2 = require('./compat').ACME.create({ RSA: RSA }); + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + console.log(web, chType, email); + return; + acme2.init(directoryUrl).then(function (body) { + console.log(body); + return; + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (opts, cb) { + + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); + + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + function onAny() { + process.stdin.pause(); + process.stdin.removeEventListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + } + , removeChallenge: function (opts, cb) { + // hostname, key + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; diff --git a/test.compat.js b/test.compat.js new file mode 100644 index 0000000..b4ec70e --- /dev/null +++ b/test.compat.js @@ -0,0 +1,57 @@ +'use strict'; + +var RSA = require('rsa-compat').RSA; + +module.exports.run = function (web, chType, email) { + console.log('[DEBUG] run', web, chType, email); + + var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); + acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) { + if (err) { console.log('err 1'); throw err; } + console.log(body); + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (hostname, token, val, cb) { + console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); + console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } + + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + } + , removeChallenge: function (hostname, key, cb) { + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options, function (err, account) { + if (err) { console.log('err 2'); throw err; } + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (err, fullchainPem) { + if (err) { console.log('err 3'); throw err; } + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; diff --git a/test.js b/test.js index 7c1f373..6a6772b 100644 --- a/test.js +++ b/test.js @@ -1,56 +1,45 @@ 'use strict'; -var RSA = require('rsa-compat').RSA; -var acme2 = require('./').ACME.create({ RSA: RSA }); - -acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) { - console.log(body); - - var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); - } - /* - , setupChallenge: function (opts) { - console.log('type:'); - console.log(ch.type); - console.log('ch.token:'); - console.log(ch.token); - console.log('thumbprint:'); - console.log(thumbprint); - console.log('keyAuthorization:'); - console.log(keyAuthorization); - console.log('dnsAuthorization:'); - console.log(dnsAuthorization); - } - */ - // teardownChallenge - , setChallenge: function (hostname, key, val, cb) { - console.log('[DEBUG] set challenge', hostname, key, val); - console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'"); - setTimeout(cb, 20 * 1000); - } - , removeChallenge: function (hostname, key, cb) { - console.log('[DEBUG] remove challenge', hostname, key); - setTimeout(cb, 1 * 1000); - } - , challengeType: 'http-01' - , email: 'coolaj86@gmail.com' - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - , domains: [ 'test.ppl.family' ] - }; - - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); - console.log(account); - - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); - console.log(fullchainPem); - }); - }); +var readline = require('readline'); +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout }); + +function getWeb() { + rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { + web = (web||'').trim().split(/,/g); + if (!web[0]) { getWeb(); return; } + + if (web.some(function (w) { return '*' === w[0]; })) { + console.log('Wildcard domains must use dns-01'); + getEmail(web, 'dns-01'); + } else { + getChallengeType(web); + } + }); +} + +function getChallengeType(web) { + rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { + chType = (chType||'').trim(); + if (!chType) { chType = 'http-01'; } + + getEmail(web, chType); + }); +} + +function getEmail(web, chType) { + rl.question('What email should we use? (optional) ', function (email) { + email = (email||'').trim(); + if (!email) { email = null; } + + rl.close(); + console.log("[DEBUG] rl blah blah"); + require('./test.compat.js').run(web, chType, email); + //require('./test.cb.js').run(web, chType, email); + //require('./test.promise.js').run(web, chType, email); + }); +} + +getWeb(); diff --git a/test.promise.js b/test.promise.js new file mode 100644 index 0000000..4da5392 --- /dev/null +++ b/test.promise.js @@ -0,0 +1,84 @@ +'use strict'; + +/* global Promise */ + +module.exports.run = function run(web, chType, email) { + var RSA = require('rsa-compat').RSA; + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + var acme2 = require('./compat').ACME.create({ RSA: RSA }); + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + console.log(web, chType, email); + return; + acme2.init(directoryUrl).then(function (body) { + console.log(body); + return; + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (opts) { + + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); + + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + return new Promise(function (resolve) { + function onAny() { + process.stdin.pause(); + process.stdin.removeEventListener('data', onAny); + process.stdin.setRawMode(false); + + resolve(); + } + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + }); + } + , removeChallenge: function (opts) { + // hostname, key + console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization); + console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'"); + + return new Promise(function (resolve) { + setTimeout(resolve, 1 * 1000); + }); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; -- 2.38.5 From fe5a48764b961bdb035263fa39be9b1e85822261 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 01:31:57 -0600 Subject: [PATCH 020/252] yay for backwards compat tested and working --- README.md | 42 ++- compat.js | 53 +++ node.js | 860 +++++++++++++++++++++++++----------------------- test.cb.js | 75 +++++ test.compat.js | 57 ++++ test.js | 95 +++--- test.promise.js | 84 +++++ 7 files changed, 786 insertions(+), 480 deletions(-) create mode 100644 compat.js create mode 100644 test.cb.js create mode 100644 test.compat.js create mode 100644 test.promise.js diff --git a/README.md b/README.md index c57a4df..a5202be 100644 --- a/README.md +++ b/README.md @@ -31,27 +31,52 @@ Todo * export http and dns challenge tests * support ECDSA keys +## Let's Encrypt Directory URLs + +``` +# Production URL +https://acme-v02.api.letsencrypt.org/directory +``` + +``` +# Staging URL +https://acme-staging-v02.api.letsencrypt.org/directory +``` + ## API ``` -var ACME = require('acme-v2.js').ACME.create({ +var ACME = require('acme-v2').ACME.create({ RSA: require('rsa-compat').RSA + + // other overrides +, request: require('request') +, promisify: require('util').promisify + + // used for constructing user-agent +, os: require('os') +, process: require('process') + + // used for overriding the default user-agent +, userAgent: 'My custom UA String' +, getUserAgentString: function (deps) { return 'My custom UA String'; } }); ``` ```javascript // Accounts -ACME.registerNewAccount(options, cb) // returns "regr" registration data +ACME.accounts.create(options) // returns Promise registration data { email: '' // valid email (server checks MX records) , accountKeypair: { // privateKeyPem or privateKeyJwt privateKeyPem: '' } - , agreeToTerms: fn (tosUrl, cb) {} // must specify agree=tosUrl to continue (or falsey to end) + , agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl } + // Registration -ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key), cert, chain (ca) }) +ACME.certificates.create(options) // returns Promise { newAuthzUrl: '' // specify acmeUrls.newAuthz , newCertUrl: '' // specify acmeUrls.newCert @@ -64,20 +89,19 @@ ACME.getCertificate(options, cb) // returns (err, pems={ privkey (key } , domains: [ 'example.com' ] - , setChallenge: fn (hostname, key, val, cb) - , removeChallenge: fn (hostname, key, cb) + , setChallenge: fn (hostname, key, val) // return Promise + , removeChallenge: fn (hostname, key) // return Promise } + // Discovery URLs -ACME.getAcmeUrls(acmeDiscoveryUrl, cb) // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) +ACME.init(acmeDirectoryUrl) // returns Promise ``` Helpers & Stuff ```javascript // Constants -ACME.productionServerUrl // https://acme-v02.api.letsencrypt.org/directory -ACME.stagingServerUrl // https://acme-staging-v02.api.letsencrypt.org/directory ACME.acmeChallengePrefix // /.well-known/acme-challenge/ ``` diff --git a/compat.js b/compat.js new file mode 100644 index 0000000..b9f6b62 --- /dev/null +++ b/compat.js @@ -0,0 +1,53 @@ +'use strict'; + +var ACME2 = require('./').ACME; + +function resolveFn(cb) { + return function (val) { + // nextTick to get out of Promise chain + process.nextTick(function () { cb(null, val); }); + }; +} +function rejectFn(cb) { + return function (err) { + console.log('reject something or other:'); + console.log(err.stack); + // nextTick to get out of Promise chain + process.nextTick(function () { cb(err); }); + }; +} + +function create(deps) { + deps.LeCore = {}; + var acme2 = ACME2.create(deps); + acme2.registerNewAccount = function (options, cb) { + acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getCertificate = function (options, cb) { + acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getAcmeUrls = function (options, cb) { + acme2.init(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; + acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + return acme2; +} + +module.exports.ACME = { }; +module.exports.defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' +, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] +, challengeTypes: [ 'http-01', 'dns-01' ] +, challengeType: 'http-01' +, keyType: 'rsa' // ecdsa +, keySize: 2048 // 256 +}; +Object.keys(module.exports.defaults).forEach(function (key) { + module.exports.ACME[key] = module.exports.defaults[key]; +}); +Object.keys(ACME2).forEach(function (key) { + module.exports.ACME[key] = ACME2[key]; + module.exports.ACME.create = create; +}); diff --git a/node.js b/node.js index f1c01e4..67544db 100644 --- a/node.js +++ b/node.js @@ -6,463 +6,487 @@ 'use strict'; /* globals Promise */ -var defaults = { - productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' -, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' -, acmeChallengePrefix: '/.well-known/acme-challenge/' -, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] -, challengeType: 'http-01' // dns-01 -, keyType: 'rsa' // ecdsa -, keySize: 2048 // 256 -}; +var ACME = module.exports.ACME = {}; -function create(deps) { - if (!deps) { deps = {}; } - deps.LeCore = {}; - deps.pkg = deps.pkg || require('./package.json'); - deps.os = deps.os || require('os'); - deps.process = deps.process || require('process'); +ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; +ACME._getUserAgentString = function (deps) { var uaDefaults = { pkg: "Greenlock/" + deps.pkg.version - , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" - , node: " Node.js/" + deps.process.version + , os: "(" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" + , node: "Node.js/" + deps.process.version , user: '' }; - //var currentUAProps; - function getUaString() { - var userAgent = ''; + var userAgent = []; - //Object.keys(currentUAProps) - Object.keys(uaDefaults).forEach(function (key) { - userAgent += uaDefaults[key]; - //userAgent += currentUAProps[key]; - }); - - return userAgent.trim(); - } - - function getRequest(opts) { - if (!opts) { opts = {}; } - - return deps.request.defaults({ - headers: { - 'User-Agent': opts.userAgent || getUaString() - } - }); - } - - var RSA = deps.RSA || require('rsa-compat').RSA; - deps.request = deps.request || require('request'); - deps.promisify = deps.promisify || require('util').promisify; - - var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; - var request = deps.promisify(getRequest({})); - - var acme2 = { - getAcmeUrls: function (_directoryUrl) { - var me = this; - return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) { - me._directoryUrls = resp.body; - me._tos = me._directoryUrls.meta.termsOfService; - return me._directoryUrls; - }); + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function (key) { + if (uaDefaults[key]) { + userAgent.push(uaDefaults[key]); } - , getNonce: function () { - var me = this; - if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } - return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - return me._nonce; - }); - } - // ACME RFC Section 7.3 Account Creation - /* - { - "protected": base64url({ - "alg": "ES256", - "jwk": {...}, - "nonce": "6S8IqOGY7eL2lsGoTZYifg", - "url": "https://example.com/acme/new-account" - }), - "payload": base64url({ - "termsOfServiceAgreed": true, - "onlyReturnExisting": false, - "contact": [ - "mailto:cert-admin@example.com", - "mailto:admin@example.com" - ] - }), - "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" - } - */ - , registerNewAccount: function (options) { - var me = this; + }); - console.log('[acme-v2] registerNewAccount'); + return userAgent.join(' ').trim(); +}; +ACME._directory = function (me) { + return me._request({ url: me.directoryUrl, json: true }); +}; +ACME._getNonce = function (me) { + if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } + return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); +}; +// ACME RFC Section 7.3 Account Creation +/* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } +*/ +ACME._registerAccount = function (me, options) { + console.log('[acme-v2] accounts.create'); - return me.getNonce().then(function () { - return new Promise(function (resolve, reject) { + return ACME._getNonce(me).then(function () { + return new Promise(function (resolve, reject) { - function agree(err, tosUrl) { - if (err) { reject(err); return; } - if (me._tos !== tosUrl) { - err = new Error("You must agree to the ToS at '" + me._tos + "'"); - err.code = "E_AGREE_TOS"; - reject(err); - return; - } - - var jwk = RSA.exportPublicJwk(options.accountKeypair); - var body = { - termsOfServiceAgreed: tosUrl === me._tos - , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] - }; - if (options.externalAccount) { - body.externalAccountBinding = RSA.signJws( - options.externalAccount.secret - , undefined - , { alg: "HS256" - , kid: options.externalAccount.id - , url: me._directoryUrls.newAccount - } - , new Buffer(JSON.stringify(jwk)) - ); - } - var payload = JSON.stringify(body); - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce - , alg: 'RS256' - , url: me._directoryUrls.newAccount - , jwk: jwk - } - , new Buffer(payload) - ); - - console.log('[acme-v2] registerNewAccount JSON body:'); - delete jws.header; - console.log(jws); - me._nonce = null; - return request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - console.log('[DEBUG] new account location:'); // the account id url - console.log(location); // the account id url - console.log(resp.toJSON()); - me._kid = location; - return resp.body; - }).then(resolve); - } - - console.log('[acme-v2] agreeToTerms'); - options.agreeToTerms(me._tos, agree); - }); - }); - } - /* - POST /acme/new-order HTTP/1.1 - Host: example.com - Content-Type: application/jose+json - - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "5XJ1L3lEkMG7tR6pA00clA", - "url": "https://example.com/acme/new-order" - }), - "payload": base64url({ - "identifiers": [{"type:"dns","value":"example.com"}], - "notBefore": "2016-01-01T00:00:00Z", - "notAfter": "2016-01-08T00:00:00Z" - }), - "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" - } - */ - , _getChallenges: function (options, auth) { - console.log('\n[DEBUG] getChallenges\n'); - return request({ method: 'GET', url: auth, json: true }).then(function (resp) { - return resp.body; - }); - } - // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 - , _postChallenge: function (options, identifier, ch) { - var me = this; - - var body = { }; - - var payload = JSON.stringify(body); - - var thumbprint = RSA.thumbprint(options.accountKeypair); - var keyAuthorization = ch.token + '.' + thumbprint; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - // /.well-known/acme-challenge/:token - - return new Promise(function (resolve, reject) { - if (options.setupChallenge) { - options.setupChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , testChallenge - ); - } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge); - } - - function testChallenge(err) { - if (err) { reject(err); return; } - - // TODO put check dns / http checks here? - // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} - // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - - function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); - return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - console.log('poll: again'); - return wait().then(pollStatus); - } - - if ('valid' === resp.body.status) { - console.log('poll: valid'); - try { - if (options.teardownChallenge) { - options.teardownChallenge( - { identifier: identifier - , type: ch.type - , token: ch.token - } - , function () {} - ); - } else { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - - return Promise.reject(new Error("[acme-v2] bad challenge state")); - }); - } - - console.log('\n[DEBUG] postChallenge\n'); - //console.log('\n[DEBUG] stop to fix things\n'); return; - - function post() { - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(payload) - ); - me._nonce = null; - return request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); - return wait().then(pollStatus).then(resolve, reject); - }); - } - - return wait(20 * 1000).then(post); - } - }); - } - , _finalizeOrder: function (options, validatedDomains) { - console.log('finalizeOrder:'); - var me = this; - - var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); - var body = { csr: csr }; - var payload = JSON.stringify(body); - - function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); - } - - function pollCert() { - var jws = RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } - , new Buffer(payload) - ); - - console.log('finalize:', me._finalize); - me._nonce = null; - return request({ - method: 'POST' - , url: me._finalize - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - - console.log('order finalized: resp.body:'); - console.log(resp.body); - - if ('processing' === resp.body.status) { - return wait().then(pollCert); - } - - if ('valid' === resp.body.status) { - me._expires = resp.body.expires; - me._certificate = resp.body.certificate; - - return resp.body; - } - - if ('invalid' === resp.body.status) { - console.error('cannot finalize: badness'); - return; - } - - console.error('(x) cannot finalize: badness'); + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + reject(err); return; - }); - } - - return pollCert(); - } - , _getCertificate: function () { - var me = this; - return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('Certificate:'); - console.log(resp.body); - return resp.body; - }); - } - , getCertificate: function (options, cb) { - console.log('[acme-v2] DEBUG get cert 1'); - var me = this; - - if (!options.challengeTypes) { - if (!options.challengeType) { - cb(new Error("challenge type must be specified")); - return Promise.reject(new Error("challenge type must be specified")); } - options.challengeTypes = [ options.challengeType ]; - } - console.log('[acme-v2] getCertificate'); - return me.getNonce().then(function () { + var jwk = me.RSA.exportPublicJwk(options.accountKeypair); var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] }; - + if (options.externalAccount) { + body.externalAccountBinding = me.RSA.signJws( + options.externalAccount.secret + , undefined + , { alg: "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , new Buffer(JSON.stringify(jwk)) + ); + } var payload = JSON.stringify(body); - var jws = RSA.signJws( + var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , { nonce: me._nonce + , alg: 'RS256' + , url: me._directoryUrls.newAccount + , jwk: jwk + } , new Buffer(payload) ); - console.log('\n[DEBUG] newOrder\n'); + console.log('[acme-v2] accounts.create JSON body:'); + delete jws.header; + console.log(jws); me._nonce = null; - return request({ + return me._request({ method: 'POST' - , url: me._directoryUrls.newOrder + , url: me._directoryUrls.newAccount , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; + console.log('[DEBUG] new account location:'); // the account id url console.log(location); // the account id url console.log(resp.toJSON()); - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + me._kid = location; + return resp.body; + }).then(resolve, reject); + } - //return resp.body; - return Promise.all(me._authorizations.map(function (authUrl) { - return me._getChallenges(options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - })[0]; - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; - } - })[0]; + console.log('[acme-v2] agreeToTerms'); + if (1 === options.agreeToTerms.length) { + return options.agreeToTerms(me._tos).then(agree, reject); + } + else if (2 === options.agreeToTerms.length) { + return options.agreeToTerms(me._tos, function (err, tosUrl) { + if (!err) { agree(tosUrl); return; } + reject(err); + }); + } + else { + reject(new Error('agreeToTerms has incorrect function signature.' + + ' Should be fn(tos) { return Promise; }')); + } + }); + }); +}; +/* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } +*/ +ACME._getChallenges = function (me, options, auth) { + console.log('\n[DEBUG] getChallenges\n'); + return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { + return resp.body; + }); +}; +ACME._wait = function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); +}; +// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 +ACME._postChallenge = function (me, options, identifier, ch) { + var body = { }; + + var payload = JSON.stringify(body); + + var thumbprint = me.RSA.thumbprint(options.accountKeypair); + var keyAuthorization = ch.token + '.' + thumbprint; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + // /.well-known/acme-challenge/:token + + return new Promise(function (resolve, reject) { + function failChallenge(err) { + if (err) { reject(err); return; } + testChallenge(); + } + + function testChallenge() { + // TODO put check dns / http checks here? + // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} + // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" + + function pollStatus() { + console.log('\n[DEBUG] statusChallenge\n'); + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('pending' === resp.body.status) { + console.log('poll: again'); + return ACME._wait(1 * 1000).then(pollStatus); + } + + if ('valid' === resp.body.status) { + console.log('poll: valid'); + try { + if (1 === options.removeChallenge.length) { + options.removeChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + ).then(function () {}, function () {}); + } else if (2 === options.removeChallenge.length) { + options.removeChallenge( + { identifier: identifier + , type: ch.type + , token: ch.token + } + , function (err) { return err; } + ); + } else { + options.removeChallenge(identifier.value, ch.token, function () {}); } + } catch(e) {} + return resp.body; + } - return me._postChallenge(options, results.identifier, challenge); - }); - })).then(function () { - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } - return me._finalizeOrder(options, validatedDomains); - }).then(function () { - return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb); - }); + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + console.log('\n[DEBUG] postChallenge\n'); + //console.log('\n[DEBUG] stop to fix things\n'); return; + + function post() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(payload) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log('respond to challenge: resp.body:'); + console.log(resp.body); + return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + }); + } + + return ACME._wait(1 * 1000).then(post); + } + + try { + if (1 === options.setChallenge.length) { + options.setChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + ).then(testChallenge, reject); + } else if (2 === options.setChallenge.length) { + options.setChallenge( + { identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + } + , failChallenge + ); + } else { + options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); + } + } catch(e) { + reject(e); + } + }); +}; +ACME._finalizeOrder = function (me, options, validatedDomains) { + console.log('finalizeOrder:'); + var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , new Buffer(payload) + ); + + console.log('finalize:', me._finalize); + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._finalize + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + + console.log('order finalized: resp.body:'); + console.log(resp.body); + + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; + } + + if ('invalid' === resp.body.status) { + console.error('cannot finalize: badness'); + return; + } + + console.error('(x) cannot finalize: badness'); + return; + }); + } + + return pollCert(); +}; +ACME._getCertificate = function (me, options) { + console.log('[acme-v2] DEBUG get cert 1'); + + if (!options.challengeTypes) { + if (!options.challengeType) { + return Promise.reject(new Error("challenge type must be specified")); + } + options.challengeTypes = [ options.challengeType ]; + } + + console.log('[acme-v2] certificates.create'); + return ACME._getNonce(me).then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , new Buffer(payload) + ); + + console.log('\n[DEBUG] newOrder\n'); + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + console.log(location); // the account id url + console.log(resp.toJSON()); + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //console.log('[DEBUG] finalize:', me._finalize); return; + + //return resp.body; + return Promise.all(me._authorizations.map(function (authUrl, i) { + console.log("Authorizations map #" + i); + return ACME._getChallenges(me, options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; + + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + } + + return ACME._postChallenge(me, options, results.identifier, challenge); + }); + })).then(function () { + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); + + return ACME._finalizeOrder(me, options, validatedDomains); + }).then(function () { + return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('Certificate:'); + console.log(resp.body); + return resp.body; }); }); + }); + }); +}; + +ACME.create = function create(me) { + if (!me) { me = {}; } + me.acmeChallengePrefix = ACME.acmeChallengePrefix; + me.RSA = me.RSA || require('rsa-compat').RSA; + me.request = me.request || require('request'); + me.promisify = me.promisify || require('util').promisify; + + + if ('function' !== typeof me.getUserAgentString) { + me.pkg = me.pkg || require('./package.json'); + me.os = me.os || require('os'); + me.process = me.process || require('process'); + me.userAgent = ACME._getUserAgentString(me); + } + + function getRequest(opts) { + if (!opts) { opts = {}; } + + return me.request.defaults({ + headers: { + 'User-Agent': opts.userAgent || me.userAgent || me.getUserAgentString(me) + } + }); + } + + if ('function' !== typeof me._request) { + me._request = me.promisify(getRequest({})); + } + + me.init = function (_directoryUrl) { + me.directoryUrl = me.directoryUrl || _directoryUrl; + return ACME._directory(me).then(function (resp) { + me._directoryUrls = resp.body; + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + }; + me.accounts = { + create: function (options) { + return ACME._registerAccount(me, options); } }; - return acme2; -} - -module.exports.ACME = { - create: create + me.certificates = { + create: function (options) { + return ACME._getCertificate(me, options); + } + }; + return me; }; -Object.keys(defaults).forEach(function (key) { - module.exports.ACME[key] = defaults[key]; -}); diff --git a/test.cb.js b/test.cb.js new file mode 100644 index 0000000..7cccb73 --- /dev/null +++ b/test.cb.js @@ -0,0 +1,75 @@ +'use strict'; + +module.exports.run = function run(web, chType, email) { + var RSA = require('rsa-compat').RSA; + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + var acme2 = require('./compat').ACME.create({ RSA: RSA }); + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + console.log(web, chType, email); + return; + acme2.init(directoryUrl).then(function (body) { + console.log(body); + return; + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (opts, cb) { + + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); + + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + function onAny() { + process.stdin.pause(); + process.stdin.removeEventListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + } + , removeChallenge: function (opts, cb) { + // hostname, key + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; diff --git a/test.compat.js b/test.compat.js new file mode 100644 index 0000000..b4ec70e --- /dev/null +++ b/test.compat.js @@ -0,0 +1,57 @@ +'use strict'; + +var RSA = require('rsa-compat').RSA; + +module.exports.run = function (web, chType, email) { + console.log('[DEBUG] run', web, chType, email); + + var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); + acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) { + if (err) { console.log('err 1'); throw err; } + console.log(body); + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (hostname, token, val, cb) { + console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); + console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } + + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + } + , removeChallenge: function (hostname, key, cb) { + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options, function (err, account) { + if (err) { console.log('err 2'); throw err; } + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (err, fullchainPem) { + if (err) { console.log('err 3'); throw err; } + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; diff --git a/test.js b/test.js index 7c1f373..6a6772b 100644 --- a/test.js +++ b/test.js @@ -1,56 +1,45 @@ 'use strict'; -var RSA = require('rsa-compat').RSA; -var acme2 = require('./').ACME.create({ RSA: RSA }); - -acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) { - console.log(body); - - var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); - } - /* - , setupChallenge: function (opts) { - console.log('type:'); - console.log(ch.type); - console.log('ch.token:'); - console.log(ch.token); - console.log('thumbprint:'); - console.log(thumbprint); - console.log('keyAuthorization:'); - console.log(keyAuthorization); - console.log('dnsAuthorization:'); - console.log(dnsAuthorization); - } - */ - // teardownChallenge - , setChallenge: function (hostname, key, val, cb) { - console.log('[DEBUG] set challenge', hostname, key, val); - console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'"); - setTimeout(cb, 20 * 1000); - } - , removeChallenge: function (hostname, key, cb) { - console.log('[DEBUG] remove challenge', hostname, key); - setTimeout(cb, 1 * 1000); - } - , challengeType: 'http-01' - , email: 'coolaj86@gmail.com' - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) - , domains: [ 'test.ppl.family' ] - }; - - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); - console.log(account); - - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); - console.log(fullchainPem); - }); - }); +var readline = require('readline'); +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout }); + +function getWeb() { + rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { + web = (web||'').trim().split(/,/g); + if (!web[0]) { getWeb(); return; } + + if (web.some(function (w) { return '*' === w[0]; })) { + console.log('Wildcard domains must use dns-01'); + getEmail(web, 'dns-01'); + } else { + getChallengeType(web); + } + }); +} + +function getChallengeType(web) { + rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { + chType = (chType||'').trim(); + if (!chType) { chType = 'http-01'; } + + getEmail(web, chType); + }); +} + +function getEmail(web, chType) { + rl.question('What email should we use? (optional) ', function (email) { + email = (email||'').trim(); + if (!email) { email = null; } + + rl.close(); + console.log("[DEBUG] rl blah blah"); + require('./test.compat.js').run(web, chType, email); + //require('./test.cb.js').run(web, chType, email); + //require('./test.promise.js').run(web, chType, email); + }); +} + +getWeb(); diff --git a/test.promise.js b/test.promise.js new file mode 100644 index 0000000..4da5392 --- /dev/null +++ b/test.promise.js @@ -0,0 +1,84 @@ +'use strict'; + +/* global Promise */ + +module.exports.run = function run(web, chType, email) { + var RSA = require('rsa-compat').RSA; + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + var acme2 = require('./compat').ACME.create({ RSA: RSA }); + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + console.log(web, chType, email); + return; + acme2.init(directoryUrl).then(function (body) { + console.log(body); + return; + + var options = { + agreeToTerms: function (tosUrl, agree) { + agree(null, tosUrl); + } + , setChallenge: function (opts) { + + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); + + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); + console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + + return new Promise(function (resolve) { + function onAny() { + process.stdin.pause(); + process.stdin.removeEventListener('data', onAny); + process.stdin.setRawMode(false); + + resolve(); + } + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + }); + } + , removeChallenge: function (opts) { + // hostname, key + console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization); + console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'"); + + return new Promise(function (resolve) { + setTimeout(resolve, 1 * 1000); + }); + } + , challengeType: chType + , email: email + , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) + , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , domains: web + }; + + acme2.registerNewAccount(options).then(function (account) { + console.log('account:'); + console.log(account); + + acme2.getCertificate(options, function (fullchainPem) { + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }).then(function (fullchainPem) { + console.log('[acme-v2] B fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); +}; -- 2.38.5 From 055c75cc947f03330066da8fe975eed269cda6d4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:13:20 -0600 Subject: [PATCH 021/252] yay for test with callback options working --- node.js | 14 ++++++++++++++ test.cb.js | 50 ++++++++++++++++++++++++++++---------------------- test.compat.js | 16 ++++++++-------- test.js | 36 +++++++++++++++++++----------------- 4 files changed, 69 insertions(+), 47 deletions(-) diff --git a/node.js b/node.js index 67544db..ed57806 100644 --- a/node.js +++ b/node.js @@ -9,6 +9,11 @@ var ACME = module.exports.ACME = {}; ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; +ACME.acmeChallengeDnsPrefix = '_acme-challenge'; +ACME.acmeChallengePrefixes = { + 'http-01': '/.well-known/acme-challenge/' +, 'dns-01': '_acme-challenge' +}; ACME._getUserAgentString = function (deps) { var uaDefaults = { @@ -368,6 +373,7 @@ ACME._getCertificate = function (me, options) { console.log('[acme-v2] certificates.create'); return ACME._getNonce(me).then(function () { + console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -401,6 +407,11 @@ ACME._getCertificate = function (me, options) { me._finalize = resp.body.finalize; //console.log('[DEBUG] finalize:', me._finalize); return; + if (!me._authorizations) { + console.log("&#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + } + console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + //return resp.body; return Promise.all(me._authorizations.map(function (authUrl, i) { console.log("Authorizations map #" + i); @@ -425,6 +436,7 @@ ACME._getCertificate = function (me, options) { return ACME._postChallenge(me, options, results.identifier, challenge); }); })).then(function () { + console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); @@ -444,6 +456,8 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } me.acmeChallengePrefix = ACME.acmeChallengePrefix; + me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; + me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); me.promisify = me.promisify || require('util').promisify; diff --git a/test.cb.js b/test.cb.js index 7cccb73..2484ffa 100644 --- a/test.cb.js +++ b/test.cb.js @@ -1,21 +1,17 @@ 'use strict'; -module.exports.run = function run(web, chType, email) { +module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { var RSA = require('rsa-compat').RSA; var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./compat').ACME.create({ RSA: RSA }); + var acme2 = require('./').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - console.log(web, chType, email); - return; - acme2.init(directoryUrl).then(function (body) { - console.log(body); - return; - + acme2.init(directoryUrl).then(function () { var options = { agreeToTerms: function (tosUrl, agree) { agree(null, tosUrl); } , setChallenge: function (opts, cb) { + var pathname; console.log(""); console.log('identifier:'); @@ -34,40 +30,50 @@ module.exports.run = function run(web, chType, email) { console.log(opts.dnsAuthorization); console.log(""); - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + if ('http-01' === opts.type) { + pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); + console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); + } else if ('dns-01' === opts.type) { + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname; + console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + } else { + cb(new Error("[acme-v2] unrecognized challenge type")); + return; + } + console.log("\nThen hit the 'any' key to continue..."); function onAny() { + console.log("'any' key was hit"); process.stdin.pause(); - process.stdin.removeEventListener('data', onAny); + process.stdin.removeListener('data', onAny); process.stdin.setRawMode(false); cb(); } + process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', onAny); } , removeChallenge: function (opts, cb) { - // hostname, key - console.log('[DEBUG] remove challenge', hostname, key); + // hostname, key + console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); setTimeout(cb, 1 * 1000); } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); + acme2.accounts.create(options).then(function (account) { + console.log('[acme-v2] account:'); console.log(account); - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); + acme2.certificates.create(options).then(function (fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); console.log(fullchainPem); }); }); diff --git a/test.compat.js b/test.compat.js index b4ec70e..00165de 100644 --- a/test.compat.js +++ b/test.compat.js @@ -2,22 +2,22 @@ var RSA = require('rsa-compat').RSA; -module.exports.run = function (web, chType, email) { +module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) { console.log('[DEBUG] run', web, chType, email); var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); - acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) { + acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { if (err) { console.log('err 1'); throw err; } - console.log(body); var options = { agreeToTerms: function (tosUrl, agree) { agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); - console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + var pathname = hostname + acme2.acmeChallengePrefix + "/" + token; + console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); + console.log("echo '" + val + "' > '" + pathname + "'"); + console.log("\nThen hit the 'any' key to continue..."); function onAny() { console.log("'any' key was hit"); @@ -37,8 +37,8 @@ module.exports.run = function (web, chType, email) { } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; diff --git a/test.js b/test.js index 6a6772b..12aec5c 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ 'use strict'; +var RSA = require('rsa-compat').RSA; var readline = require('readline'); var rl = readline.createInterface({ input: process.stdin, @@ -7,9 +8,9 @@ var rl = readline.createInterface({ }); function getWeb() { - rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { - web = (web||'').trim().split(/,/g); - if (!web[0]) { getWeb(); return; } + rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { + web = (web||'').trim().split(/,/g); + if (!web[0]) { getWeb(); return; } if (web.some(function (w) { return '*' === w[0]; })) { console.log('Wildcard domains must use dns-01'); @@ -17,29 +18,30 @@ function getWeb() { } else { getChallengeType(web); } - }); + }); } function getChallengeType(web) { - rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { - chType = (chType||'').trim(); - if (!chType) { chType = 'http-01'; } + rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { + chType = (chType||'').trim(); + if (!chType) { chType = 'http-01'; } - getEmail(web, chType); - }); + getEmail(web, chType); + }); } function getEmail(web, chType) { - rl.question('What email should we use? (optional) ', function (email) { - email = (email||'').trim(); - if (!email) { email = null; } + rl.question('What email should we use? (optional) ', function (email) { + email = (email||'').trim(); + if (!email) { email = null; } rl.close(); - console.log("[DEBUG] rl blah blah"); - require('./test.compat.js').run(web, chType, email); - //require('./test.cb.js').run(web, chType, email); - //require('./test.promise.js').run(web, chType, email); - }); + var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); + var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); + //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); + require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); + //require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + }); } getWeb(); -- 2.38.5 From ef0505ed699a0cb21c84b3583178126f9f8bbf29 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:13:20 -0600 Subject: [PATCH 022/252] yay for test with callback options working --- node.js | 14 ++++++++++++++ test.cb.js | 50 ++++++++++++++++++++++++++++---------------------- test.compat.js | 16 ++++++++-------- test.js | 36 +++++++++++++++++++----------------- 4 files changed, 69 insertions(+), 47 deletions(-) diff --git a/node.js b/node.js index 67544db..ed57806 100644 --- a/node.js +++ b/node.js @@ -9,6 +9,11 @@ var ACME = module.exports.ACME = {}; ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; +ACME.acmeChallengeDnsPrefix = '_acme-challenge'; +ACME.acmeChallengePrefixes = { + 'http-01': '/.well-known/acme-challenge/' +, 'dns-01': '_acme-challenge' +}; ACME._getUserAgentString = function (deps) { var uaDefaults = { @@ -368,6 +373,7 @@ ACME._getCertificate = function (me, options) { console.log('[acme-v2] certificates.create'); return ACME._getNonce(me).then(function () { + console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -401,6 +407,11 @@ ACME._getCertificate = function (me, options) { me._finalize = resp.body.finalize; //console.log('[DEBUG] finalize:', me._finalize); return; + if (!me._authorizations) { + console.log("&#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + } + console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + //return resp.body; return Promise.all(me._authorizations.map(function (authUrl, i) { console.log("Authorizations map #" + i); @@ -425,6 +436,7 @@ ACME._getCertificate = function (me, options) { return ACME._postChallenge(me, options, results.identifier, challenge); }); })).then(function () { + console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); @@ -444,6 +456,8 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } me.acmeChallengePrefix = ACME.acmeChallengePrefix; + me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; + me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); me.promisify = me.promisify || require('util').promisify; diff --git a/test.cb.js b/test.cb.js index 7cccb73..2484ffa 100644 --- a/test.cb.js +++ b/test.cb.js @@ -1,21 +1,17 @@ 'use strict'; -module.exports.run = function run(web, chType, email) { +module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { var RSA = require('rsa-compat').RSA; var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./compat').ACME.create({ RSA: RSA }); + var acme2 = require('./').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - console.log(web, chType, email); - return; - acme2.init(directoryUrl).then(function (body) { - console.log(body); - return; - + acme2.init(directoryUrl).then(function () { var options = { agreeToTerms: function (tosUrl, agree) { agree(null, tosUrl); } , setChallenge: function (opts, cb) { + var pathname; console.log(""); console.log('identifier:'); @@ -34,40 +30,50 @@ module.exports.run = function run(web, chType, email) { console.log(opts.dnsAuthorization); console.log(""); - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + if ('http-01' === opts.type) { + pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); + console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); + } else if ('dns-01' === opts.type) { + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname; + console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + } else { + cb(new Error("[acme-v2] unrecognized challenge type")); + return; + } + console.log("\nThen hit the 'any' key to continue..."); function onAny() { + console.log("'any' key was hit"); process.stdin.pause(); - process.stdin.removeEventListener('data', onAny); + process.stdin.removeListener('data', onAny); process.stdin.setRawMode(false); cb(); } + process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', onAny); } , removeChallenge: function (opts, cb) { - // hostname, key - console.log('[DEBUG] remove challenge', hostname, key); + // hostname, key + console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); setTimeout(cb, 1 * 1000); } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); + acme2.accounts.create(options).then(function (account) { + console.log('[acme-v2] account:'); console.log(account); - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); + acme2.certificates.create(options).then(function (fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); console.log(fullchainPem); }); }); diff --git a/test.compat.js b/test.compat.js index b4ec70e..00165de 100644 --- a/test.compat.js +++ b/test.compat.js @@ -2,22 +2,22 @@ var RSA = require('rsa-compat').RSA; -module.exports.run = function (web, chType, email) { +module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) { console.log('[DEBUG] run', web, chType, email); var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); - acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) { + acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { if (err) { console.log('err 1'); throw err; } - console.log(body); var options = { agreeToTerms: function (tosUrl, agree) { agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); - console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); + var pathname = hostname + acme2.acmeChallengePrefix + "/" + token; + console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); + console.log("echo '" + val + "' > '" + pathname + "'"); + console.log("\nThen hit the 'any' key to continue..."); function onAny() { console.log("'any' key was hit"); @@ -37,8 +37,8 @@ module.exports.run = function (web, chType, email) { } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; diff --git a/test.js b/test.js index 6a6772b..12aec5c 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ 'use strict'; +var RSA = require('rsa-compat').RSA; var readline = require('readline'); var rl = readline.createInterface({ input: process.stdin, @@ -7,9 +8,9 @@ var rl = readline.createInterface({ }); function getWeb() { - rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { - web = (web||'').trim().split(/,/g); - if (!web[0]) { getWeb(); return; } + rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { + web = (web||'').trim().split(/,/g); + if (!web[0]) { getWeb(); return; } if (web.some(function (w) { return '*' === w[0]; })) { console.log('Wildcard domains must use dns-01'); @@ -17,29 +18,30 @@ function getWeb() { } else { getChallengeType(web); } - }); + }); } function getChallengeType(web) { - rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { - chType = (chType||'').trim(); - if (!chType) { chType = 'http-01'; } + rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { + chType = (chType||'').trim(); + if (!chType) { chType = 'http-01'; } - getEmail(web, chType); - }); + getEmail(web, chType); + }); } function getEmail(web, chType) { - rl.question('What email should we use? (optional) ', function (email) { - email = (email||'').trim(); - if (!email) { email = null; } + rl.question('What email should we use? (optional) ', function (email) { + email = (email||'').trim(); + if (!email) { email = null; } rl.close(); - console.log("[DEBUG] rl blah blah"); - require('./test.compat.js').run(web, chType, email); - //require('./test.cb.js').run(web, chType, email); - //require('./test.promise.js').run(web, chType, email); - }); + var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); + var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); + //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); + require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); + //require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + }); } getWeb(); -- 2.38.5 From cd48c624fa75787befcf653f94a0c0dc492b148f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:28:29 -0600 Subject: [PATCH 023/252] yay for promise-only tests working --- README.md | 3 +- test.cb.js | 2 +- test.js | 4 +- test.promise.js | 103 +++++++++++++++++++++++++----------------------- 4 files changed, 58 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index a5202be..3b78172 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ In progress * Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) * Mar 21, 2018 - can now accept values (not hard coded) * Mar 21, 2018 - *mostly* matches le-acme-core.js API +* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) Todo -* completely match api for acme v1 (le-acme-core.js) +* test wildcard * test http and dns challenges * export http and dns challenge tests * support ECDSA keys diff --git a/test.cb.js b/test.cb.js index 2484ffa..23a7874 100644 --- a/test.cb.js +++ b/test.cb.js @@ -35,7 +35,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname; + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { diff --git a/test.js b/test.js index 12aec5c..6490e34 100644 --- a/test.js +++ b/test.js @@ -39,8 +39,8 @@ function getEmail(web, chType) { var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); - require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); - //require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + //require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); + require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); }); } diff --git a/test.promise.js b/test.promise.js index 4da5392..0b99aa2 100644 --- a/test.promise.js +++ b/test.promise.js @@ -1,82 +1,85 @@ 'use strict'; /* global Promise */ - -module.exports.run = function run(web, chType, email) { +module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { var RSA = require('rsa-compat').RSA; var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./compat').ACME.create({ RSA: RSA }); + var acme2 = require('./').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - console.log(web, chType, email); - return; - acme2.init(directoryUrl).then(function (body) { - console.log(body); - return; - + acme2.init(directoryUrl).then(function () { var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); + agreeToTerms: function (tosUrl) { + return Promise.resolve(tosUrl); } , setChallenge: function (opts) { + return new Promise(function (resolve, reject) { + var pathname; - console.log(""); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(""); + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); - - return new Promise(function (resolve) { - function onAny() { - process.stdin.pause(); - process.stdin.removeEventListener('data', onAny); - process.stdin.setRawMode(false); - - resolve(); + if ('http-01' === opts.type) { + pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); + console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); + } else if ('dns-01' === opts.type) { + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, '');; + console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + } else { + reject(new Error("[acme-v2] unrecognized challenge type")); + return; } + console.log("\nThen hit the 'any' key to continue..."); + + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + resolve(); + return; + } + process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', onAny); }); } , removeChallenge: function (opts) { - // hostname, key - console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization); - console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'"); - + console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); return new Promise(function (resolve) { + // hostname, key setTimeout(resolve, 1 * 1000); }); } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); + acme2.accounts.create(options).then(function (account) { + console.log('[acme-v2] account:'); console.log(account); - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); + acme2.certificates.create(options).then(function (fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); console.log(fullchainPem); }); }); -- 2.38.5 From f486bca73e9c9aaea96cab0f1b395e03020abe65 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:28:29 -0600 Subject: [PATCH 024/252] yay for promise-only tests working --- README.md | 3 +- test.cb.js | 2 +- test.js | 4 +- test.promise.js | 103 +++++++++++++++++++++++++----------------------- 4 files changed, 58 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index a5202be..3b78172 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ In progress * Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) * Mar 21, 2018 - can now accept values (not hard coded) * Mar 21, 2018 - *mostly* matches le-acme-core.js API +* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) Todo -* completely match api for acme v1 (le-acme-core.js) +* test wildcard * test http and dns challenges * export http and dns challenge tests * support ECDSA keys diff --git a/test.cb.js b/test.cb.js index 2484ffa..23a7874 100644 --- a/test.cb.js +++ b/test.cb.js @@ -35,7 +35,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname; + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { diff --git a/test.js b/test.js index 12aec5c..6490e34 100644 --- a/test.js +++ b/test.js @@ -39,8 +39,8 @@ function getEmail(web, chType) { var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); - require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); - //require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + //require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); + require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); }); } diff --git a/test.promise.js b/test.promise.js index 4da5392..0b99aa2 100644 --- a/test.promise.js +++ b/test.promise.js @@ -1,82 +1,85 @@ 'use strict'; /* global Promise */ - -module.exports.run = function run(web, chType, email) { +module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { var RSA = require('rsa-compat').RSA; var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./compat').ACME.create({ RSA: RSA }); + var acme2 = require('./').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - console.log(web, chType, email); - return; - acme2.init(directoryUrl).then(function (body) { - console.log(body); - return; - + acme2.init(directoryUrl).then(function () { var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); + agreeToTerms: function (tosUrl) { + return Promise.resolve(tosUrl); } , setChallenge: function (opts) { + return new Promise(function (resolve, reject) { + var pathname; - console.log(""); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(""); + console.log(""); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(""); - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); - console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); - - return new Promise(function (resolve) { - function onAny() { - process.stdin.pause(); - process.stdin.removeEventListener('data', onAny); - process.stdin.setRawMode(false); - - resolve(); + if ('http-01' === opts.type) { + pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); + console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); + } else if ('dns-01' === opts.type) { + pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, '');; + console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + } else { + reject(new Error("[acme-v2] unrecognized challenge type")); + return; } + console.log("\nThen hit the 'any' key to continue..."); + + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + resolve(); + return; + } + process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', onAny); }); } , removeChallenge: function (opts) { - // hostname, key - console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization); - console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'"); - + console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); return new Promise(function (resolve) { + // hostname, key setTimeout(resolve, 1 * 1000); }); } , challengeType: chType , email: email - , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) - , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + , accountKeypair: accountKeypair + , domainKeypair: domainKeypair , domains: web }; - acme2.registerNewAccount(options).then(function (account) { - console.log('account:'); + acme2.accounts.create(options).then(function (account) { + console.log('[acme-v2] account:'); console.log(account); - acme2.getCertificate(options, function (fullchainPem) { - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }).then(function (fullchainPem) { - console.log('[acme-v2] B fullchain.pem:'); + acme2.certificates.create(options).then(function (fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); console.log(fullchainPem); }); }); -- 2.38.5 From 1148e82706ee8f13aa785ae1bc26d91f63f87921 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:48:10 -0600 Subject: [PATCH 025/252] yay for wildcard test passing! --- README.md | 2 +- node.js | 63 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 3b78172..bb1d3da 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ In progress * Mar 21, 2018 - can now accept values (not hard coded) * Mar 21, 2018 - *mostly* matches le-acme-core.js API * Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +* Apr 5, 2018 - test wildcard Todo -* test wildcard * test http and dns challenges * export http and dns challenge tests * support ECDSA keys diff --git a/node.js b/node.js index ed57806..f87a88f 100644 --- a/node.js +++ b/node.js @@ -65,7 +65,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - console.log('[acme-v2] accounts.create'); + if (me.debug) { console.log('[acme-v2] accounts.create'); } return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -108,9 +108,9 @@ ACME._registerAccount = function (me, options) { , new Buffer(payload) ); - console.log('[acme-v2] accounts.create JSON body:'); delete jws.header; - console.log(jws); + if (me.debug) { console.log('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.log(jws); } me._nonce = null; return me._request({ method: 'POST' @@ -120,15 +120,18 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - console.log('[DEBUG] new account location:'); // the account id url - console.log(location); // the account id url - console.log(resp.toJSON()); + if (me.debug) { + // the account id url + console.log('[DEBUG] new account location:'); + console.log(location); // the account id url + console.log(resp.toJSON()); + } me._kid = location; return resp.body; }).then(resolve, reject); } - console.log('[acme-v2] agreeToTerms'); + if (me.debug) { console.log('[acme-v2] agreeToTerms'); } if (1 === options.agreeToTerms.length) { return options.agreeToTerms(me._tos).then(agree, reject); } @@ -166,7 +169,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - console.log('\n[DEBUG] getChallenges\n'); + if (me.debug) { console.log('\n[DEBUG] getChallenges\n'); } return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -199,18 +202,18 @@ ACME._postChallenge = function (me, options, identifier, ch) { // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); + if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { console.error('poll: resp.body:'); console.error(resp.body); if ('pending' === resp.body.status) { - console.log('poll: again'); + if (me.debug) { console.log('poll: again'); } return ACME._wait(1 * 1000).then(pollStatus); } if ('valid' === resp.body.status) { - console.log('poll: valid'); + if (me.debug) { console.log('poll: valid'); } try { if (1 === options.removeChallenge.length) { options.removeChallenge( @@ -248,7 +251,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); } - console.log('\n[DEBUG] postChallenge\n'); + if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } //console.log('\n[DEBUG] stop to fix things\n'); return; function post() { @@ -266,8 +269,8 @@ ACME._postChallenge = function (me, options, identifier, ch) { , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); + if (me.debug) { console.log('respond to challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); }); } @@ -312,7 +315,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - console.log('finalizeOrder:'); + if (me.debug) { console.log('finalizeOrder:'); } var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -325,7 +328,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , new Buffer(payload) ); - console.log('finalize:', me._finalize); + if (me.debug) { console.log('finalize:', me._finalize); } me._nonce = null; return me._request({ method: 'POST' @@ -335,8 +338,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('order finalized: resp.body:'); - console.log(resp.body); + if (me.debug) { console.log('order finalized: resp.body:'); } + if (me.debug) { console.log(resp.body); } if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); @@ -362,7 +365,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - console.log('[acme-v2] DEBUG get cert 1'); + if (me.debug) { console.log('[acme-v2] DEBUG get cert 1'); } if (!options.challengeTypes) { if (!options.challengeType) { @@ -371,9 +374,9 @@ ACME._getCertificate = function (me, options) { options.challengeTypes = [ options.challengeType ]; } - console.log('[acme-v2] certificates.create'); + if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { - console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -390,7 +393,7 @@ ACME._getCertificate = function (me, options) { , new Buffer(payload) ); - console.log('\n[DEBUG] newOrder\n'); + if (me.debug) { console.log('\n[DEBUG] newOrder\n'); } me._nonce = null; return me._request({ method: 'POST' @@ -400,21 +403,23 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - console.log(location); // the account id url - console.log(resp.toJSON()); + if (me.debug) { + console.log(location); // the account id url + console.log(resp.toJSON()); + } me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; //console.log('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.log("&#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + console.error("[acme-v2.js] authorizations were not fetched"); } - console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } //return resp.body; return Promise.all(me._authorizations.map(function (authUrl, i) { - console.log("Authorizations map #" + i); + if (me.debug) { console.log("Authorizations map #" + i); } return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value var chType = options.challengeTypes.filter(function (chType) { @@ -436,7 +441,7 @@ ACME._getCertificate = function (me, options) { return ACME._postChallenge(me, options, results.identifier, challenge); }); })).then(function () { - console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); @@ -444,8 +449,6 @@ ACME._getCertificate = function (me, options) { return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('Certificate:'); - console.log(resp.body); return resp.body; }); }); -- 2.38.5 From 38cefafe33b47c2e2d348d1cf909de95f7554ac0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 02:48:10 -0600 Subject: [PATCH 026/252] yay for wildcard test passing! --- README.md | 2 +- node.js | 63 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 3b78172..bb1d3da 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ In progress * Mar 21, 2018 - can now accept values (not hard coded) * Mar 21, 2018 - *mostly* matches le-acme-core.js API * Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +* Apr 5, 2018 - test wildcard Todo -* test wildcard * test http and dns challenges * export http and dns challenge tests * support ECDSA keys diff --git a/node.js b/node.js index ed57806..f87a88f 100644 --- a/node.js +++ b/node.js @@ -65,7 +65,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - console.log('[acme-v2] accounts.create'); + if (me.debug) { console.log('[acme-v2] accounts.create'); } return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -108,9 +108,9 @@ ACME._registerAccount = function (me, options) { , new Buffer(payload) ); - console.log('[acme-v2] accounts.create JSON body:'); delete jws.header; - console.log(jws); + if (me.debug) { console.log('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.log(jws); } me._nonce = null; return me._request({ method: 'POST' @@ -120,15 +120,18 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - console.log('[DEBUG] new account location:'); // the account id url - console.log(location); // the account id url - console.log(resp.toJSON()); + if (me.debug) { + // the account id url + console.log('[DEBUG] new account location:'); + console.log(location); // the account id url + console.log(resp.toJSON()); + } me._kid = location; return resp.body; }).then(resolve, reject); } - console.log('[acme-v2] agreeToTerms'); + if (me.debug) { console.log('[acme-v2] agreeToTerms'); } if (1 === options.agreeToTerms.length) { return options.agreeToTerms(me._tos).then(agree, reject); } @@ -166,7 +169,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - console.log('\n[DEBUG] getChallenges\n'); + if (me.debug) { console.log('\n[DEBUG] getChallenges\n'); } return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -199,18 +202,18 @@ ACME._postChallenge = function (me, options, identifier, ch) { // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" function pollStatus() { - console.log('\n[DEBUG] statusChallenge\n'); + if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { console.error('poll: resp.body:'); console.error(resp.body); if ('pending' === resp.body.status) { - console.log('poll: again'); + if (me.debug) { console.log('poll: again'); } return ACME._wait(1 * 1000).then(pollStatus); } if ('valid' === resp.body.status) { - console.log('poll: valid'); + if (me.debug) { console.log('poll: valid'); } try { if (1 === options.removeChallenge.length) { options.removeChallenge( @@ -248,7 +251,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); } - console.log('\n[DEBUG] postChallenge\n'); + if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } //console.log('\n[DEBUG] stop to fix things\n'); return; function post() { @@ -266,8 +269,8 @@ ACME._postChallenge = function (me, options, identifier, ch) { , json: jws }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('respond to challenge: resp.body:'); - console.log(resp.body); + if (me.debug) { console.log('respond to challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); }); } @@ -312,7 +315,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - console.log('finalizeOrder:'); + if (me.debug) { console.log('finalizeOrder:'); } var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -325,7 +328,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , new Buffer(payload) ); - console.log('finalize:', me._finalize); + if (me.debug) { console.log('finalize:', me._finalize); } me._nonce = null; return me._request({ method: 'POST' @@ -335,8 +338,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - console.log('order finalized: resp.body:'); - console.log(resp.body); + if (me.debug) { console.log('order finalized: resp.body:'); } + if (me.debug) { console.log(resp.body); } if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); @@ -362,7 +365,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - console.log('[acme-v2] DEBUG get cert 1'); + if (me.debug) { console.log('[acme-v2] DEBUG get cert 1'); } if (!options.challengeTypes) { if (!options.challengeType) { @@ -371,9 +374,9 @@ ACME._getCertificate = function (me, options) { options.challengeTypes = [ options.challengeType ]; } - console.log('[acme-v2] certificates.create'); + if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { - console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -390,7 +393,7 @@ ACME._getCertificate = function (me, options) { , new Buffer(payload) ); - console.log('\n[DEBUG] newOrder\n'); + if (me.debug) { console.log('\n[DEBUG] newOrder\n'); } me._nonce = null; return me._request({ method: 'POST' @@ -400,21 +403,23 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - console.log(location); // the account id url - console.log(resp.toJSON()); + if (me.debug) { + console.log(location); // the account id url + console.log(resp.toJSON()); + } me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; //console.log('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.log("&#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + console.error("[acme-v2.js] authorizations were not fetched"); } - console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } //return resp.body; return Promise.all(me._authorizations.map(function (authUrl, i) { - console.log("Authorizations map #" + i); + if (me.debug) { console.log("Authorizations map #" + i); } return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value var chType = options.challengeTypes.filter(function (chType) { @@ -436,7 +441,7 @@ ACME._getCertificate = function (me, options) { return ACME._postChallenge(me, options, results.identifier, challenge); }); })).then(function () { - console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); @@ -444,8 +449,6 @@ ACME._getCertificate = function (me, options) { return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('Certificate:'); - console.log(resp.body); return resp.body; }); }); -- 2.38.5 From 1647818326eacf9d7a30ae4bc94b04a4d009a9f0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 03:37:41 -0600 Subject: [PATCH 027/252] sequence auths, more testing --- README.md | 2 ++ node.js | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bb1d3da..ee602bf 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ In progress * Mar 21, 2018 - *mostly* matches le-acme-core.js API * Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) * Apr 5, 2018 - test wildcard +* Apr 5, 2018 - test two subdomains +* Apr 5, 2018 - test subdomains and its wildcard Todo diff --git a/node.js b/node.js index f87a88f..50cae2f 100644 --- a/node.js +++ b/node.js @@ -218,15 +218,27 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.removeChallenge.length) { options.removeChallenge( { identifier: identifier + , hostname: identifier.value , type: ch.type , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) } ).then(function () {}, function () {}); } else if (2 === options.removeChallenge.length) { options.removeChallenge( { identifier: identifier + , hostname: identifier.value , type: ch.type , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) } , function (err) { return err; } ); @@ -403,6 +415,7 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; + var auths; if (me.debug) { console.log(location); // the account id url console.log(resp.toJSON()); @@ -418,8 +431,12 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } //return resp.body; - return Promise.all(me._authorizations.map(function (authUrl, i) { - if (me.debug) { console.log("Authorizations map #" + i); } + auths = me._authorizations.slice(0); + + function next() { + var authUrl = auths.shift(); + if (!authUrl) { return; } + return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value var chType = options.challengeTypes.filter(function (chType) { @@ -439,8 +456,12 @@ ACME._getCertificate = function (me, options) { } return ACME._postChallenge(me, options, results.identifier, challenge); + }).then(function () { + return next(); }); - })).then(function () { + } + + return next().then(function () { if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; -- 2.38.5 From 2e747bada2a9e1d16bc702456dda297e1cd191a3 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 03:37:41 -0600 Subject: [PATCH 028/252] sequence auths, more testing --- README.md | 2 ++ node.js | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bb1d3da..ee602bf 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ In progress * Mar 21, 2018 - *mostly* matches le-acme-core.js API * Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) * Apr 5, 2018 - test wildcard +* Apr 5, 2018 - test two subdomains +* Apr 5, 2018 - test subdomains and its wildcard Todo diff --git a/node.js b/node.js index f87a88f..50cae2f 100644 --- a/node.js +++ b/node.js @@ -218,15 +218,27 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.removeChallenge.length) { options.removeChallenge( { identifier: identifier + , hostname: identifier.value , type: ch.type , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) } ).then(function () {}, function () {}); } else if (2 === options.removeChallenge.length) { options.removeChallenge( { identifier: identifier + , hostname: identifier.value , type: ch.type , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) } , function (err) { return err; } ); @@ -403,6 +415,7 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; + var auths; if (me.debug) { console.log(location); // the account id url console.log(resp.toJSON()); @@ -418,8 +431,12 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } //return resp.body; - return Promise.all(me._authorizations.map(function (authUrl, i) { - if (me.debug) { console.log("Authorizations map #" + i); } + auths = me._authorizations.slice(0); + + function next() { + var authUrl = auths.shift(); + if (!authUrl) { return; } + return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value var chType = options.challengeTypes.filter(function (chType) { @@ -439,8 +456,12 @@ ACME._getCertificate = function (me, options) { } return ACME._postChallenge(me, options, results.identifier, challenge); + }).then(function () { + return next(); }); - })).then(function () { + } + + return next().then(function () { if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; -- 2.38.5 From 33e10c77d8c64971f824cb2b3797009a398cd72d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 05:44:02 -0600 Subject: [PATCH 029/252] more testing --- README.md | 8 +- node.js | 312 ++++++++++++++++++++++++++++++------------------ test.cb.js | 2 +- test.compat.js | 2 +- test.promise.js | 2 +- 5 files changed, 206 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index ee602bf..16b63df 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,12 @@ In progress * Apr 5, 2018 - test wildcard * Apr 5, 2018 - test two subdomains * Apr 5, 2018 - test subdomains and its wildcard +* Apr 5, 2018 - test http and dns challenges (success and failure) +* Apr 5, 2018 - export http and dns challenge tests Todo -* test http and dns challenges -* export http and dns challenge tests +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' * support ECDSA keys ## Let's Encrypt Directory URLs @@ -63,6 +64,9 @@ var ACME = require('acme-v2').ACME.create({ // used for overriding the default user-agent , userAgent: 'My custom UA String' , getUserAgentString: function (deps) { return 'My custom UA String'; } + + // don't try to validate challenges locally +, skipChallengeTest: false }); ``` diff --git a/node.js b/node.js index 50cae2f..7a6bca8 100644 --- a/node.js +++ b/node.js @@ -11,9 +11,43 @@ var ACME = module.exports.ACME = {}; ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; ACME.acmeChallengeDnsPrefix = '_acme-challenge'; ACME.acmeChallengePrefixes = { - 'http-01': '/.well-known/acme-challenge/' + 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' }; +ACME.challengeTests = { + 'http-01': function (me, auth) { + var url = 'http://' + auth.hostname + ACME.acmeChallengePrefixes['http-01'] + '/' + auth.token; + return me._request({ url: url }).then(function (resp) { + var err; + + if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + return true; + } + + err = new Error("self check does not pass"); + err.code = 'E_RETRY'; + return Promise.reject(err); + }); + } +, 'dns-01': function (me, auth) { + return me._dig({ + type: 'TXT' + , name: ACME.acmeChallengePrefixes['dns-01'] + '.' + auth.hostname + }).then(function (ans) { + var err; + + if (ans.answer.some(function (txt) { + return auth.dnsAuthorization === txt.data[0]; + })) { + return true; + } + + err = new Error("self check does not pass"); + err.code = 'E_RETRY'; + return Promise.reject(err); + }); + } +}; ACME._getUserAgentString = function (deps) { var uaDefaults = { @@ -181,19 +215,153 @@ ACME._wait = function wait(ms) { }; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { - var body = { }; - - var payload = JSON.stringify(body); + var count = 0; var thumbprint = me.RSA.thumbprint(options.accountKeypair); var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token + var auth = { + identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + }; return new Promise(function (resolve, reject) { + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(JSON.stringify({ "status": "deactivated" })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + console.log('[acme-v2.js] deactivate:'); + console.log(resp.headers); + console.log(resp.body); + console.log(); + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.log('deactivate challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } + return ACME._wait(10 * 1000); + }); + } + + function pollStatus() { + if (count >= 5) { + return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + } + + count += 1; + + if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('processing' === resp.body.status) { + if (me.debug) { console.log('poll: again'); } + return ACME._wait(1 * 1000).then(pollStatus); + } + + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= 4) { + return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); + } + if (me.debug) { console.log('poll: again'); } + return ACME._wait(1 * 1000).then(testChallenge); + } + + if ('valid' === resp.body.status) { + if (me.debug) { console.log('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 { + options.removeChallenge(identifier.value, ch.token, function () {}); + } + } catch(e) {} + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(JSON.stringify({ })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + console.log('[acme-v2.js] challenge accepted!'); + console.log(resp.headers); + console.log(resp.body); + console.log(); + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.log('respond to challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } + return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + }); + } + function failChallenge(err) { if (err) { reject(err); return; } - testChallenge(); + return testChallenge(); } function testChallenge() { @@ -201,123 +369,21 @@ ACME._postChallenge = function (me, options, identifier, ch) { // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - function pollStatus() { - if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - if (me.debug) { console.log('poll: again'); } - return ACME._wait(1 * 1000).then(pollStatus); - } - - if ('valid' === resp.body.status) { - if (me.debug) { console.log('poll: valid'); } - try { - if (1 === options.removeChallenge.length) { - options.removeChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - ).then(function () {}, function () {}); - } else if (2 === options.removeChallenge.length) { - options.removeChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , function (err) { return err; } - ); - } else { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - - return Promise.reject(new Error("[acme-v2] bad challenge state")); - }); - } - if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } //console.log('\n[DEBUG] stop to fix things\n'); return; - function post() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(payload) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('respond to challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } - return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); - }); - } - - return ACME._wait(1 * 1000).then(post); + return ACME._wait(1 * 1000).then(function () { + if (!me.skipChallengeTest) { + return ACME.challengeTests[ch.type](me, auth); + } + }).then(respondToChallenge); } try { if (1 === options.setChallenge.length) { - options.setChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - ).then(testChallenge, reject); + options.setChallenge(auth).then(testChallenge, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , failChallenge - ); + options.setChallenge(auth, failChallenge); } else { options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); } @@ -388,7 +454,6 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { - if (me.debug) { console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -484,6 +549,23 @@ ACME.create = function create(me) { me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); + me._dig = function (query) { + // TODO use digd.js + return new Promise(function (resolve, reject) { + var dns = require('dns'); + dns.resolveTxt(query.name, function (err, records) { + if (err) { reject(err); return; } + + resolve({ + answer: records.map(function (rr) { + return { + data: rr + }; + }) + }); + }); + }); + }; me.promisify = me.promisify || require('util').promisify; diff --git a/test.cb.js b/test.cb.js index 23a7874..d101435 100644 --- a/test.cb.js +++ b/test.cb.js @@ -31,7 +31,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { diff --git a/test.compat.js b/test.compat.js index 00165de..73060bd 100644 --- a/test.compat.js +++ b/test.compat.js @@ -14,7 +14,7 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - var pathname = hostname + acme2.acmeChallengePrefix + "/" + token; + var pathname = hostname + acme2.acmeChallengePrefix + token; console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); console.log("echo '" + val + "' > '" + pathname + "'"); console.log("\nThen hit the 'any' key to continue..."); diff --git a/test.promise.js b/test.promise.js index 0b99aa2..326bd43 100644 --- a/test.promise.js +++ b/test.promise.js @@ -33,7 +33,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { -- 2.38.5 From 3a6269aafa1b890c947ba50af8c319c735bb36cc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Apr 2018 05:44:02 -0600 Subject: [PATCH 030/252] more testing --- README.md | 8 +- node.js | 312 ++++++++++++++++++++++++++++++------------------ test.cb.js | 2 +- test.compat.js | 2 +- test.promise.js | 2 +- 5 files changed, 206 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index ee602bf..16b63df 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,12 @@ In progress * Apr 5, 2018 - test wildcard * Apr 5, 2018 - test two subdomains * Apr 5, 2018 - test subdomains and its wildcard +* Apr 5, 2018 - test http and dns challenges (success and failure) +* Apr 5, 2018 - export http and dns challenge tests Todo -* test http and dns challenges -* export http and dns challenge tests +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' * support ECDSA keys ## Let's Encrypt Directory URLs @@ -63,6 +64,9 @@ var ACME = require('acme-v2').ACME.create({ // used for overriding the default user-agent , userAgent: 'My custom UA String' , getUserAgentString: function (deps) { return 'My custom UA String'; } + + // don't try to validate challenges locally +, skipChallengeTest: false }); ``` diff --git a/node.js b/node.js index 50cae2f..7a6bca8 100644 --- a/node.js +++ b/node.js @@ -11,9 +11,43 @@ var ACME = module.exports.ACME = {}; ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; ACME.acmeChallengeDnsPrefix = '_acme-challenge'; ACME.acmeChallengePrefixes = { - 'http-01': '/.well-known/acme-challenge/' + 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' }; +ACME.challengeTests = { + 'http-01': function (me, auth) { + var url = 'http://' + auth.hostname + ACME.acmeChallengePrefixes['http-01'] + '/' + auth.token; + return me._request({ url: url }).then(function (resp) { + var err; + + if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + return true; + } + + err = new Error("self check does not pass"); + err.code = 'E_RETRY'; + return Promise.reject(err); + }); + } +, 'dns-01': function (me, auth) { + return me._dig({ + type: 'TXT' + , name: ACME.acmeChallengePrefixes['dns-01'] + '.' + auth.hostname + }).then(function (ans) { + var err; + + if (ans.answer.some(function (txt) { + return auth.dnsAuthorization === txt.data[0]; + })) { + return true; + } + + err = new Error("self check does not pass"); + err.code = 'E_RETRY'; + return Promise.reject(err); + }); + } +}; ACME._getUserAgentString = function (deps) { var uaDefaults = { @@ -181,19 +215,153 @@ ACME._wait = function wait(ms) { }; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { - var body = { }; - - var payload = JSON.stringify(body); + var count = 0; var thumbprint = me.RSA.thumbprint(options.accountKeypair); var keyAuthorization = ch.token + '.' + thumbprint; // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) // /.well-known/acme-challenge/:token + var auth = { + identifier: identifier + , hostname: identifier.value + , type: ch.type + , token: ch.token + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + }; return new Promise(function (resolve, reject) { + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(JSON.stringify({ "status": "deactivated" })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + console.log('[acme-v2.js] deactivate:'); + console.log(resp.headers); + console.log(resp.body); + console.log(); + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.log('deactivate challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } + return ACME._wait(10 * 1000); + }); + } + + function pollStatus() { + if (count >= 5) { + return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + } + + count += 1; + + if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + console.error('poll: resp.body:'); + console.error(resp.body); + + if ('processing' === resp.body.status) { + if (me.debug) { console.log('poll: again'); } + return ACME._wait(1 * 1000).then(pollStatus); + } + + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= 4) { + return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); + } + if (me.debug) { console.log('poll: again'); } + return ACME._wait(1 * 1000).then(testChallenge); + } + + if ('valid' === resp.body.status) { + if (me.debug) { console.log('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 { + options.removeChallenge(identifier.value, ch.token, function () {}); + } + } catch(e) {} + return resp.body; + } + + if (!resp.body.status) { + console.error("[acme-v2] (y) bad challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (x) invalid challenge state:"); + } + else { + console.error("[acme-v2] (z) bad challenge state:"); + } + + return Promise.reject(new Error("[acme-v2] bad challenge state")); + }); + } + + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , new Buffer(JSON.stringify({ })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + console.log('[acme-v2.js] challenge accepted!'); + console.log(resp.headers); + console.log(resp.body); + console.log(); + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.log('respond to challenge: resp.body:'); } + if (me.debug) { console.log(resp.body); } + return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + }); + } + function failChallenge(err) { if (err) { reject(err); return; } - testChallenge(); + return testChallenge(); } function testChallenge() { @@ -201,123 +369,21 @@ ACME._postChallenge = function (me, options, identifier, ch) { // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - function pollStatus() { - if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); - - if ('pending' === resp.body.status) { - if (me.debug) { console.log('poll: again'); } - return ACME._wait(1 * 1000).then(pollStatus); - } - - if ('valid' === resp.body.status) { - if (me.debug) { console.log('poll: valid'); } - try { - if (1 === options.removeChallenge.length) { - options.removeChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - ).then(function () {}, function () {}); - } else if (2 === options.removeChallenge.length) { - options.removeChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , function (err) { return err; } - ); - } else { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } - - if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); - } - else { - console.error("[acme-v2] (z) bad challenge state:"); - } - - return Promise.reject(new Error("[acme-v2] bad challenge state")); - }); - } - if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } //console.log('\n[DEBUG] stop to fix things\n'); return; - function post() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(payload) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('respond to challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } - return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); - }); - } - - return ACME._wait(1 * 1000).then(post); + return ACME._wait(1 * 1000).then(function () { + if (!me.skipChallengeTest) { + return ACME.challengeTests[ch.type](me, auth); + } + }).then(respondToChallenge); } try { if (1 === options.setChallenge.length) { - options.setChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - ).then(testChallenge, reject); + options.setChallenge(auth).then(testChallenge, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge( - { identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - } - , failChallenge - ); + options.setChallenge(auth, failChallenge); } else { options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); } @@ -388,7 +454,6 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { - if (me.debug) { console.log("27 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } var body = { identifiers: options.domains.map(function (hostname) { return { type: "dns" , value: hostname }; @@ -484,6 +549,23 @@ ACME.create = function create(me) { me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); + me._dig = function (query) { + // TODO use digd.js + return new Promise(function (resolve, reject) { + var dns = require('dns'); + dns.resolveTxt(query.name, function (err, records) { + if (err) { reject(err); return; } + + resolve({ + answer: records.map(function (rr) { + return { + data: rr + }; + }) + }); + }); + }); + }; me.promisify = me.promisify || require('util').promisify; diff --git a/test.cb.js b/test.cb.js index 23a7874..d101435 100644 --- a/test.cb.js +++ b/test.cb.js @@ -31,7 +31,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { diff --git a/test.compat.js b/test.compat.js index 00165de..73060bd 100644 --- a/test.compat.js +++ b/test.compat.js @@ -14,7 +14,7 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - var pathname = hostname + acme2.acmeChallengePrefix + "/" + token; + var pathname = hostname + acme2.acmeChallengePrefix + token; console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); console.log("echo '" + val + "' > '" + pathname + "'"); console.log("\nThen hit the 'any' key to continue..."); diff --git a/test.promise.js b/test.promise.js index 0b99aa2..326bd43 100644 --- a/test.promise.js +++ b/test.promise.js @@ -33,7 +33,7 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefix + "/" + opts.token; + pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { -- 2.38.5 From 7db66d710ba861bd587ad6365a778870e526d4f1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 07:22:42 +0000 Subject: [PATCH 031/252] working even better --- README.md | 3 ++- compat.js | 26 +++++++++++++++++++++++--- node.js | 20 +++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16b63df..01539b3 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ In progress * Apr 5, 2018 - test subdomains and its wildcard * Apr 5, 2018 - test http and dns challenges (success and failure) * Apr 5, 2018 - export http and dns challenge tests +* Apr 10, 2018 - tested backwards-compatibility using greenlock.js Todo -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' * support ECDSA keys +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' ## Let's Encrypt Directory URLs diff --git a/compat.js b/compat.js index b9f6b62..8b35579 100644 --- a/compat.js +++ b/compat.js @@ -24,11 +24,30 @@ function create(deps) { acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); }; acme2.getCertificate = function (options, cb) { - acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb)); + options.agreeToTerms = options.agreeToTerms || function (tos) { + return Promise.resolve(tos); + }; + acme2.certificates.create(options).then(function (chainPem) { + var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); + resolveFn(cb)({ + cert: chainPem.split(/[\r\n]{2,}/g)[0] + '\r\n' + , privkey: privkeyPem + , chain: chainPem.split(/[\r\n]{2,}/g)[1] + '\r\n' + }); + }, rejectFn(cb)); }; acme2.getAcmeUrls = function (options, cb) { acme2.init(options).then(resolveFn(cb), rejectFn(cb)); }; + acme2.getOptions = function () { + var defs = {}; + + Object.keys(module.exports.defaults).forEach(function (key) { + defs[key] = defs[deps] || module.exports.defaults[key]; + }); + + return defs; + }; acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; acme2.productionServerUrl = module.exports.defaults.productionServerUrl; return acme2; @@ -41,8 +60,9 @@ module.exports.defaults = { , knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] , challengeTypes: [ 'http-01', 'dns-01' ] , challengeType: 'http-01' -, keyType: 'rsa' // ecdsa -, keySize: 2048 // 256 +//, keyType: 'rsa' // ecdsa +//, keySize: 2048 // 256 +, rsaKeySize: 2048 // 256 }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; diff --git a/node.js b/node.js index 7a6bca8..5964c44 100644 --- a/node.js +++ b/node.js @@ -452,6 +452,17 @@ ACME._getCertificate = function (me, options) { options.challengeTypes = [ options.challengeType ]; } + if (!me._kid) { + if (options.accountKid) { + me._kid = options.accountKid; + } else { + //return Promise.reject(new Error("must include KeyID")); + return ACME._registerAccount(me, options).then(function () { + return ACME._getCertificate(me, options); + }); + } + } + if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { @@ -491,7 +502,9 @@ ACME._getCertificate = function (me, options) { //console.log('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched"); + console.error("[acme-v2.js] authorizations were not fetched:"); + console.error(resp.body); + return Promise.reject(new Error("authorizations were not fetched")); } if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } @@ -534,7 +547,10 @@ ACME._getCertificate = function (me, options) { return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { + console.log('acme-v2: order was finalized'); return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('acme-v2: csr submitted and cert received:'); + console.log(resp.body); return resp.body; }); }); @@ -544,6 +560,8 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } + // + me.debug = true; me.acmeChallengePrefix = ACME.acmeChallengePrefix; me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; -- 2.38.5 From da8b49d46bc18a2ae2ec469c1d1811115cfeb46a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 07:22:42 +0000 Subject: [PATCH 032/252] working even better --- README.md | 3 ++- compat.js | 26 +++++++++++++++++++++++--- node.js | 20 +++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16b63df..01539b3 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ In progress * Apr 5, 2018 - test subdomains and its wildcard * Apr 5, 2018 - test http and dns challenges (success and failure) * Apr 5, 2018 - export http and dns challenge tests +* Apr 10, 2018 - tested backwards-compatibility using greenlock.js Todo -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' * support ECDSA keys +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' ## Let's Encrypt Directory URLs diff --git a/compat.js b/compat.js index b9f6b62..8b35579 100644 --- a/compat.js +++ b/compat.js @@ -24,11 +24,30 @@ function create(deps) { acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); }; acme2.getCertificate = function (options, cb) { - acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb)); + options.agreeToTerms = options.agreeToTerms || function (tos) { + return Promise.resolve(tos); + }; + acme2.certificates.create(options).then(function (chainPem) { + var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); + resolveFn(cb)({ + cert: chainPem.split(/[\r\n]{2,}/g)[0] + '\r\n' + , privkey: privkeyPem + , chain: chainPem.split(/[\r\n]{2,}/g)[1] + '\r\n' + }); + }, rejectFn(cb)); }; acme2.getAcmeUrls = function (options, cb) { acme2.init(options).then(resolveFn(cb), rejectFn(cb)); }; + acme2.getOptions = function () { + var defs = {}; + + Object.keys(module.exports.defaults).forEach(function (key) { + defs[key] = defs[deps] || module.exports.defaults[key]; + }); + + return defs; + }; acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; acme2.productionServerUrl = module.exports.defaults.productionServerUrl; return acme2; @@ -41,8 +60,9 @@ module.exports.defaults = { , knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] , challengeTypes: [ 'http-01', 'dns-01' ] , challengeType: 'http-01' -, keyType: 'rsa' // ecdsa -, keySize: 2048 // 256 +//, keyType: 'rsa' // ecdsa +//, keySize: 2048 // 256 +, rsaKeySize: 2048 // 256 }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; diff --git a/node.js b/node.js index 7a6bca8..5964c44 100644 --- a/node.js +++ b/node.js @@ -452,6 +452,17 @@ ACME._getCertificate = function (me, options) { options.challengeTypes = [ options.challengeType ]; } + if (!me._kid) { + if (options.accountKid) { + me._kid = options.accountKid; + } else { + //return Promise.reject(new Error("must include KeyID")); + return ACME._registerAccount(me, options).then(function () { + return ACME._getCertificate(me, options); + }); + } + } + if (me.debug) { console.log('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { @@ -491,7 +502,9 @@ ACME._getCertificate = function (me, options) { //console.log('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched"); + console.error("[acme-v2.js] authorizations were not fetched:"); + console.error(resp.body); + return Promise.reject(new Error("authorizations were not fetched")); } if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } @@ -534,7 +547,10 @@ ACME._getCertificate = function (me, options) { return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { + console.log('acme-v2: order was finalized'); return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + console.log('acme-v2: csr submitted and cert received:'); + console.log(resp.body); return resp.body; }); }); @@ -544,6 +560,8 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } + // + me.debug = true; me.acmeChallengePrefix = ACME.acmeChallengePrefix; me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; -- 2.38.5 From 69b624c6320445b4da581caf5abed4a408fa2e47 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 07:22:58 +0000 Subject: [PATCH 033/252] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ca8433..46975a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.6.2", + "version": "0.9.0", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From b630c118ccf2c58625548e358052ef8cf5695135 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 07:22:58 +0000 Subject: [PATCH 034/252] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ca8433..46975a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "0.6.2", + "version": "0.9.0", "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 8af828a737961a67452af94837e9ead45cb55df0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 17:34:18 +0000 Subject: [PATCH 035/252] closer to v1 --- README.md | 113 ++++++++++++++++++++++++++++++++++-------------- compat.js | 4 +- node.js | 12 ++--- test.cb.js | 4 +- test.promise.js | 4 +- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 01539b3..771fede 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,12 @@ -acme-v2.js +acme-v2.js (draft 11) ========== | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. +A framework for building letsencrypt v2 (IETF ACME draft 11), forked from `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 -In progress - -* Mar 15, 2018 - get directory -* Mar 15, 2018 - get nonce -* Mar 15, 2018 - generate account keypair -* Mar 15, 2018 - create account -* Mar 16, 2018 - new order -* Mar 16, 2018 - get challenges -* Mar 20, 2018 - respond to challenges -* Mar 20, 2018 - generate domain keypair -* Mar 20, 2018 - finalize order (submit csr) -* Mar 20, 2018 - poll for status -* Mar 20, 2018 - download certificate -* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -* Mar 21, 2018 - can now accept values (not hard coded) -* Mar 21, 2018 - *mostly* matches le-acme-core.js API -* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) -* Apr 5, 2018 - test wildcard -* Apr 5, 2018 - test two subdomains -* Apr 5, 2018 - test subdomains and its wildcard -* Apr 5, 2018 - test http and dns challenges (success and failure) -* Apr 5, 2018 - export http and dns challenge tests -* Apr 10, 2018 - tested backwards-compatibility using greenlock.js - -Todo - -* support ECDSA keys -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' - ## Let's Encrypt Directory URLs ``` @@ -48,7 +19,48 @@ https://acme-v02.api.letsencrypt.org/directory https://acme-staging-v02.api.letsencrypt.org/directory ``` -## API +## Two API versions, Two Implementations + +This library (acme-v2.js) supports ACME [*draft 11*](https://tools.ietf.org/html/draft-ietf-acme-acme-11), +otherwise known as Let's Encrypt v2 (or v02). + + * ACME draft 11 + * Let's Encrypt v2 + * Let's Encrypt v02 + +The predecessor (le-acme-core) supports Let's Encrypt v1 (or v01), which was a +[hodge-podge of various drafts](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) +of the ACME spec early on. + + * ACME early draft + * Let's Encrypt v1 + * Let's Encrypt v01 + +This library maintains compatibility with le-acme-core so that it can be used as a **drop-in replacement** +and requires **no changes to existing code**, +but also provides an updated API more congruent with draft 11. + +## le-acme-core-compatible API (recommended) + +Status: Stable, Locked, Bugfix-only + +``` +var RSA = require('rsa-compat').RSA; +var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); + +// +// Use exactly the same as le-acme-core +// +``` + +See documentation at + +## draft API (dev) + +Status: Almost stable, not locked + +This API is a simple evolution of le-acme-core, +but tries to provide a better mapping to the new draft 11 APIs. ``` var ACME = require('acme-v2').ACME.create({ @@ -110,6 +122,41 @@ Helpers & Stuff ```javascript // Constants -ACME.acmeChallengePrefix // /.well-known/acme-challenge/ +ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' +ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` +Todo +---- + +* support ECDSA keys +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' + * this may be a bug in the staging API as it appears it cannot be cancelled either, but returns success status code + +Changelog +--------- + +* v1.0.0 + * Compat API is ready for use + * Eliminate debug logging +* Apr 10, 2018 - tested backwards-compatibility using greenlock.js +* Apr 5, 2018 - export http and dns challenge tests +* Apr 5, 2018 - test http and dns challenges (success and failure) +* Apr 5, 2018 - test subdomains and its wildcard +* Apr 5, 2018 - test two subdomains +* Apr 5, 2018 - test wildcard +* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +* Mar 21, 2018 - *mostly* matches le-acme-core.js API +* Mar 21, 2018 - can now accept values (not hard coded) +* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +* Mar 20, 2018 - download certificate +* Mar 20, 2018 - poll for status +* Mar 20, 2018 - finalize order (submit csr) +* Mar 20, 2018 - generate domain keypair +* Mar 20, 2018 - respond to challenges +* Mar 16, 2018 - get challenges +* Mar 16, 2018 - new order +* Mar 15, 2018 - create account +* Mar 15, 2018 - generate account keypair +* Mar 15, 2018 - get nonce +* Mar 15, 2018 - get directory diff --git a/compat.js b/compat.js index 8b35579..8ca1941 100644 --- a/compat.js +++ b/compat.js @@ -50,6 +50,7 @@ function create(deps) { }; acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; return acme2; } @@ -63,11 +64,12 @@ module.exports.defaults = { //, keyType: 'rsa' // ecdsa //, keySize: 2048 // 256 , rsaKeySize: 2048 // 256 +, acmeChallengePrefix: '/.well-known/acme-challenge/'; }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; }); Object.keys(ACME2).forEach(function (key) { module.exports.ACME[key] = ACME2[key]; - module.exports.ACME.create = create; }); +module.exports.ACME.create = create; diff --git a/node.js b/node.js index 5964c44..b23e42f 100644 --- a/node.js +++ b/node.js @@ -8,15 +8,13 @@ var ACME = module.exports.ACME = {}; -ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; -ACME.acmeChallengeDnsPrefix = '_acme-challenge'; -ACME.acmeChallengePrefixes = { +ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' }; ACME.challengeTests = { 'http-01': function (me, auth) { - var url = 'http://' + auth.hostname + ACME.acmeChallengePrefixes['http-01'] + '/' + auth.token; + var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; return me._request({ url: url }).then(function (resp) { var err; @@ -32,7 +30,7 @@ ACME.challengeTests = { , 'dns-01': function (me, auth) { return me._dig({ type: 'TXT' - , name: ACME.acmeChallengePrefixes['dns-01'] + '.' + auth.hostname + , name: ACME.challengePrefixes['dns-01'] + '.' + auth.hostname }).then(function (ans) { var err; @@ -562,9 +560,7 @@ ACME.create = function create(me) { if (!me) { me = {}; } // me.debug = true; - me.acmeChallengePrefix = ACME.acmeChallengePrefix; - me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; - me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; + me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); me._dig = function (query) { diff --git a/test.cb.js b/test.cb.js index d101435..f2c232c 100644 --- a/test.cb.js +++ b/test.cb.js @@ -31,11 +31,11 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; + pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, ''); + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { diff --git a/test.promise.js b/test.promise.js index 326bd43..bf04f60 100644 --- a/test.promise.js +++ b/test.promise.js @@ -33,11 +33,11 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; + pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, '');; + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');; console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { -- 2.38.5 From 263eed0475cf354c1caffd0456095a522d2c796e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 17:34:18 +0000 Subject: [PATCH 036/252] closer to v1 --- README.md | 113 ++++++++++++++++++++++++++++++++++-------------- compat.js | 4 +- node.js | 12 ++--- test.cb.js | 4 +- test.promise.js | 4 +- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 01539b3..771fede 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,12 @@ -acme-v2.js +acme-v2.js (draft 11) ========== | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. +A framework for building letsencrypt v2 (IETF ACME draft 11), forked from `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 -In progress - -* Mar 15, 2018 - get directory -* Mar 15, 2018 - get nonce -* Mar 15, 2018 - generate account keypair -* Mar 15, 2018 - create account -* Mar 16, 2018 - new order -* Mar 16, 2018 - get challenges -* Mar 20, 2018 - respond to challenges -* Mar 20, 2018 - generate domain keypair -* Mar 20, 2018 - finalize order (submit csr) -* Mar 20, 2018 - poll for status -* Mar 20, 2018 - download certificate -* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -* Mar 21, 2018 - can now accept values (not hard coded) -* Mar 21, 2018 - *mostly* matches le-acme-core.js API -* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) -* Apr 5, 2018 - test wildcard -* Apr 5, 2018 - test two subdomains -* Apr 5, 2018 - test subdomains and its wildcard -* Apr 5, 2018 - test http and dns challenges (success and failure) -* Apr 5, 2018 - export http and dns challenge tests -* Apr 10, 2018 - tested backwards-compatibility using greenlock.js - -Todo - -* support ECDSA keys -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' - ## Let's Encrypt Directory URLs ``` @@ -48,7 +19,48 @@ https://acme-v02.api.letsencrypt.org/directory https://acme-staging-v02.api.letsencrypt.org/directory ``` -## API +## Two API versions, Two Implementations + +This library (acme-v2.js) supports ACME [*draft 11*](https://tools.ietf.org/html/draft-ietf-acme-acme-11), +otherwise known as Let's Encrypt v2 (or v02). + + * ACME draft 11 + * Let's Encrypt v2 + * Let's Encrypt v02 + +The predecessor (le-acme-core) supports Let's Encrypt v1 (or v01), which was a +[hodge-podge of various drafts](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) +of the ACME spec early on. + + * ACME early draft + * Let's Encrypt v1 + * Let's Encrypt v01 + +This library maintains compatibility with le-acme-core so that it can be used as a **drop-in replacement** +and requires **no changes to existing code**, +but also provides an updated API more congruent with draft 11. + +## le-acme-core-compatible API (recommended) + +Status: Stable, Locked, Bugfix-only + +``` +var RSA = require('rsa-compat').RSA; +var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); + +// +// Use exactly the same as le-acme-core +// +``` + +See documentation at + +## draft API (dev) + +Status: Almost stable, not locked + +This API is a simple evolution of le-acme-core, +but tries to provide a better mapping to the new draft 11 APIs. ``` var ACME = require('acme-v2').ACME.create({ @@ -110,6 +122,41 @@ Helpers & Stuff ```javascript // Constants -ACME.acmeChallengePrefix // /.well-known/acme-challenge/ +ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' +ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` +Todo +---- + +* support ECDSA keys +* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' + * this may be a bug in the staging API as it appears it cannot be cancelled either, but returns success status code + +Changelog +--------- + +* v1.0.0 + * Compat API is ready for use + * Eliminate debug logging +* Apr 10, 2018 - tested backwards-compatibility using greenlock.js +* Apr 5, 2018 - export http and dns challenge tests +* Apr 5, 2018 - test http and dns challenges (success and failure) +* Apr 5, 2018 - test subdomains and its wildcard +* Apr 5, 2018 - test two subdomains +* Apr 5, 2018 - test wildcard +* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +* Mar 21, 2018 - *mostly* matches le-acme-core.js API +* Mar 21, 2018 - can now accept values (not hard coded) +* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +* Mar 20, 2018 - download certificate +* Mar 20, 2018 - poll for status +* Mar 20, 2018 - finalize order (submit csr) +* Mar 20, 2018 - generate domain keypair +* Mar 20, 2018 - respond to challenges +* Mar 16, 2018 - get challenges +* Mar 16, 2018 - new order +* Mar 15, 2018 - create account +* Mar 15, 2018 - generate account keypair +* Mar 15, 2018 - get nonce +* Mar 15, 2018 - get directory diff --git a/compat.js b/compat.js index 8b35579..8ca1941 100644 --- a/compat.js +++ b/compat.js @@ -50,6 +50,7 @@ function create(deps) { }; acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; return acme2; } @@ -63,11 +64,12 @@ module.exports.defaults = { //, keyType: 'rsa' // ecdsa //, keySize: 2048 // 256 , rsaKeySize: 2048 // 256 +, acmeChallengePrefix: '/.well-known/acme-challenge/'; }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; }); Object.keys(ACME2).forEach(function (key) { module.exports.ACME[key] = ACME2[key]; - module.exports.ACME.create = create; }); +module.exports.ACME.create = create; diff --git a/node.js b/node.js index 5964c44..b23e42f 100644 --- a/node.js +++ b/node.js @@ -8,15 +8,13 @@ var ACME = module.exports.ACME = {}; -ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; -ACME.acmeChallengeDnsPrefix = '_acme-challenge'; -ACME.acmeChallengePrefixes = { +ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' }; ACME.challengeTests = { 'http-01': function (me, auth) { - var url = 'http://' + auth.hostname + ACME.acmeChallengePrefixes['http-01'] + '/' + auth.token; + var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; return me._request({ url: url }).then(function (resp) { var err; @@ -32,7 +30,7 @@ ACME.challengeTests = { , 'dns-01': function (me, auth) { return me._dig({ type: 'TXT' - , name: ACME.acmeChallengePrefixes['dns-01'] + '.' + auth.hostname + , name: ACME.challengePrefixes['dns-01'] + '.' + auth.hostname }).then(function (ans) { var err; @@ -562,9 +560,7 @@ ACME.create = function create(me) { if (!me) { me = {}; } // me.debug = true; - me.acmeChallengePrefix = ACME.acmeChallengePrefix; - me.acmeChallengeDnsPrefix = ACME.acmeChallengeDnsPrefix; - me.acmeChallengePrefixes = ACME.acmeChallengePrefixes; + me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); me._dig = function (query) { diff --git a/test.cb.js b/test.cb.js index d101435..f2c232c 100644 --- a/test.cb.js +++ b/test.cb.js @@ -31,11 +31,11 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; + pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, ''); + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { diff --git a/test.promise.js b/test.promise.js index 326bd43..bf04f60 100644 --- a/test.promise.js +++ b/test.promise.js @@ -33,11 +33,11 @@ module.exports.run = function run(web, chType, email, accountKeypair, domainKeyp console.log(""); if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.acmeChallengePrefixes['http-01'] + "/" + opts.token; + pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.acmeChallengeDnsPrefix + "." + opts.hostname.replace(/^\*\./, '');; + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');; console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { -- 2.38.5 From 0c3ff85d2ffcbe86bc63fe3404aaf484d8efaf17 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 17:37:10 +0000 Subject: [PATCH 037/252] typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 771fede..ed1e892 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ acme-v2.js (draft 11) | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt v2 (IETF ACME draft 11), forked from `le-acme-core.js`. +A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 -- 2.38.5 From e31e72b0b826216d8c4abe4cc5f9c00f5344ad61 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 17:37:10 +0000 Subject: [PATCH 038/252] typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 771fede..ed1e892 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ acme-v2.js (draft 11) | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt v2 (IETF ACME draft 11), forked from `le-acme-core.js`. +A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 -- 2.38.5 From 851d6d4299c1c09c1b109bd766dd5ce6f4bc653d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 18:03:40 +0000 Subject: [PATCH 039/252] v1 --- compat.js | 8 +++-- node.js | 91 +++++++++++++++++++++++++--------------------------- package.json | 13 ++++++-- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/compat.js b/compat.js index 8ca1941..e2e03ff 100644 --- a/compat.js +++ b/compat.js @@ -10,10 +10,14 @@ function resolveFn(cb) { } function rejectFn(cb) { return function (err) { - console.log('reject something or other:'); - console.log(err.stack); + console.error('[acme-v2] handled(?) rejection as errback:'); + console.error(err.stack); + // nextTick to get out of Promise chain process.nextTick(function () { cb(err); }); + + // do not resolve promise further + return new Promise(function () {}); }; } diff --git a/node.js b/node.js index b23e42f..c743db9 100644 --- a/node.js +++ b/node.js @@ -97,7 +97,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - if (me.debug) { console.log('[acme-v2] accounts.create'); } + if (me.debug) console.debug('[acme-v2] accounts.create'); return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -141,8 +141,8 @@ ACME._registerAccount = function (me, options) { ); delete jws.header; - if (me.debug) { console.log('[acme-v2] accounts.create JSON body:'); } - if (me.debug) { console.log(jws); } + if (me.debug) console.debug('[acme-v2] accounts.create JSON body:'); + if (me.debug) console.debug(jws); me._nonce = null; return me._request({ method: 'POST' @@ -152,18 +152,16 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - if (me.debug) { - // the account id url - console.log('[DEBUG] new account location:'); - console.log(location); // the account id url - console.log(resp.toJSON()); - } + // the account id url me._kid = location; + if (me.debug) console.debug('[DEBUG] new account location:'); + if (me.debug) console.debug(location); + if (me.debug) console.debug(resp.toJSON()); return resp.body; }).then(resolve, reject); } - if (me.debug) { console.log('[acme-v2] agreeToTerms'); } + if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (1 === options.agreeToTerms.length) { return options.agreeToTerms(me._tos).then(agree, reject); } @@ -201,7 +199,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - if (me.debug) { console.log('\n[DEBUG] getChallenges\n'); } + if (me.debug) console.debug('\n[DEBUG] getChallenges\n'); return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -264,14 +262,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - console.log('[acme-v2.js] deactivate:'); - console.log(resp.headers); - console.log(resp.body); - console.log(); + if (me.debug) console.debug('[acme-v2.js] deactivate:'); + if (me.debug) console.debug(resp.headers); + if (me.debug) console.debug(resp.body); + if (me.debug) console.debug(); me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('deactivate challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('deactivate challenge: resp.body:'); + if (me.debug) console.debug(resp.body); return ACME._wait(10 * 1000); }); } @@ -283,13 +281,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { count += 1; - if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } + if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { console.error('poll: resp.body:'); console.error(resp.body); if ('processing' === resp.body.status) { - if (me.debug) { console.log('poll: again'); } + if (me.debug) console.debug('poll: again'); return ACME._wait(1 * 1000).then(pollStatus); } @@ -298,12 +296,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (count >= 4) { return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); } - if (me.debug) { console.log('poll: again'); } + if (me.debug) console.debug('poll: again'); return ACME._wait(1 * 1000).then(testChallenge); } if ('valid' === resp.body.status) { - if (me.debug) { console.log('poll: valid'); } + if (me.debug) console.debug('poll: valid'); try { if (1 === options.removeChallenge.length) { @@ -345,14 +343,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - console.log('[acme-v2.js] challenge accepted!'); - console.log(resp.headers); - console.log(resp.body); - console.log(); + if (me.debug) console.debug('[acme-v2.js] challenge accepted!'); + if (me.debug) console.debug(resp.headers); + if (me.debug) console.debug(resp.body); + if (me.debug) console.debug(); me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('respond to challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('respond to challenge: resp.body:'); + if (me.debug) console.debug(resp.body); return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); }); } @@ -367,8 +365,8 @@ ACME._postChallenge = function (me, options, identifier, ch) { // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } - //console.log('\n[DEBUG] stop to fix things\n'); return; + if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } + //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; return ACME._wait(1 * 1000).then(function () { if (!me.skipChallengeTest) { @@ -391,7 +389,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - if (me.debug) { console.log('finalizeOrder:'); } + if (me.debug) console.debug('finalizeOrder:'); var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -404,7 +402,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , new Buffer(payload) ); - if (me.debug) { console.log('finalize:', me._finalize); } + if (me.debug) console.debug('finalize:', me._finalize); me._nonce = null; return me._request({ method: 'POST' @@ -414,8 +412,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('order finalized: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('order finalized: resp.body:'); + if (me.debug) console.debug(resp.body); if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); @@ -441,7 +439,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - if (me.debug) { console.log('[acme-v2] DEBUG get cert 1'); } + if (me.debug) console.debug('[acme-v2] DEBUG get cert 1'); if (!options.challengeTypes) { if (!options.challengeType) { @@ -461,7 +459,7 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) { console.log('[acme-v2] certificates.create'); } + if (me.debug) console.debug('[acme-v2] certificates.create'); return ACME._getNonce(me).then(function () { var body = { identifiers: options.domains.map(function (hostname) { @@ -479,7 +477,7 @@ ACME._getCertificate = function (me, options) { , new Buffer(payload) ); - if (me.debug) { console.log('\n[DEBUG] newOrder\n'); } + if (me.debug) console.debug('\n[DEBUG] newOrder\n'); me._nonce = null; return me._request({ method: 'POST' @@ -490,21 +488,19 @@ ACME._getCertificate = function (me, options) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; var auths; - if (me.debug) { - console.log(location); // the account id url - console.log(resp.toJSON()); - } + if (me.debug) console.debug(location); // the account id url + if (me.debug) console.debug(resp.toJSON()); me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { console.error("[acme-v2.js] authorizations were not fetched:"); console.error(resp.body); return Promise.reject(new Error("authorizations were not fetched")); } - if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } + if (me.debug) console.debug("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); //return resp.body; auths = me._authorizations.slice(0); @@ -538,17 +534,17 @@ ACME._getCertificate = function (me, options) { } return next().then(function () { - if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } + if (me.debug) console.debug("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { - console.log('acme-v2: order was finalized'); + if (me.debug) console.debug('acme-v2: order was finalized'); return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('acme-v2: csr submitted and cert received:'); - console.log(resp.body); + if (me.debug) console.debug('acme-v2: csr submitted and cert received:'); + if (me.debug) console.debug(resp.body); return resp.body; }); }); @@ -558,8 +554,7 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } - // - me.debug = true; + // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); diff --git a/package.json b/package.json index 46975a3..14d3e98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "acme-v2", - "version": "0.9.0", - "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "version": "1.0.0", + "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", "scripts": { @@ -13,16 +13,25 @@ }, "keywords": [ "acmev2", + "acmev02", "acme-v2", + "acme-v02", "acme", "acme2", + "acme11", + "acme-draft11", + "acme-draft-11", + "draft", + "11", "ssl", "tls", "https", "Let's Encrypt", "letsencrypt", "letsencrypt-v2", + "letsencrypt-v02", "letsencryptv2", + "letsencryptv02", "letsencrypt2", "greenlock", "greenlock2" -- 2.38.5 From 71e0faec95d8b44adf06e085111fa7ce58ec4332 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Apr 2018 18:03:40 +0000 Subject: [PATCH 040/252] v1 --- compat.js | 8 +++-- node.js | 91 +++++++++++++++++++++++++--------------------------- package.json | 13 ++++++-- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/compat.js b/compat.js index 8ca1941..e2e03ff 100644 --- a/compat.js +++ b/compat.js @@ -10,10 +10,14 @@ function resolveFn(cb) { } function rejectFn(cb) { return function (err) { - console.log('reject something or other:'); - console.log(err.stack); + console.error('[acme-v2] handled(?) rejection as errback:'); + console.error(err.stack); + // nextTick to get out of Promise chain process.nextTick(function () { cb(err); }); + + // do not resolve promise further + return new Promise(function () {}); }; } diff --git a/node.js b/node.js index b23e42f..c743db9 100644 --- a/node.js +++ b/node.js @@ -97,7 +97,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - if (me.debug) { console.log('[acme-v2] accounts.create'); } + if (me.debug) console.debug('[acme-v2] accounts.create'); return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -141,8 +141,8 @@ ACME._registerAccount = function (me, options) { ); delete jws.header; - if (me.debug) { console.log('[acme-v2] accounts.create JSON body:'); } - if (me.debug) { console.log(jws); } + if (me.debug) console.debug('[acme-v2] accounts.create JSON body:'); + if (me.debug) console.debug(jws); me._nonce = null; return me._request({ method: 'POST' @@ -152,18 +152,16 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - if (me.debug) { - // the account id url - console.log('[DEBUG] new account location:'); - console.log(location); // the account id url - console.log(resp.toJSON()); - } + // the account id url me._kid = location; + if (me.debug) console.debug('[DEBUG] new account location:'); + if (me.debug) console.debug(location); + if (me.debug) console.debug(resp.toJSON()); return resp.body; }).then(resolve, reject); } - if (me.debug) { console.log('[acme-v2] agreeToTerms'); } + if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (1 === options.agreeToTerms.length) { return options.agreeToTerms(me._tos).then(agree, reject); } @@ -201,7 +199,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - if (me.debug) { console.log('\n[DEBUG] getChallenges\n'); } + if (me.debug) console.debug('\n[DEBUG] getChallenges\n'); return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -264,14 +262,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - console.log('[acme-v2.js] deactivate:'); - console.log(resp.headers); - console.log(resp.body); - console.log(); + if (me.debug) console.debug('[acme-v2.js] deactivate:'); + if (me.debug) console.debug(resp.headers); + if (me.debug) console.debug(resp.body); + if (me.debug) console.debug(); me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('deactivate challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('deactivate challenge: resp.body:'); + if (me.debug) console.debug(resp.body); return ACME._wait(10 * 1000); }); } @@ -283,13 +281,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { count += 1; - if (me.debug) { console.log('\n[DEBUG] statusChallenge\n'); } + if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { console.error('poll: resp.body:'); console.error(resp.body); if ('processing' === resp.body.status) { - if (me.debug) { console.log('poll: again'); } + if (me.debug) console.debug('poll: again'); return ACME._wait(1 * 1000).then(pollStatus); } @@ -298,12 +296,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (count >= 4) { return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); } - if (me.debug) { console.log('poll: again'); } + if (me.debug) console.debug('poll: again'); return ACME._wait(1 * 1000).then(testChallenge); } if ('valid' === resp.body.status) { - if (me.debug) { console.log('poll: valid'); } + if (me.debug) console.debug('poll: valid'); try { if (1 === options.removeChallenge.length) { @@ -345,14 +343,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - console.log('[acme-v2.js] challenge accepted!'); - console.log(resp.headers); - console.log(resp.body); - console.log(); + if (me.debug) console.debug('[acme-v2.js] challenge accepted!'); + if (me.debug) console.debug(resp.headers); + if (me.debug) console.debug(resp.body); + if (me.debug) console.debug(); me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('respond to challenge: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('respond to challenge: resp.body:'); + if (me.debug) console.debug(resp.body); return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); }); } @@ -367,8 +365,8 @@ ACME._postChallenge = function (me, options, identifier, ch) { // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - if (me.debug) {console.log('\n[DEBUG] postChallenge\n'); } - //console.log('\n[DEBUG] stop to fix things\n'); return; + if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } + //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; return ACME._wait(1 * 1000).then(function () { if (!me.skipChallengeTest) { @@ -391,7 +389,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - if (me.debug) { console.log('finalizeOrder:'); } + if (me.debug) console.debug('finalizeOrder:'); var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -404,7 +402,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , new Buffer(payload) ); - if (me.debug) { console.log('finalize:', me._finalize); } + if (me.debug) console.debug('finalize:', me._finalize); me._nonce = null; return me._request({ method: 'POST' @@ -414,8 +412,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.log('order finalized: resp.body:'); } - if (me.debug) { console.log(resp.body); } + if (me.debug) console.debug('order finalized: resp.body:'); + if (me.debug) console.debug(resp.body); if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); @@ -441,7 +439,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - if (me.debug) { console.log('[acme-v2] DEBUG get cert 1'); } + if (me.debug) console.debug('[acme-v2] DEBUG get cert 1'); if (!options.challengeTypes) { if (!options.challengeType) { @@ -461,7 +459,7 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) { console.log('[acme-v2] certificates.create'); } + if (me.debug) console.debug('[acme-v2] certificates.create'); return ACME._getNonce(me).then(function () { var body = { identifiers: options.domains.map(function (hostname) { @@ -479,7 +477,7 @@ ACME._getCertificate = function (me, options) { , new Buffer(payload) ); - if (me.debug) { console.log('\n[DEBUG] newOrder\n'); } + if (me.debug) console.debug('\n[DEBUG] newOrder\n'); me._nonce = null; return me._request({ method: 'POST' @@ -490,21 +488,19 @@ ACME._getCertificate = function (me, options) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; var auths; - if (me.debug) { - console.log(location); // the account id url - console.log(resp.toJSON()); - } + if (me.debug) console.debug(location); // the account id url + if (me.debug) console.debug(resp.toJSON()); me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; - //console.log('[DEBUG] finalize:', me._finalize); return; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { console.error("[acme-v2.js] authorizations were not fetched:"); console.error(resp.body); return Promise.reject(new Error("authorizations were not fetched")); } - if (me.debug) { console.log("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } + if (me.debug) console.debug("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); //return resp.body; auths = me._authorizations.slice(0); @@ -538,17 +534,17 @@ ACME._getCertificate = function (me, options) { } return next().then(function () { - if (me.debug) { console.log("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); } + if (me.debug) console.debug("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); return ACME._finalizeOrder(me, options, validatedDomains); }).then(function () { - console.log('acme-v2: order was finalized'); + if (me.debug) console.debug('acme-v2: order was finalized'); return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - console.log('acme-v2: csr submitted and cert received:'); - console.log(resp.body); + if (me.debug) console.debug('acme-v2: csr submitted and cert received:'); + if (me.debug) console.debug(resp.body); return resp.body; }); }); @@ -558,8 +554,7 @@ ACME._getCertificate = function (me, options) { ACME.create = function create(me) { if (!me) { me = {}; } - // - me.debug = true; + // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; me.request = me.request || require('request'); diff --git a/package.json b/package.json index 46975a3..14d3e98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "acme-v2", - "version": "0.9.0", - "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "version": "1.0.0", + "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", "scripts": { @@ -13,16 +13,25 @@ }, "keywords": [ "acmev2", + "acmev02", "acme-v2", + "acme-v02", "acme", "acme2", + "acme11", + "acme-draft11", + "acme-draft-11", + "draft", + "11", "ssl", "tls", "https", "Let's Encrypt", "letsencrypt", "letsencrypt-v2", + "letsencrypt-v02", "letsencryptv2", + "letsencryptv02", "letsencrypt2", "greenlock", "greenlock2" -- 2.38.5 From 6fb08390fd153783fd9312d7089f7848c21f5c85 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 13 Apr 2018 23:23:08 +0000 Subject: [PATCH 041/252] typo fix --- compat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat.js b/compat.js index e2e03ff..aaed431 100644 --- a/compat.js +++ b/compat.js @@ -68,7 +68,7 @@ module.exports.defaults = { //, keyType: 'rsa' // ecdsa //, keySize: 2048 // 256 , rsaKeySize: 2048 // 256 -, acmeChallengePrefix: '/.well-known/acme-challenge/'; +, acmeChallengePrefix: '/.well-known/acme-challenge/' }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; -- 2.38.5 From fdd5a88de893f421936ed6a2bcee37ca6315464e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 13 Apr 2018 23:23:08 +0000 Subject: [PATCH 042/252] typo fix --- compat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat.js b/compat.js index e2e03ff..aaed431 100644 --- a/compat.js +++ b/compat.js @@ -68,7 +68,7 @@ module.exports.defaults = { //, keyType: 'rsa' // ecdsa //, keySize: 2048 // 256 , rsaKeySize: 2048 // 256 -, acmeChallengePrefix: '/.well-known/acme-challenge/'; +, acmeChallengePrefix: '/.well-known/acme-challenge/' }; Object.keys(module.exports.defaults).forEach(function (key) { module.exports.ACME[key] = module.exports.defaults[key]; -- 2.38.5 From 52406344ed2d3882391b03e389e89ab3289f75f9 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 13 Apr 2018 23:26:29 +0000 Subject: [PATCH 043/252] v1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14d3e98..5983681 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.0", + "version": "1.0.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 9cdac50dbc1f578e86098d7fb0bfe49f722cc1f7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 13 Apr 2018 23:26:29 +0000 Subject: [PATCH 044/252] v1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14d3e98..5983681 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.0", + "version": "1.0.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 28f5baf3d3d88e0cc22447ceef558db249c86818 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 16 Apr 2018 01:04:06 +0000 Subject: [PATCH 045/252] v1.0.2 --- README.md | 58 +++++++++++++++++++++++++++-- test.js => examples/cli.js | 31 ++++++++++++--- examples/genkeypair.js | 22 +++++++++++ examples/http-server.js | 7 ++++ examples/https-server.js | 11 ++++++ genkeypair.js | 18 --------- node.js | 30 ++++++++++++++- package.json | 2 +- test.cb.js => tests/cb.js | 6 +-- test.compat.js => tests/compat.js | 10 ++--- test.promise.js => tests/promise.js | 6 +-- 11 files changed, 158 insertions(+), 43 deletions(-) rename test.js => examples/cli.js (51%) create mode 100644 examples/genkeypair.js create mode 100644 examples/http-server.js create mode 100644 examples/https-server.js delete mode 100644 genkeypair.js rename test.cb.js => tests/cb.js (91%) rename test.compat.js => tests/compat.js (86%) rename test.promise.js => tests/promise.js (91%) diff --git a/README.md b/README.md index ed1e892..2c6c45e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,59 @@ acme-v2.js (draft 11) ========== +| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) +| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) +| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. +A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. +Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 +## Looking for Quick 'n' Easy™? + +If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). +If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). + +* [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +* [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + +## How to build ACME clients + +As this is intended to build ACME clients, there is not a simple 2-line example. + +I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: + +```bash +node examples/cli.js +``` + +The example cli has the following prompts: + +``` +What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) +What challenge will you be testing today? http-01 or dns-01? [http-01] +What email should we use? (optional) +What API style would you like to test? v1-compat or promise? [v1-compat] + +Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +Then hit the 'any' key to continue... +``` + +When you've completed the challenge you can hit a key to continue the process. + +If you place the certificate you receive back in `tests/fullchain.pem` +you can then test it with `examples/https-server.js`. + +``` +examples/cli.js +examples/genkeypair.js +tests/compat.js +``` ## Let's Encrypt Directory URLs @@ -136,7 +184,11 @@ Todo Changelog --------- -* v1.0.0 +* v1.0.2 + * use `options.contact` to provide raw contact array + * made `options.email` optional + * file cleanup +* v1.0.1 * Compat API is ready for use * Eliminate debug logging * Apr 10, 2018 - tested backwards-compatibility using greenlock.js diff --git a/test.js b/examples/cli.js similarity index 51% rename from test.js rename to examples/cli.js index 6490e34..f26354a 100644 --- a/test.js +++ b/examples/cli.js @@ -7,6 +7,8 @@ var rl = readline.createInterface({ output: process.stdout }); +require('./genkeypair.js'); + function getWeb() { rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { web = (web||'').trim().split(/,/g); @@ -35,12 +37,31 @@ function getEmail(web, chType) { email = (email||'').trim(); if (!email) { email = null; } + getApiStyle(web, chType, email); + }); +} + +function getApiStyle(web, chType, email) { + var defaultStyle = 'compat'; + rl.question('What API style would you like to test? v1-compat or promise? [v1-compat] ', function (apiStyle) { + apiStyle = (apiStyle||'').trim(); + if (!apiStyle) { apiStyle = 'v1-compat'; } + rl.close(); - var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); - var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); - //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); - //require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); - require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + + var RSA = require('rsa-compat').RSA; + var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/account.privkey.pem') }); + var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/privkey.pem') }); + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + + if ('promise' === apiStyle) { + require('../tests/promise.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else if ('cb' === apiStyle) { + require('../tests/cb.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else { + if ('v1-compat' !== apiStyle) { console.warn("Didn't understand '" + apiStyle + "', using 'v1-compat' instead..."); } + require('../tests/compat.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } }); } diff --git a/examples/genkeypair.js b/examples/genkeypair.js new file mode 100644 index 0000000..2c7e3c6 --- /dev/null +++ b/examples/genkeypair.js @@ -0,0 +1,22 @@ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); + }); +} + +if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); + }); +} diff --git a/examples/http-server.js b/examples/http-server.js new file mode 100644 index 0000000..4195455 --- /dev/null +++ b/examples/http-server.js @@ -0,0 +1,7 @@ +'use strict'; + +var http = require('http'); +var express = require('express'); +var server = http.createServer(express.static('../tests')).listen(80, function () { + console.log('Listening on', this.address()); +}); diff --git a/examples/https-server.js b/examples/https-server.js new file mode 100644 index 0000000..5dd2c2c --- /dev/null +++ b/examples/https-server.js @@ -0,0 +1,11 @@ +'use strict'; + +var https = require('https'); +var server = https.createServer({ + key: require('fs').readFileSync('../tests/privkey.pem') +, cert: require('fs').readFileSync('../tests/fullchain.pem') +}, function (req, res) { + res.end("Hello, World!"); +}).listen(443, function () { + console.log('Listening on', this.address()); +}); diff --git a/genkeypair.js b/genkeypair.js deleted file mode 100644 index f029ade..0000000 --- a/genkeypair.js +++ /dev/null @@ -1,18 +0,0 @@ -var RSA = require('rsa-compat').RSA; -var fs = require('fs'); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem); -}); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem); -}); diff --git a/node.js b/node.js index c743db9..c64d8df 100644 --- a/node.js +++ b/node.js @@ -112,10 +112,16 @@ ACME._registerAccount = function (me, options) { } var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = [ 'mailto:' + options.email ] + } var body = { termsOfServiceAgreed: tosUrl === me._tos , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] + , contact: contact }; if (options.externalAccount) { body.externalAccountBinding = me.RSA.signJws( @@ -150,6 +156,8 @@ ACME._registerAccount = function (me, options) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { + var account = resp.body; + me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; // the account id url @@ -157,15 +165,33 @@ ACME._registerAccount = function (me, options) { if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug(location); if (me.debug) console.debug(resp.toJSON()); - return resp.body; + + /* + { + id: 5925245, + key: + { kty: 'RSA', + n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ', + e: 'AQAB' }, + contact: [], + initialIp: '198.199.82.211', + createdAt: '2018-04-16T00:41:00.720584972Z', + status: 'valid' + } + */ + if (!account) { account = { _emptyResponse: true, key: {} }; } + account.key.kid = me._kid; + return account; }).then(resolve, reject); } if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (1 === options.agreeToTerms.length) { + // newer promise API return options.agreeToTerms(me._tos).then(agree, reject); } else if (2 === options.agreeToTerms.length) { + // backwards compat cb API return options.agreeToTerms(me._tos, function (err, tosUrl) { if (!err) { agree(tosUrl); return; } reject(err); diff --git a/package.json b/package.json index 5983681..618cd9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.1", + "version": "1.0.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", diff --git a/test.cb.js b/tests/cb.js similarity index 91% rename from test.cb.js rename to tests/cb.js index f2c232c..550c285 100644 --- a/test.cb.js +++ b/tests/cb.js @@ -1,10 +1,8 @@ 'use strict'; -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + var acme2 = require('../').ACME.create({ RSA: RSA }); acme2.init(directoryUrl).then(function () { var options = { agreeToTerms: function (tosUrl, agree) { diff --git a/test.compat.js b/tests/compat.js similarity index 86% rename from test.compat.js rename to tests/compat.js index 73060bd..d0a66b1 100644 --- a/test.compat.js +++ b/tests/compat.js @@ -1,11 +1,9 @@ 'use strict'; -var RSA = require('rsa-compat').RSA; - -module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) { +module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { console.log('[DEBUG] run', web, chType, email); - var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); + var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { if (err) { console.log('err 1'); throw err; } @@ -44,8 +42,8 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair acme2.registerNewAccount(options, function (err, account) { if (err) { console.log('err 2'); throw err; } - console.log('account:'); - console.log(account); + if (options.debug) console.debug('account:'); + if (options.debug) console.log(account); acme2.getCertificate(options, function (err, fullchainPem) { if (err) { console.log('err 3'); throw err; } diff --git a/test.promise.js b/tests/promise.js similarity index 91% rename from test.promise.js rename to tests/promise.js index bf04f60..ee2a028 100644 --- a/test.promise.js +++ b/tests/promise.js @@ -1,10 +1,8 @@ 'use strict'; /* global Promise */ -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { + var acme2 = require('../').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' acme2.init(directoryUrl).then(function () { var options = { -- 2.38.5 From a88486c313225b1aa624f1d99586ffb9f6b18204 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 16 Apr 2018 01:04:06 +0000 Subject: [PATCH 046/252] v1.0.2 --- README.md | 58 +++++++++++++++++++++++++++-- test.js => examples/cli.js | 31 ++++++++++++--- examples/genkeypair.js | 22 +++++++++++ examples/http-server.js | 7 ++++ examples/https-server.js | 11 ++++++ genkeypair.js | 18 --------- node.js | 30 ++++++++++++++- package.json | 2 +- test.cb.js => tests/cb.js | 6 +-- test.compat.js => tests/compat.js | 10 ++--- test.promise.js => tests/promise.js | 6 +-- 11 files changed, 158 insertions(+), 43 deletions(-) rename test.js => examples/cli.js (51%) create mode 100644 examples/genkeypair.js create mode 100644 examples/http-server.js create mode 100644 examples/https-server.js delete mode 100644 genkeypair.js rename test.cb.js => tests/cb.js (91%) rename test.compat.js => tests/compat.js (86%) rename test.promise.js => tests/promise.js (91%) diff --git a/README.md b/README.md index ed1e892..2c6c45e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,59 @@ acme-v2.js (draft 11) ========== +| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) +| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) +| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + | Sponsored by [ppl](https://ppl.family) -A framework for building letsencrypt v2 (IETF ACME draft 11) clients, successor to `le-acme-core.js`. +A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. +Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -Summary of spec that I'm working off of here: https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8 +## Looking for Quick 'n' Easy™? + +If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). +If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). + +* [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +* [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) + +## How to build ACME clients + +As this is intended to build ACME clients, there is not a simple 2-line example. + +I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: + +```bash +node examples/cli.js +``` + +The example cli has the following prompts: + +``` +What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) +What challenge will you be testing today? http-01 or dns-01? [http-01] +What email should we use? (optional) +What API style would you like to test? v1-compat or promise? [v1-compat] + +Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' + +Then hit the 'any' key to continue... +``` + +When you've completed the challenge you can hit a key to continue the process. + +If you place the certificate you receive back in `tests/fullchain.pem` +you can then test it with `examples/https-server.js`. + +``` +examples/cli.js +examples/genkeypair.js +tests/compat.js +``` ## Let's Encrypt Directory URLs @@ -136,7 +184,11 @@ Todo Changelog --------- -* v1.0.0 +* v1.0.2 + * use `options.contact` to provide raw contact array + * made `options.email` optional + * file cleanup +* v1.0.1 * Compat API is ready for use * Eliminate debug logging * Apr 10, 2018 - tested backwards-compatibility using greenlock.js diff --git a/test.js b/examples/cli.js similarity index 51% rename from test.js rename to examples/cli.js index 6490e34..f26354a 100644 --- a/test.js +++ b/examples/cli.js @@ -7,6 +7,8 @@ var rl = readline.createInterface({ output: process.stdout }); +require('./genkeypair.js'); + function getWeb() { rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { web = (web||'').trim().split(/,/g); @@ -35,12 +37,31 @@ function getEmail(web, chType) { email = (email||'').trim(); if (!email) { email = null; } + getApiStyle(web, chType, email); + }); +} + +function getApiStyle(web, chType, email) { + var defaultStyle = 'compat'; + rl.question('What API style would you like to test? v1-compat or promise? [v1-compat] ', function (apiStyle) { + apiStyle = (apiStyle||'').trim(); + if (!apiStyle) { apiStyle = 'v1-compat'; } + rl.close(); - var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }); - var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }); - //require('./test.compat.js').run(web, chType, email, accountKeypair, domainKeypair); - //require('./test.cb.js').run(web, chType, email, accountKeypair, domainKeypair); - require('./test.promise.js').run(web, chType, email, accountKeypair, domainKeypair); + + var RSA = require('rsa-compat').RSA; + var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/account.privkey.pem') }); + var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/privkey.pem') }); + var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + + if ('promise' === apiStyle) { + require('../tests/promise.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else if ('cb' === apiStyle) { + require('../tests/cb.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } else { + if ('v1-compat' !== apiStyle) { console.warn("Didn't understand '" + apiStyle + "', using 'v1-compat' instead..."); } + require('../tests/compat.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); + } }); } diff --git a/examples/genkeypair.js b/examples/genkeypair.js new file mode 100644 index 0000000..2c7e3c6 --- /dev/null +++ b/examples/genkeypair.js @@ -0,0 +1,22 @@ +var RSA = require('rsa-compat').RSA; +var fs = require('fs'); + +if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); + }); +} + +if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) { + RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair) + console.log(privkeyPem); + + fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); + }); +} diff --git a/examples/http-server.js b/examples/http-server.js new file mode 100644 index 0000000..4195455 --- /dev/null +++ b/examples/http-server.js @@ -0,0 +1,7 @@ +'use strict'; + +var http = require('http'); +var express = require('express'); +var server = http.createServer(express.static('../tests')).listen(80, function () { + console.log('Listening on', this.address()); +}); diff --git a/examples/https-server.js b/examples/https-server.js new file mode 100644 index 0000000..5dd2c2c --- /dev/null +++ b/examples/https-server.js @@ -0,0 +1,11 @@ +'use strict'; + +var https = require('https'); +var server = https.createServer({ + key: require('fs').readFileSync('../tests/privkey.pem') +, cert: require('fs').readFileSync('../tests/fullchain.pem') +}, function (req, res) { + res.end("Hello, World!"); +}).listen(443, function () { + console.log('Listening on', this.address()); +}); diff --git a/genkeypair.js b/genkeypair.js deleted file mode 100644 index f029ade..0000000 --- a/genkeypair.js +++ /dev/null @@ -1,18 +0,0 @@ -var RSA = require('rsa-compat').RSA; -var fs = require('fs'); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/account.privkey.pem', privkeyPem); -}); - -RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); - - fs.writeFileSync(__dirname + '/privkey.pem', privkeyPem); -}); diff --git a/node.js b/node.js index c743db9..c64d8df 100644 --- a/node.js +++ b/node.js @@ -112,10 +112,16 @@ ACME._registerAccount = function (me, options) { } var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = [ 'mailto:' + options.email ] + } var body = { termsOfServiceAgreed: tosUrl === me._tos , onlyReturnExisting: false - , contact: [ 'mailto:' + options.email ] + , contact: contact }; if (options.externalAccount) { body.externalAccountBinding = me.RSA.signJws( @@ -150,6 +156,8 @@ ACME._registerAccount = function (me, options) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { + var account = resp.body; + me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; // the account id url @@ -157,15 +165,33 @@ ACME._registerAccount = function (me, options) { if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug(location); if (me.debug) console.debug(resp.toJSON()); - return resp.body; + + /* + { + id: 5925245, + key: + { kty: 'RSA', + n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ', + e: 'AQAB' }, + contact: [], + initialIp: '198.199.82.211', + createdAt: '2018-04-16T00:41:00.720584972Z', + status: 'valid' + } + */ + if (!account) { account = { _emptyResponse: true, key: {} }; } + account.key.kid = me._kid; + return account; }).then(resolve, reject); } if (me.debug) console.debug('[acme-v2] agreeToTerms'); if (1 === options.agreeToTerms.length) { + // newer promise API return options.agreeToTerms(me._tos).then(agree, reject); } else if (2 === options.agreeToTerms.length) { + // backwards compat cb API return options.agreeToTerms(me._tos, function (err, tosUrl) { if (!err) { agree(tosUrl); return; } reject(err); diff --git a/package.json b/package.json index 5983681..618cd9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.1", + "version": "1.0.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", diff --git a/test.cb.js b/tests/cb.js similarity index 91% rename from test.cb.js rename to tests/cb.js index f2c232c..550c285 100644 --- a/test.cb.js +++ b/tests/cb.js @@ -1,10 +1,8 @@ 'use strict'; -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + var acme2 = require('../').ACME.create({ RSA: RSA }); acme2.init(directoryUrl).then(function () { var options = { agreeToTerms: function (tosUrl, agree) { diff --git a/test.compat.js b/tests/compat.js similarity index 86% rename from test.compat.js rename to tests/compat.js index 73060bd..d0a66b1 100644 --- a/test.compat.js +++ b/tests/compat.js @@ -1,11 +1,9 @@ 'use strict'; -var RSA = require('rsa-compat').RSA; - -module.exports.run = function (web, chType, email, accountKeypair, domainKeypair) { +module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { console.log('[DEBUG] run', web, chType, email); - var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); + var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { if (err) { console.log('err 1'); throw err; } @@ -44,8 +42,8 @@ module.exports.run = function (web, chType, email, accountKeypair, domainKeypair acme2.registerNewAccount(options, function (err, account) { if (err) { console.log('err 2'); throw err; } - console.log('account:'); - console.log(account); + if (options.debug) console.debug('account:'); + if (options.debug) console.log(account); acme2.getCertificate(options, function (err, fullchainPem) { if (err) { console.log('err 3'); throw err; } diff --git a/test.promise.js b/tests/promise.js similarity index 91% rename from test.promise.js rename to tests/promise.js index bf04f60..ee2a028 100644 --- a/test.promise.js +++ b/tests/promise.js @@ -1,10 +1,8 @@ 'use strict'; /* global Promise */ -module.exports.run = function run(web, chType, email, accountKeypair, domainKeypair) { - var RSA = require('rsa-compat').RSA; - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; - var acme2 = require('./').ACME.create({ RSA: RSA }); +module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { + var acme2 = require('../').ACME.create({ RSA: RSA }); // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' acme2.init(directoryUrl).then(function () { var options = { -- 2.38.5 From 324becf04fa87f8fe09fb3bdb8bd269035b3d072 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 16 Apr 2018 01:10:48 +0000 Subject: [PATCH 047/252] add other examples --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2c6c45e..6a839c8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ you can then test it with `examples/https-server.js`. examples/cli.js examples/genkeypair.js tests/compat.js +examples/https-server.js +examples/http-server.js ``` ## Let's Encrypt Directory URLs -- 2.38.5 From 65ff4a0feb9eb218fc837062ea9ada28c2d19244 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 16 Apr 2018 01:10:48 +0000 Subject: [PATCH 048/252] add other examples --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2c6c45e..6a839c8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ you can then test it with `examples/https-server.js`. examples/cli.js examples/genkeypair.js tests/compat.js +examples/https-server.js +examples/http-server.js ``` ## Let's Encrypt Directory URLs -- 2.38.5 From f05a18a19e93e8d24cb7f0e8e9cb0d45687076fd Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 06:51:02 +0000 Subject: [PATCH 049/252] move sponsorship --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6a839c8..befef6b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -acme-v2.js (draft 11) -========== - +| Sponsored by [ppl](https://ppl.family) | [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) +| -| Sponsored by [ppl](https://ppl.family) +acme-v2.js +========== A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -- 2.38.5 From d0a58be97dbdd8faabf425bec7a6c0ce5fcccadc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 06:51:02 +0000 Subject: [PATCH 050/252] move sponsorship --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6a839c8..befef6b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -acme-v2.js (draft 11) -========== - +| Sponsored by [ppl](https://ppl.family) | [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) +| -| Sponsored by [ppl](https://ppl.family) +acme-v2.js +========== A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -- 2.38.5 From b79bece356d93e8848b75056b55f44724f8d47b5 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 07:41:46 +0000 Subject: [PATCH 051/252] cleanup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index befef6b..8490a22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ | Sponsored by [ppl](https://ppl.family) -| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) +| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) -- 2.38.5 From 12d5518be3d65ce8ff708e0778019a539eaf13a1 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 07:41:46 +0000 Subject: [PATCH 052/252] cleanup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index befef6b..8490a22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ | Sponsored by [ppl](https://ppl.family) -| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) +| **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) -- 2.38.5 From bbdf35525c9c472aa0c53dd3850aebc18ffcbb84 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:42:24 -0600 Subject: [PATCH 053/252] v1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 618cd9f..5393f34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.2", + "version": "1.0.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 8c8e3d843ac4bcffe692f4bf82cb0fe5712c1a55 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:42:24 -0600 Subject: [PATCH 054/252] v1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 618cd9f..5393f34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.2", + "version": "1.0.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 5b254d7fd912c36137132f2c96364dac8c2ba36d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:44:41 -0600 Subject: [PATCH 055/252] changelog v1.0.3 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8490a22..11554cd 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ Todo Changelog --------- +* v1.0.3 + * documentation cleanup * v1.0.2 * use `options.contact` to provide raw contact array * made `options.email` optional -- 2.38.5 From deb87cdec9aa4828cf923527278632eb1395611f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:44:41 -0600 Subject: [PATCH 056/252] changelog v1.0.3 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8490a22..11554cd 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ Todo Changelog --------- +* v1.0.3 + * documentation cleanup * v1.0.2 * use `options.contact` to provide raw contact array * made `options.email` optional -- 2.38.5 From dfbb24beb3d76c15637ca413cc8804c675245e87 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:48:17 -0600 Subject: [PATCH 057/252] cleanup --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11554cd..4d58c09 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ but also provides an updated API more congruent with draft 11. Status: Stable, Locked, Bugfix-only +See Full Documentation at + ``` var RSA = require('rsa-compat').RSA; var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); @@ -103,16 +105,15 @@ var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); // ``` -See documentation at +## Promise API (dev) -## draft API (dev) - -Status: Almost stable, not locked +Status: Almost stable, but **not semver locked** This API is a simple evolution of le-acme-core, but tries to provide a better mapping to the new draft 11 APIs. ``` +// Create Instance (Dependency Injection) var ACME = require('acme-v2').ACME.create({ RSA: require('rsa-compat').RSA @@ -131,9 +132,12 @@ var ACME = require('acme-v2').ACME.create({ // don't try to validate challenges locally , skipChallengeTest: false }); -``` -```javascript + +// Discover Directory URLs +ACME.init(acmeDirectoryUrl) // returns Promise + + // Accounts ACME.accounts.create(options) // returns Promise registration data @@ -162,10 +166,6 @@ ACME.certificates.create(options) // returns Promise ``` Helpers & Stuff -- 2.38.5 From e2c3faeb82c7ed11b1b17d7466af2cd1d3a3495c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 20 Apr 2018 01:48:17 -0600 Subject: [PATCH 058/252] cleanup --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11554cd..4d58c09 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ but also provides an updated API more congruent with draft 11. Status: Stable, Locked, Bugfix-only +See Full Documentation at + ``` var RSA = require('rsa-compat').RSA; var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); @@ -103,16 +105,15 @@ var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); // ``` -See documentation at +## Promise API (dev) -## draft API (dev) - -Status: Almost stable, not locked +Status: Almost stable, but **not semver locked** This API is a simple evolution of le-acme-core, but tries to provide a better mapping to the new draft 11 APIs. ``` +// Create Instance (Dependency Injection) var ACME = require('acme-v2').ACME.create({ RSA: require('rsa-compat').RSA @@ -131,9 +132,12 @@ var ACME = require('acme-v2').ACME.create({ // don't try to validate challenges locally , skipChallengeTest: false }); -``` -```javascript + +// Discover Directory URLs +ACME.init(acmeDirectoryUrl) // returns Promise + + // Accounts ACME.accounts.create(options) // returns Promise registration data @@ -162,10 +166,6 @@ ACME.certificates.create(options) // returns Promise ``` Helpers & Stuff -- 2.38.5 From 79e368d783d7d7e2e32026782ec4e938f5c24585 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:38:45 -0600 Subject: [PATCH 059/252] v1.0.4 backcompat with node v6 --- node.js | 4 ++-- package.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/node.js b/node.js index c64d8df..e3f0ecf 100644 --- a/node.js +++ b/node.js @@ -165,7 +165,7 @@ ACME._registerAccount = function (me, options) { if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug(location); if (me.debug) console.debug(resp.toJSON()); - + /* { id: 5925245, @@ -601,7 +601,7 @@ ACME.create = function create(me) { }); }); }; - me.promisify = me.promisify || require('util').promisify; + me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; if ('function' !== typeof me.getUserAgentString) { diff --git a/package.json b/package.json index 5393f34..b906549 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.3", + "version": "1.0.4", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -41,5 +41,8 @@ "dependencies": { "request": "^2.85.0", "rsa-compat": "^1.3.0" + }, + "optionalDependencies": { + "bluebird": "^3.5.1" } } -- 2.38.5 From 474bb64004afb94f99e4f86cdf7efef07c0f52fb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:38:45 -0600 Subject: [PATCH 060/252] v1.0.4 backcompat with node v6 --- node.js | 4 ++-- package.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/node.js b/node.js index c64d8df..e3f0ecf 100644 --- a/node.js +++ b/node.js @@ -165,7 +165,7 @@ ACME._registerAccount = function (me, options) { if (me.debug) console.debug('[DEBUG] new account location:'); if (me.debug) console.debug(location); if (me.debug) console.debug(resp.toJSON()); - + /* { id: 5925245, @@ -601,7 +601,7 @@ ACME.create = function create(me) { }); }); }; - me.promisify = me.promisify || require('util').promisify; + me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; if ('function' !== typeof me.getUserAgentString) { diff --git a/package.json b/package.json index 5393f34..b906549 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.3", + "version": "1.0.4", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -41,5 +41,8 @@ "dependencies": { "request": "^2.85.0", "rsa-compat": "^1.3.0" + }, + "optionalDependencies": { + "bluebird": "^3.5.1" } } -- 2.38.5 From 10ab61e07e751537635a79cea5dab5110074d9eb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:56:46 -0600 Subject: [PATCH 061/252] v1.0.5 remove junk logging --- node.js | 10 ++++------ package.json | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/node.js b/node.js index e3f0ecf..14800d2 100644 --- a/node.js +++ b/node.js @@ -309,8 +309,6 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); if ('processing' === resp.body.status) { if (me.debug) console.debug('poll: again'); @@ -342,16 +340,16 @@ ACME._postChallenge = function (me, options, identifier, ch) { } if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); + console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); + console.error("[acme-v2] (E_STATE_INVALID) invalid challenge state:"); } else { - console.error("[acme-v2] (z) bad challenge state:"); + console.error("[acme-v2] (E_STATE_UKN) unkown challenge state:"); } - return Promise.reject(new Error("[acme-v2] bad challenge state")); + return Promise.reject(new Error("[acme-v2] challenge state error")); }); } diff --git a/package.json b/package.json index b906549..bc4cfc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.4", + "version": "1.0.5", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 290251a1b016ce4b5e4dd75e35caf280a05bd69d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:56:46 -0600 Subject: [PATCH 062/252] v1.0.5 remove junk logging --- node.js | 10 ++++------ package.json | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/node.js b/node.js index e3f0ecf..14800d2 100644 --- a/node.js +++ b/node.js @@ -309,8 +309,6 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - console.error('poll: resp.body:'); - console.error(resp.body); if ('processing' === resp.body.status) { if (me.debug) console.debug('poll: again'); @@ -342,16 +340,16 @@ ACME._postChallenge = function (me, options, identifier, ch) { } if (!resp.body.status) { - console.error("[acme-v2] (y) bad challenge state:"); + console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (x) invalid challenge state:"); + console.error("[acme-v2] (E_STATE_INVALID) invalid challenge state:"); } else { - console.error("[acme-v2] (z) bad challenge state:"); + console.error("[acme-v2] (E_STATE_UKN) unkown challenge state:"); } - return Promise.reject(new Error("[acme-v2] bad challenge state")); + return Promise.reject(new Error("[acme-v2] challenge state error")); }); } diff --git a/package.json b/package.json index b906549..bc4cfc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.4", + "version": "1.0.5", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 26e702952e0f7a23c152c9035c3e43c30e791fc8 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:58:14 -0600 Subject: [PATCH 063/252] update changelog --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d58c09..b9309ed 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,9 @@ Todo Changelog --------- -* v1.0.3 - * documentation cleanup +* v1.0.5 - cleanup logging +* v1.0.4 - v6- compat use `promisify` from node's util or bluebird +* v1.0.3 - documentation cleanup * v1.0.2 * use `options.contact` to provide raw contact array * made `options.email` optional -- 2.38.5 From 4e2649d797fa7d01779f3113bced322b58ee25df Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:58:14 -0600 Subject: [PATCH 064/252] update changelog --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d58c09..b9309ed 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,9 @@ Todo Changelog --------- -* v1.0.3 - * documentation cleanup +* v1.0.5 - cleanup logging +* v1.0.4 - v6- compat use `promisify` from node's util or bluebird +* v1.0.3 - documentation cleanup * v1.0.2 * use `options.contact` to provide raw contact array * made `options.email` optional -- 2.38.5 From 2f0bf17b3972f1a857927d3801339535b87c0255 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:58:45 -0600 Subject: [PATCH 065/252] remove TODO section (now in issues) --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index b9309ed..fd13d68 100644 --- a/README.md +++ b/README.md @@ -176,13 +176,6 @@ ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` -Todo ----- - -* support ECDSA keys -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' - * this may be a bug in the staging API as it appears it cannot be cancelled either, but returns success status code - Changelog --------- -- 2.38.5 From 3cf7824bedf69726f68be26ceea0713c84aa8297 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 24 Apr 2018 11:58:45 -0600 Subject: [PATCH 066/252] remove TODO section (now in issues) --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index b9309ed..fd13d68 100644 --- a/README.md +++ b/README.md @@ -176,13 +176,6 @@ ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` -Todo ----- - -* support ECDSA keys -* Apr 5, 2018 - appears that sometimes 'pending' status cannot be progressed to 'processing' nor 'deactivated' - * this may be a bug in the staging API as it appears it cannot be cancelled either, but returns success status code - Changelog --------- -- 2.38.5 From bfc50ac9d20139d86b106dab86b54445b3a2e807 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:46:43 -0600 Subject: [PATCH 067/252] better error handling --- node.js | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index 14800d2..2bb3b96 100644 --- a/node.js +++ b/node.js @@ -434,15 +434,13 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) console.debug('order finalized: resp.body:'); if (me.debug) console.debug(resp.body); - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - if ('valid' === resp.body.status) { me._expires = resp.body.expires; me._certificate = resp.body.certificate; @@ -450,13 +448,43 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return resp.body; } - if ('invalid' === resp.body.status) { - console.error('cannot finalize: badness'); - return; + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); } - console.error('(x) cannot finalize: badness'); - return; + if ('pending' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'pending'." + + " Best guess: You have not accepted at least one challenge for each domain." + "\n\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('invalid' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'invalid'." + + " Best guess: One or more of the domain challenges could not be verified" + + " (or the order was canceled)." + "\n\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('ready' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'." + "\n\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); + } + + return Promise.reject(new Error( + "Didn't finalize order: Unhandled status '" + resp.body.status + "'." + + " This is not one of the known statuses...\n\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); }); } -- 2.38.5 From 473f373de32851cca233852ebfa8973e2817ad12 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:46:43 -0600 Subject: [PATCH 068/252] better error handling --- node.js | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index 14800d2..2bb3b96 100644 --- a/node.js +++ b/node.js @@ -434,15 +434,13 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) console.debug('order finalized: resp.body:'); if (me.debug) console.debug(resp.body); - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - if ('valid' === resp.body.status) { me._expires = resp.body.expires; me._certificate = resp.body.certificate; @@ -450,13 +448,43 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return resp.body; } - if ('invalid' === resp.body.status) { - console.error('cannot finalize: badness'); - return; + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); } - console.error('(x) cannot finalize: badness'); - return; + if ('pending' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'pending'." + + " Best guess: You have not accepted at least one challenge for each domain." + "\n\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('invalid' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'invalid'." + + " Best guess: One or more of the domain challenges could not be verified" + + " (or the order was canceled)." + "\n\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('ready' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'." + "\n\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); + } + + return Promise.reject(new Error( + "Didn't finalize order: Unhandled status '" + resp.body.status + "'." + + " This is not one of the known statuses...\n\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); }); } -- 2.38.5 From 3e23ea5f8d50bc08821305f91a902010257618cd Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:48:52 -0600 Subject: [PATCH 069/252] better error handling --- node.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node.js b/node.js index 2bb3b96..e844a5b 100644 --- a/node.js +++ b/node.js @@ -452,6 +452,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return ACME._wait().then(pollCert); } + if (me.debug) console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); + if ('pending' === resp.body.status) { return Promise.reject(new Error( "Did not finalize order: status 'pending'." -- 2.38.5 From d54c1380f3b66c0f079e60205c8e03c0f31ebb50 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:48:52 -0600 Subject: [PATCH 070/252] better error handling --- node.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node.js b/node.js index 2bb3b96..e844a5b 100644 --- a/node.js +++ b/node.js @@ -452,6 +452,8 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return ACME._wait().then(pollCert); } + if (me.debug) console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); + if ('pending' === resp.body.status) { return Promise.reject(new Error( "Did not finalize order: status 'pending'." -- 2.38.5 From ba71e12ff2ae9e1b752d097b0a0dd363c0f7f217 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:49:19 -0600 Subject: [PATCH 071/252] v1.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc4cfc1..91b7003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.5", + "version": "1.0.6", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From d0745a634736be14913353fb5c0e8afb141d5423 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 9 May 2018 23:49:19 -0600 Subject: [PATCH 072/252] v1.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc4cfc1..91b7003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.5", + "version": "1.0.6", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 5037ad6b0b2ab42e249cc9f5b5a296b84432d8aa Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 18 May 2018 00:55:50 -0600 Subject: [PATCH 073/252] improve error message as per https://git.coolaj86.com/coolaj86/greenlock.js/issues/12 --- node.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/node.js b/node.js index e844a5b..fa26105 100644 --- a/node.js +++ b/node.js @@ -22,15 +22,20 @@ ACME.challengeTests = { return true; } - err = new Error("self check does not pass"); - err.code = 'E_RETRY'; + err = new Error( + "Error: Failed HTTP-01 Dry Run.\n" + + "curl '" + url + "' does not return '" + auth.keyAuthorization + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; return Promise.reject(err); }); } , 'dns-01': function (me, auth) { + var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname; return me._dig({ type: 'TXT' - , name: ACME.challengePrefixes['dns-01'] + '.' + auth.hostname + , name: hostname }).then(function (ans) { var err; @@ -40,8 +45,12 @@ ACME.challengeTests = { return true; } - err = new Error("self check does not pass"); - err.code = 'E_RETRY'; + err = new Error( + "Error: Failed DNS-01 Dry Run.\n" + + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; return Promise.reject(err); }); } -- 2.38.5 From bfff22f053694a3e79f6fd7b09ca118b8f6ecde3 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 18 May 2018 00:55:50 -0600 Subject: [PATCH 074/252] improve error message as per https://git.coolaj86.com/coolaj86/greenlock.js/issues/12 --- node.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/node.js b/node.js index e844a5b..fa26105 100644 --- a/node.js +++ b/node.js @@ -22,15 +22,20 @@ ACME.challengeTests = { return true; } - err = new Error("self check does not pass"); - err.code = 'E_RETRY'; + err = new Error( + "Error: Failed HTTP-01 Dry Run.\n" + + "curl '" + url + "' does not return '" + auth.keyAuthorization + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; return Promise.reject(err); }); } , 'dns-01': function (me, auth) { + var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname; return me._dig({ type: 'TXT' - , name: ACME.challengePrefixes['dns-01'] + '.' + auth.hostname + , name: hostname }).then(function (ans) { var err; @@ -40,8 +45,12 @@ ACME.challengeTests = { return true; } - err = new Error("self check does not pass"); - err.code = 'E_RETRY'; + err = new Error( + "Error: Failed DNS-01 Dry Run.\n" + + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; return Promise.reject(err); }); } -- 2.38.5 From 89487c203003913613598ddbc502b2acd8ccdbaf Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 18 May 2018 00:58:15 -0600 Subject: [PATCH 075/252] v1.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b7003..914d837 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.6", + "version": "1.0.7", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 84c3e91ea87306a439737a2bb497b40271c81874 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 18 May 2018 00:58:15 -0600 Subject: [PATCH 076/252] v1.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b7003..914d837 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.6", + "version": "1.0.7", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From e909c0fff0c517f57edf4b034717037fb9a1651b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 23 May 2018 03:08:58 -0600 Subject: [PATCH 077/252] update LICENSE --- LICENSE | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..693448a --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +Copyright 2018 AJ ONeal + +This is open source software; you can redistribute it and/or modify it under the +terms of either: + + a) the "MIT License" + b) the "Apache-2.0 License" + +MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Apache-2.0 License Summary + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -- 2.38.5 From 6b78aa7cee2b10802881f14df464181d2f29e868 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 23 May 2018 03:08:58 -0600 Subject: [PATCH 078/252] update LICENSE --- LICENSE | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..693448a --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +Copyright 2018 AJ ONeal + +This is open source software; you can redistribute it and/or modify it under the +terms of either: + + a) the "MIT License" + b) the "Apache-2.0 License" + +MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Apache-2.0 License Summary + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -- 2.38.5 From 80b54a5a8758fad92abf41686b7ce323d2488b33 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 23 May 2018 03:09:41 -0600 Subject: [PATCH 079/252] v1.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 914d837..000f811 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.7", + "version": "1.0.8", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 208834130d8053e8137f06823a3c3681a2202148 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 23 May 2018 03:09:41 -0600 Subject: [PATCH 080/252] v1.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 914d837..000f811 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.7", + "version": "1.0.8", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 08aa9bcdeb0574be27a7b4ad6819a151642baab3 Mon Sep 17 00:00:00 2001 From: John Shaver Date: Tue, 5 Jun 2018 16:07:54 -0700 Subject: [PATCH 081/252] Fixed error handling for non promise setChallenge. --- node.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index fa26105..50927e5 100644 --- a/node.js +++ b/node.js @@ -384,15 +384,10 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) console.debug('respond to challenge: resp.body:'); if (me.debug) console.debug(resp.body); - return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + return ACME._wait(1 * 1000).then(pollStatus); }); } - function failChallenge(err) { - if (err) { reject(err); return; } - return testChallenge(); - } - function testChallenge() { // TODO put check dns / http checks here? // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} @@ -410,11 +405,23 @@ ACME._postChallenge = function (me, options, identifier, ch) { try { if (1 === options.setChallenge.length) { - options.setChallenge(auth).then(testChallenge, reject); + options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge(auth, failChallenge); + options.setChallenge(auth, function(err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); + options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); } } catch(e) { reject(e); -- 2.38.5 From 119f5a9ae4f28c9a1214a860a68cedb38398ad11 Mon Sep 17 00:00:00 2001 From: John Shaver Date: Tue, 5 Jun 2018 16:07:54 -0700 Subject: [PATCH 082/252] Fixed error handling for non promise setChallenge. --- node.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index fa26105..50927e5 100644 --- a/node.js +++ b/node.js @@ -384,15 +384,10 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) console.debug('respond to challenge: resp.body:'); if (me.debug) console.debug(resp.body); - return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); + return ACME._wait(1 * 1000).then(pollStatus); }); } - function failChallenge(err) { - if (err) { reject(err); return; } - return testChallenge(); - } - function testChallenge() { // TODO put check dns / http checks here? // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} @@ -410,11 +405,23 @@ ACME._postChallenge = function (me, options, identifier, ch) { try { if (1 === options.setChallenge.length) { - options.setChallenge(auth).then(testChallenge, reject); + options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge(auth, failChallenge); + options.setChallenge(auth, function(err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); + options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); } } catch(e) { reject(e); -- 2.38.5 From 08088acd1f78a1a421fe05a3278aa4baf2d552b5 Mon Sep 17 00:00:00 2001 From: John Shaver Date: Wed, 6 Jun 2018 12:34:23 -0700 Subject: [PATCH 083/252] 1.0.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 000f811..3b7a7ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.8", + "version": "1.0.9", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 16cbe77dff6525dfd3fa1e438163e2084aeced01 Mon Sep 17 00:00:00 2001 From: John Shaver Date: Wed, 6 Jun 2018 12:34:23 -0700 Subject: [PATCH 084/252] 1.0.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 000f811..3b7a7ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.8", + "version": "1.0.9", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 1dd07715db707947a158577765e0aa86433d7915 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 19 Jun 2018 01:28:03 -0600 Subject: [PATCH 085/252] request => @coolaj86/urequest --- node.js | 14 +++++++------- package.json | 5 +---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/node.js b/node.js index 50927e5..4ea2b78 100644 --- a/node.js +++ b/node.js @@ -140,7 +140,7 @@ ACME._registerAccount = function (me, options) { , kid: options.externalAccount.id , url: me._directoryUrls.newAccount } - , new Buffer(JSON.stringify(jwk)) + , Buffer.from(JSON.stringify(jwk)) ); } var payload = JSON.stringify(body); @@ -152,7 +152,7 @@ ACME._registerAccount = function (me, options) { , url: me._directoryUrls.newAccount , jwk: jwk } - , new Buffer(payload) + , Buffer.from(payload) ); delete jws.header; @@ -288,7 +288,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(JSON.stringify({ "status": "deactivated" })) + , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; return me._request({ @@ -367,7 +367,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(JSON.stringify({ })) + , Buffer.from(JSON.stringify({ })) ); me._nonce = null; return me._request({ @@ -439,7 +439,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } - , new Buffer(payload) + , Buffer.from(payload) ); if (me.debug) console.debug('finalize:', me._finalize); @@ -544,7 +544,7 @@ ACME._getCertificate = function (me, options) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , new Buffer(payload) + , Buffer.from(payload) ); if (me.debug) console.debug('\n[DEBUG] newOrder\n'); @@ -627,7 +627,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; - me.request = me.request || require('request'); + me.request = me.request || require('@coolaj86/urequest'); me._dig = function (query) { // TODO use digd.js return new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index 3b7a7ec..6225182 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,7 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "dependencies": { - "request": "^2.85.0", + "@coolaj86/urequest": "^1.1.1", "rsa-compat": "^1.3.0" - }, - "optionalDependencies": { - "bluebird": "^3.5.1" } } -- 2.38.5 From f7d1c5615eba7129bed417114828699bc2bfdd56 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 19 Jun 2018 01:28:03 -0600 Subject: [PATCH 086/252] request => @coolaj86/urequest --- node.js | 14 +++++++------- package.json | 5 +---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/node.js b/node.js index 50927e5..4ea2b78 100644 --- a/node.js +++ b/node.js @@ -140,7 +140,7 @@ ACME._registerAccount = function (me, options) { , kid: options.externalAccount.id , url: me._directoryUrls.newAccount } - , new Buffer(JSON.stringify(jwk)) + , Buffer.from(JSON.stringify(jwk)) ); } var payload = JSON.stringify(body); @@ -152,7 +152,7 @@ ACME._registerAccount = function (me, options) { , url: me._directoryUrls.newAccount , jwk: jwk } - , new Buffer(payload) + , Buffer.from(payload) ); delete jws.header; @@ -288,7 +288,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(JSON.stringify({ "status": "deactivated" })) + , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; return me._request({ @@ -367,7 +367,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , new Buffer(JSON.stringify({ })) + , Buffer.from(JSON.stringify({ })) ); me._nonce = null; return me._request({ @@ -439,7 +439,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } - , new Buffer(payload) + , Buffer.from(payload) ); if (me.debug) console.debug('finalize:', me._finalize); @@ -544,7 +544,7 @@ ACME._getCertificate = function (me, options) { options.accountKeypair , undefined , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , new Buffer(payload) + , Buffer.from(payload) ); if (me.debug) console.debug('\n[DEBUG] newOrder\n'); @@ -627,7 +627,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; - me.request = me.request || require('request'); + me.request = me.request || require('@coolaj86/urequest'); me._dig = function (query) { // TODO use digd.js return new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index 3b7a7ec..6225182 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,7 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "dependencies": { - "request": "^2.85.0", + "@coolaj86/urequest": "^1.1.1", "rsa-compat": "^1.3.0" - }, - "optionalDependencies": { - "bluebird": "^3.5.1" } } -- 2.38.5 From 700f400df6d0c991ad25ce20ca07f0f126aef184 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 19 Jun 2018 01:28:24 -0600 Subject: [PATCH 087/252] v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6225182..92b5d93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.9", + "version": "1.1.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 0214d80f80aeff0ff2f44a3e34463e838fc88131 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 19 Jun 2018 01:28:24 -0600 Subject: [PATCH 088/252] v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6225182..92b5d93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.0.9", + "version": "1.1.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From bbeea080f6e09199e5fe1e25198c322d4ea5f5f0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:10:43 -0600 Subject: [PATCH 089/252] update line endings (and add brackets to logs...) --- compat.js | 10 ++-- node.js | 105 ++++++++++++++++++++++--------------- tests/fullchain-formats.js | 77 +++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 49 deletions(-) create mode 100644 tests/fullchain-formats.js diff --git a/compat.js b/compat.js index aaed431..674c028 100644 --- a/compat.js +++ b/compat.js @@ -1,4 +1,5 @@ 'use strict'; +/* global Promise */ var ACME2 = require('./').ACME; @@ -31,13 +32,10 @@ function create(deps) { options.agreeToTerms = options.agreeToTerms || function (tos) { return Promise.resolve(tos); }; - acme2.certificates.create(options).then(function (chainPem) { + acme2.certificates.create(options).then(function (certs) { var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); - resolveFn(cb)({ - cert: chainPem.split(/[\r\n]{2,}/g)[0] + '\r\n' - , privkey: privkeyPem - , chain: chainPem.split(/[\r\n]{2,}/g)[1] + '\r\n' - }); + certs.privkey = privkeyPem; + resolveFn(cb)(certs); }, rejectFn(cb)); }; acme2.getAcmeUrls = function (options, cb) { diff --git a/node.js b/node.js index 4ea2b78..6017f17 100644 --- a/node.js +++ b/node.js @@ -8,6 +8,15 @@ var ACME = module.exports.ACME = {}; +ACME.formatPemChain = function formatPemChain(str) { + return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; +}; +ACME.splitPemChain = function splitPemChain(str) { + return str.trim().split(/[\r\n]{2,}/g).map(function (str) { + return str + '\n'; + }); +}; + ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' @@ -106,7 +115,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - if (me.debug) console.debug('[acme-v2] accounts.create'); + if (me.debug) { console.debug('[acme-v2] accounts.create'); } return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -125,7 +134,7 @@ ACME._registerAccount = function (me, options) { if (options.contact) { contact = options.contact.slice(0); } else if (options.email) { - contact = [ 'mailto:' + options.email ] + contact = [ 'mailto:' + options.email ]; } var body = { termsOfServiceAgreed: tosUrl === me._tos @@ -156,8 +165,8 @@ ACME._registerAccount = function (me, options) { ); delete jws.header; - if (me.debug) console.debug('[acme-v2] accounts.create JSON body:'); - if (me.debug) console.debug(jws); + if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.debug(jws); } me._nonce = null; return me._request({ method: 'POST' @@ -171,9 +180,9 @@ ACME._registerAccount = function (me, options) { var location = resp.toJSON().headers.location; // the account id url me._kid = location; - if (me.debug) console.debug('[DEBUG] new account location:'); - if (me.debug) console.debug(location); - if (me.debug) console.debug(resp.toJSON()); + if (me.debug) { console.debug('[DEBUG] new account location:'); } + if (me.debug) { console.debug(location); } + if (me.debug) { console.debug(resp.toJSON()); } /* { @@ -194,7 +203,7 @@ ACME._registerAccount = function (me, options) { }).then(resolve, reject); } - if (me.debug) console.debug('[acme-v2] agreeToTerms'); + if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } if (1 === options.agreeToTerms.length) { // newer promise API return options.agreeToTerms(me._tos).then(agree, reject); @@ -234,7 +243,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - if (me.debug) console.debug('\n[DEBUG] getChallenges\n'); + if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -297,14 +306,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - if (me.debug) console.debug('[acme-v2.js] deactivate:'); - if (me.debug) console.debug(resp.headers); - if (me.debug) console.debug(resp.body); - if (me.debug) console.debug(); + if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('deactivate challenge: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('deactivate challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } return ACME._wait(10 * 1000); }); } @@ -316,11 +325,11 @@ ACME._postChallenge = function (me, options, identifier, ch) { count += 1; - if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); + if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { if ('processing' === resp.body.status) { - if (me.debug) console.debug('poll: again'); + if (me.debug) { console.debug('poll: again'); } return ACME._wait(1 * 1000).then(pollStatus); } @@ -329,12 +338,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (count >= 4) { return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); } - if (me.debug) console.debug('poll: again'); + if (me.debug) { console.debug('poll: again'); } return ACME._wait(1 * 1000).then(testChallenge); } if ('valid' === resp.body.status) { - if (me.debug) console.debug('poll: valid'); + if (me.debug) { console.debug('poll: valid'); } try { if (1 === options.removeChallenge.length) { @@ -376,14 +385,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - if (me.debug) console.debug('[acme-v2.js] challenge accepted!'); - if (me.debug) console.debug(resp.headers); - if (me.debug) console.debug(resp.body); - if (me.debug) console.debug(); + if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('respond to challenge: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('respond to challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } return ACME._wait(1 * 1000).then(pollStatus); }); } @@ -429,7 +438,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - if (me.debug) console.debug('finalizeOrder:'); + if (me.debug) { console.debug('finalizeOrder:'); } var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -442,7 +451,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , Buffer.from(payload) ); - if (me.debug) console.debug('finalize:', me._finalize); + if (me.debug) { console.debug('finalize:', me._finalize); } me._nonce = null; return me._request({ method: 'POST' @@ -454,21 +463,21 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('order finalized: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('order finalized: resp.body:'); } + if (me.debug) { console.debug(resp.body); } if ('valid' === resp.body.status) { me._expires = resp.body.expires; me._certificate = resp.body.certificate; - return resp.body; + return resp.body; // return order } if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); } - if (me.debug) console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); + if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } if ('pending' === resp.body.status) { return Promise.reject(new Error( @@ -509,7 +518,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - if (me.debug) console.debug('[acme-v2] DEBUG get cert 1'); + if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } if (!options.challengeTypes) { if (!options.challengeType) { @@ -529,7 +538,7 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) console.debug('[acme-v2] certificates.create'); + if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { identifiers: options.domains.map(function (hostname) { @@ -547,7 +556,7 @@ ACME._getCertificate = function (me, options) { , Buffer.from(payload) ); - if (me.debug) console.debug('\n[DEBUG] newOrder\n'); + if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } me._nonce = null; return me._request({ method: 'POST' @@ -558,8 +567,8 @@ ACME._getCertificate = function (me, options) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; var auths; - if (me.debug) console.debug(location); // the account id url - if (me.debug) console.debug(resp.toJSON()); + if (me.debug) { console.debug(location); } // the account id url + if (me.debug) { console.debug(resp.toJSON()); } me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; @@ -570,7 +579,7 @@ ACME._getCertificate = function (me, options) { console.error(resp.body); return Promise.reject(new Error("authorizations were not fetched")); } - if (me.debug) console.debug("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } //return resp.body; auths = me._authorizations.slice(0); @@ -604,18 +613,28 @@ ACME._getCertificate = function (me, options) { } return next().then(function () { - if (me.debug) console.debug("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.debug("[getCertificate] next.then"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); return ACME._finalizeOrder(me, options, validatedDomains); - }).then(function () { - if (me.debug) console.debug('acme-v2: order was finalized'); + }).then(function (order) { + if (me.debug) { console.debug('acme-v2: order was finalized'); } return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - if (me.debug) console.debug('acme-v2: csr submitted and cert received:'); - if (me.debug) console.debug(resp.body); - return resp.body; + if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + var certs = { + expires: order.expires + , identifiers: order.identifiers + //, authorizations: order.authorizations + , cert: certsarr.shift() + //, privkey: privkeyPem + , chain: certsarr.join('\n') + }; + if (me.debug) { console.debug(certs); } + return certs; }); }); }); diff --git a/tests/fullchain-formats.js b/tests/fullchain-formats.js new file mode 100644 index 0000000..cb4f67c --- /dev/null +++ b/tests/fullchain-formats.js @@ -0,0 +1,77 @@ +'use strict'; + +/* +-----BEGIN CERTIFICATE-----LF +xxxLF +yyyLF +-----END CERTIFICATE-----LF +LF +-----BEGIN CERTIFICATE-----LF +xxxLF +yyyLF +-----END CERTIFICATE-----LF + +Rules + * Only Unix LF (\n) Line endings + * Each PEM's lines are separated with \n + * Each PEM ends with \n + * Each PEM is separated with a \n (just like commas separating an array) +*/ + +// https://github.com/certbot/certbot/issues/5721#issuecomment-402362709 +var expected = "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n"; +var tests = [ + "----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" +, "----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" +, "----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----" +, "----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----" +, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----" +, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n" +, "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n" +, "----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n" +]; + +function formatPemChain(str) { + return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; +} +function splitPemChain(str) { + return str.trim().split(/[\r\n]{2,}/g).map(function (str) { + return str + '\n'; + }); +} + +tests.forEach(function (str) { + var actual = formatPemChain(str); + if (expected !== actual) { + console.error('input: ', JSON.stringify(str)); + console.error('expected:', JSON.stringify(expected)); + console.error('actual: ', JSON.stringify(actual)); + throw new Error("did not pass"); + } +}); + +if ( + "----\nxxxx\nyyyy\n----\n" + !== + formatPemChain("\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n") +) { + throw new Error("Not proper for single cert in chain"); +} + +if ( + "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" + !== + formatPemChain("\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n") +) { + throw new Error("Not proper for three certs in chain"); +} + +splitPemChain( + "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" +).forEach(function (str) { + if ("--B--\nxxxx\nyyyy\n--E--\n" !== str) { + throw new Error("bad thingy"); + } +}); + +console.info('PASS'); -- 2.38.5 From 2ba7db13275f459b96538f4482a7e2dd823168d0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:10:43 -0600 Subject: [PATCH 090/252] update line endings (and add brackets to logs...) --- compat.js | 10 ++-- node.js | 105 ++++++++++++++++++++++--------------- tests/fullchain-formats.js | 77 +++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 49 deletions(-) create mode 100644 tests/fullchain-formats.js diff --git a/compat.js b/compat.js index aaed431..674c028 100644 --- a/compat.js +++ b/compat.js @@ -1,4 +1,5 @@ 'use strict'; +/* global Promise */ var ACME2 = require('./').ACME; @@ -31,13 +32,10 @@ function create(deps) { options.agreeToTerms = options.agreeToTerms || function (tos) { return Promise.resolve(tos); }; - acme2.certificates.create(options).then(function (chainPem) { + acme2.certificates.create(options).then(function (certs) { var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); - resolveFn(cb)({ - cert: chainPem.split(/[\r\n]{2,}/g)[0] + '\r\n' - , privkey: privkeyPem - , chain: chainPem.split(/[\r\n]{2,}/g)[1] + '\r\n' - }); + certs.privkey = privkeyPem; + resolveFn(cb)(certs); }, rejectFn(cb)); }; acme2.getAcmeUrls = function (options, cb) { diff --git a/node.js b/node.js index 4ea2b78..6017f17 100644 --- a/node.js +++ b/node.js @@ -8,6 +8,15 @@ var ACME = module.exports.ACME = {}; +ACME.formatPemChain = function formatPemChain(str) { + return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; +}; +ACME.splitPemChain = function splitPemChain(str) { + return str.trim().split(/[\r\n]{2,}/g).map(function (str) { + return str + '\n'; + }); +}; + ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' @@ -106,7 +115,7 @@ ACME._getNonce = function (me) { } */ ACME._registerAccount = function (me, options) { - if (me.debug) console.debug('[acme-v2] accounts.create'); + if (me.debug) { console.debug('[acme-v2] accounts.create'); } return ACME._getNonce(me).then(function () { return new Promise(function (resolve, reject) { @@ -125,7 +134,7 @@ ACME._registerAccount = function (me, options) { if (options.contact) { contact = options.contact.slice(0); } else if (options.email) { - contact = [ 'mailto:' + options.email ] + contact = [ 'mailto:' + options.email ]; } var body = { termsOfServiceAgreed: tosUrl === me._tos @@ -156,8 +165,8 @@ ACME._registerAccount = function (me, options) { ); delete jws.header; - if (me.debug) console.debug('[acme-v2] accounts.create JSON body:'); - if (me.debug) console.debug(jws); + if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.debug(jws); } me._nonce = null; return me._request({ method: 'POST' @@ -171,9 +180,9 @@ ACME._registerAccount = function (me, options) { var location = resp.toJSON().headers.location; // the account id url me._kid = location; - if (me.debug) console.debug('[DEBUG] new account location:'); - if (me.debug) console.debug(location); - if (me.debug) console.debug(resp.toJSON()); + if (me.debug) { console.debug('[DEBUG] new account location:'); } + if (me.debug) { console.debug(location); } + if (me.debug) { console.debug(resp.toJSON()); } /* { @@ -194,7 +203,7 @@ ACME._registerAccount = function (me, options) { }).then(resolve, reject); } - if (me.debug) console.debug('[acme-v2] agreeToTerms'); + if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } if (1 === options.agreeToTerms.length) { // newer promise API return options.agreeToTerms(me._tos).then(agree, reject); @@ -234,7 +243,7 @@ ACME._registerAccount = function (me, options) { } */ ACME._getChallenges = function (me, options, auth) { - if (me.debug) console.debug('\n[DEBUG] getChallenges\n'); + if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { return resp.body; }); @@ -297,14 +306,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - if (me.debug) console.debug('[acme-v2.js] deactivate:'); - if (me.debug) console.debug(resp.headers); - if (me.debug) console.debug(resp.body); - if (me.debug) console.debug(); + if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('deactivate challenge: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('deactivate challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } return ACME._wait(10 * 1000); }); } @@ -316,11 +325,11 @@ ACME._postChallenge = function (me, options, identifier, ch) { count += 1; - if (me.debug) console.debug('\n[DEBUG] statusChallenge\n'); + if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { if ('processing' === resp.body.status) { - if (me.debug) console.debug('poll: again'); + if (me.debug) { console.debug('poll: again'); } return ACME._wait(1 * 1000).then(pollStatus); } @@ -329,12 +338,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (count >= 4) { return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); } - if (me.debug) console.debug('poll: again'); + if (me.debug) { console.debug('poll: again'); } return ACME._wait(1 * 1000).then(testChallenge); } if ('valid' === resp.body.status) { - if (me.debug) console.debug('poll: valid'); + if (me.debug) { console.debug('poll: valid'); } try { if (1 === options.removeChallenge.length) { @@ -376,14 +385,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { - if (me.debug) console.debug('[acme-v2.js] challenge accepted!'); - if (me.debug) console.debug(resp.headers); - if (me.debug) console.debug(resp.body); - if (me.debug) console.debug(); + if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('respond to challenge: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('respond to challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } return ACME._wait(1 * 1000).then(pollStatus); }); } @@ -429,7 +438,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { - if (me.debug) console.debug('finalizeOrder:'); + if (me.debug) { console.debug('finalizeOrder:'); } var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); var body = { csr: csr }; var payload = JSON.stringify(body); @@ -442,7 +451,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { , Buffer.from(payload) ); - if (me.debug) console.debug('finalize:', me._finalize); + if (me.debug) { console.debug('finalize:', me._finalize); } me._nonce = null; return me._request({ method: 'POST' @@ -454,21 +463,21 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) console.debug('order finalized: resp.body:'); - if (me.debug) console.debug(resp.body); + if (me.debug) { console.debug('order finalized: resp.body:'); } + if (me.debug) { console.debug(resp.body); } if ('valid' === resp.body.status) { me._expires = resp.body.expires; me._certificate = resp.body.certificate; - return resp.body; + return resp.body; // return order } if ('processing' === resp.body.status) { return ACME._wait().then(pollCert); } - if (me.debug) console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); + if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } if ('pending' === resp.body.status) { return Promise.reject(new Error( @@ -509,7 +518,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return pollCert(); }; ACME._getCertificate = function (me, options) { - if (me.debug) console.debug('[acme-v2] DEBUG get cert 1'); + if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } if (!options.challengeTypes) { if (!options.challengeType) { @@ -529,7 +538,7 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) console.debug('[acme-v2] certificates.create'); + if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { identifiers: options.domains.map(function (hostname) { @@ -547,7 +556,7 @@ ACME._getCertificate = function (me, options) { , Buffer.from(payload) ); - if (me.debug) console.debug('\n[DEBUG] newOrder\n'); + if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } me._nonce = null; return me._request({ method: 'POST' @@ -558,8 +567,8 @@ ACME._getCertificate = function (me, options) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; var auths; - if (me.debug) console.debug(location); // the account id url - if (me.debug) console.debug(resp.toJSON()); + if (me.debug) { console.debug(location); } // the account id url + if (me.debug) { console.debug(resp.toJSON()); } me._authorizations = resp.body.authorizations; me._order = location; me._finalize = resp.body.finalize; @@ -570,7 +579,7 @@ ACME._getCertificate = function (me, options) { console.error(resp.body); return Promise.reject(new Error("authorizations were not fetched")); } - if (me.debug) console.debug("47 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } //return resp.body; auths = me._authorizations.slice(0); @@ -604,18 +613,28 @@ ACME._getCertificate = function (me, options) { } return next().then(function () { - if (me.debug) console.debug("37 &#&#&#&#&#&#&&##&#&#&#&#&#&#&#&"); + if (me.debug) { console.debug("[getCertificate] next.then"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; }); return ACME._finalizeOrder(me, options, validatedDomains); - }).then(function () { - if (me.debug) console.debug('acme-v2: order was finalized'); + }).then(function (order) { + if (me.debug) { console.debug('acme-v2: order was finalized'); } return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - if (me.debug) console.debug('acme-v2: csr submitted and cert received:'); - if (me.debug) console.debug(resp.body); - return resp.body; + if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + var certs = { + expires: order.expires + , identifiers: order.identifiers + //, authorizations: order.authorizations + , cert: certsarr.shift() + //, privkey: privkeyPem + , chain: certsarr.join('\n') + }; + if (me.debug) { console.debug(certs); } + return certs; }); }); }); diff --git a/tests/fullchain-formats.js b/tests/fullchain-formats.js new file mode 100644 index 0000000..cb4f67c --- /dev/null +++ b/tests/fullchain-formats.js @@ -0,0 +1,77 @@ +'use strict'; + +/* +-----BEGIN CERTIFICATE-----LF +xxxLF +yyyLF +-----END CERTIFICATE-----LF +LF +-----BEGIN CERTIFICATE-----LF +xxxLF +yyyLF +-----END CERTIFICATE-----LF + +Rules + * Only Unix LF (\n) Line endings + * Each PEM's lines are separated with \n + * Each PEM ends with \n + * Each PEM is separated with a \n (just like commas separating an array) +*/ + +// https://github.com/certbot/certbot/issues/5721#issuecomment-402362709 +var expected = "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n"; +var tests = [ + "----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" +, "----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" +, "----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----" +, "----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----" +, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----" +, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n" +, "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n" +, "----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n" +]; + +function formatPemChain(str) { + return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; +} +function splitPemChain(str) { + return str.trim().split(/[\r\n]{2,}/g).map(function (str) { + return str + '\n'; + }); +} + +tests.forEach(function (str) { + var actual = formatPemChain(str); + if (expected !== actual) { + console.error('input: ', JSON.stringify(str)); + console.error('expected:', JSON.stringify(expected)); + console.error('actual: ', JSON.stringify(actual)); + throw new Error("did not pass"); + } +}); + +if ( + "----\nxxxx\nyyyy\n----\n" + !== + formatPemChain("\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n") +) { + throw new Error("Not proper for single cert in chain"); +} + +if ( + "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" + !== + formatPemChain("\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n") +) { + throw new Error("Not proper for three certs in chain"); +} + +splitPemChain( + "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" +).forEach(function (str) { + if ("--B--\nxxxx\nyyyy\n--E--\n" !== str) { + throw new Error("bad thingy"); + } +}); + +console.info('PASS'); -- 2.38.5 From a67256e8a9e4ed08aeeadc7787268f51d75bf499 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:11:20 -0600 Subject: [PATCH 091/252] add .jshintrc --- .jshintrc | 1 + 1 file changed, 1 insertion(+) create mode 120000 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 120000 index 0000000..d5794d7 --- /dev/null +++ b/.jshintrc @@ -0,0 +1 @@ +/Users/aj/dotfiles/dummy/.jshintrc \ No newline at end of file -- 2.38.5 From 8117b1fd6664ad6b0751b62c27895d95e0729c31 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:11:20 -0600 Subject: [PATCH 092/252] add .jshintrc --- .jshintrc | 1 + 1 file changed, 1 insertion(+) create mode 120000 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 120000 index 0000000..d5794d7 --- /dev/null +++ b/.jshintrc @@ -0,0 +1 @@ +/Users/aj/dotfiles/dummy/.jshintrc \ No newline at end of file -- 2.38.5 From 63284386ebd6069ddb3653d0cd76a13e248b40ca Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:28:00 -0600 Subject: [PATCH 093/252] update compat to allow testing dns --- node.js | 6 +++++- package.json | 2 +- tests/compat.js | 21 +++++++++++++++++---- tests/promise.js | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/node.js b/node.js index 6017f17..ddb2866 100644 --- a/node.js +++ b/node.js @@ -416,13 +416,17 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.setChallenge.length) { options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge(auth, function(err) { + var challengeCb = function (err) { if(err) { reject(err); } else { testChallenge().then(resolve, reject); } + }; + Object.keys(auth).forEach(function (key) { + challengeCb[key] = auth[key]; }); + options.setChallenge(auth, challengeCb); } else { options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { if(err) { diff --git a/package.json b/package.json index 92b5d93..452c256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.0", + "version": "1.1.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", diff --git a/tests/compat.js b/tests/compat.js index d0a66b1..b48cfef 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -12,10 +12,23 @@ module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKey agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - var pathname = hostname + acme2.acmeChallengePrefix + token; - console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); - console.log("echo '" + val + "' > '" + pathname + "'"); - console.log("\nThen hit the 'any' key to continue..."); + var pathname; + + if ('http-01' === cb.type) { + pathname = hostname + acme2.acmeChallengePrefix + token; + console.log("Put the string '" + val /*keyAuthorization*/ + "' into a file at '" + pathname + "'"); + console.log("echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'"); + console.log("\nThen hit the 'any' key to continue..."); + } else if ('dns-01' === cb.type) { + // forwards-backwards compat + pathname = acme2.challengePrefixes['dns-01'] + "." + hostname.replace(/^\*\./, ''); + console.log("Put the string '" + cb.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'"); + console.log("\nThen hit the 'any' key to continue..."); + } else { + cb(new Error("[acme-v2] unrecognized challenge type")); + return; + } function onAny() { console.log("'any' key was hit"); diff --git a/tests/promise.js b/tests/promise.js index ee2a028..7c1c17f 100644 --- a/tests/promise.js +++ b/tests/promise.js @@ -35,9 +35,9 @@ module.exports.run = function run(directoryUrl, RSA, web, chType, email, account console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');; + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); - console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + console.log("dig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { reject(new Error("[acme-v2] unrecognized challenge type")); return; -- 2.38.5 From 668e2bb0ac5e174662982e449a2087b87e399831 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:28:00 -0600 Subject: [PATCH 094/252] update compat to allow testing dns --- node.js | 6 +++++- package.json | 2 +- tests/compat.js | 21 +++++++++++++++++---- tests/promise.js | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/node.js b/node.js index 6017f17..ddb2866 100644 --- a/node.js +++ b/node.js @@ -416,13 +416,17 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.setChallenge.length) { options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - options.setChallenge(auth, function(err) { + var challengeCb = function (err) { if(err) { reject(err); } else { testChallenge().then(resolve, reject); } + }; + Object.keys(auth).forEach(function (key) { + challengeCb[key] = auth[key]; }); + options.setChallenge(auth, challengeCb); } else { options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { if(err) { diff --git a/package.json b/package.json index 92b5d93..452c256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.0", + "version": "1.1.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", diff --git a/tests/compat.js b/tests/compat.js index d0a66b1..b48cfef 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -12,10 +12,23 @@ module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKey agree(null, tosUrl); } , setChallenge: function (hostname, token, val, cb) { - var pathname = hostname + acme2.acmeChallengePrefix + token; - console.log("Put the string '" + val + "' into a file at '" + pathname + "'"); - console.log("echo '" + val + "' > '" + pathname + "'"); - console.log("\nThen hit the 'any' key to continue..."); + var pathname; + + if ('http-01' === cb.type) { + pathname = hostname + acme2.acmeChallengePrefix + token; + console.log("Put the string '" + val /*keyAuthorization*/ + "' into a file at '" + pathname + "'"); + console.log("echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'"); + console.log("\nThen hit the 'any' key to continue..."); + } else if ('dns-01' === cb.type) { + // forwards-backwards compat + pathname = acme2.challengePrefixes['dns-01'] + "." + hostname.replace(/^\*\./, ''); + console.log("Put the string '" + cb.dnsAuthorization + "' into the TXT record '" + pathname + "'"); + console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'"); + console.log("\nThen hit the 'any' key to continue..."); + } else { + cb(new Error("[acme-v2] unrecognized challenge type")); + return; + } function onAny() { console.log("'any' key was hit"); diff --git a/tests/promise.js b/tests/promise.js index ee2a028..7c1c17f 100644 --- a/tests/promise.js +++ b/tests/promise.js @@ -35,9 +35,9 @@ module.exports.run = function run(directoryUrl, RSA, web, chType, email, account console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); } else if ('dns-01' === opts.type) { - pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, '');; + pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); - console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); + console.log("dig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); } else { reject(new Error("[acme-v2] unrecognized challenge type")); return; -- 2.38.5 From 97e39aaeb04943e3979eef8df384370dd1ba9091 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:29:28 -0600 Subject: [PATCH 095/252] add .gitignore --- .gitignore | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf23d29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +letsencrypt.work +letsencrypt.logs +letsencrypt.config + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules -- 2.38.5 From 6c811d880ce43844bd0b7b3806114c105b979038 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:29:28 -0600 Subject: [PATCH 096/252] add .gitignore --- .gitignore | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf23d29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +letsencrypt.work +letsencrypt.logs +letsencrypt.config + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules -- 2.38.5 From 125d31b2c419d0d554a574f93155f2a2267f33a5 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:36:33 -0600 Subject: [PATCH 097/252] * the other callback --- .gitignore | 1 + node.js | 20 ++++++++++---------- tests/compat.js | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index bf23d29..052547a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.pem letsencrypt.work letsencrypt.logs letsencrypt.config diff --git a/node.js b/node.js index ddb2866..34c5b5c 100644 --- a/node.js +++ b/node.js @@ -416,7 +416,15 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.setChallenge.length) { options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - var challengeCb = function (err) { + options.setChallenge(auth, function (err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); + } else { + var challengeCb = function(err) { if(err) { reject(err); } else { @@ -426,15 +434,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); - options.setChallenge(auth, challengeCb); - } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } - }); + options.setChallenge(identifier.value, ch.token, keyAuthorization, challengeCb); } } catch(e) { reject(e); diff --git a/tests/compat.js b/tests/compat.js index b48cfef..e643e2e 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -26,7 +26,7 @@ module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKey console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'"); console.log("\nThen hit the 'any' key to continue..."); } else { - cb(new Error("[acme-v2] unrecognized challenge type")); + cb(new Error("[acme-v2] unrecognized challenge type: " + cb.type)); return; } -- 2.38.5 From 0a5a72e2fc5018620de2fb39d69b4904263892dc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 4 Jul 2018 00:36:33 -0600 Subject: [PATCH 098/252] * the other callback --- .gitignore | 1 + node.js | 20 ++++++++++---------- tests/compat.js | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index bf23d29..052547a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.pem letsencrypt.work letsencrypt.logs letsencrypt.config diff --git a/node.js b/node.js index ddb2866..34c5b5c 100644 --- a/node.js +++ b/node.js @@ -416,7 +416,15 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (1 === options.setChallenge.length) { options.setChallenge(auth).then(testChallenge).then(resolve, reject); } else if (2 === options.setChallenge.length) { - var challengeCb = function (err) { + options.setChallenge(auth, function (err) { + if(err) { + reject(err); + } else { + testChallenge().then(resolve, reject); + } + }); + } else { + var challengeCb = function(err) { if(err) { reject(err); } else { @@ -426,15 +434,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); - options.setChallenge(auth, challengeCb); - } else { - options.setChallenge(identifier.value, ch.token, keyAuthorization, function(err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } - }); + options.setChallenge(identifier.value, ch.token, keyAuthorization, challengeCb); } } catch(e) { reject(e); diff --git a/tests/compat.js b/tests/compat.js index b48cfef..e643e2e 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -26,7 +26,7 @@ module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKey console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'"); console.log("\nThen hit the 'any' key to continue..."); } else { - cb(new Error("[acme-v2] unrecognized challenge type")); + cb(new Error("[acme-v2] unrecognized challenge type: " + cb.type)); return; } -- 2.38.5 From 91f90d5689b9c58c9c43911bebc8621409998dce Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 7 Jul 2018 14:00:22 -0600 Subject: [PATCH 099/252] simplify keywords, add draft-12 --- package.json | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 452c256..e4d5a75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.1", + "version": "1.1.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -12,29 +12,16 @@ "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" }, "keywords": [ - "acmev2", - "acmev02", - "acme-v2", - "acme-v02", - "acme", - "acme2", - "acme11", - "acme-draft11", - "acme-draft-11", - "draft", - "11", - "ssl", - "tls", - "https", "Let's Encrypt", - "letsencrypt", - "letsencrypt-v2", - "letsencrypt-v02", - "letsencryptv2", - "letsencryptv02", - "letsencrypt2", - "greenlock", - "greenlock2" + "ACME", + "v02", + "v2", + "draft-11", + "draft-12", + "free ssl", + "tls", + "automated https", + "letsencrypt" ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", -- 2.38.5 From e704419cdb179cfd129603b756ee35685c5cd475 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 7 Jul 2018 14:00:22 -0600 Subject: [PATCH 100/252] simplify keywords, add draft-12 --- package.json | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 452c256..e4d5a75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.1", + "version": "1.1.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -12,29 +12,16 @@ "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" }, "keywords": [ - "acmev2", - "acmev02", - "acme-v2", - "acme-v02", - "acme", - "acme2", - "acme11", - "acme-draft11", - "acme-draft-11", - "draft", - "11", - "ssl", - "tls", - "https", "Let's Encrypt", - "letsencrypt", - "letsencrypt-v2", - "letsencrypt-v02", - "letsencryptv2", - "letsencryptv02", - "letsencrypt2", - "greenlock", - "greenlock2" + "ACME", + "v02", + "v2", + "draft-11", + "draft-12", + "free ssl", + "tls", + "automated https", + "letsencrypt" ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", -- 2.38.5 From 65ff6267165cacbf404dc53dcfe2581317a4b2c4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 18:06:48 +0000 Subject: [PATCH 101/252] Delete '.jshintrc' --- .jshintrc | 1 - 1 file changed, 1 deletion(-) delete mode 120000 .jshintrc diff --git a/.jshintrc b/.jshintrc deleted file mode 120000 index d5794d7..0000000 --- a/.jshintrc +++ /dev/null @@ -1 +0,0 @@ -/Users/aj/dotfiles/dummy/.jshintrc \ No newline at end of file -- 2.38.5 From 8e345c09aea02b255db4d3971ab9e22d93775ec7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 18:06:48 +0000 Subject: [PATCH 102/252] Delete '.jshintrc' --- .jshintrc | 1 - 1 file changed, 1 deletion(-) delete mode 120000 .jshintrc diff --git a/.jshintrc b/.jshintrc deleted file mode 120000 index d5794d7..0000000 --- a/.jshintrc +++ /dev/null @@ -1 +0,0 @@ -/Users/aj/dotfiles/dummy/.jshintrc \ No newline at end of file -- 2.38.5 From 77761132103d37995c328f1ab6ad697e96f9509b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 18:07:53 +0000 Subject: [PATCH 103/252] Add '.jshintrc' --- .jshintrc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..0bcd788 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,18 @@ +{ "node": true +, "browser": true +, "jquery": true +, "globals": { "angular": true, "Promise": true } + +, "indent": 2 +, "onevar": true +, "laxcomma": true +, "laxbreak": true +, "curly": true +, "nonbsp": true + +, "eqeqeq": true +, "immed": true +, "undef": true +, "unused": true +, "latedef": true +} \ No newline at end of file -- 2.38.5 From aa42853639f980b606a0a0f69de91086564eee2c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 18:07:53 +0000 Subject: [PATCH 104/252] Add '.jshintrc' --- .jshintrc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..0bcd788 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,18 @@ +{ "node": true +, "browser": true +, "jquery": true +, "globals": { "angular": true, "Promise": true } + +, "indent": 2 +, "onevar": true +, "laxcomma": true +, "laxbreak": true +, "curly": true +, "nonbsp": true + +, "eqeqeq": true +, "immed": true +, "undef": true +, "unused": true +, "latedef": true +} \ No newline at end of file -- 2.38.5 From b99c6e4b4e71d8864ea1be0a6341f8b330b2767e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 22:35:38 -0600 Subject: [PATCH 105/252] add comment on privkey and other useful pem data --- node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node.js b/node.js index 34c5b5c..fe6ef67 100644 --- a/node.js +++ b/node.js @@ -629,6 +629,7 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } // https://github.com/certbot/certbot/issues/5721 var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ var certs = { expires: order.expires , identifiers: order.identifiers -- 2.38.5 From 0f20783f12a20515b8c24be9a03cb1f6224ffad8 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 11 Jul 2018 22:35:38 -0600 Subject: [PATCH 106/252] add comment on privkey and other useful pem data --- node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node.js b/node.js index 34c5b5c..fe6ef67 100644 --- a/node.js +++ b/node.js @@ -629,6 +629,7 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } // https://github.com/certbot/certbot/issues/5721 var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ var certs = { expires: order.expires , identifiers: order.identifiers -- 2.38.5 From 098e05f3ef16ceaa86831a2a05183d31b85d01d2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 12 Jul 2018 01:50:15 -0600 Subject: [PATCH 107/252] v1.2.0: Fix #8 Production API changed to be in-spec --- node.js | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index fe6ef67..9441876 100644 --- a/node.js +++ b/node.js @@ -176,6 +176,10 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { var account = resp.body; + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } + me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; // the account id url @@ -186,18 +190,14 @@ ACME._registerAccount = function (me, options) { /* { - id: 5925245, - key: - { kty: 'RSA', - n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ', - e: 'AQAB' }, - contact: [], - initialIp: '198.199.82.211', - createdAt: '2018-04-16T00:41:00.720584972Z', + contact: ["mailto:jon@example.com"], + orders: "https://some-url", status: 'valid' } */ if (!account) { account = { _emptyResponse: true, key: {} }; } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { account.key = {}; } account.key.kid = me._kid; return account; }).then(resolve, reject); diff --git a/package.json b/package.json index e4d5a75..0742de4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.2", + "version": "1.2.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 2051fb0e4b1b3a576deb1ea91b7ff9d836764391 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 12 Jul 2018 01:50:15 -0600 Subject: [PATCH 108/252] v1.2.0: Fix #8 Production API changed to be in-spec --- node.js | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index fe6ef67..9441876 100644 --- a/node.js +++ b/node.js @@ -176,6 +176,10 @@ ACME._registerAccount = function (me, options) { }).then(function (resp) { var account = resp.body; + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } + me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; // the account id url @@ -186,18 +190,14 @@ ACME._registerAccount = function (me, options) { /* { - id: 5925245, - key: - { kty: 'RSA', - n: 'tBr7m1hVaUNQjUeakznGidnrYyegVUQrsQjNrcipljI9Vxvxd0baHc3vvRZWFyFO5BlS7UDl-KHQdbdqb-MQzfP6T2sNXsOHARQ41pCGY5BYzIPRJF0nD48-CY717is-7BKISv8rf9yx5iSjvK1wZ3Ke3YIpxzK2fWRqccVxXQ92VYioxOfGObACgEUSvdoEttWV2B0Uv4Sdi6zZbk5eo2zALvyGb1P4fKVfQycGLXC41AyhHOAuTqzNCyIkiWEkbfh2lZNcYClP2epS0pHRFXYyjJN6-c8InfM3PISo4k6Qew65HZ-oqUow0tTIgNwuen9q5O6Hc73GvU-2npGJVQ', - e: 'AQAB' }, - contact: [], - initialIp: '198.199.82.211', - createdAt: '2018-04-16T00:41:00.720584972Z', + contact: ["mailto:jon@example.com"], + orders: "https://some-url", status: 'valid' } */ if (!account) { account = { _emptyResponse: true, key: {} }; } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { account.key = {}; } account.key.kid = me._kid; return account; }).then(resolve, reject); diff --git a/package.json b/package.json index e4d5a75..0742de4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.1.2", + "version": "1.2.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From ca15b8faf0a11b15d9973a9786c652c117d2547d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 16 Aug 2018 18:32:14 -0600 Subject: [PATCH 109/252] v1.2.1: made magic numbers (for status polling) configurable, updated deps --- README.md | 9 +++++++++ node.js | 26 +++++++++++++++----------- package.json | 6 +++--- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fd13d68..f37bea6 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,17 @@ var ACME = require('acme-v2').ACME.create({ , userAgent: 'My custom UA String' , getUserAgentString: function (deps) { return 'My custom UA String'; } + // don't try to validate challenges locally , skipChallengeTest: false + // ask if the certificate can be issued up to 10 times before failing +, retryPoll: 8 + // ask if the certificate has been validated up to 6 times before cancelling +, retryPending: 4 + // Wait 1000ms between retries +, retryInterval: 1000 + // Wait 10,000ms after deauthorizing a challenge before retrying +, deauthWait: 10 * 1000 }); diff --git a/node.js b/node.js index 9441876..b0b1c8e 100644 --- a/node.js +++ b/node.js @@ -255,6 +255,10 @@ ACME._wait = function wait(ms) { }; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { + var RETRY_INTERVAL = me.retryInterval || 1000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; var count = 0; var thumbprint = me.RSA.thumbprint(options.accountKeypair); @@ -314,12 +318,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) { console.debug('deactivate challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } - return ACME._wait(10 * 1000); + return ACME._wait(DEAUTH_INTERVAL); }); } function pollStatus() { - if (count >= 5) { + if (count >= MAX_POLL) { return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); } @@ -330,16 +334,16 @@ ACME._postChallenge = function (me, options, identifier, ch) { if ('processing' === resp.body.status) { if (me.debug) { console.debug('poll: again'); } - return ACME._wait(1 * 1000).then(pollStatus); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); } // This state should never occur if ('pending' === resp.body.status) { - if (count >= 4) { - return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL).then(deactivate).then(testChallenge); } if (me.debug) { console.debug('poll: again'); } - return ACME._wait(1 * 1000).then(testChallenge); + return ACME._wait(RETRY_INTERVAL).then(testChallenge); } if ('valid' === resp.body.status) { @@ -361,13 +365,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) invalid challenge state:"); + console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); } else { - console.error("[acme-v2] (E_STATE_UKN) unkown challenge state:"); + console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); } - return Promise.reject(new Error("[acme-v2] challenge state error")); + return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); }); } @@ -393,7 +397,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) { console.debug('respond to challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } - return ACME._wait(1 * 1000).then(pollStatus); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); }); } @@ -405,7 +409,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; - return ACME._wait(1 * 1000).then(function () { + return ACME._wait(RETRY_INTERVAL).then(function () { if (!me.skipChallengeTest) { return ACME.challengeTests[ch.type](me, auth); } diff --git a/package.json b/package.json index 0742de4..3ccac1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.2.0", + "version": "1.2.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -26,7 +26,7 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "dependencies": { - "@coolaj86/urequest": "^1.1.1", - "rsa-compat": "^1.3.0" + "@coolaj86/urequest": "^1.3.6", + "rsa-compat": "^1.5.1" } } -- 2.38.5 From 3ae21fe62a463062f5457c9b670e3062f4b8745a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 16 Aug 2018 18:32:14 -0600 Subject: [PATCH 110/252] v1.2.1: made magic numbers (for status polling) configurable, updated deps --- README.md | 9 +++++++++ node.js | 26 +++++++++++++++----------- package.json | 6 +++--- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fd13d68..f37bea6 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,17 @@ var ACME = require('acme-v2').ACME.create({ , userAgent: 'My custom UA String' , getUserAgentString: function (deps) { return 'My custom UA String'; } + // don't try to validate challenges locally , skipChallengeTest: false + // ask if the certificate can be issued up to 10 times before failing +, retryPoll: 8 + // ask if the certificate has been validated up to 6 times before cancelling +, retryPending: 4 + // Wait 1000ms between retries +, retryInterval: 1000 + // Wait 10,000ms after deauthorizing a challenge before retrying +, deauthWait: 10 * 1000 }); diff --git a/node.js b/node.js index 9441876..b0b1c8e 100644 --- a/node.js +++ b/node.js @@ -255,6 +255,10 @@ ACME._wait = function wait(ms) { }; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { + var RETRY_INTERVAL = me.retryInterval || 1000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; var count = 0; var thumbprint = me.RSA.thumbprint(options.accountKeypair); @@ -314,12 +318,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) { console.debug('deactivate challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } - return ACME._wait(10 * 1000); + return ACME._wait(DEAUTH_INTERVAL); }); } function pollStatus() { - if (count >= 5) { + if (count >= MAX_POLL) { return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); } @@ -330,16 +334,16 @@ ACME._postChallenge = function (me, options, identifier, ch) { if ('processing' === resp.body.status) { if (me.debug) { console.debug('poll: again'); } - return ACME._wait(1 * 1000).then(pollStatus); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); } // This state should never occur if ('pending' === resp.body.status) { - if (count >= 4) { - return ACME._wait(1 * 1000).then(deactivate).then(testChallenge); + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL).then(deactivate).then(testChallenge); } if (me.debug) { console.debug('poll: again'); } - return ACME._wait(1 * 1000).then(testChallenge); + return ACME._wait(RETRY_INTERVAL).then(testChallenge); } if ('valid' === resp.body.status) { @@ -361,13 +365,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) invalid challenge state:"); + console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); } else { - console.error("[acme-v2] (E_STATE_UKN) unkown challenge state:"); + console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); } - return Promise.reject(new Error("[acme-v2] challenge state error")); + return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); }); } @@ -393,7 +397,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { me._nonce = resp.toJSON().headers['replay-nonce']; if (me.debug) { console.debug('respond to challenge: resp.body:'); } if (me.debug) { console.debug(resp.body); } - return ACME._wait(1 * 1000).then(pollStatus); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); }); } @@ -405,7 +409,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; - return ACME._wait(1 * 1000).then(function () { + return ACME._wait(RETRY_INTERVAL).then(function () { if (!me.skipChallengeTest) { return ACME.challengeTests[ch.type](me, auth); } diff --git a/package.json b/package.json index 0742de4..3ccac1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.2.0", + "version": "1.2.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -26,7 +26,7 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "dependencies": { - "@coolaj86/urequest": "^1.1.1", - "rsa-compat": "^1.3.0" + "@coolaj86/urequest": "^1.3.6", + "rsa-compat": "^1.5.1" } } -- 2.38.5 From 87e49578f79d0c7fff048bfe38d4c129f4831a95 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 4 Nov 2018 13:42:55 -0700 Subject: [PATCH 111/252] note need to limit download size --- node.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node.js b/node.js index b0b1c8e..59db271 100644 --- a/node.js +++ b/node.js @@ -27,13 +27,16 @@ ACME.challengeTests = { return me._request({ url: url }).then(function (resp) { var err; + // TODO limit the number of bytes that are allowed to be downloaded if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { return true; } err = new Error( "Error: Failed HTTP-01 Dry Run.\n" - + "curl '" + url + "' does not return '" + auth.keyAuthorization + "'\n" + + "curl '" + url + "'\n" + + "Expected: '" + auth.keyAuthorization + "'\n" + + "Got: '" + resp.body + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); err.code = 'E_FAIL_DRY_CHALLENGE'; -- 2.38.5 From 2406c870e6ba362f75544200caf0e52165713847 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 4 Nov 2018 13:42:55 -0700 Subject: [PATCH 112/252] note need to limit download size --- node.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node.js b/node.js index b0b1c8e..59db271 100644 --- a/node.js +++ b/node.js @@ -27,13 +27,16 @@ ACME.challengeTests = { return me._request({ url: url }).then(function (resp) { var err; + // TODO limit the number of bytes that are allowed to be downloaded if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { return true; } err = new Error( "Error: Failed HTTP-01 Dry Run.\n" - + "curl '" + url + "' does not return '" + auth.keyAuthorization + "'\n" + + "curl '" + url + "'\n" + + "Expected: '" + auth.keyAuthorization + "'\n" + + "Got: '" + resp.body + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); err.code = 'E_FAIL_DRY_CHALLENGE'; -- 2.38.5 From 642a25935e3a954e3d54205b1097c94446726ba2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 16 Dec 2018 21:19:20 -0700 Subject: [PATCH 113/252] v1.3.1: reduce deps, update rsa-compat, fix rando JWK bug --- LICENSE | 394 ++++++++++++++++++++++++++++++++++--- README.md | 37 +++- compat.js | 4 + examples/cli.js | 4 + examples/genkeypair.js | 4 + examples/http-server.js | 4 + examples/https-server.js | 4 + node.js | 9 +- package-lock.json | 50 +++++ package.json | 6 +- tests/cb.js | 4 + tests/compat.js | 4 + tests/fullchain-formats.js | 4 + tests/promise.js | 4 + 14 files changed, 486 insertions(+), 46 deletions(-) create mode 100644 package-lock.json diff --git a/LICENSE b/LICENSE index 693448a..3435503 100644 --- a/LICENSE +++ b/LICENSE @@ -1,41 +1,375 @@ Copyright 2018 AJ ONeal -This is open source software; you can redistribute it and/or modify it under the -terms of either: +Mozilla Public License Version 2.0 +================================== - a) the "MIT License" - b) the "Apache-2.0 License" +1. Definitions +-------------- -MIT License +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. +1.3. "Contribution" + means Covered Software of a particular Contributor. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. -Apache-2.0 License Summary +1.5. "Incompatible With Secondary Licenses" + means - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or - http://www.apache.org/licenses/LICENSE-2.0 + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index f37bea6..912f251 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,33 @@ -| Sponsored by [ppl](https://ppl.family) | **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) | -acme-v2.js -========== +| A [Root](https://therootcompany.com) Project -A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. +# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) + +A lightweight, **Low Dependency*** framework for building +Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). +* although `node-forge` and `ursa` are included as `optionalDependencies` +for backwards compatibility with older versions of node, there are no other +dependencies except those that I wrote for this (and related) projects. + ## Looking for Quick 'n' Easy™? -If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). +If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). * [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) * [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) ## How to build ACME clients -As this is intended to build ACME clients, there is not a simple 2-line example. +As this is intended to build ACME clients, there is not a simple 2-line example +(and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)). I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: @@ -185,9 +191,17 @@ ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` -Changelog ---------- +# Changelog +* v1.3 + * Use node RSA keygen by default + * No non-optional external deps! +* v1.2 + * fix some API out-of-specness + * doc some magic numbers (status) + * updated deps +* v1.1.0 + * reduce dependencies (use lightweight @coolaj86/request instead of request) * v1.0.5 - cleanup logging * v1.0.4 - v6- compat use `promisify` from node's util or bluebird * v1.0.3 - documentation cleanup @@ -219,3 +233,10 @@ Changelog * Mar 15, 2018 - generate account keypair * Mar 15, 2018 - get nonce * Mar 15, 2018 - get directory + +# Legal + +[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/compat.js b/compat.js index 674c028..73f2961 100644 --- a/compat.js +++ b/compat.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* global Promise */ diff --git a/examples/cli.js b/examples/cli.js index f26354a..21ff620 100644 --- a/examples/cli.js +++ b/examples/cli.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var RSA = require('rsa-compat').RSA; diff --git a/examples/genkeypair.js b/examples/genkeypair.js index 2c7e3c6..4a5bad6 100644 --- a/examples/genkeypair.js +++ b/examples/genkeypair.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ var RSA = require('rsa-compat').RSA; var fs = require('fs'); diff --git a/examples/http-server.js b/examples/http-server.js index 4195455..f923472 100644 --- a/examples/http-server.js +++ b/examples/http-server.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var http = require('http'); diff --git a/examples/https-server.js b/examples/https-server.js index 5dd2c2c..5520041 100644 --- a/examples/https-server.js +++ b/examples/https-server.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var https = require('https'); diff --git a/node.js b/node.js index 59db271..63c64c9 100644 --- a/node.js +++ b/node.js @@ -1,8 +1,7 @@ -/*! - * acme-v2.js - * Copyright(c) 2018 AJ ONeal https://ppl.family - * Apache-2.0 OR MIT (and hence also MPL 2.0) - */ +// Copyright 2018 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/. */ 'use strict'; /* globals Promise */ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..396afda --- /dev/null +++ b/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "acme-v2", + "version": "1.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@coolaj86/urequest": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", + "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" + }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", + "optional": true + }, + "nan": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", + "integrity": "sha512-zT5nC0JhbljmyEf+Z456nvm7iO7XgRV2hYxoBtPpnyp+0Q4aCoP6uWNn76v/I6k2kCYNLWqWbwBWQcjsNI/bjw==", + "optional": true + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", + "optional": true + }, + "rsa-compat": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-1.9.2.tgz", + "integrity": "sha512-XY4I/74W+QENMd99zVsyHQcxYxWTXd0EihVXsI4oeb1bz7DYxEKasQrjyzYPnR1tZT7fTPu5HP/vTKfs9lzdGA==", + "requires": { + "node-forge": "^0.7.6", + "ursa-optional": "^0.9.10" + } + }, + "ursa-optional": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/ursa-optional/-/ursa-optional-0.9.10.tgz", + "integrity": "sha512-RvEbhnxlggX4MXon7KQulTFiJQtLJZpSb9ZSa7ZTkOW0AzqiVTaLjI4vxaSzJBDH9dwZ3ltZadFiBaZslp6haA==", + "optional": true, + "requires": { + "bindings": "^1.3.0", + "nan": "^2.11.1" + } + } + } +} diff --git a/package.json b/package.json index 3ccac1b..6f84c2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.2.1", + "version": "1.3.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -24,9 +24,9 @@ "letsencrypt" ], "author": "AJ ONeal (https://coolaj86.com/)", - "license": "(MIT OR Apache-2.0)", + "license": "MPL-2.0", "dependencies": { "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^1.5.1" + "rsa-compat": "^1.9.2" } } diff --git a/tests/cb.js b/tests/cb.js index 550c285..c5676ff 100644 --- a/tests/cb.js +++ b/tests/cb.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { diff --git a/tests/compat.js b/tests/compat.js index e643e2e..a993fd5 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { diff --git a/tests/fullchain-formats.js b/tests/fullchain-formats.js index cb4f67c..f1ac284 100644 --- a/tests/fullchain-formats.js +++ b/tests/fullchain-formats.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* diff --git a/tests/promise.js b/tests/promise.js index 7c1c17f..1d7a266 100644 --- a/tests/promise.js +++ b/tests/promise.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* global Promise */ -- 2.38.5 From 85a38f7b54a6f34c6cc126615f57918b14127d04 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 16 Dec 2018 21:19:20 -0700 Subject: [PATCH 114/252] v1.3.1: reduce deps, update rsa-compat, fix rando JWK bug --- LICENSE | 394 ++++++++++++++++++++++++++++++++++--- README.md | 37 +++- compat.js | 4 + examples/cli.js | 4 + examples/genkeypair.js | 4 + examples/http-server.js | 4 + examples/https-server.js | 4 + node.js | 9 +- package-lock.json | 50 +++++ package.json | 6 +- tests/cb.js | 4 + tests/compat.js | 4 + tests/fullchain-formats.js | 4 + tests/promise.js | 4 + 14 files changed, 486 insertions(+), 46 deletions(-) create mode 100644 package-lock.json diff --git a/LICENSE b/LICENSE index 693448a..3435503 100644 --- a/LICENSE +++ b/LICENSE @@ -1,41 +1,375 @@ Copyright 2018 AJ ONeal -This is open source software; you can redistribute it and/or modify it under the -terms of either: +Mozilla Public License Version 2.0 +================================== - a) the "MIT License" - b) the "Apache-2.0 License" +1. Definitions +-------------- -MIT License +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. +1.3. "Contribution" + means Covered Software of a particular Contributor. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. -Apache-2.0 License Summary +1.5. "Incompatible With Secondary Licenses" + means - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or - http://www.apache.org/licenses/LICENSE-2.0 + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index f37bea6..912f251 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,33 @@ -| Sponsored by [ppl](https://ppl.family) | **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) | -acme-v2.js -========== +| A [Root](https://therootcompany.com) Project -A framework for building Let's Encrypt v2 (ACME draft 11) clients, successor to `le-acme-core.js`. +# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) + +A lightweight, **Low Dependency*** framework for building +Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). +* although `node-forge` and `ursa` are included as `optionalDependencies` +for backwards compatibility with older versions of node, there are no other +dependencies except those that I wrote for this (and related) projects. + ## Looking for Quick 'n' Easy™? -If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). +If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). * [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) * [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) ## How to build ACME clients -As this is intended to build ACME clients, there is not a simple 2-line example. +As this is intended to build ACME clients, there is not a simple 2-line example +(and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)). I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: @@ -185,9 +191,17 @@ ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' ACME.challengePrefixes['dns-01'] // '_acme-challenge' ``` -Changelog ---------- +# Changelog +* v1.3 + * Use node RSA keygen by default + * No non-optional external deps! +* v1.2 + * fix some API out-of-specness + * doc some magic numbers (status) + * updated deps +* v1.1.0 + * reduce dependencies (use lightweight @coolaj86/request instead of request) * v1.0.5 - cleanup logging * v1.0.4 - v6- compat use `promisify` from node's util or bluebird * v1.0.3 - documentation cleanup @@ -219,3 +233,10 @@ Changelog * Mar 15, 2018 - generate account keypair * Mar 15, 2018 - get nonce * Mar 15, 2018 - get directory + +# Legal + +[acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/compat.js b/compat.js index 674c028..73f2961 100644 --- a/compat.js +++ b/compat.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* global Promise */ diff --git a/examples/cli.js b/examples/cli.js index f26354a..21ff620 100644 --- a/examples/cli.js +++ b/examples/cli.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var RSA = require('rsa-compat').RSA; diff --git a/examples/genkeypair.js b/examples/genkeypair.js index 2c7e3c6..4a5bad6 100644 --- a/examples/genkeypair.js +++ b/examples/genkeypair.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ var RSA = require('rsa-compat').RSA; var fs = require('fs'); diff --git a/examples/http-server.js b/examples/http-server.js index 4195455..f923472 100644 --- a/examples/http-server.js +++ b/examples/http-server.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var http = require('http'); diff --git a/examples/https-server.js b/examples/https-server.js index 5dd2c2c..5520041 100644 --- a/examples/https-server.js +++ b/examples/https-server.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; var https = require('https'); diff --git a/node.js b/node.js index 59db271..63c64c9 100644 --- a/node.js +++ b/node.js @@ -1,8 +1,7 @@ -/*! - * acme-v2.js - * Copyright(c) 2018 AJ ONeal https://ppl.family - * Apache-2.0 OR MIT (and hence also MPL 2.0) - */ +// Copyright 2018 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/. */ 'use strict'; /* globals Promise */ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..396afda --- /dev/null +++ b/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "acme-v2", + "version": "1.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@coolaj86/urequest": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", + "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" + }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", + "optional": true + }, + "nan": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", + "integrity": "sha512-zT5nC0JhbljmyEf+Z456nvm7iO7XgRV2hYxoBtPpnyp+0Q4aCoP6uWNn76v/I6k2kCYNLWqWbwBWQcjsNI/bjw==", + "optional": true + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", + "optional": true + }, + "rsa-compat": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-1.9.2.tgz", + "integrity": "sha512-XY4I/74W+QENMd99zVsyHQcxYxWTXd0EihVXsI4oeb1bz7DYxEKasQrjyzYPnR1tZT7fTPu5HP/vTKfs9lzdGA==", + "requires": { + "node-forge": "^0.7.6", + "ursa-optional": "^0.9.10" + } + }, + "ursa-optional": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/ursa-optional/-/ursa-optional-0.9.10.tgz", + "integrity": "sha512-RvEbhnxlggX4MXon7KQulTFiJQtLJZpSb9ZSa7ZTkOW0AzqiVTaLjI4vxaSzJBDH9dwZ3ltZadFiBaZslp6haA==", + "optional": true, + "requires": { + "bindings": "^1.3.0", + "nan": "^2.11.1" + } + } + } +} diff --git a/package.json b/package.json index 3ccac1b..6f84c2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.2.1", + "version": "1.3.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -24,9 +24,9 @@ "letsencrypt" ], "author": "AJ ONeal (https://coolaj86.com/)", - "license": "(MIT OR Apache-2.0)", + "license": "MPL-2.0", "dependencies": { "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^1.5.1" + "rsa-compat": "^1.9.2" } } diff --git a/tests/cb.js b/tests/cb.js index 550c285..c5676ff 100644 --- a/tests/cb.js +++ b/tests/cb.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { diff --git a/tests/compat.js b/tests/compat.js index e643e2e..a993fd5 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { diff --git a/tests/fullchain-formats.js b/tests/fullchain-formats.js index cb4f67c..f1ac284 100644 --- a/tests/fullchain-formats.js +++ b/tests/fullchain-formats.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* diff --git a/tests/promise.js b/tests/promise.js index 7c1c17f..1d7a266 100644 --- a/tests/promise.js +++ b/tests/promise.js @@ -1,3 +1,7 @@ +// Copyright 2018 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/. */ 'use strict'; /* global Promise */ -- 2.38.5 From 83137766bcbadb0dcc92146f23e9a846d7e33e0f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 22 Dec 2018 05:27:19 -0700 Subject: [PATCH 115/252] v1.5.0: perform full test challenge first --- README.md | 2 + node.js | 469 +++++++++++++++++++++++++++------------------------ package.json | 2 +- 3 files changed, 248 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 912f251..a242145 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ ACME.challengePrefixes['dns-01'] // '_acme-challenge' # Changelog +* v1.5 + * perform full test challenge first (even before nonce) * v1.3 * Use node RSA keygen by default * No non-optional external deps! diff --git a/node.js b/node.js index 63c64c9..b3c5b50 100644 --- a/node.js +++ b/node.js @@ -16,6 +16,9 @@ ACME.splitPemChain = function splitPemChain(str) { }); }; + +// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} +// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' @@ -255,6 +258,37 @@ ACME._wait = function wait(ms) { setTimeout(resolve, (ms || 1100)); }); }; + +ACME._testChallenges = function (me, options) { + if (me.skipChallengeTest) { + return Promise.resolve(); + } + + return Promise.all(options.domains.map(function (identifierValue) { + // TODO we really only need one to pass, not all to pass + return Promise.all(options.challengeTypes.map(function (chType) { + var chToken = require('crypto').randomBytes(16).toString('hex'); + var thumbprint = me.RSA.thumbprint(options.accountKeypair); + var keyAuthorization = chToken + '.' + thumbprint; + var auth = { + identifier: { type: "dns", value: identifierValue } + , hostname: identifierValue + , type: chType + , token: chToken + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + }; + + return ACME._setChallenge(me, options, auth).then(function () { + return ACME.challengeTests[chType](me, auth); + }); + })); + })); +}; + // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { var RETRY_INTERVAL = me.retryInterval || 1000; @@ -279,172 +313,157 @@ ACME._postChallenge = function (me, options, identifier, ch) { ) }; - return new Promise(function (resolve, reject) { - /* - POST /acme/authz/1234 HTTP/1.1 - Host: example.com - Content-Type: application/jose+json + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "xWCM9lGbIyCgue8di6ueWQ", - "url": "https://example.com/acme/authz/1234" - }), - "payload": base64url({ - "status": "deactivated" - }), - "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" - } - */ - function deactivate() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , Buffer.from(JSON.stringify({ "status": "deactivated" })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , Buffer.from(JSON.stringify({ "status": "deactivated" })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('deactivate challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(DEAUTH_INTERVAL); - }); + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('deactivate challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); } - function pollStatus() { - if (count >= MAX_POLL) { - return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + count += 1; + + if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + if ('processing' === resp.body.status) { + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); } - count += 1; - - if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - - if ('processing' === resp.body.status) { - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); } + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); + } - // This state should never occur - if ('pending' === resp.body.status) { - if (count >= MAX_PEND) { - return ACME._wait(RETRY_INTERVAL).then(deactivate).then(testChallenge); + if ('valid' === resp.body.status) { + 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 { + options.removeChallenge(identifier.value, ch.token, function () {}); } - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(testChallenge); - } + } catch(e) {} + return resp.body; + } - if ('valid' === resp.body.status) { - if (me.debug) { console.debug('poll: valid'); } + if (!resp.body.status) { + console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); + } + else { + console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); + } - 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 { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } + return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); + }); + } - if (!resp.body.status) { - console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); - } - else { - console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); - } + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , Buffer.from(JSON.stringify({ })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } - return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); - }); - } - - function respondToChallenge() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , Buffer.from(JSON.stringify({ })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } - - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('respond to challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - }); - } - - function testChallenge() { - // TODO put check dns / http checks here? - // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} - // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - - if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } - //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; - - return ACME._wait(RETRY_INTERVAL).then(function () { - if (!me.skipChallengeTest) { - return ACME.challengeTests[ch.type](me, auth); - } - }).then(respondToChallenge); - } + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('respond to challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + }); + } + return ACME._setChallenge(me, options, auth).then(respondToChallenge); +}; +ACME._setChallenge = function (me, options, auth) { + return new Promise(function (resolve, reject) { try { if (1 === options.setChallenge.length) { - options.setChallenge(auth).then(testChallenge).then(resolve, reject); + options.setChallenge(auth).then(resolve).catch(reject); } else if (2 === options.setChallenge.length) { options.setChallenge(auth, function (err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } + if(err) { reject(err); } else { resolve(); } }); } else { var challengeCb = function(err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } + if(err) { reject(err); } else { resolve(); } }; + // for backwards compat adding extra keys without changing params length Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); - options.setChallenge(identifier.value, ch.token, keyAuthorization, challengeCb); + options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); } } catch(e) { reject(e); } + }).then(function () { + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = me.setChallengeWait || 500; + if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); } + return ACME._wait(DELAY); }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { @@ -548,104 +567,106 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) { console.debug('[acme-v2] certificates.create'); } - return ACME._getNonce(me).then(function () { - var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; + return ACME._testChallenges(me, options).then(function () { + if (me.debug) { console.debug('[acme-v2] certificates.create'); } + return ACME._getNonce(me).then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; - var payload = JSON.stringify(body); - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , Buffer.from(payload) - ); + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , Buffer.from(payload) + ); - if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._directoryUrls.newOrder - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - var auths; - if (me.debug) { console.debug(location); } // the account id url - if (me.debug) { console.debug(resp.toJSON()); } - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; + if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + var auths; + if (me.debug) { console.debug(location); } // the account id url + if (me.debug) { console.debug(resp.toJSON()); } + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; - if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched:"); - console.error(resp.body); - return Promise.reject(new Error("authorizations were not fetched")); - } - if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } + if (!me._authorizations) { + console.error("[acme-v2.js] authorizations were not fetched:"); + console.error(resp.body); + return Promise.reject(new Error("authorizations were not fetched")); + } + if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } - //return resp.body; - auths = me._authorizations.slice(0); + //return resp.body; + auths = me._authorizations.slice(0); - function next() { - var authUrl = auths.shift(); - if (!authUrl) { return; } + function next() { + var authUrl = auths.shift(); + if (!authUrl) { return; } - return ACME._getChallenges(me, options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - })[0]; + return ACME._getChallenges(me, options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); } - })[0]; - if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); - } + return ACME._postChallenge(me, options, results.identifier, challenge); + }).then(function () { + return next(); + }); + } - return ACME._postChallenge(me, options, results.identifier, challenge); - }).then(function () { - return next(); - }); - } + return next().then(function () { + if (me.debug) { console.debug("[getCertificate] next.then"); } + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); - return next().then(function () { - if (me.debug) { console.debug("[getCertificate] next.then"); } - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); - - return ACME._finalizeOrder(me, options, validatedDomains); - }).then(function (order) { - if (me.debug) { console.debug('acme-v2: order was finalized'); } - return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } - // https://github.com/certbot/certbot/issues/5721 - var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); - // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ - var certs = { - expires: order.expires - , identifiers: order.identifiers - //, authorizations: order.authorizations - , cert: certsarr.shift() - //, privkey: privkeyPem - , chain: certsarr.join('\n') - }; - if (me.debug) { console.debug(certs); } - return certs; + return ACME._finalizeOrder(me, options, validatedDomains); + }).then(function (order) { + if (me.debug) { console.debug('acme-v2: order was finalized'); } + return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + var certs = { + expires: order.expires + , identifiers: order.identifiers + //, authorizations: order.authorizations + , cert: certsarr.shift() + //, privkey: privkeyPem + , chain: certsarr.join('\n') + }; + if (me.debug) { console.debug(certs); } + return certs; + }); }); }); }); diff --git a/package.json b/package.json index 6f84c2e..594de84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.3.1", + "version": "1.5.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From d802fb4957973127a665e0177053a582af76edad Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 22 Dec 2018 05:27:19 -0700 Subject: [PATCH 116/252] v1.5.0: perform full test challenge first --- README.md | 2 + node.js | 469 +++++++++++++++++++++++++++------------------------ package.json | 2 +- 3 files changed, 248 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 912f251..a242145 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ ACME.challengePrefixes['dns-01'] // '_acme-challenge' # Changelog +* v1.5 + * perform full test challenge first (even before nonce) * v1.3 * Use node RSA keygen by default * No non-optional external deps! diff --git a/node.js b/node.js index 63c64c9..b3c5b50 100644 --- a/node.js +++ b/node.js @@ -16,6 +16,9 @@ ACME.splitPemChain = function splitPemChain(str) { }); }; + +// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} +// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" ACME.challengePrefixes = { 'http-01': '/.well-known/acme-challenge' , 'dns-01': '_acme-challenge' @@ -255,6 +258,37 @@ ACME._wait = function wait(ms) { setTimeout(resolve, (ms || 1100)); }); }; + +ACME._testChallenges = function (me, options) { + if (me.skipChallengeTest) { + return Promise.resolve(); + } + + return Promise.all(options.domains.map(function (identifierValue) { + // TODO we really only need one to pass, not all to pass + return Promise.all(options.challengeTypes.map(function (chType) { + var chToken = require('crypto').randomBytes(16).toString('hex'); + var thumbprint = me.RSA.thumbprint(options.accountKeypair); + var keyAuthorization = chToken + '.' + thumbprint; + var auth = { + identifier: { type: "dns", value: identifierValue } + , hostname: identifierValue + , type: chType + , token: chToken + , thumbprint: thumbprint + , keyAuthorization: keyAuthorization + , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') + ) + }; + + return ACME._setChallenge(me, options, auth).then(function () { + return ACME.challengeTests[chType](me, auth); + }); + })); + })); +}; + // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { var RETRY_INTERVAL = me.retryInterval || 1000; @@ -279,172 +313,157 @@ ACME._postChallenge = function (me, options, identifier, ch) { ) }; - return new Promise(function (resolve, reject) { - /* - POST /acme/authz/1234 HTTP/1.1 - Host: example.com - Content-Type: application/jose+json + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json - { - "protected": base64url({ - "alg": "ES256", - "kid": "https://example.com/acme/acct/1", - "nonce": "xWCM9lGbIyCgue8di6ueWQ", - "url": "https://example.com/acme/authz/1234" - }), - "payload": base64url({ - "status": "deactivated" - }), - "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" - } - */ - function deactivate() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , Buffer.from(JSON.stringify({ "status": "deactivated" })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , Buffer.from(JSON.stringify({ "status": "deactivated" })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('deactivate challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(DEAUTH_INTERVAL); - }); + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('deactivate challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); } - function pollStatus() { - if (count >= MAX_POLL) { - return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + count += 1; + + if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } + return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + if ('processing' === resp.body.status) { + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); } - count += 1; - - if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { - - if ('processing' === resp.body.status) { - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); } + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); + } - // This state should never occur - if ('pending' === resp.body.status) { - if (count >= MAX_PEND) { - return ACME._wait(RETRY_INTERVAL).then(deactivate).then(testChallenge); + if ('valid' === resp.body.status) { + 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 { + options.removeChallenge(identifier.value, ch.token, function () {}); } - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(testChallenge); - } + } catch(e) {} + return resp.body; + } - if ('valid' === resp.body.status) { - if (me.debug) { console.debug('poll: valid'); } + if (!resp.body.status) { + console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); + } + else if ('invalid' === resp.body.status) { + console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); + } + else { + console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); + } - 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 { - options.removeChallenge(identifier.value, ch.token, function () {}); - } - } catch(e) {} - return resp.body; - } + return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); + }); + } - if (!resp.body.status) { - console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); - } - else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); - } - else { - console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); - } + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , Buffer.from(JSON.stringify({ })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: ch.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } - return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); - }); - } - - function respondToChallenge() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } - , Buffer.from(JSON.stringify({ })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: ch.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } - - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('respond to challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - }); - } - - function testChallenge() { - // TODO put check dns / http checks here? - // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} - // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" - - if (me.debug) {console.debug('\n[DEBUG] postChallenge\n'); } - //if (me.debug) console.debug('\n[DEBUG] stop to fix things\n'); return; - - return ACME._wait(RETRY_INTERVAL).then(function () { - if (!me.skipChallengeTest) { - return ACME.challengeTests[ch.type](me, auth); - } - }).then(respondToChallenge); - } + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('respond to challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + }); + } + return ACME._setChallenge(me, options, auth).then(respondToChallenge); +}; +ACME._setChallenge = function (me, options, auth) { + return new Promise(function (resolve, reject) { try { if (1 === options.setChallenge.length) { - options.setChallenge(auth).then(testChallenge).then(resolve, reject); + options.setChallenge(auth).then(resolve).catch(reject); } else if (2 === options.setChallenge.length) { options.setChallenge(auth, function (err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } + if(err) { reject(err); } else { resolve(); } }); } else { var challengeCb = function(err) { - if(err) { - reject(err); - } else { - testChallenge().then(resolve, reject); - } + if(err) { reject(err); } else { resolve(); } }; + // for backwards compat adding extra keys without changing params length Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); - options.setChallenge(identifier.value, ch.token, keyAuthorization, challengeCb); + options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); } } catch(e) { reject(e); } + }).then(function () { + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = me.setChallengeWait || 500; + if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); } + return ACME._wait(DELAY); }); }; ACME._finalizeOrder = function (me, options, validatedDomains) { @@ -548,104 +567,106 @@ ACME._getCertificate = function (me, options) { } } - if (me.debug) { console.debug('[acme-v2] certificates.create'); } - return ACME._getNonce(me).then(function () { - var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; + return ACME._testChallenges(me, options).then(function () { + if (me.debug) { console.debug('[acme-v2] certificates.create'); } + return ACME._getNonce(me).then(function () { + var body = { + identifiers: options.domains.map(function (hostname) { + return { type: "dns" , value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; - var payload = JSON.stringify(body); - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , Buffer.from(payload) - ); + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } + , Buffer.from(payload) + ); - if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._directoryUrls.newOrder - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - var auths; - if (me.debug) { console.debug(location); } // the account id url - if (me.debug) { console.debug(resp.toJSON()); } - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; + if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + var auths; + if (me.debug) { console.debug(location); } // the account id url + if (me.debug) { console.debug(resp.toJSON()); } + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; - if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched:"); - console.error(resp.body); - return Promise.reject(new Error("authorizations were not fetched")); - } - if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } + if (!me._authorizations) { + console.error("[acme-v2.js] authorizations were not fetched:"); + console.error(resp.body); + return Promise.reject(new Error("authorizations were not fetched")); + } + if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } - //return resp.body; - auths = me._authorizations.slice(0); + //return resp.body; + auths = me._authorizations.slice(0); - function next() { - var authUrl = auths.shift(); - if (!authUrl) { return; } + function next() { + var authUrl = auths.shift(); + if (!authUrl) { return; } - return ACME._getChallenges(me, options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - })[0]; + return ACME._getChallenges(me, options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + var chType = options.challengeTypes.filter(function (chType) { + return results.challenges.some(function (ch) { + return ch.type === chType; + }); + })[0]; - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; + var challenge = results.challenges.filter(function (ch) { + if (chType === ch.type) { + return ch; + } + })[0]; + + if (!challenge) { + return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); } - })[0]; - if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); - } + return ACME._postChallenge(me, options, results.identifier, challenge); + }).then(function () { + return next(); + }); + } - return ACME._postChallenge(me, options, results.identifier, challenge); - }).then(function () { - return next(); - }); - } + return next().then(function () { + if (me.debug) { console.debug("[getCertificate] next.then"); } + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); - return next().then(function () { - if (me.debug) { console.debug("[getCertificate] next.then"); } - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); - - return ACME._finalizeOrder(me, options, validatedDomains); - }).then(function (order) { - if (me.debug) { console.debug('acme-v2: order was finalized'); } - return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } - // https://github.com/certbot/certbot/issues/5721 - var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); - // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ - var certs = { - expires: order.expires - , identifiers: order.identifiers - //, authorizations: order.authorizations - , cert: certsarr.shift() - //, privkey: privkeyPem - , chain: certsarr.join('\n') - }; - if (me.debug) { console.debug(certs); } - return certs; + return ACME._finalizeOrder(me, options, validatedDomains); + }).then(function (order) { + if (me.debug) { console.debug('acme-v2: order was finalized'); } + return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + var certs = { + expires: order.expires + , identifiers: order.identifiers + //, authorizations: order.authorizations + , cert: certsarr.shift() + //, privkey: privkeyPem + , chain: certsarr.join('\n') + }; + if (me.debug) { console.debug(certs); } + return certs; + }); }); }); }); diff --git a/package.json b/package.json index 6f84c2e..594de84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.3.1", + "version": "1.5.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From b41c2e8db90cdf187b6a04416aa0c94dd2414b87 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 22 Dec 2018 15:09:16 -0700 Subject: [PATCH 117/252] v1.5.1: more detailed error messages --- node.js | 44 +++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/node.js b/node.js index b3c5b50..80b614e 100644 --- a/node.js +++ b/node.js @@ -35,7 +35,7 @@ ACME.challengeTests = { } err = new Error( - "Error: Failed HTTP-01 Dry Run.\n" + "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" + "curl '" + url + "'\n" + "Expected: '" + auth.keyAuthorization + "'\n" + "Got: '" + resp.body + "'\n" @@ -60,7 +60,7 @@ ACME.challengeTests = { } err = new Error( - "Error: Failed DNS-01 Dry Run.\n" + "Error: Failed DNS-01 Pre-Flight Dry Run.\n" + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); @@ -359,7 +359,9 @@ ACME._postChallenge = function (me, options, identifier, ch) { function pollStatus() { if (count >= MAX_POLL) { - return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + return Promise.reject(new Error( + "[acme-v2] stuck in bad pending/processing state for '" + identifier.value + "'" + )); } count += 1; @@ -395,17 +397,18 @@ ACME._postChallenge = function (me, options, identifier, ch) { return resp.body; } + var errmsg; if (!resp.body.status) { - console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); + errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + identifier.value + "':"; } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); + errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; } else { - console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); + errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; } - return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); + return Promise.reject(new Error(errmsg)); }); } @@ -511,7 +514,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { if ('pending' === resp.body.status) { return Promise.reject(new Error( "Did not finalize order: status 'pending'." - + " Best guess: You have not accepted at least one challenge for each domain." + "\n\n" + + " Best guess: You have not accepted at least one challenge for each domain:\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) )); } @@ -520,7 +525,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Did not finalize order: status 'invalid'." + " Best guess: One or more of the domain challenges could not be verified" - + " (or the order was canceled)." + "\n\n" + + " (or the order was canceled).\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) )); } @@ -529,7 +536,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Did not finalize order: status 'ready'." + " Hmmm... this state shouldn't be possible here. That was the last state." - + " This one should at least be 'processing'." + "\n\n" + + " This one should at least be 'processing'.\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) + "\n\n" + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" )); @@ -537,7 +546,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Didn't finalize order: Unhandled status '" + resp.body.status + "'." - + " This is not one of the known statuses...\n\n" + + " This is not one of the known statuses...\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) + "\n\n" + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" )); @@ -605,9 +616,10 @@ ACME._getCertificate = function (me, options) { //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched:"); - console.error(resp.body); - return Promise.reject(new Error("authorizations were not fetched")); + return Promise.reject(new Error( + "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" + + JSON.stringify(resp.body) + )); } if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } @@ -633,7 +645,9 @@ ACME._getCertificate = function (me, options) { })[0]; if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + return Promise.reject(new Error( + "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." + )); } return ACME._postChallenge(me, options, results.identifier, challenge); diff --git a/package.json b/package.json index 594de84..87132b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.0", + "version": "1.5.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 382ef3c95cee90e0dbfe09b03fb44f5bfdaf8401 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 22 Dec 2018 15:09:16 -0700 Subject: [PATCH 118/252] v1.5.1: more detailed error messages --- node.js | 44 +++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/node.js b/node.js index b3c5b50..80b614e 100644 --- a/node.js +++ b/node.js @@ -35,7 +35,7 @@ ACME.challengeTests = { } err = new Error( - "Error: Failed HTTP-01 Dry Run.\n" + "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" + "curl '" + url + "'\n" + "Expected: '" + auth.keyAuthorization + "'\n" + "Got: '" + resp.body + "'\n" @@ -60,7 +60,7 @@ ACME.challengeTests = { } err = new Error( - "Error: Failed DNS-01 Dry Run.\n" + "Error: Failed DNS-01 Pre-Flight Dry Run.\n" + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); @@ -359,7 +359,9 @@ ACME._postChallenge = function (me, options, identifier, ch) { function pollStatus() { if (count >= MAX_POLL) { - return Promise.reject(new Error("[acme-v2] stuck in bad pending/processing state")); + return Promise.reject(new Error( + "[acme-v2] stuck in bad pending/processing state for '" + identifier.value + "'" + )); } count += 1; @@ -395,17 +397,18 @@ ACME._postChallenge = function (me, options, identifier, ch) { return resp.body; } + var errmsg; if (!resp.body.status) { - console.error("[acme-v2] (E_STATE_EMPTY) empty challenge state:"); + errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + identifier.value + "':"; } else if ('invalid' === resp.body.status) { - console.error("[acme-v2] (E_STATE_INVALID) challenge state: '" + resp.body.status + "'"); + errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; } else { - console.error("[acme-v2] (E_STATE_UKN) challenge state: '" + resp.body.status + "'"); + errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; } - return Promise.reject(new Error("[acme-v2] [error] unacceptable challenge state '" + resp.body.status + "'")); + return Promise.reject(new Error(errmsg)); }); } @@ -511,7 +514,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { if ('pending' === resp.body.status) { return Promise.reject(new Error( "Did not finalize order: status 'pending'." - + " Best guess: You have not accepted at least one challenge for each domain." + "\n\n" + + " Best guess: You have not accepted at least one challenge for each domain:\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) )); } @@ -520,7 +525,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Did not finalize order: status 'invalid'." + " Best guess: One or more of the domain challenges could not be verified" - + " (or the order was canceled)." + "\n\n" + + " (or the order was canceled).\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) )); } @@ -529,7 +536,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Did not finalize order: status 'ready'." + " Hmmm... this state shouldn't be possible here. That was the last state." - + " This one should at least be 'processing'." + "\n\n" + + " This one should at least be 'processing'.\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) + "\n\n" + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" )); @@ -537,7 +546,9 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { return Promise.reject(new Error( "Didn't finalize order: Unhandled status '" + resp.body.status + "'." - + " This is not one of the known statuses...\n\n" + + " This is not one of the known statuses...\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + JSON.stringify(resp.body, null, 2) + "\n\n" + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" )); @@ -605,9 +616,10 @@ ACME._getCertificate = function (me, options) { //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; if (!me._authorizations) { - console.error("[acme-v2.js] authorizations were not fetched:"); - console.error(resp.body); - return Promise.reject(new Error("authorizations were not fetched")); + return Promise.reject(new Error( + "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" + + JSON.stringify(resp.body) + )); } if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } @@ -633,7 +645,9 @@ ACME._getCertificate = function (me, options) { })[0]; if (!challenge) { - return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); + return Promise.reject(new Error( + "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." + )); } return ACME._postChallenge(me, options, results.identifier, challenge); diff --git a/package.json b/package.json index 594de84..87132b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.0", + "version": "1.5.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 1af2fb29589c1a3f41b27bec21111c676bffeaa0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 Feb 2019 22:30:55 -0700 Subject: [PATCH 119/252] v1.5.2: fix dns-01 wildcard bug --- node.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 80b614e..1bc21ee 100644 --- a/node.js +++ b/node.js @@ -46,7 +46,8 @@ ACME.challengeTests = { }); } , 'dns-01': function (me, auth) { - var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname; + // remove leading *. on wildcard domains + var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname.replace(/^\*\./, ''); return me._dig({ type: 'TXT' , name: hostname diff --git a/package.json b/package.json index 87132b2..6d3cfe2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.1", + "version": "1.5.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 8175a084958e9ed421c6058208a0a44a707986f7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 Feb 2019 22:30:55 -0700 Subject: [PATCH 120/252] v1.5.2: fix dns-01 wildcard bug --- node.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 80b614e..1bc21ee 100644 --- a/node.js +++ b/node.js @@ -46,7 +46,8 @@ ACME.challengeTests = { }); } , 'dns-01': function (me, auth) { - var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname; + // remove leading *. on wildcard domains + var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname.replace(/^\*\./, ''); return me._dig({ type: 'TXT' , name: hostname diff --git a/package.json b/package.json index 87132b2..6d3cfe2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.1", + "version": "1.5.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From f0f9feb519f82c9c766bb467762078220688f187 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 Feb 2019 23:04:24 -0700 Subject: [PATCH 121/252] #11 skip challenge when valid --- node.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node.js b/node.js index 1bc21ee..244abf8 100644 --- a/node.js +++ b/node.js @@ -637,6 +637,13 @@ ACME._getCertificate = function (me, options) { return results.challenges.some(function (ch) { return ch.type === chType; }); + }).sort(function (aType, bType) { + var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; + var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; + + if ('valid' === a.status) { return 1; } + if ('valid' === b.status) { return -1; } + return 0; })[0]; var challenge = results.challenges.filter(function (ch) { @@ -651,6 +658,10 @@ ACME._getCertificate = function (me, options) { )); } + if ("valid" === challenge.status) { + return; + } + return ACME._postChallenge(me, options, results.identifier, challenge); }).then(function () { return next(); -- 2.38.5 From 6d346552763bc32f9c978e0915f3202f84b3f384 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 Feb 2019 23:04:24 -0700 Subject: [PATCH 122/252] #11 skip challenge when valid --- node.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node.js b/node.js index 1bc21ee..244abf8 100644 --- a/node.js +++ b/node.js @@ -637,6 +637,13 @@ ACME._getCertificate = function (me, options) { return results.challenges.some(function (ch) { return ch.type === chType; }); + }).sort(function (aType, bType) { + var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; + var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; + + if ('valid' === a.status) { return 1; } + if ('valid' === b.status) { return -1; } + return 0; })[0]; var challenge = results.challenges.filter(function (ch) { @@ -651,6 +658,10 @@ ACME._getCertificate = function (me, options) { )); } + if ("valid" === challenge.status) { + return; + } + return ACME._postChallenge(me, options, results.identifier, challenge); }).then(function () { return next(); -- 2.38.5 From 621e04ffe6bf3cc15b8468488d455e7b3baa7f0c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 14 Mar 2019 12:15:08 -0600 Subject: [PATCH 123/252] v1.5.3: merge fix for #11 --- node.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/node.js b/node.js index 244abf8..caaf61d 100644 --- a/node.js +++ b/node.js @@ -148,6 +148,7 @@ ACME._registerAccount = function (me, options) { , contact: contact }; if (options.externalAccount) { + // TODO is this really done by HMAC or is it arbitrary? body.externalAccountBinding = me.RSA.signJws( options.externalAccount.secret , undefined diff --git a/package.json b/package.json index 6d3cfe2..804bed8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.2", + "version": "1.5.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From ddeaeb17d51b9dc81a5c5222bf8b399fa5dae7bb Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 14 Mar 2019 12:15:08 -0600 Subject: [PATCH 124/252] v1.5.3: merge fix for #11 --- node.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/node.js b/node.js index 244abf8..caaf61d 100644 --- a/node.js +++ b/node.js @@ -148,6 +148,7 @@ ACME._registerAccount = function (me, options) { , contact: contact }; if (options.externalAccount) { + // TODO is this really done by HMAC or is it arbitrary? body.externalAccountBinding = me.RSA.signJws( options.externalAccount.secret , undefined diff --git a/package.json b/package.json index 6d3cfe2..804bed8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.2", + "version": "1.5.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 21e2afdd7e5e9a9f729488d96122c9170ba3f880 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 31 Mar 2019 02:55:26 -0600 Subject: [PATCH 125/252] v1.6.0: switch to latest rsa-compat --- package-lock.json | 52 +++++++++++++++++++---------------------------- package.json | 4 ++-- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 396afda..114b766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.3.0", + "version": "1.5.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9,41 +9,31 @@ "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" }, - "bindings": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", - "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", - "optional": true + "eckles": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", + "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" }, - "nan": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", - "integrity": "sha512-zT5nC0JhbljmyEf+Z456nvm7iO7XgRV2hYxoBtPpnyp+0Q4aCoP6uWNn76v/I6k2kCYNLWqWbwBWQcjsNI/bjw==", - "optional": true - }, - "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", - "optional": true - }, - "rsa-compat": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-1.9.2.tgz", - "integrity": "sha512-XY4I/74W+QENMd99zVsyHQcxYxWTXd0EihVXsI4oeb1bz7DYxEKasQrjyzYPnR1tZT7fTPu5HP/vTKfs9lzdGA==", + "keypairs": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", + "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", "requires": { - "node-forge": "^0.7.6", - "ursa-optional": "^0.9.10" + "eckles": "^1.4.1", + "rasha": "^1.2.4" } }, - "ursa-optional": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/ursa-optional/-/ursa-optional-0.9.10.tgz", - "integrity": "sha512-RvEbhnxlggX4MXon7KQulTFiJQtLJZpSb9ZSa7ZTkOW0AzqiVTaLjI4vxaSzJBDH9dwZ3ltZadFiBaZslp6haA==", - "optional": true, + "rasha": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", + "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" + }, + "rsa-compat": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", + "integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", "requires": { - "bindings": "^1.3.0", - "nan": "^2.11.1" + "keypairs": "^1.2.14" } } } diff --git a/package.json b/package.json index 804bed8..582b150 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.3", + "version": "1.6.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -27,6 +27,6 @@ "license": "MPL-2.0", "dependencies": { "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^1.9.2" + "rsa-compat": "^2.0.6" } } -- 2.38.5 From 401535a5ab52c95042b26ea6eb74bbc0edc3ff82 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 31 Mar 2019 02:55:26 -0600 Subject: [PATCH 126/252] v1.6.0: switch to latest rsa-compat --- package-lock.json | 52 +++++++++++++++++++---------------------------- package.json | 4 ++-- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 396afda..114b766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.3.0", + "version": "1.5.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9,41 +9,31 @@ "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" }, - "bindings": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", - "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==", - "optional": true + "eckles": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", + "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" }, - "nan": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", - "integrity": "sha512-zT5nC0JhbljmyEf+Z456nvm7iO7XgRV2hYxoBtPpnyp+0Q4aCoP6uWNn76v/I6k2kCYNLWqWbwBWQcjsNI/bjw==", - "optional": true - }, - "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==", - "optional": true - }, - "rsa-compat": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-1.9.2.tgz", - "integrity": "sha512-XY4I/74W+QENMd99zVsyHQcxYxWTXd0EihVXsI4oeb1bz7DYxEKasQrjyzYPnR1tZT7fTPu5HP/vTKfs9lzdGA==", + "keypairs": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", + "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", "requires": { - "node-forge": "^0.7.6", - "ursa-optional": "^0.9.10" + "eckles": "^1.4.1", + "rasha": "^1.2.4" } }, - "ursa-optional": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/ursa-optional/-/ursa-optional-0.9.10.tgz", - "integrity": "sha512-RvEbhnxlggX4MXon7KQulTFiJQtLJZpSb9ZSa7ZTkOW0AzqiVTaLjI4vxaSzJBDH9dwZ3ltZadFiBaZslp6haA==", - "optional": true, + "rasha": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", + "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" + }, + "rsa-compat": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", + "integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", "requires": { - "bindings": "^1.3.0", - "nan": "^2.11.1" + "keypairs": "^1.2.14" } } } diff --git a/package.json b/package.json index 804bed8..582b150 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.5.3", + "version": "1.6.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", @@ -27,6 +27,6 @@ "license": "MPL-2.0", "dependencies": { "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^1.9.2" + "rsa-compat": "^2.0.6" } } -- 2.38.5 From b1182457cd571259aba349efa9e61c64afee0035 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 16:04:15 -0600 Subject: [PATCH 127/252] v1.7.0: better error checking and challenge type handling --- node.js | 159 ++++++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/node.js b/node.js index caaf61d..e490b8d 100644 --- a/node.js +++ b/node.js @@ -261,6 +261,36 @@ ACME._wait = function wait(ms) { }); }; +ACME._testChallengeOptions = function () { + var chToken = require('crypto').randomBytes(16).toString('hex'); + return [ + { + "type": "http-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/0", + "token": "test-" + chToken + "-0" + } + , { + "type": "dns-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/1", + "token": "test-" + chToken + "-1", + "_wildcard": true + } + , { + "type": "tls-sni-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/2", + "token": "test-" + chToken + "-2" + } + , { + "type": "tls-alpn-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/3", + "token": "test-" + chToken + "-3" + } + ]; +}; ACME._testChallenges = function (me, options) { if (me.skipChallengeTest) { return Promise.resolve(); @@ -268,28 +298,58 @@ ACME._testChallenges = function (me, options) { return Promise.all(options.domains.map(function (identifierValue) { // TODO we really only need one to pass, not all to pass - return Promise.all(options.challengeTypes.map(function (chType) { - var chToken = require('crypto').randomBytes(16).toString('hex'); + var results = ACME._testChallengeOptions(); + if (identifierValue.inludes("*")) { + results = results.filter(function (ch) { return ch._wildcard; }); + } + var challenge = ACME._chooseChallenge(options, results); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = options.challengeTypes.join(', ') || 'none'; + var suitable = results.map(function (r) { return r.type; }).join(', ') || 'none'; + return Promise.reject(new Error( + "None of the challenge types that you've enabled ( " + enabled + " )" + + " are suitable for validating the domain you've selected (" + identifierValue + ")." + + " You must enable one of ( " + suitable + " )." + )); + } + return Promise.resolve().then(function () { var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = chToken + '.' + thumbprint; + var keyAuthorization = challenge.token + '.' + thumbprint; var auth = { identifier: { type: "dns", value: identifierValue } , hostname: identifierValue - , type: chType - , token: chToken + , type: challenge.type + , token: challenge.token , thumbprint: thumbprint , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + , dnsAuthorization: ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') ) }; return ACME._setChallenge(me, options, auth).then(function () { - return ACME.challengeTests[chType](me, auth); + return ACME.challengeTests[challenge.type](me, auth); }); - })); + }); })); }; +ACME._chooseChallenge = function(options, results) { + // For each of the challenge types that we support + var challenge; + options.challengesTypes.some(function (chType) { + // And for each of the challenge types that are allowed + return results.challenges.some(function (ch) { + // Check to see if there are any matches + if (ch.type === chType) { + challenge = ch; + return true; + } + }); + }); + + return challenge; +}; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { @@ -310,7 +370,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { , token: ch.token , thumbprint: thumbprint , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + , dnsAuthorization: ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') ) }; @@ -337,7 +397,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: ch.url, kid: me._kid } , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; @@ -562,24 +622,50 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } - if (!options.challengeTypes) { - if (!options.challengeType) { - return Promise.reject(new Error("challenge type must be specified")); - } - options.challengeTypes = [ options.challengeType ]; + // Lot's of error checking to inform the user of mistakes + if (!(options.challengeTypes||[]).length) { + options.challengeTypes = Object.keys(options.challenges||{}); } + if (!options.challengeTypes.length) { + options.challengeTypes = [ options.challengeType ].filter(Boolean); + } + if (options.challengeType) { + options.challengeTypes.sort(function (a, b) { + if (a === options.challengeType) { return -1; } + if (b === options.challengeType) { return 1; } + return 0; + }); + if (options.challengeType !== options.challengeTypes[0]) { + return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," + + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); + } + } + // TODO check that all challengeTypes are represented in challenges + if (!options.challengeTypes.length) { + return Promise.reject(new Error("options.challengesTypes (string array) must be specified" + + " (and in order of preferential priority).")); + } + 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 domain (or options.subject must specified).")); + } + if (!options.subject) { options.subject = options.domains[0]; } + // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { - if (options.accountKid) { - me._kid = options.accountKid; + if (options.accountKid || options.account.kid) { + me._kid = options.accountKid || options.account.kid; } else { //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. return ACME._registerAccount(me, options).then(function () { + // start back from the top return ACME._getCertificate(me, options); }); } } + // Do a little dry-run / self-test return ACME._testChallenges(me, options).then(function () { if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { @@ -592,11 +678,14 @@ ACME._getCertificate = function (me, options) { }; var payload = JSON.stringify(body); + // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? + me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); + me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , Buffer.from(payload) + , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } + , Buffer.from(payload, 'utf8') ); if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } @@ -634,35 +723,20 @@ ACME._getCertificate = function (me, options) { return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - }).sort(function (aType, bType) { - var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; - var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; - if ('valid' === a.status) { return 1; } - if ('valid' === b.status) { return -1; } - return 0; - })[0]; - - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; - } - })[0]; + // If it's already valid, we're golden it regardless + if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { + return; + } + var challenge = ACME._chooseChallenge(options, results); if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail return Promise.reject(new Error( "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." )); } - if ("valid" === challenge.status) { - return; - } - return ACME._postChallenge(me, options, results.identifier, challenge); }).then(function () { return next(); @@ -705,6 +779,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; + //me.Keypairs = me.Keypairs || require('keypairs'); me.request = me.request || require('@coolaj86/urequest'); me._dig = function (query) { // TODO use digd.js @@ -767,3 +842,7 @@ ACME.create = function create(me) { }; return me; }; + +ACME._toWebsafeBase64 = function (b64) { + return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); +}; diff --git a/package.json b/package.json index 582b150..ffdc6f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.6.0", + "version": "1.7.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From b1d566d54ece8d41355f8adb509733d602baa65f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 16:04:15 -0600 Subject: [PATCH 128/252] v1.7.0: better error checking and challenge type handling --- node.js | 159 ++++++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/node.js b/node.js index caaf61d..e490b8d 100644 --- a/node.js +++ b/node.js @@ -261,6 +261,36 @@ ACME._wait = function wait(ms) { }); }; +ACME._testChallengeOptions = function () { + var chToken = require('crypto').randomBytes(16).toString('hex'); + return [ + { + "type": "http-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/0", + "token": "test-" + chToken + "-0" + } + , { + "type": "dns-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/1", + "token": "test-" + chToken + "-1", + "_wildcard": true + } + , { + "type": "tls-sni-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/2", + "token": "test-" + chToken + "-2" + } + , { + "type": "tls-alpn-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/3", + "token": "test-" + chToken + "-3" + } + ]; +}; ACME._testChallenges = function (me, options) { if (me.skipChallengeTest) { return Promise.resolve(); @@ -268,28 +298,58 @@ ACME._testChallenges = function (me, options) { return Promise.all(options.domains.map(function (identifierValue) { // TODO we really only need one to pass, not all to pass - return Promise.all(options.challengeTypes.map(function (chType) { - var chToken = require('crypto').randomBytes(16).toString('hex'); + var results = ACME._testChallengeOptions(); + if (identifierValue.inludes("*")) { + results = results.filter(function (ch) { return ch._wildcard; }); + } + var challenge = ACME._chooseChallenge(options, results); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = options.challengeTypes.join(', ') || 'none'; + var suitable = results.map(function (r) { return r.type; }).join(', ') || 'none'; + return Promise.reject(new Error( + "None of the challenge types that you've enabled ( " + enabled + " )" + + " are suitable for validating the domain you've selected (" + identifierValue + ")." + + " You must enable one of ( " + suitable + " )." + )); + } + return Promise.resolve().then(function () { var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = chToken + '.' + thumbprint; + var keyAuthorization = challenge.token + '.' + thumbprint; var auth = { identifier: { type: "dns", value: identifierValue } , hostname: identifierValue - , type: chType - , token: chToken + , type: challenge.type + , token: challenge.token , thumbprint: thumbprint , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + , dnsAuthorization: ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') ) }; return ACME._setChallenge(me, options, auth).then(function () { - return ACME.challengeTests[chType](me, auth); + return ACME.challengeTests[challenge.type](me, auth); }); - })); + }); })); }; +ACME._chooseChallenge = function(options, results) { + // For each of the challenge types that we support + var challenge; + options.challengesTypes.some(function (chType) { + // And for each of the challenge types that are allowed + return results.challenges.some(function (ch) { + // Check to see if there are any matches + if (ch.type === chType) { + challenge = ch; + return true; + } + }); + }); + + return challenge; +}; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 ACME._postChallenge = function (me, options, identifier, ch) { @@ -310,7 +370,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { , token: ch.token , thumbprint: thumbprint , keyAuthorization: keyAuthorization - , dnsAuthorization: me.RSA.utils.toWebsafeBase64( + , dnsAuthorization: ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') ) }; @@ -337,7 +397,7 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: ch.url, kid: me._kid } , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; @@ -562,24 +622,50 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } - if (!options.challengeTypes) { - if (!options.challengeType) { - return Promise.reject(new Error("challenge type must be specified")); - } - options.challengeTypes = [ options.challengeType ]; + // Lot's of error checking to inform the user of mistakes + if (!(options.challengeTypes||[]).length) { + options.challengeTypes = Object.keys(options.challenges||{}); } + if (!options.challengeTypes.length) { + options.challengeTypes = [ options.challengeType ].filter(Boolean); + } + if (options.challengeType) { + options.challengeTypes.sort(function (a, b) { + if (a === options.challengeType) { return -1; } + if (b === options.challengeType) { return 1; } + return 0; + }); + if (options.challengeType !== options.challengeTypes[0]) { + return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," + + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); + } + } + // TODO check that all challengeTypes are represented in challenges + if (!options.challengeTypes.length) { + return Promise.reject(new Error("options.challengesTypes (string array) must be specified" + + " (and in order of preferential priority).")); + } + 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 domain (or options.subject must specified).")); + } + if (!options.subject) { options.subject = options.domains[0]; } + // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { - if (options.accountKid) { - me._kid = options.accountKid; + if (options.accountKid || options.account.kid) { + me._kid = options.accountKid || options.account.kid; } else { //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. return ACME._registerAccount(me, options).then(function () { + // start back from the top return ACME._getCertificate(me, options); }); } } + // Do a little dry-run / self-test return ACME._testChallenges(me, options).then(function () { if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { @@ -592,11 +678,14 @@ ACME._getCertificate = function (me, options) { }; var payload = JSON.stringify(body); + // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? + me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); + me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } - , Buffer.from(payload) + , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } + , Buffer.from(payload, 'utf8') ); if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } @@ -634,35 +723,20 @@ ACME._getCertificate = function (me, options) { return ACME._getChallenges(me, options, authUrl).then(function (results) { // var domain = options.domains[i]; // results.identifier.value - var chType = options.challengeTypes.filter(function (chType) { - return results.challenges.some(function (ch) { - return ch.type === chType; - }); - }).sort(function (aType, bType) { - var a = results.challenges.filter(function (ch) { return ch.type === aType; })[0]; - var b = results.challenges.filter(function (ch) { return ch.type === bType; })[0]; - if ('valid' === a.status) { return 1; } - if ('valid' === b.status) { return -1; } - return 0; - })[0]; - - var challenge = results.challenges.filter(function (ch) { - if (chType === ch.type) { - return ch; - } - })[0]; + // If it's already valid, we're golden it regardless + if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { + return; + } + var challenge = ACME._chooseChallenge(options, results); if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail return Promise.reject(new Error( "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." )); } - if ("valid" === challenge.status) { - return; - } - return ACME._postChallenge(me, options, results.identifier, challenge); }).then(function () { return next(); @@ -705,6 +779,7 @@ ACME.create = function create(me) { // me.debug = true; me.challengePrefixes = ACME.challengePrefixes; me.RSA = me.RSA || require('rsa-compat').RSA; + //me.Keypairs = me.Keypairs || require('keypairs'); me.request = me.request || require('@coolaj86/urequest'); me._dig = function (query) { // TODO use digd.js @@ -767,3 +842,7 @@ ACME.create = function create(me) { }; return me; }; + +ACME._toWebsafeBase64 = function (b64) { + return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); +}; diff --git a/package.json b/package.json index 582b150..ffdc6f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.6.0", + "version": "1.7.0", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From e5e7377712f62d36660a27cf0cf96fa8d45d45e4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 19:13:58 -0600 Subject: [PATCH 129/252] v1.7.1: don't self-poison dns cache, more consistency --- node.js | 196 +++++++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 129 insertions(+), 69 deletions(-) diff --git a/node.js b/node.js index e490b8d..e32021c 100644 --- a/node.js +++ b/node.js @@ -47,10 +47,9 @@ ACME.challengeTests = { } , 'dns-01': function (me, auth) { // remove leading *. on wildcard domains - var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname.replace(/^\*\./, ''); return me._dig({ type: 'TXT' - , name: hostname + , name: auth.dnsHost }).then(function (ans) { var err; @@ -62,7 +61,7 @@ ACME.challengeTests = { err = new Error( "Error: Failed DNS-01 Pre-Flight Dry Run.\n" - + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); err.code = 'E_FAIL_DRY_CHALLENGE'; @@ -164,7 +163,7 @@ ACME._registerAccount = function (me, options) { options.accountKeypair , undefined , { nonce: me._nonce - , alg: 'RS256' + , alg: (me._alg || 'RS256') , url: me._directoryUrls.newAccount , jwk: jwk } @@ -296,48 +295,58 @@ ACME._testChallenges = function (me, options) { 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 - var results = ACME._testChallengeOptions(); - if (identifierValue.inludes("*")) { - results = results.filter(function (ch) { return ch._wildcard; }); + var challenges = ACME._testChallengeOptions(); + if (identifierValue.includes("*")) { + challenges = challenges.filter(function (ch) { return ch._wildcard; }); } - var challenge = ACME._chooseChallenge(options, results); + + var challenge = ACME._chooseChallenge(options, { challenges: challenges }); if (!challenge) { // For example, wildcards require dns-01 and, if we don't have that, we have to bail var enabled = options.challengeTypes.join(', ') || 'none'; - var suitable = results.map(function (r) { return r.type; }).join(', ') || 'none'; + var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; return Promise.reject(new Error( "None of the challenge types that you've enabled ( " + enabled + " )" + " are suitable for validating the domain you've selected (" + identifierValue + ")." + " You must enable one of ( " + suitable + " )." )); } - return Promise.resolve().then(function () { - var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = challenge.token + '.' + thumbprint; - var auth = { - identifier: { type: "dns", value: identifierValue } - , hostname: identifierValue - , type: challenge.type - , token: challenge.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: ACME._toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - }; + if ('dns-01' === challenge.type) { + // nameservers take a second to propagate + CHECK_DELAY = 5 * 1000; + } + return Promise.resolve().then(function () { + var results = { + identifier: { + type: "dns" + , value: identifierValue.replace(/^\*\./, '') + , wildcard: identifierValue.includes('*.') || undefined + } + , challenges: [ challenge ] + , expires: new Date(Date.now() + (60 * 1000)).toISOString() + }; + var dryrun = true; + var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); return ACME._setChallenge(me, options, auth).then(function () { - return ACME.challengeTests[challenge.type](me, auth); + return auth; }); }); - })); + })).then(function (auths) { + return ACME._wait(CHECK_DELAY).then(function () { + return Promise.all(auths.map(function (auth) { + return ACME.challengeTests[auth.type](me, auth); + })); + }); + }); }; ACME._chooseChallenge = function(options, results) { // For each of the challenge types that we support var challenge; - options.challengesTypes.some(function (chType) { + options.challengeTypes.some(function (chType) { // And for each of the challenge types that are allowed return results.challenges.some(function (ch) { // Check to see if there are any matches @@ -350,30 +359,57 @@ ACME._chooseChallenge = function(options, results) { return challenge; }; +ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes['dns-01']; + if (dryrun) { + dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + Math.random().toString().slice(2,6)); + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function (key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function (key) { + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = request.identifier.value; + auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + auth.dnsAuthorization = ACME._toWebsafeBase64( + require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') + ); + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(request.identifier.value, request.wildcard); + + return auth; +}; + +ACME._untame = function (name, wild) { + if (wild) { name = '*.' + name.replace('*.', ''); } + return name; +}; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 -ACME._postChallenge = function (me, options, identifier, ch) { +ACME._postChallenge = function (me, options, auth) { var RETRY_INTERVAL = me.retryInterval || 1000; var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; var MAX_POLL = me.retryPoll || 8; var MAX_PEND = me.retryPending || 4; var count = 0; - var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = ch.token + '.' + thumbprint; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - // /.well-known/acme-challenge/:token - var auth = { - identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: ACME._toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - }; + var altname = ACME._untame(auth.identifier.value, auth.wildcard); /* POST /acme/authz/1234 HTTP/1.1 @@ -397,13 +433,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; return me._request({ method: 'POST' - , url: ch.url + , url: auth.url , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { @@ -422,14 +458,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { function pollStatus() { if (count >= MAX_POLL) { return Promise.reject(new Error( - "[acme-v2] stuck in bad pending/processing state for '" + identifier.value + "'" + "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" )); } count += 1; if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { if ('processing' === resp.body.status) { if (me.debug) { console.debug('poll: again'); } return ACME._wait(RETRY_INTERVAL).then(pollStatus); @@ -453,7 +489,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { } else if (2 === options.removeChallenge.length) { options.removeChallenge(auth, function (err) { return err; }); } else { - options.removeChallenge(identifier.value, ch.token, function () {}); + 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 () {}); } } catch(e) {} return resp.body; @@ -461,13 +502,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var errmsg; if (!resp.body.status) { - errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + identifier.value + "':"; + errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; } else if ('invalid' === resp.body.status) { - errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; + errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; } else { - errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; + errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; } return Promise.reject(new Error(errmsg)); @@ -478,13 +519,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } , Buffer.from(JSON.stringify({ })) ); me._nonce = null; return me._request({ method: 'POST' - , url: ch.url + , url: auth.url , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { @@ -519,6 +560,11 @@ ACME._setChallenge = function (me, options, auth) { Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); + if (!ACME._setChallengeWarn) { + console.warn("Please update to acme-v2 setChallenge(options) or setChallenge(options, cb)."); + console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); + ACME._setChallengeWarn = true; + } options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); } } catch(e) { @@ -541,7 +587,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } , Buffer.from(payload) ); @@ -642,14 +688,13 @@ ACME._getCertificate = function (me, options) { } // TODO check that all challengeTypes are represented in challenges if (!options.challengeTypes.length) { - return Promise.reject(new Error("options.challengesTypes (string array) must be specified" + return Promise.reject(new Error("options.challengeTypes (string array) must be specified" + " (and in order of preferential priority).")); } 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 domain (or options.subject must specified).")); } - if (!options.subject) { options.subject = options.domains[0]; } // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { @@ -670,8 +715,15 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; + // raw wildcard syntax MUST be used here + identifiers: options.domains.sort(function (a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { return 0; } + if (options.subject === a) { return -1; } + if (options.subject === b) { return 1; } + return 0; + }).map(function (hostname) { + return { type: "dns", value: hostname }; }) //, "notBefore": "2016-01-01T00:00:00Z" //, "notAfter": "2016-01-08T00:00:00Z" @@ -698,7 +750,8 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - var auths; + var setAuths; + var auths = []; if (me.debug) { console.debug(location); } // the account id url if (me.debug) { console.debug(resp.toJSON()); } me._authorizations = resp.body.authorizations; @@ -713,12 +766,10 @@ ACME._getCertificate = function (me, options) { )); } if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } + setAuths = me._authorizations.slice(0); - //return resp.body; - auths = me._authorizations.slice(0); - - function next() { - var authUrl = auths.shift(); + function setNext() { + var authUrl = setAuths.shift(); if (!authUrl) { return; } return ACME._getChallenges(me, options, authUrl).then(function (results) { @@ -726,7 +777,7 @@ ACME._getCertificate = function (me, options) { // If it's already valid, we're golden it regardless if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { - return; + return setNext(); } var challenge = ACME._chooseChallenge(options, results); @@ -737,13 +788,22 @@ ACME._getCertificate = function (me, options) { )); } - return ACME._postChallenge(me, options, results.identifier, challenge); - }).then(function () { - return next(); + var auth = ACME._challengeToAuth(me, options, results, challenge); + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); }); } - return next().then(function () { + function challengeNext() { + var auth = auths.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 () { if (me.debug) { console.debug("[getCertificate] next.then"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; diff --git a/package.json b/package.json index ffdc6f5..add69cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.0", + "version": "1.7.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 652112154879b4a4232d9b7fcbadd07c4dcc1c88 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 19:13:58 -0600 Subject: [PATCH 130/252] v1.7.1: don't self-poison dns cache, more consistency --- node.js | 196 +++++++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 129 insertions(+), 69 deletions(-) diff --git a/node.js b/node.js index e490b8d..e32021c 100644 --- a/node.js +++ b/node.js @@ -47,10 +47,9 @@ ACME.challengeTests = { } , 'dns-01': function (me, auth) { // remove leading *. on wildcard domains - var hostname = ACME.challengePrefixes['dns-01'] + '.' + auth.hostname.replace(/^\*\./, ''); return me._dig({ type: 'TXT' - , name: hostname + , name: auth.dnsHost }).then(function (ans) { var err; @@ -62,7 +61,7 @@ ACME.challengeTests = { err = new Error( "Error: Failed DNS-01 Pre-Flight Dry Run.\n" - + "dig TXT '" + hostname + "' does not return '" + auth.dnsAuthorization + "'\n" + + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" ); err.code = 'E_FAIL_DRY_CHALLENGE'; @@ -164,7 +163,7 @@ ACME._registerAccount = function (me, options) { options.accountKeypair , undefined , { nonce: me._nonce - , alg: 'RS256' + , alg: (me._alg || 'RS256') , url: me._directoryUrls.newAccount , jwk: jwk } @@ -296,48 +295,58 @@ ACME._testChallenges = function (me, options) { 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 - var results = ACME._testChallengeOptions(); - if (identifierValue.inludes("*")) { - results = results.filter(function (ch) { return ch._wildcard; }); + var challenges = ACME._testChallengeOptions(); + if (identifierValue.includes("*")) { + challenges = challenges.filter(function (ch) { return ch._wildcard; }); } - var challenge = ACME._chooseChallenge(options, results); + + var challenge = ACME._chooseChallenge(options, { challenges: challenges }); if (!challenge) { // For example, wildcards require dns-01 and, if we don't have that, we have to bail var enabled = options.challengeTypes.join(', ') || 'none'; - var suitable = results.map(function (r) { return r.type; }).join(', ') || 'none'; + var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; return Promise.reject(new Error( "None of the challenge types that you've enabled ( " + enabled + " )" + " are suitable for validating the domain you've selected (" + identifierValue + ")." + " You must enable one of ( " + suitable + " )." )); } - return Promise.resolve().then(function () { - var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = challenge.token + '.' + thumbprint; - var auth = { - identifier: { type: "dns", value: identifierValue } - , hostname: identifierValue - , type: challenge.type - , token: challenge.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: ACME._toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - }; + if ('dns-01' === challenge.type) { + // nameservers take a second to propagate + CHECK_DELAY = 5 * 1000; + } + return Promise.resolve().then(function () { + var results = { + identifier: { + type: "dns" + , value: identifierValue.replace(/^\*\./, '') + , wildcard: identifierValue.includes('*.') || undefined + } + , challenges: [ challenge ] + , expires: new Date(Date.now() + (60 * 1000)).toISOString() + }; + var dryrun = true; + var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); return ACME._setChallenge(me, options, auth).then(function () { - return ACME.challengeTests[challenge.type](me, auth); + return auth; }); }); - })); + })).then(function (auths) { + return ACME._wait(CHECK_DELAY).then(function () { + return Promise.all(auths.map(function (auth) { + return ACME.challengeTests[auth.type](me, auth); + })); + }); + }); }; ACME._chooseChallenge = function(options, results) { // For each of the challenge types that we support var challenge; - options.challengesTypes.some(function (chType) { + options.challengeTypes.some(function (chType) { // And for each of the challenge types that are allowed return results.challenges.some(function (ch) { // Check to see if there are any matches @@ -350,30 +359,57 @@ ACME._chooseChallenge = function(options, results) { return challenge; }; +ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes['dns-01']; + if (dryrun) { + dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + Math.random().toString().slice(2,6)); + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function (key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function (key) { + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = request.identifier.value; + auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + auth.dnsAuthorization = ACME._toWebsafeBase64( + require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') + ); + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(request.identifier.value, request.wildcard); + + return auth; +}; + +ACME._untame = function (name, wild) { + if (wild) { name = '*.' + name.replace('*.', ''); } + return name; +}; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 -ACME._postChallenge = function (me, options, identifier, ch) { +ACME._postChallenge = function (me, options, auth) { var RETRY_INTERVAL = me.retryInterval || 1000; var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; var MAX_POLL = me.retryPoll || 8; var MAX_PEND = me.retryPending || 4; var count = 0; - var thumbprint = me.RSA.thumbprint(options.accountKeypair); - var keyAuthorization = ch.token + '.' + thumbprint; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - // /.well-known/acme-challenge/:token - var auth = { - identifier: identifier - , hostname: identifier.value - , type: ch.type - , token: ch.token - , thumbprint: thumbprint - , keyAuthorization: keyAuthorization - , dnsAuthorization: ACME._toWebsafeBase64( - require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') - ) - }; + var altname = ACME._untame(auth.identifier.value, auth.wildcard); /* POST /acme/authz/1234 HTTP/1.1 @@ -397,13 +433,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } , Buffer.from(JSON.stringify({ "status": "deactivated" })) ); me._nonce = null; return me._request({ method: 'POST' - , url: ch.url + , url: auth.url , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { @@ -422,14 +458,14 @@ ACME._postChallenge = function (me, options, identifier, ch) { function pollStatus() { if (count >= MAX_POLL) { return Promise.reject(new Error( - "[acme-v2] stuck in bad pending/processing state for '" + identifier.value + "'" + "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" )); } count += 1; if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { + return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { if ('processing' === resp.body.status) { if (me.debug) { console.debug('poll: again'); } return ACME._wait(RETRY_INTERVAL).then(pollStatus); @@ -453,7 +489,12 @@ ACME._postChallenge = function (me, options, identifier, ch) { } else if (2 === options.removeChallenge.length) { options.removeChallenge(auth, function (err) { return err; }); } else { - options.removeChallenge(identifier.value, ch.token, function () {}); + 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 () {}); } } catch(e) {} return resp.body; @@ -461,13 +502,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var errmsg; if (!resp.body.status) { - errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + identifier.value + "':"; + errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; } else if ('invalid' === resp.body.status) { - errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; + errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; } else { - errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + identifier.value + "': '" + resp.body.status + "'"; + errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; } return Promise.reject(new Error(errmsg)); @@ -478,13 +519,13 @@ ACME._postChallenge = function (me, options, identifier, ch) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } , Buffer.from(JSON.stringify({ })) ); me._nonce = null; return me._request({ method: 'POST' - , url: ch.url + , url: auth.url , headers: { 'Content-Type': 'application/jose+json' } , json: jws }).then(function (resp) { @@ -519,6 +560,11 @@ ACME._setChallenge = function (me, options, auth) { Object.keys(auth).forEach(function (key) { challengeCb[key] = auth[key]; }); + if (!ACME._setChallengeWarn) { + console.warn("Please update to acme-v2 setChallenge(options) or setChallenge(options, cb)."); + console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); + ACME._setChallengeWarn = true; + } options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); } } catch(e) { @@ -541,7 +587,7 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { var jws = me.RSA.signJws( options.accountKeypair , undefined - , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } , Buffer.from(payload) ); @@ -642,14 +688,13 @@ ACME._getCertificate = function (me, options) { } // TODO check that all challengeTypes are represented in challenges if (!options.challengeTypes.length) { - return Promise.reject(new Error("options.challengesTypes (string array) must be specified" + return Promise.reject(new Error("options.challengeTypes (string array) must be specified" + " (and in order of preferential priority).")); } 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 domain (or options.subject must specified).")); } - if (!options.subject) { options.subject = options.domains[0]; } // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { @@ -670,8 +715,15 @@ ACME._getCertificate = function (me, options) { if (me.debug) { console.debug('[acme-v2] certificates.create'); } return ACME._getNonce(me).then(function () { var body = { - identifiers: options.domains.map(function (hostname) { - return { type: "dns" , value: hostname }; + // raw wildcard syntax MUST be used here + identifiers: options.domains.sort(function (a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { return 0; } + if (options.subject === a) { return -1; } + if (options.subject === b) { return 1; } + return 0; + }).map(function (hostname) { + return { type: "dns", value: hostname }; }) //, "notBefore": "2016-01-01T00:00:00Z" //, "notAfter": "2016-01-08T00:00:00Z" @@ -698,7 +750,8 @@ ACME._getCertificate = function (me, options) { }).then(function (resp) { me._nonce = resp.toJSON().headers['replay-nonce']; var location = resp.toJSON().headers.location; - var auths; + var setAuths; + var auths = []; if (me.debug) { console.debug(location); } // the account id url if (me.debug) { console.debug(resp.toJSON()); } me._authorizations = resp.body.authorizations; @@ -713,12 +766,10 @@ ACME._getCertificate = function (me, options) { )); } if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } + setAuths = me._authorizations.slice(0); - //return resp.body; - auths = me._authorizations.slice(0); - - function next() { - var authUrl = auths.shift(); + function setNext() { + var authUrl = setAuths.shift(); if (!authUrl) { return; } return ACME._getChallenges(me, options, authUrl).then(function (results) { @@ -726,7 +777,7 @@ ACME._getCertificate = function (me, options) { // If it's already valid, we're golden it regardless if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { - return; + return setNext(); } var challenge = ACME._chooseChallenge(options, results); @@ -737,13 +788,22 @@ ACME._getCertificate = function (me, options) { )); } - return ACME._postChallenge(me, options, results.identifier, challenge); - }).then(function () { - return next(); + var auth = ACME._challengeToAuth(me, options, results, challenge); + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); }); } - return next().then(function () { + function challengeNext() { + var auth = auths.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 () { if (me.debug) { console.debug("[getCertificate] next.then"); } var validatedDomains = body.identifiers.map(function (ident) { return ident.value; diff --git a/package.json b/package.json index ffdc6f5..add69cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.0", + "version": "1.7.1", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From a7ea231c2e45ae07b89b75183edafa72955a0a34 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 19:25:41 -0600 Subject: [PATCH 131/252] v1.7.2: don't set challenge twice --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index e32021c..8ecfa5d 100644 --- a/node.js +++ b/node.js @@ -541,7 +541,7 @@ ACME._postChallenge = function (me, options, auth) { }); } - return ACME._setChallenge(me, options, auth).then(respondToChallenge); + return respondToChallenge(); }; ACME._setChallenge = function (me, options, auth) { return new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index add69cf..cdc69d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.1", + "version": "1.7.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 1195956ce1fd5dba475078aced0e449a603c9d04 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 19:25:41 -0600 Subject: [PATCH 132/252] v1.7.2: don't set challenge twice --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index e32021c..8ecfa5d 100644 --- a/node.js +++ b/node.js @@ -541,7 +541,7 @@ ACME._postChallenge = function (me, options, auth) { }); } - return ACME._setChallenge(me, options, auth).then(respondToChallenge); + return respondToChallenge(); }; ACME._setChallenge = function (me, options, auth) { return new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index add69cf..cdc69d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.1", + "version": "1.7.2", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 6cd4bd15872ec3563eca0107b5d4f3526cf1c0fe Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 20:40:07 -0600 Subject: [PATCH 133/252] v1.7.3: don't wait so longer on dns test --- node.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node.js b/node.js index 8ecfa5d..67189a3 100644 --- a/node.js +++ b/node.js @@ -315,8 +315,8 @@ ACME._testChallenges = function (me, options) { )); } if ('dns-01' === challenge.type) { - // nameservers take a second to propagate - CHECK_DELAY = 5 * 1000; + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; } return Promise.resolve().then(function () { diff --git a/package.json b/package.json index cdc69d0..c5431f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.2", + "version": "1.7.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 6deb67d740ae6215d4be7676dd9c940edff419a4 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 20:40:07 -0600 Subject: [PATCH 134/252] v1.7.3: don't wait so longer on dns test --- node.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node.js b/node.js index 8ecfa5d..67189a3 100644 --- a/node.js +++ b/node.js @@ -315,8 +315,8 @@ ACME._testChallenges = function (me, options) { )); } if ('dns-01' === challenge.type) { - // nameservers take a second to propagate - CHECK_DELAY = 5 * 1000; + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; } return Promise.resolve().then(function () { diff --git a/package.json b/package.json index cdc69d0..c5431f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.2", + "version": "1.7.3", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From d41e7e565013c14817d6dc117f29736a402041ba Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 21:34:38 -0600 Subject: [PATCH 135/252] v2.7.3: make dry-run properly shows wildcards --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 67189a3..f46d9d9 100644 --- a/node.js +++ b/node.js @@ -324,10 +324,10 @@ ACME._testChallenges = function (me, options) { identifier: { type: "dns" , value: identifierValue.replace(/^\*\./, '') - , wildcard: identifierValue.includes('*.') || undefined } , challenges: [ challenge ] , expires: new Date(Date.now() + (60 * 1000)).toISOString() + , wildcard: identifierValue.includes('*.') || undefined }; var dryrun = true; var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); diff --git a/package.json b/package.json index c5431f2..ab73729 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.3", + "version": "1.7.4", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From ea97f537ef7200b613542f792550b2ac3aec3f03 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Apr 2019 21:34:38 -0600 Subject: [PATCH 136/252] v2.7.3: make dry-run properly shows wildcards --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 67189a3..f46d9d9 100644 --- a/node.js +++ b/node.js @@ -324,10 +324,10 @@ ACME._testChallenges = function (me, options) { identifier: { type: "dns" , value: identifierValue.replace(/^\*\./, '') - , wildcard: identifierValue.includes('*.') || undefined } , challenges: [ challenge ] , expires: new Date(Date.now() + (60 * 1000)).toISOString() + , wildcard: identifierValue.includes('*.') || undefined }; var dryrun = true; var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); diff --git a/package.json b/package.json index c5431f2..ab73729 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.3", + "version": "1.7.4", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From de9afbbab90b3a89ae1cbe4a205b181efe8e49f6 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 4 Apr 2019 14:08:41 -0600 Subject: [PATCH 137/252] v1.7.5: bugfix unchecked property thanks to #19 --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index f46d9d9..afca1ef 100644 --- a/node.js +++ b/node.js @@ -698,7 +698,7 @@ ACME._getCertificate = function (me, options) { // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { - if (options.accountKid || options.account.kid) { + if (options.accountKid || options.account && options.account.kid) { me._kid = options.accountKid || options.account.kid; } else { //return Promise.reject(new Error("must include KeyID")); diff --git a/package.json b/package.json index ab73729..84048d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.4", + "version": "1.7.5", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 3f4e5adeeffb3d2d24279c8bdb5305e2ee57507c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 4 Apr 2019 14:08:41 -0600 Subject: [PATCH 138/252] v1.7.5: bugfix unchecked property thanks to #19 --- node.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index f46d9d9..afca1ef 100644 --- a/node.js +++ b/node.js @@ -698,7 +698,7 @@ ACME._getCertificate = function (me, options) { // It's just fine if there's no account, we'll go get the key id we need via the public key if (!me._kid) { - if (options.accountKid || options.account.kid) { + if (options.accountKid || options.account && options.account.kid) { me._kid = options.accountKid || options.account.kid; } else { //return Promise.reject(new Error("must include KeyID")); diff --git a/package.json b/package.json index ab73729..84048d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.4", + "version": "1.7.5", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 48c6f842b4524c7715f4edd683355ea9e7f4fabd Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 14:54:02 -0600 Subject: [PATCH 139/252] v1.7.6: add http-01 url to challenge --- node.js | 16 ++++++++++++---- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index afca1ef..85154bf 100644 --- a/node.js +++ b/node.js @@ -378,20 +378,28 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { // { type, status, url, token } // (note the duplicate status overwrites the one above, but they should be the same) Object.keys(challenge).forEach(function (key) { - auth[key] = challenge[key]; + // don't confused devs with the id url + if ('url' === key) { + //auth.uri = challenge.url; + } else { + auth[key] = challenge[key]; + } }); // batteries-included helpers - auth.hostname = request.identifier.value; + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense + // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) + auth.url = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') ); - // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases - auth.altname = ACME._untame(request.identifier.value, request.wildcard); return auth; }; diff --git a/package-lock.json b/package-lock.json index 114b766..c763d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "acme-v2", - "version": "1.5.3", + "version": "1.7.6", "lockfileVersion": 1, "requires": true, "dependencies": { "@coolaj86/urequest": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", - "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", + "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" }, "eckles": { "version": "1.4.1", diff --git a/package.json b/package.json index 84048d8..98ef2a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.5", + "version": "1.7.6", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From a750d1b0b4b14fe9e68317c3631c707bd932d629 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 14:54:02 -0600 Subject: [PATCH 140/252] v1.7.6: add http-01 url to challenge --- node.js | 16 ++++++++++++---- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/node.js b/node.js index afca1ef..85154bf 100644 --- a/node.js +++ b/node.js @@ -378,20 +378,28 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { // { type, status, url, token } // (note the duplicate status overwrites the one above, but they should be the same) Object.keys(challenge).forEach(function (key) { - auth[key] = challenge[key]; + // don't confused devs with the id url + if ('url' === key) { + //auth.uri = challenge.url; + } else { + auth[key] = challenge[key]; + } }); // batteries-included helpers - auth.hostname = request.identifier.value; + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense + // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) + auth.url = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') ); - // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases - auth.altname = ACME._untame(request.identifier.value, request.wildcard); return auth; }; diff --git a/package-lock.json b/package-lock.json index 114b766..c763d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "acme-v2", - "version": "1.5.3", + "version": "1.7.6", "lockfileVersion": 1, "requires": true, "dependencies": { "@coolaj86/urequest": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", - "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", + "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" }, "eckles": { "version": "1.4.1", diff --git a/package.json b/package.json index 84048d8..98ef2a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.5", + "version": "1.7.6", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 0b8aec0f8cee78fb5bec6f7d57115d6bd305bd22 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 21:08:58 -0600 Subject: [PATCH 141/252] v1.7.7: revert v1.7.6 --- node.js | 8 ++------ package.json | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/node.js b/node.js index 85154bf..2dfc3d8 100644 --- a/node.js +++ b/node.js @@ -379,11 +379,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { // (note the duplicate status overwrites the one above, but they should be the same) Object.keys(challenge).forEach(function (key) { // don't confused devs with the id url - if ('url' === key) { - //auth.uri = challenge.url; - } else { - auth[key] = challenge[key]; - } + auth[key] = challenge[key]; }); // batteries-included helpers @@ -395,7 +391,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) - auth.url = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; + auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') diff --git a/package.json b/package.json index 98ef2a3..3da21c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.6", + "version": "1.7.7", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 54e9e9ec16f66a26a280feec363af32780e4ece3 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 21:08:58 -0600 Subject: [PATCH 142/252] v1.7.7: revert v1.7.6 --- node.js | 8 ++------ package.json | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/node.js b/node.js index 85154bf..2dfc3d8 100644 --- a/node.js +++ b/node.js @@ -379,11 +379,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { // (note the duplicate status overwrites the one above, but they should be the same) Object.keys(challenge).forEach(function (key) { // don't confused devs with the id url - if ('url' === key) { - //auth.uri = challenge.url; - } else { - auth[key] = challenge[key]; - } + auth[key] = challenge[key]; }); // batteries-included helpers @@ -395,7 +391,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) - auth.url = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; + auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') diff --git a/package.json b/package.json index 98ef2a3..3da21c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acme-v2", - "version": "1.7.6", + "version": "1.7.7", "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "main": "node.js", -- 2.38.5 From 466de61232b6314ca3066512469b06d0c8ce4872 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 21:16:02 -0600 Subject: [PATCH 143/252] update comment --- node.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node.js b/node.js index 2dfc3d8..8582269 100644 --- a/node.js +++ b/node.js @@ -389,8 +389,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; - // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense - // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) + // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( -- 2.38.5 From 17a1535dcc132d4a34b34ae7f7ed3cae1cf1022a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 7 Apr 2019 21:16:02 -0600 Subject: [PATCH 144/252] update comment --- node.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node.js b/node.js index 2dfc3d8..8582269 100644 --- a/node.js +++ b/node.js @@ -389,8 +389,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; - // conflicts with ACME challenge id url, if we ever decide to use it, but this just makes sense - // (as opposed to httpUrl or challengeUrl or uri, etc - I'd be happier to call the id url a uri) + // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); auth.dnsAuthorization = ACME._toWebsafeBase64( -- 2.38.5 From bfc4ab67951d23e393342416762113b077326779 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 18 Apr 2019 00:20:51 -0600 Subject: [PATCH 145/252] initial commit --- LICENSE | 375 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 9 ++ app.js | 59 ++++++++ index.html | 54 +++++++ lib/keypairs.js | 86 +++++++++++ 5 files changed, 583 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 index.html create mode 100644 lib/keypairs.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7007c8a --- /dev/null +++ b/LICENSE @@ -0,0 +1,375 @@ +Copyright 2017-present AJ ONeal + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..891922c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Bluecrypt™ Keypairs + +A port of [keypairs.js](https://git.coolaj86.com/coolaj86/keypairs.js) to the browser. + +* Keypairs + * Eckles (ECDSA) + * Rasha (RSA) + * X509 + * ASN1 diff --git a/app.js b/app.js new file mode 100644 index 0000000..968f38d --- /dev/null +++ b/app.js @@ -0,0 +1,59 @@ +(function () { +'use strict'; + +var Keypairs = window.Keypairs; + +function $(sel) { + return document.querySelector(sel); +} +function $$(sel) { + return Array.prototype.slice.call(document.querySelectorAll(sel)); +} + +function run() { + console.log('hello'); + + // Show different options for ECDSA vs RSA + $$('input[name="kty"]').forEach(function ($el) { + $el.addEventListener('change', function (ev) { + console.log(this); + console.log(ev); + if ("RSA" === ev.target.value) { + $('.js-rsa-opts').hidden = false; + $('.js-ec-opts').hidden = true; + } else { + $('.js-rsa-opts').hidden = true; + $('.js-ec-opts').hidden = false; + } + }); + }); + + // Generate a key on submit + $('form.js-keygen').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + $('.js-loading').hidden = false; + $('.js-jwk').hidden = true; + $$('input').map(function ($el) { $el.disabled = true; }); + $$('button').map(function ($el) { $el.disabled = true; }); + var opts = { + kty: $('input[name="kty"]:checked').value + , namedCurve: $('input[name="ec-crv"]:checked').value + , modulusLength: $('input[name="rsa-len"]:checked').value + }; + console.log(opts); + Keypairs.generate(opts).then(function (results) { + $('.js-jwk').innerText = JSON.stringify(results, null, 2); + // + $('.js-loading').hidden = true; + $('.js-jwk').hidden = false; + $$('input').map(function ($el) { $el.disabled = false; }); + $$('button').map(function ($el) { $el.disabled = false; }); + }); + }); + + $('.js-generate').hidden = false; +} + +window.addEventListener('load', run); +}()); diff --git a/index.html b/index.html new file mode 100644 index 0000000..909a44a --- /dev/null +++ b/index.html @@ -0,0 +1,54 @@ + + + BlueCrypt + + +

BlueCrypt for the Browser

+

BlueCrypt is universal crypto for the browser. It's lightweight, fast, and based on native webcrypto. + This means it's easy-to-use crypto in kilobytes, not megabytes.

+ +

Keypair Generation

+
+

Key Type:

+
+ + + + +
+
+

EC Options:

+ + + + + +
+ + + + + +
 
+ + + + + diff --git a/lib/keypairs.js b/lib/keypairs.js new file mode 100644 index 0000000..bf530b8 --- /dev/null +++ b/lib/keypairs.js @@ -0,0 +1,86 @@ +/*global Promise*/ +(function (exports) { +'use strict'; + +var Keypairs = exports.Keypairs = {}; + +Keypairs._stance = "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; +Keypairs.generate = function (opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = 'EC'; + } + + // ECDSA has only the P curves and an associated bitlength + if (/^EC/i.test(opts.kty)) { + wcOpts.name = 'ECDSA'; + if (!opts.namedCurve) { + opts.namedCurve = 'P-256'; + } + wcOpts.namedCurve = opts.namedCurve; // true for supported curves + if (/256/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-256'; + wcOpts.hash = { name: "SHA-256" }; + } else if (/384/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-384'; + wcOpts.hash = { name: "SHA-384" }; + } else { + return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " + + " Please choose either 'P-256' or 'P-384'. " + + Keypairs._stance)); + } + } else if (/^RSA$/i.test(opts.kty)) { + // Support PSS? I don't think it's used for Let's Encrypt + wcOpts.name = 'RSASSA-PKCS1-v1_5'; + if (!opts.modulusLength) { + opts.modulusLength = 2048; + } + wcOpts.modulusLength = opts.modulusLength; + if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { + // erring on the small side... for no good reason + wcOpts.hash = { name: "SHA-256" }; + } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { + wcOpts.hash = { name: "SHA-384" }; + } else if (wcOpts.modulusLength < 4097) { + wcOpts.hash = { name: "SHA-512" }; + } else { + // Public key thumbprints should be paired with a hash of similar length, + // so anything above SHA-512's keyspace would be left under-represented anyway. + return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" + + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" + + " divisible by 8 are allowed. " + Keypairs._stance)); + } + // TODO maybe allow this to be set to any of the standard values? + wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); + } else { + return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." + + Keypairs._universal + + " Please choose either 'EC' or 'RSA' keys.")); + } + + var extractable = true; + return window.crypto.subtle.generateKey( + wcOpts + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { + // TODO remove + console.log('private jwk:'); + console.log(JSON.stringify(privJwk, null, 2)); + return { + privateKey: privJwk + }; + }); + }); +}; + +}(window)); -- 2.38.5 From 692301e37d323bb37ba3284c3e069b2f30672757 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 18 Apr 2019 01:56:32 -0600 Subject: [PATCH 146/252] making headway --- app.js | 2 +- index.html | 2 + lib/acme.js | 699 +++++++++++++++++++++++++++++++++++++++++++ lib/ecdsa.js | 112 +++++++ lib/keypairs.js | 155 ++++++---- lib/keypairs.js.min2 | 86 ++++++ lib/rsa.js | 122 ++++++++ 7 files changed, 1111 insertions(+), 67 deletions(-) create mode 100644 lib/acme.js create mode 100644 lib/ecdsa.js create mode 100644 lib/keypairs.js.min2 create mode 100644 lib/rsa.js diff --git a/app.js b/app.js index 968f38d..d144211 100644 --- a/app.js +++ b/app.js @@ -41,7 +41,7 @@ function run() { , namedCurve: $('input[name="ec-crv"]:checked').value , modulusLength: $('input[name="rsa-len"]:checked').value }; - console.log(opts); + console.log('opts', opts); Keypairs.generate(opts).then(function (results) { $('.js-jwk').innerText = JSON.stringify(results, null, 2); // diff --git a/index.html b/index.html index 909a44a..575da3b 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,8 @@
 
+ + diff --git a/lib/acme.js b/lib/acme.js new file mode 100644 index 0000000..4fba0fe --- /dev/null +++ b/lib/acme.js @@ -0,0 +1,699 @@ +/*global CSR*/ +// CSR takes a while to load after the page load +(function (exports) { +'use strict'; + +var BACME = exports.ACME = {}; +var webFetch = exports.fetch; +var Keypairs = exports.Keypairs; +var Promise = exports.Promise; + +var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; +var directory; + +var nonceUrl; +var nonce; + +var accountKeypair; +var accountJwk; + +var accountUrl; + +BACME.challengePrefixes = { + 'http-01': '/.well-known/acme-challenge' +, 'dns-01': '_acme-challenge' +}; + +BACME._logHeaders = function (resp) { + console.log('Headers:'); + Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); +}; + +BACME._logBody = function (body) { + console.log('Body:'); + console.log(JSON.stringify(body, null, 2)); + console.log(''); +}; + +BACME.directory = function (opts) { + return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { + BACME._logHeaders(resp); + return resp.json().then(function (reply) { + if (/error/.test(reply.type)) { + return Promise.reject(new Error(reply.detail || reply.type)); + } + directory = reply; + nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; + accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; + orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; + BACME._logBody(reply); + return reply; + }); + }); +}; + +BACME.nonce = function () { + return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { + BACME._logHeaders(resp); + nonce = resp.headers.get('replay-nonce'); + console.log('Nonce:', nonce); + // resp.body is empty + return resp.headers.get('replay-nonce'); + }); +}; + +BACME.accounts = {}; + +// type = ECDSA +// bitlength = 256 +BACME.accounts.generateKeypair = function (opts) { + return BACME.generateKeypair(opts).then(function (result) { + accountKeypair = result; + + return webCrypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { + + accountJwk = privJwk; + console.log('private jwk:'); + console.log(JSON.stringify(privJwk, null, 2)); + + return privJwk; + /* + return webCrypto.subtle.exportKey( + "pkcs8" + , result.privateKey + ).then(function (keydata) { + console.log('pkcs8:'); + console.log(Array.from(new Uint8Array(keydata))); + + return privJwk; + //return accountKeypair; + }); + */ + }); + }); +}; + +// json to url-safe base64 +BACME._jsto64 = function (json) { + return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +}; + +var textEncoder = new TextEncoder(); + +BACME._importKey = function (jwk) { + var alg; // I think the 256 refers to the hash + var wcOpts = {}; + var extractable = true; // TODO make optionally false? + var priv = jwk; + var pub; + + // ECDSA + if (/^EC/i.test(jwk.kty)) { + wcOpts.name = 'ECDSA'; + wcOpts.namedCurve = jwk.crv; + alg = 'ES256'; + pub = { + crv: priv.crv + , kty: priv.kty + , x: priv.x + , y: priv.y + }; + if (!priv.d) { + priv = null; + } + } + + // RSA + if (/^RS/i.test(jwk.kty)) { + wcOpts.name = 'RSASSA-PKCS1-v1_5'; + wcOpts.hash = { name: "SHA-256" }; + alg = 'RS256'; + pub = { + e: priv.e + , kty: priv.kty + , n: priv.n + }; + if (!priv.p) { + priv = null; + } + } + + return window.crypto.subtle.importKey( + "jwk" + , pub + , wcOpts + , extractable + , [ "verify" ] + ).then(function (publicKey) { + function give(privateKey) { + return { + wcPub: publicKey + , wcKey: privateKey + , wcKeypair: { publicKey: publicKey, privateKey: privateKey } + , meta: { + alg: alg + , name: wcOpts.name + , hash: wcOpts.hash + } + , jwk: jwk + }; + } + if (!priv) { + return give(); + } + return window.crypto.subtle.importKey( + "jwk" + , priv + , wcOpts + , extractable + , [ "sign"/*, "verify"*/ ] + ).then(give); + }); +}; +BACME._sign = function (opts) { + var wcPrivKey = opts.abstractKey.wcKeypair.privateKey; + var wcOpts = opts.abstractKey.meta; + var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash + var signHash; + + console.log('kty', opts.abstractKey.jwk.kty); + signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') }; + + var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64); + console.log('msg:', msg); + return window.crypto.subtle.sign( + { name: wcOpts.name, hash: signHash } + , wcPrivKey + , msg + ).then(function (signature) { + //console.log('sig1:', signature); + //console.log('sig2:', new Uint8Array(signature)); + //console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature))); + // convert buffer to urlsafe base64 + var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { + return String.fromCharCode(ch); + }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + + console.log('[1] URL-safe Base64 Signature:'); + console.log(sig64); + + var signedMsg = { + protected: opts.protected64 + , payload: opts.payload64 + , signature: sig64 + }; + + console.log('Signed Base64 Msg:'); + console.log(JSON.stringify(signedMsg, null, 2)); + + return signedMsg; + }); +}; +// email = john.doe@gmail.com +// jwk = { ... } +// agree = true +BACME.accounts.sign = function (opts) { + + return BACME._importKey(opts.jwk).then(function (abstractKey) { + + var payloadJson = + { termsOfServiceAgreed: opts.agree + , onlyReturnExisting: false + , contact: opts.contacts || [ 'mailto:' + opts.email ] + }; + console.log('payload:'); + console.log(payloadJson); + var payload64 = BACME._jsto64( + payloadJson + ); + + var protectedJson = + { nonce: opts.nonce + , url: accountUrl + , alg: abstractKey.meta.alg + , jwk: null + }; + + if (/EC/i.test(opts.jwk.kty)) { + protectedJson.jwk = { + crv: opts.jwk.crv + , kty: opts.jwk.kty + , x: opts.jwk.x + , y: opts.jwk.y + }; + } else if (/RS/i.test(opts.jwk.kty)) { + protectedJson.jwk = { + e: opts.jwk.e + , kty: opts.jwk.kty + , n: opts.jwk.n + }; + } else { + return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'")); + } + + console.log('protected:'); + console.log(protectedJson); + var protected64 = BACME._jsto64( + protectedJson + ); + + // Note: this function hashes before signing so send data, not the hash + return BACME._sign({ + abstractKey: abstractKey + , payload64: payload64 + , protected64: protected64 + }); + }); +}; + +var accountId; + +BACME.accounts.set = function (opts) { + nonce = null; + return window.fetch(accountUrl, { + mode: 'cors' + , method: 'POST' + , headers: { 'Content-Type': 'application/jose+json' } + , body: JSON.stringify(opts.signedAccount) + }).then(function (resp) { + BACME._logHeaders(resp); + nonce = resp.headers.get('replay-nonce'); + accountId = resp.headers.get('location'); + console.log('Next nonce:', nonce); + console.log('Location/kid:', accountId); + + if (!resp.headers.get('content-type')) { + console.log('Body: '); + + 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/ecdsa.js b/lib/ecdsa.js new file mode 100644 index 0000000..dedc4fb --- /dev/null +++ b/lib/ecdsa.js @@ -0,0 +1,112 @@ +/*global Promise*/ +(function (exports) { +'use strict'; + +var EC = exports.Eckles = {}; +if ('undefined' !== typeof module) { module.exports = EC; } +var Enc = {}; +var textEncoder = new TextEncoder(); + +EC._stance = "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +EC._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; +EC.generate = function (opts) { + var wcOpts = {}; + if (!opts) { opts = {}; } + if (!opts.kty) { opts.kty = 'EC'; } + + // ECDSA has only the P curves and an associated bitlength + wcOpts.name = 'ECDSA'; + if (!opts.namedCurve) { + opts.namedCurve = 'P-256'; + } + wcOpts.namedCurve = opts.namedCurve; // true for supported curves + if (/256/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-256'; + wcOpts.hash = { name: "SHA-256" }; + } else if (/384/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-384'; + wcOpts.hash = { name: "SHA-384" }; + } else { + return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " + + " Please choose either 'P-256' or 'P-384'. " + + EC._stance)); + } + + var extractable = true; + return window.crypto.subtle.generateKey( + wcOpts + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { + return { + private: privJwk + , public: EC.neuter({ jwk: privJwk }) + }; + }); + }); +}; + +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +EC.neuter = function (opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function (k) { + if ('undefined' === typeof opts.jwk[k]) { return; } + // ignore EC private parts + if ('d' === k) { return; } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; + +// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk +EC.__thumbprint = function (jwk) { + // Use the same entropy for SHA as for key + var alg = 'SHA-256'; + if (/384/.test(jwk.crv)) { + alg = 'SHA-384'; + } + return window.crypto.subtle.digest( + { name: alg } + , textEncoder.encode('{"crv":"' + jwk.crv + '","kty":"EC","x":"' + jwk.x + '","y":"' + jwk.y + '"}') + ).then(function (hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); +}; + +EC.thumbprint = function (opts) { + return Promise.resolve().then(function () { + var jwk; + if ('EC' === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return EC.import(opts).then(function (jwk) { + return EC.__thumbprint(jwk); + }); + } + return EC.__thumbprint(jwk); + }); +}; + +Enc.bufToUrlBase64 = function (u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; + +Enc.bufToBase64 = function (u8) { + var bin = ''; + u8.forEach(function (i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +}('undefined' !== typeof module ? module.exports : window)); diff --git a/lib/keypairs.js b/lib/keypairs.js index bf530b8..1f3196c 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -3,84 +3,107 @@ 'use strict'; var Keypairs = exports.Keypairs = {}; +var Rasha = exports.Rasha || require('rasha'); +var Eckles = exports.Eckles || require('eckles'); Keypairs._stance = "We take the stance that if you're knowledgeable enough to" + " 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 + opts = opts || {}; + var p; + if (!opts.kty) { opts.kty = opts.type; } + if (!opts.kty) { opts.kty = 'EC'; } 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)); - } + p = Eckles.generate(opts); } 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]); + p = Rasha.generate(opts); } else { return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." + Keypairs._universal - + " Please choose either 'EC' or 'RSA' keys.")); + + " Please choose 'EC', or 'RSA' if you have good reason to.")); } - - 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 - }; + return p.then(function (pair) { + return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { + pair.private.kid = thumb; // maybe not the same id on the private key? + pair.public.kid = thumb; + return pair; }); }); }; -}(window)); + +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +Keypairs.neuter = Keypairs._neuter = function (opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function (k) { + if ('undefined' === typeof opts.jwk[k]) { return; } + // ignore RSA and EC private parts + if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; + +Keypairs.thumbprint = function (opts) { + return Promise.resolve().then(function () { + if (/EC/i.test(opts.jwk.kty)) { + return Eckles.thumbprint(opts); + } else { + return Rasha.thumbprint(opts); + } + }); +}; + +Keypairs.publish = function (opts) { + if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } + + // returns a copy + var jwk = Keypairs.neuter(opts); + + if (jwk.exp) { + jwk.exp = setTime(jwk.exp); + } else { + if (opts.exp) { jwk.exp = setTime(opts.exp); } + else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; } + else if (opts.expiresAt) { jwk.exp = opts.expiresAt; } + } + if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; } + + if (jwk.kid) { return Promise.resolve(jwk); } + return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; }); +}; + +function setTime(time) { + if ('number' === typeof time) { return time; } + + var t = time.match(/^(\-?\d+)([dhms])$/i); + if (!t || !t[0]) { + throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"); + } + + var now = Math.round(Date.now()/1000); + var num = parseInt(t[1], 10); + var unit = t[2]; + var mult = 1; + switch(unit) { + // fancy fallthrough, what fun! + case 'd': + mult *= 24; + /*falls through*/ + case 'h': + mult *= 60; + /*falls through*/ + case 'm': + mult *= 60; + /*falls through*/ + case 's': + mult *= 1; + } + + return now + (mult * num); +} + +}('undefined' !== typeof module ? module.exports : window)); diff --git a/lib/keypairs.js.min2 b/lib/keypairs.js.min2 new file mode 100644 index 0000000..bf530b8 --- /dev/null +++ b/lib/keypairs.js.min2 @@ -0,0 +1,86 @@ +/*global Promise*/ +(function (exports) { +'use strict'; + +var Keypairs = exports.Keypairs = {}; + +Keypairs._stance = "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; +Keypairs.generate = function (opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = 'EC'; + } + + // ECDSA has only the P curves and an associated bitlength + if (/^EC/i.test(opts.kty)) { + wcOpts.name = 'ECDSA'; + if (!opts.namedCurve) { + opts.namedCurve = 'P-256'; + } + wcOpts.namedCurve = opts.namedCurve; // true for supported curves + if (/256/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-256'; + wcOpts.hash = { name: "SHA-256" }; + } else if (/384/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-384'; + wcOpts.hash = { name: "SHA-384" }; + } else { + return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " + + " Please choose either 'P-256' or 'P-384'. " + + Keypairs._stance)); + } + } else if (/^RSA$/i.test(opts.kty)) { + // Support PSS? I don't think it's used for Let's Encrypt + wcOpts.name = 'RSASSA-PKCS1-v1_5'; + if (!opts.modulusLength) { + opts.modulusLength = 2048; + } + wcOpts.modulusLength = opts.modulusLength; + if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { + // erring on the small side... for no good reason + wcOpts.hash = { name: "SHA-256" }; + } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { + wcOpts.hash = { name: "SHA-384" }; + } else if (wcOpts.modulusLength < 4097) { + wcOpts.hash = { name: "SHA-512" }; + } else { + // Public key thumbprints should be paired with a hash of similar length, + // so anything above SHA-512's keyspace would be left under-represented anyway. + return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" + + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" + + " divisible by 8 are allowed. " + Keypairs._stance)); + } + // TODO maybe allow this to be set to any of the standard values? + wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); + } else { + return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." + + Keypairs._universal + + " Please choose either 'EC' or 'RSA' keys.")); + } + + var extractable = true; + return window.crypto.subtle.generateKey( + wcOpts + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { + // TODO remove + console.log('private jwk:'); + console.log(JSON.stringify(privJwk, null, 2)); + return { + privateKey: privJwk + }; + }); + }); +}; + +}(window)); diff --git a/lib/rsa.js b/lib/rsa.js new file mode 100644 index 0000000..4ec7e07 --- /dev/null +++ b/lib/rsa.js @@ -0,0 +1,122 @@ +/*global Promise*/ +(function (exports) { +'use strict'; + +var RSA = exports.Rasha = {}; +if ('undefined' !== typeof module) { module.exports = RSA; } +var Enc = {}; +var textEncoder = new TextEncoder(); + +RSA._stance = "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +RSA._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; +RSA.generate = function (opts) { + var wcOpts = {}; + if (!opts) { opts = {}; } + if (!opts.kty) { opts.kty = 'RSA'; } + + // Support PSS? I don't think it's used for Let's Encrypt + wcOpts.name = 'RSASSA-PKCS1-v1_5'; + if (!opts.modulusLength) { + opts.modulusLength = 2048; + } + wcOpts.modulusLength = opts.modulusLength; + if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { + // erring on the small side... for no good reason + wcOpts.hash = { name: "SHA-256" }; + } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { + wcOpts.hash = { name: "SHA-384" }; + } else if (wcOpts.modulusLength < 4097) { + wcOpts.hash = { name: "SHA-512" }; + } else { + // Public key thumbprints should be paired with a hash of similar length, + // so anything above SHA-512's keyspace would be left under-represented anyway. + return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" + + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" + + " divisible by 8 are allowed. " + RSA._stance)); + } + // TODO maybe allow this to be set to any of the standard values? + wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); + + var extractable = true; + return window.crypto.subtle.generateKey( + wcOpts + , extractable + , [ 'sign', 'verify' ] + ).then(function (result) { + return window.crypto.subtle.exportKey( + "jwk" + , result.privateKey + ).then(function (privJwk) { + return { + private: privJwk + , public: RSA.neuter({ jwk: privJwk }) + }; + }); + }); +}; + +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +RSA.neuter = function (opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function (k) { + if ('undefined' === typeof opts.jwk[k]) { return; } + // ignore RSA private parts + if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; + +// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk +RSA.__thumbprint = function (jwk) { + // Use the same entropy for SHA as for key + var len = Math.floor(jwk.n.length * 0.75); + var alg = 'SHA-256'; + // TODO this may be a bug + // need to confirm that the padding is no more or less than 1 byte + if (len >= 511) { + alg = 'SHA-512'; + } else if (len >= 383) { + alg = 'SHA-384'; + } + return window.crypto.subtle.digest( + { name: alg } + , textEncoder.encode('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}') + ).then(function (hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); +}; + +RSA.thumbprint = function (opts) { + return Promise.resolve().then(function () { + var jwk; + if ('EC' === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return RSA.import(opts).then(function (jwk) { + return RSA.__thumbprint(jwk); + }); + } + return RSA.__thumbprint(jwk); + }); +}; + +Enc.bufToUrlBase64 = function (u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; + +Enc.bufToBase64 = function (u8) { + var bin = ''; + u8.forEach(function (i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +}('undefined' !== typeof module ? module.exports : window)); -- 2.38.5 From 66e2cb70a845dce619e112a7845896abe73c7537 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 19 Apr 2019 23:14:36 -0600 Subject: [PATCH 147/252] rename --- lib/{acme.js => browser-acme.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/{acme.js => browser-acme.js} (100%) diff --git a/lib/acme.js b/lib/browser-acme.js similarity index 100% rename from lib/acme.js rename to lib/browser-acme.js -- 2.38.5 From 959d2ff009639720f6a3bc26bf8b5550e01118a7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 20 Apr 2019 00:09:36 -0600 Subject: [PATCH 148/252] begin node -> browser conversion --- index.html | 1 + lib/acme.js | 935 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 936 insertions(+) create mode 100644 lib/acme.js diff --git a/index.html b/index.html index 575da3b..ea6b027 100644 --- a/index.html +++ b/index.html @@ -51,6 +51,7 @@ + diff --git a/lib/acme.js b/lib/acme.js new file mode 100644 index 0000000..3e6fb96 --- /dev/null +++ b/lib/acme.js @@ -0,0 +1,935 @@ +// 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'; +/* globals Promise */ + +var ACME = exports.ACME = {}; +var Enc = exports.Enc || {}; +var Crypto = exports.Crypto || {}; + +ACME.formatPemChain = function formatPemChain(str) { + return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; +}; +ACME.splitPemChain = function splitPemChain(str) { + return str.trim().split(/[\r\n]{2,}/g).map(function (str) { + return str + '\n'; + }); +}; + + +// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} +// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" +ACME.challengePrefixes = { + 'http-01': '/.well-known/acme-challenge' +, 'dns-01': '_acme-challenge' +}; +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) { + var err; + + // TODO limit the number of bytes that are allowed to be downloaded + if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + return true; + } + + err = new Error( + "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" + + "curl '" + url + "'\n" + + "Expected: '" + auth.keyAuthorization + "'\n" + + "Got: '" + resp.body + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + } +, 'dns-01': function (me, auth) { + // remove leading *. on wildcard domains + return me.dig({ + type: 'TXT' + , name: auth.dnsHost + }).then(function (ans) { + var err; + + if (ans.answer.some(function (txt) { + return auth.dnsAuthorization === txt.data[0]; + })) { + return true; + } + + err = new Error( + "Error: Failed DNS-01 Pre-Flight Dry Run.\n" + + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + } +}; + +ACME._directory = function (me) { + // GET-as-GET ok + return me._request({ method: 'GET', url: me.directoryUrl, json: true }); +}; +ACME._getNonce = function (me) { + // GET-as-GET, HEAD-as-HEAD ok + if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } + return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); +}; +// ACME RFC Section 7.3 Account Creation +/* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } +*/ +ACME._registerAccount = function (me, options) { + if (me.debug) { console.debug('[acme-v2] accounts.create'); } + + return ACME._getNonce(me).then(function () { + return new Promise(function (resolve, reject) { + + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + reject(err); + return; + } + + var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = [ 'mailto:' + options.email ]; + } + var body = { + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: contact + }; + if (options.externalAccount) { + // TODO is this really done by HMAC or is it arbitrary? + body.externalAccountBinding = me.RSA.signJws( + options.externalAccount.secret + , undefined + , { alg: "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , Buffer.from(JSON.stringify(jwk)) + ); + } + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce + , alg: (me._alg || 'RS256') + , url: me._directoryUrls.newAccount + , jwk: jwk + } + , Buffer.from(payload) + ); + + delete jws.header; + if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.debug(jws); } + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + var account = resp.body; + + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + // the account id url + me._kid = location; + if (me.debug) { console.debug('[DEBUG] new account location:'); } + if (me.debug) { console.debug(location); } + if (me.debug) { console.debug(resp.toJSON()); } + + /* + { + contact: ["mailto:jon@example.com"], + orders: "https://some-url", + status: 'valid' + } + */ + if (!account) { account = { _emptyResponse: true, key: {} }; } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { account.key = {}; } + account.key.kid = me._kid; + return account; + }).then(resolve, reject); + } + + if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } + if (1 === options.agreeToTerms.length) { + // newer promise API + return options.agreeToTerms(me._tos).then(agree, reject); + } + else if (2 === options.agreeToTerms.length) { + // backwards compat cb API + return options.agreeToTerms(me._tos, function (err, tosUrl) { + if (!err) { agree(tosUrl); return; } + reject(err); + }); + } + else { + reject(new Error('agreeToTerms has incorrect function signature.' + + ' Should be fn(tos) { return Promise; }')); + } + }); + }); +}; +/* + POST /acme/new-order HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "5XJ1L3lEkMG7tR6pA00clA", + "url": "https://example.com/acme/new-order" + }), + "payload": base64url({ + "identifiers": [{"type:"dns","value":"example.com"}], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z" + }), + "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" + } +*/ +ACME._getChallenges = function (me, options, auth) { + if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } + // TODO POST-as-GET + return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { + return resp.body; + }); +}; +ACME._wait = function wait(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, (ms || 1100)); + }); +}; + +ACME._testChallengeOptions = function () { + var chToken = ACME._prnd(16); + return [ + { + "type": "http-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/0", + "token": "test-" + chToken + "-0" + } + , { + "type": "dns-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/1", + "token": "test-" + chToken + "-1", + "_wildcard": true + } + , { + "type": "tls-sni-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/2", + "token": "test-" + chToken + "-2" + } + , { + "type": "tls-alpn-01", + "status": "pending", + "url": "https://acme-staging-v02.example.com/3", + "token": "test-" + chToken + "-3" + } + ]; +}; +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 + var challenges = ACME._testChallengeOptions(); + if (identifierValue.includes("*")) { + challenges = challenges.filter(function (ch) { return ch._wildcard; }); + } + + var challenge = ACME._chooseChallenge(options, { challenges: challenges }); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = options.challengeTypes.join(', ') || 'none'; + var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; + return Promise.reject(new Error( + "None of the challenge types that you've enabled ( " + enabled + " )" + + " are suitable for validating the domain you've selected (" + identifierValue + ")." + + " You must enable one of ( " + suitable + " )." + )); + } + if ('dns-01' === challenge.type) { + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; + } + + return Promise.resolve().then(function () { + var results = { + identifier: { + type: "dns" + , value: identifierValue.replace(/^\*\./, '') + } + , challenges: [ challenge ] + , expires: new Date(Date.now() + (60 * 1000)).toISOString() + , wildcard: identifierValue.includes('*.') || undefined + }; + var dryrun = true; + var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); + return ACME._setChallenge(me, options, auth).then(function () { + return auth; + }); + }); + })).then(function (auths) { + return ACME._wait(CHECK_DELAY).then(function () { + return Promise.all(auths.map(function (auth) { + return ACME.challengeTests[auth.type](me, auth); + })); + }); + }); +}; +ACME._chooseChallenge = function(options, results) { + // For each of the challenge types that we support + var challenge; + options.challengeTypes.some(function (chType) { + // And for each of the challenge types that are allowed + return results.challenges.some(function (ch) { + // Check to see if there are any matches + if (ch.type === chType) { + challenge = ch; + return true; + } + }); + }); + + return challenge; +}; +ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes['dns-01']; + if (dryrun) { + dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function (key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function (key) { + // don't confused devs with the id url + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); + auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); + // 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 + auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + + return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { + auth.dnsAuthorization = hash; + return auth; + }); +}; + +ACME._untame = function (name, wild) { + if (wild) { name = '*.' + name.replace('*.', ''); } + return name; +}; + +// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 +ACME._postChallenge = function (me, options, auth) { + var RETRY_INTERVAL = me.retryInterval || 1000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; + var count = 0; + + var altname = ACME._untame(auth.identifier.value, auth.wildcard); + + /* + POST /acme/authz/1234 HTTP/1.1 + Host: example.com + Content-Type: application/jose+json + + { + "protected": base64url({ + "alg": "ES256", + "kid": "https://example.com/acme/acct/1", + "nonce": "xWCM9lGbIyCgue8di6ueWQ", + "url": "https://example.com/acme/authz/1234" + }), + "payload": base64url({ + "status": "deactivated" + }), + "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" + } + */ + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } + , Buffer.from(JSON.stringify({ "status": "deactivated" })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: auth.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('deactivate challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + return Promise.reject(new Error( + "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" + )); + } + + count += 1; + + if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } + // TODO POST-as-GET + return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { + if ('processing' === resp.body.status) { + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + } + + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); + } + if (me.debug) { console.debug('poll: again'); } + return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); + } + + if ('valid' === resp.body.status) { + 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 () {}); + } + } catch(e) {} + return resp.body; + } + + var errmsg; + if (!resp.body.status) { + errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; + } + else if ('invalid' === resp.body.status) { + errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; + } + else { + errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; + } + + return Promise.reject(new Error(errmsg)); + }); + } + + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } + , Buffer.from(JSON.stringify({ })) + ); + me._nonce = null; + return me._request({ + method: 'POST' + , url: auth.url + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } + if (me.debug) { console.debug(resp.headers); } + if (me.debug) { console.debug(resp.body); } + if (me.debug) { console.debug(); } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { console.debug('respond to challenge: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + }); + } + + return respondToChallenge(); +}; +ACME._setChallenge = function (me, options, auth) { + return new Promise(function (resolve, reject) { + try { + if (1 === options.setChallenge.length) { + options.setChallenge(auth).then(resolve).catch(reject); + } else if (2 === options.setChallenge.length) { + options.setChallenge(auth, function (err) { + if(err) { reject(err); } else { resolve(); } + }); + } else { + var challengeCb = function(err) { + if(err) { reject(err); } else { resolve(); } + }; + // for backwards compat adding extra keys without changing params length + Object.keys(auth).forEach(function (key) { + challengeCb[key] = auth[key]; + }); + if (!ACME._setChallengeWarn) { + console.warn("Please update to acme-v2 setChallenge(options) or setChallenge(options, cb)."); + console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); + ACME._setChallengeWarn = true; + } + options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); + } + } catch(e) { + reject(e); + } + }).then(function () { + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = me.setChallengeWait || 500; + if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); } + return ACME._wait(DELAY); + }); +}; +ACME._finalizeOrder = function (me, options, validatedDomains) { + if (me.debug) { console.debug('finalizeOrder:'); } + var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } + , Buffer.from(payload) + ); + + if (me.debug) { console.debug('finalize:', me._finalize); } + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._finalize + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" + me._nonce = resp.toJSON().headers['replay-nonce']; + + if (me.debug) { console.debug('order finalized: resp.body:'); } + if (me.debug) { console.debug(resp.body); } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; // return order + } + + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); + } + + if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } + + if ('pending' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'pending'." + + " Best guess: You have not accepted at least one challenge for each domain:\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('invalid' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'invalid'." + + " Best guess: One or more of the domain challenges could not be verified" + + " (or the order was canceled).\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + + JSON.stringify(resp.body, null, 2) + )); + } + + if ('ready' === resp.body.status) { + return Promise.reject(new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'.\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); + } + + return Promise.reject(new Error( + "Didn't finalize order: Unhandled status '" + resp.body.status + "'." + + " This is not one of the known statuses...\n" + + "Requested: '" + options.domains.join(', ') + "'\n" + + "Validated: '" + validatedDomains.join(', ') + "'\n" + + JSON.stringify(resp.body, null, 2) + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + )); + }); + } + + return pollCert(); +}; +ACME._getCertificate = function (me, options) { + if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } + + // Lot's of error checking to inform the user of mistakes + if (!(options.challengeTypes||[]).length) { + options.challengeTypes = Object.keys(options.challenges||{}); + } + if (!options.challengeTypes.length) { + options.challengeTypes = [ options.challengeType ].filter(Boolean); + } + if (options.challengeType) { + options.challengeTypes.sort(function (a, b) { + if (a === options.challengeType) { return -1; } + if (b === options.challengeType) { return 1; } + return 0; + }); + if (options.challengeType !== options.challengeTypes[0]) { + return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," + + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); + } + } + // TODO check that all challengeTypes are represented in challenges + if (!options.challengeTypes.length) { + return Promise.reject(new Error("options.challengeTypes (string array) must be specified" + + " (and in order of preferential priority).")); + } + 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 domain (or options.subject must specified).")); + } + + // It's just fine if there's no account, we'll go get the key id we need via the public key + if (!me._kid) { + if (options.accountKid || options.account && options.account.kid) { + me._kid = options.accountKid || options.account.kid; + } else { + //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. + return ACME._registerAccount(me, options).then(function () { + // start back from the top + return ACME._getCertificate(me, options); + }); + } + } + + // Do a little dry-run / self-test + return ACME._testChallenges(me, options).then(function () { + if (me.debug) { console.debug('[acme-v2] certificates.create'); } + return ACME._getNonce(me).then(function () { + var body = { + // raw wildcard syntax MUST be used here + identifiers: options.domains.sort(function (a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { return 0; } + if (options.subject === a) { return -1; } + if (options.subject === b) { return 1; } + return 0; + }).map(function (hostname) { + return { type: "dns", value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? + me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); + me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) + var jws = me.RSA.signJws( + options.accountKeypair + , undefined + , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } + , Buffer.from(payload, 'utf8') + ); + + if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } + me._nonce = null; + return me._request({ + method: 'POST' + , url: me._directoryUrls.newOrder + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + var setAuths; + var auths = []; + if (me.debug) { console.debug(location); } // the account id url + if (me.debug) { console.debug(resp.toJSON()); } + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; + + if (!me._authorizations) { + return Promise.reject(new Error( + "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" + + JSON.stringify(resp.body) + )); + } + if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } + setAuths = me._authorizations.slice(0); + + function setNext() { + var authUrl = setAuths.shift(); + if (!authUrl) { return; } + + return ACME._getChallenges(me, options, authUrl).then(function (results) { + // var domain = options.domains[i]; // results.identifier.value + + // If it's already valid, we're golden it regardless + if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { + return setNext(); + } + + var challenge = ACME._chooseChallenge(options, results); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + return Promise.reject(new Error( + "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." + )); + } + + return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); + }); + }); + } + + function challengeNext() { + var auth = auths.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 () { + if (me.debug) { console.debug("[getCertificate] next.then"); } + var validatedDomains = body.identifiers.map(function (ident) { + return ident.value; + }); + + return ACME._finalizeOrder(me, options, validatedDomains); + }).then(function (order) { + if (me.debug) { console.debug('acme-v2: order was finalized'); } + // TODO POST-as-GET + return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { + if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + var certs = { + expires: order.expires + , identifiers: order.identifiers + //, authorizations: order.authorizations + , cert: certsarr.shift() + //, privkey: privkeyPem + , chain: certsarr.join('\n') + }; + if (me.debug) { console.debug(certs); } + return certs; + }); + }); + }); + }); + }); +}; + +ACME.create = function create(me) { + if (!me) { me = {}; } + // me.debug = true; + me.challengePrefixes = ACME.challengePrefixes; + me.RSA = me.RSA || require('rsa-compat').RSA; + //me.Keypairs = me.Keypairs || require('keypairs'); + me.request = me.request || require('@coolaj86/urequest'); + if (!me.dig) { + me.dig = function (query) { + // TODO use digd.js + return new Promise(function (resolve, reject) { + var dns = require('dns'); + dns.resolveTxt(query.name, function (err, records) { + if (err) { reject(err); return; } + + resolve({ + answer: records.map(function (rr) { + return { + data: rr + }; + }) + }); + }); + }); + }; + } + me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; + + + if ('function' !== typeof me._request) { + // MUST have a User-Agent string (see node.js version) + me._request = function (opts) { + return window.fetch(opts.url, opts).then(function (resp) { + return resp.json().then(function (json) { + var headers = {}; + Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); + return { headers: headers , body: json }; + }); + }); + }; + } + + me.init = function (_directoryUrl) { + me.directoryUrl = me.directoryUrl || _directoryUrl; + return ACME._directory(me).then(function (resp) { + me._directoryUrls = resp.body; + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + }; + me.accounts = { + create: function (options) { + return ACME._registerAccount(me, options); + } + }; + me.certificates = { + create: function (options) { + return ACME._getCertificate(me, options); + } + }; + return me; +}; + +ACME._toWebsafeBase64 = function (b64) { + return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); +}; + +// In v8 this is crypto random, but we're just using it for pseudorandom +ACME._prnd = function (n) { + var rnd = ''; + while (rnd.length / 2 < n) { + var num = Math.random().toString().substr(2); + if (num.length % 2) { + num = '0' + num; + } + var pairs = num.match(/(..?)/g); + rnd += pairs.map(ACME._toHex).join(''); + } + return rnd.substr(0, n*2); +}; +ACME._toHex = function (pair) { + return parseInt(pair, 10).toString(16); +}; + +Enc.bufToUrlBase64 = function (u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; +Enc.bufToBase64 = function (u8) { + var bin = ''; + u8.forEach(function (i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +Crypto._sha = function (sha, str) { + var encoder = new TextEncoder(); + var data = encoder.encode(str); + sha = 'SHA-' + sha.replace(/^sha-?/i, ''); + return window.crypto.subtle.digest(sha, data).then(function (hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); +}; + +}('undefined' === typeof window ? module.exports : window)); -- 2.38.5 From 3156229e2ca96ff24e5d345f1586e055dace61bc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 26 Apr 2019 16:26:33 -0600 Subject: [PATCH 149/252] WIP acme accounts --- app.js | 9 +++ index.html | 23 +++++- lib/acme.js | 144 ++++++++++++++++++++----------------- lib/asn1-packer.js | 127 ++++++++++++++++++++++++++++++++ lib/asn1-parser.js | 161 +++++++++++++++++++++++++++++++++++++++++ lib/keypairs.js | 175 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 573 insertions(+), 66 deletions(-) create mode 100644 lib/asn1-packer.js create mode 100644 lib/asn1-parser.js diff --git a/app.js b/app.js index d144211..fcacb77 100644 --- a/app.js +++ b/app.js @@ -49,10 +49,19 @@ function run() { $('.js-jwk').hidden = false; $$('input').map(function ($el) { $el.disabled = false; }); $$('button').map(function ($el) { $el.disabled = false; }); + $('.js-toc-jwk').hidden = false; }); }); + $('form.js-acme-account').addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + $('.js-loading').hidden = false; + ACME.accounts.create + }); + $('.js-generate').hidden = false; + $('.js-create-account').hidden = false; } window.addEventListener('load', run); diff --git a/index.html b/index.html index ea6b027..da066a9 100644 --- a/index.html +++ b/index.html @@ -43,10 +43,29 @@ - + + +

ACME Account

+ -
 
+ + + + diff --git a/lib/acme.js b/lib/acme.js index 3e6fb96..afbedf4 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -7,6 +7,7 @@ /* globals Promise */ var ACME = exports.ACME = {}; +var Keypairs = exports.Keypairs || {}; var Enc = exports.Enc || {}; var Crypto = exports.Crypto || {}; @@ -120,79 +121,94 @@ ACME._registerAccount = function (me, options) { return; } - var jwk = me.RSA.exportPublicJwk(options.accountKeypair); - var contact; - if (options.contact) { - contact = options.contact.slice(0); - } else if (options.email) { - contact = [ 'mailto:' + options.email ]; + var jwk = options.accountKeypair.privateKeyJwk; + var p; + if (jwk) { + p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); + } else { + p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); } - var body = { - termsOfServiceAgreed: tosUrl === me._tos - , onlyReturnExisting: false - , contact: contact - }; - if (options.externalAccount) { - // TODO is this really done by HMAC or is it arbitrary? - body.externalAccountBinding = me.RSA.signJws( - options.externalAccount.secret + return p.then(function (pair) { + if (pair.public.kid) { + pair = JSON.parse(JSON.stringify(pair)); + delete pair.public.kid; + delete pair.private.kid; + } + return pair; + }).then(function (pair) { + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = [ 'mailto:' + options.email ]; + } + var body = { + termsOfServiceAgreed: tosUrl === me._tos + , onlyReturnExisting: false + , contact: contact + }; + if (options.externalAccount) { + body.externalAccountBinding = me.RSA.signJws( + // TODO is HMAC the standard, or is this arbitrary? + options.externalAccount.secret + , undefined + , { alg: options.externalAccount.alg || "HS256" + , kid: options.externalAccount.id + , url: me._directoryUrls.newAccount + } + , Buffer.from(JSON.stringify(pair.public)) + ); + } + var payload = JSON.stringify(body); + var jws = Keypairs.signJws( + options.accountKeypair , undefined - , { alg: "HS256" - , kid: options.externalAccount.id + , { nonce: me._nonce + , alg: (me._alg || 'RS256') , url: me._directoryUrls.newAccount + , jwk: pair.public } - , Buffer.from(JSON.stringify(jwk)) + , Buffer.from(payload) ); - } - var payload = JSON.stringify(body); - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce - , alg: (me._alg || 'RS256') + + delete jws.header; + if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } + if (me.debug) { console.debug(jws); } + me._nonce = null; + return me._request({ + method: 'POST' , url: me._directoryUrls.newAccount - , jwk: jwk - } - , Buffer.from(payload) - ); + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + var account = resp.body; - delete jws.header; - if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } - if (me.debug) { console.debug(jws); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - var account = resp.body; + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } - if (2 !== Math.floor(resp.statusCode / 100)) { - throw new Error('account error: ' + JSON.stringify(body)); - } + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + // the account id url + me._kid = location; + if (me.debug) { console.debug('[DEBUG] new account location:'); } + if (me.debug) { console.debug(location); } + if (me.debug) { console.debug(resp.toJSON()); } - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - // the account id url - me._kid = location; - if (me.debug) { console.debug('[DEBUG] new account location:'); } - if (me.debug) { console.debug(location); } - if (me.debug) { console.debug(resp.toJSON()); } - - /* - { - contact: ["mailto:jon@example.com"], - orders: "https://some-url", - status: 'valid' - } - */ - if (!account) { account = { _emptyResponse: true, key: {} }; } - // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 - if (!account.key) { account.key = {}; } - account.key.kid = me._kid; - return account; - }).then(resolve, reject); + /* + { + contact: ["mailto:jon@example.com"], + orders: "https://some-url", + status: 'valid' + } + */ + if (!account) { account = { _emptyResponse: true, key: {} }; } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { account.key = {}; } + account.key.kid = me._kid; + return account; + }).then(resolve, reject); + }); } if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } diff --git a/lib/asn1-packer.js b/lib/asn1-packer.js new file mode 100644 index 0000000..a57043c --- /dev/null +++ b/lib/asn1-packer.js @@ -0,0 +1,127 @@ +;(function (exports) { +'use strict'; + +if (!exports.ASN1) { exports.ASN1 = {}; } +if (!exports.Enc) { exports.Enc = {}; } +if (!exports.PEM) { exports.PEM = {}; } + +var ASN1 = exports.ASN1; +var Enc = exports.Enc; +var PEM = exports.PEM; + +// +// Packer +// + +// Almost every ASN.1 type that's important for CSR +// can be represented generically with only a few rules. +exports.ASN1 = function ASN1(/*type, hexstrings...*/) { + var args = Array.prototype.slice.call(arguments); + var typ = args.shift(); + var str = args.join('').replace(/\s+/g, '').toLowerCase(); + var len = (str.length/2); + var lenlen = 0; + var hex = typ; + + // We can't have an odd number of hex chars + if (len !== Math.round(len)) { + throw new Error("invalid hex"); + } + + // The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc) + // The second byte is either the size of the value, or the size of its size + + // 1. If the second byte is < 0x80 (128) it is considered the size + // 2. If it is > 0x80 then it describes the number of bytes of the size + // ex: 0x82 means the next 2 bytes describe the size of the value + // 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file) + + if (len > 127) { + lenlen += 1; + while (len > 255) { + lenlen += 1; + len = len >> 8; + } + } + + if (lenlen) { hex += Enc.numToHex(0x80 + lenlen); } + return hex + Enc.numToHex(str.length/2) + str; +}; + +// The Integer type has some special rules +ASN1.UInt = function UINT() { + var str = Array.prototype.slice.call(arguments).join(''); + var first = parseInt(str.slice(0, 2), 16); + + // If the first byte is 0x80 or greater, the number is considered negative + // Therefore we add a '00' prefix if the 0x80 bit is set + if (0x80 & first) { str = '00' + str; } + + return ASN1('02', str); +}; + +// The Bit String type also has a special rule +ASN1.BitStr = function BITSTR() { + var str = Array.prototype.slice.call(arguments).join(''); + // '00' is a mask of how many bits of the next byte to ignore + return ASN1('03', '00' + str); +}; + +ASN1.pack = function (arr) { + var typ = Enc.numToHex(arr[0]); + var str = ''; + if (Array.isArray(arr[1])) { + arr[1].forEach(function (a) { + str += ASN1.pack(a); + }); + } else if ('string' === typeof arr[1]) { + str = arr[1]; + } else { + throw new Error("unexpected array"); + } + if ('03' === typ) { + return ASN1.BitStr(str); + } else if ('02' === typ) { + return ASN1.UInt(str); + } else { + return ASN1(typ, str); + } +}; +Object.keys(ASN1).forEach(function (k) { + exports.ASN1[k] = ASN1[k]; +}); +ASN1 = exports.ASN1; + +PEM.packBlock = function (opts) { + // TODO allow for headers? + return '-----BEGIN ' + opts.type + '-----\n' + + Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n' + + '-----END ' + opts.type + '-----' + ; +}; + +Enc.bufToBase64 = function (u8) { + var bin = ''; + u8.forEach(function (i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +Enc.hexToBuf = function (hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function (h) { + arr.push(parseInt(h, 16)); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; + +Enc.numToHex = function (d) { + d = d.toString(16); + if (d.length % 2) { + return '0' + d; + } + return d; +}; + +}('undefined' !== typeof window ? window : module.exports)); diff --git a/lib/asn1-parser.js b/lib/asn1-parser.js new file mode 100644 index 0000000..82f7cd0 --- /dev/null +++ b/lib/asn1-parser.js @@ -0,0 +1,161 @@ +// Copyright 2018 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'; + +if (!exports.ASN1) { exports.ASN1 = {}; } +if (!exports.Enc) { exports.Enc = {}; } +if (!exports.PEM) { exports.PEM = {}; } + +var ASN1 = exports.ASN1; +var Enc = exports.Enc; +var PEM = exports.PEM; + +// +// Parser +// + +// Although I've only seen 9 max in https certificates themselves, +// but each domain list could have up to 100 +ASN1.ELOOPN = 102; +ASN1.ELOOP = "uASN1.js Error: iterated over " + ASN1.ELOOPN + "+ elements (probably a malformed file)"; +// I've seen https certificates go 29 deep +ASN1.EDEEPN = 60; +ASN1.EDEEP = "uASN1.js Error: element nested " + ASN1.EDEEPN + "+ layers deep (probably a malformed file)"; +// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1) +// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82) +// Bit String (0x03) and Octet String (0x04) may be values or containers +// Sometimes Bit String is used as a container (RSA Pub Spki) +ASN1.CTYPES = [ 0x30, 0x31, 0xa0, 0xa1 ]; +ASN1.VTYPES = [ 0x01, 0x02, 0x05, 0x06, 0x0c, 0x82 ]; +ASN1.parse = function parseAsn1Helper(buf) { + //var ws = ' '; + function parseAsn1(buf, depth, eager) { + if (depth.length >= ASN1.EDEEPN) { throw new Error(ASN1.EDEEP); } + + var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1) + var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] }; + var child; + var iters = 0; + var adjust = 0; + var adjustedLen; + + // Determine how many bytes the length uses, and what it is + if (0x80 & asn1.length) { + asn1.lengthSize = 0x7f & asn1.length; + // I think that buf->hex->int solves the problem of Endianness... not sure + asn1.length = parseInt(Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)), 16); + index += asn1.lengthSize; + } + + // High-order bit Integers have a leading 0x00 to signify that they are positive. + // Bit Streams use the first byte to signify padding, which x.509 doesn't use. + if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) { + // However, 0x00 on its own is a valid number + if (asn1.length > 1) { + index += 1; + adjust = -1; + } + } + adjustedLen = asn1.length + adjust; + + //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); + + function parseChildren(eager) { + asn1.children = []; + //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); + while (iters < ASN1.ELOOPN && index < (2 + asn1.length + asn1.lengthSize)) { + iters += 1; + depth.length += 1; + child = parseAsn1(buf.slice(index, index + adjustedLen), depth, eager); + depth.length -= 1; + // The numbers don't match up exactly and I don't remember why... + // probably something with adjustedLen or some such, but the tests pass + index += (2 + child.lengthSize + child.length); + //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length)); + if (index > (2 + asn1.lengthSize + asn1.length)) { + if (!eager) { console.error(JSON.stringify(asn1, ASN1._replacer, 2)); } + throw new Error("Parse error: child value length (" + child.length + + ") is greater than remaining parent length (" + (asn1.length - index) + + " = " + asn1.length + " - " + index + ")"); + } + asn1.children.push(child); + //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); + } + if (index !== (2 + asn1.lengthSize + asn1.length)) { + //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length)); + throw new Error("premature end-of-file"); + } + if (iters >= ASN1.ELOOPN) { throw new Error(ASN1.ELOOP); } + + delete asn1.value; + return asn1; + } + + // Recurse into types that are _always_ containers + if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) { return parseChildren(eager); } + + // Return types that are _always_ values + asn1.value = buf.slice(index, index + adjustedLen); + if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { return asn1; } + + // For ambigious / unknown types, recurse and return on failure + // (and return child array size to zero) + try { return parseChildren(true); } + catch(e) { asn1.children.length = 0; return asn1; } + } + + var asn1 = parseAsn1(buf, []); + var len = buf.byteLength || buf.length; + if (len !== 2 + asn1.lengthSize + asn1.length) { + throw new Error("Length of buffer does not match length of ASN.1 sequence."); + } + return asn1; +}; +ASN1._replacer = function (k, v) { + if ('type' === k) { return '0x' + Enc.numToHex(v); } + if (v && 'value' === k) { return '0x' + Enc.bufToHex(v.data || v); } + return v; +}; + +// 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 { der: Enc.base64ToBuf(der) }; +}; + +Enc.base64ToBuf = function (b64) { + return Enc.binToBuf(atob(b64)); +}; +Enc.binToBuf = function (bin) { + var arr = bin.split('').map(function (ch) { + return ch.charCodeAt(0); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; +Enc.bufToHex = function (u8) { + var hex = []; + var i, h; + var len = (u8.byteLength || u8.length); + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { h = '0' + h; } + hex.push(h); + } + + return hex.join('').toLowerCase(); +}; +Enc.numToHex = function (d) { + d = d.toString(16); + if (d.length % 2) { + return '0' + d; + } + return d; +}; + +}('undefined' !== typeof window ? window : module.exports)); diff --git a/lib/keypairs.js b/lib/keypairs.js index 1f3196c..1492954 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -5,6 +5,7 @@ var Keypairs = exports.Keypairs = {}; var Rasha = exports.Rasha || require('rasha'); var Eckles = exports.Eckles || require('eckles'); +var Enc = exports.Enc || {}; 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."; @@ -76,6 +77,163 @@ Keypairs.publish = function (opts) { return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; }); }; +// JWT a.k.a. JWS with Claims using Compact Serialization +Keypairs.signJwt = function (opts) { + return Keypairs.thumbprint({ jwk: opts.jwk }).then(function (thumb) { + var header = opts.header || {}; + var claims = JSON.parse(JSON.stringify(opts.claims || {})); + header.typ = 'JWT'; + + if (!header.kid) { header.kid = thumb; } + if (!header.alg && opts.alg) { header.alg = opts.alg; } + if (!claims.iat && (false === claims.iat || false === opts.iat)) { + claims.iat = undefined; + } else if (!claims.iat) { + claims.iat = Math.round(Date.now()/1000); + } + + if (opts.exp) { + claims.exp = setTime(opts.exp); + } else if (!claims.exp && (false === claims.exp || false === opts.exp)) { + claims.exp = undefined; + } else if (!claims.exp) { + throw new Error("opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"); + } + + if (opts.iss) { claims.iss = opts.iss; } + if (!claims.iss && (false === claims.iss || false === opts.iss)) { + claims.iss = undefined; + } else if (!claims.iss) { + throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url"); + } + + return Keypairs.signJws({ + jwk: opts.jwk + , pem: opts.pem + , protected: header + , header: undefined + , payload: claims + }).then(function (jws) { + return [ jws.protected, jws.payload, jws.signature ].join('.'); + }); + }); +}; + +Keypairs.signJws = function (opts) { + return Keypairs.thumbprint(opts).then(function (thumb) { + + function alg() { + if (!opts.jwk) { + throw new Error("opts.jwk must exist and must declare 'typ'"); + } + return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256"; + } + + function sign(pem) { + var header = opts.header; + var protect = opts.protected; + var payload = opts.payload; + + // Compute JWS signature + var protectedHeader = ""; + // Because unprotected headers are allowed, regrettably... + // https://stackoverflow.com/a/46288694 + if (false !== protect) { + if (!protect) { protect = {}; } + if (!protect.alg) { protect.alg = alg(); } + // There's a particular request where Let's Encrypt explicitly doesn't use a kid + if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } + protectedHeader = JSON.stringify(protect); + } + + // Not sure how to handle the empty case since ACME POST-as-GET must be empty + //if (!payload) { + // throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)"); + //} + // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc) + if (payload && ('string' !== typeof payload) + && ('undefined' === typeof payload.byteLength) + && ('undefined' === typeof payload.byteLength) + ) { + payload = JSON.stringify(payload); + } + // Converting to a buffer, even if it was just converted to a string + if ('string' === typeof payload) { + payload = Enc.binToBuf(payload); + } + + // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway) + var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); + var protected64 = Enc.strToUrlBase64(protectedHeader); + var payload64 = Enc.bufToUrlBase64(payload); + var binsig = require('crypto') + .createSign(nodeAlg) + .update(protect ? (protected64 + "." + payload64) : payload64) + .sign(pem) + ; + 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 sig = binsig.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, '') + ; + + return { + header: header + , protected: protected64 || undefined + , payload: payload64 + , signature: sig + }; + } + + function convertIfEcdsa(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); + } + + if (opts.pem && opts.jwk) { + return sign(opts.pem); + } else { + return Keypairs.export({ jwk: opts.jwk }).then(sign); + } + }); +}; + function setTime(time) { if ('number' === typeof time) { return time; } @@ -106,4 +264,21 @@ function setTime(time) { return now + (mult * num); } +Enc.hexToBuf = function (hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function (h) { + arr.push(parseInt(h, 16)); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; +Enc.strToUrlBase64 = function (str) { + return Enc.bufToUrlBase64(Enc.binToBuf(str)); +}; +Enc.binToBuf = function (bin) { + var arr = bin.split('').map(function (ch) { + return ch.charCodeAt(0); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; + }('undefined' !== typeof module ? module.exports : window)); -- 2.38.5 From 10f817a51c78c7bcabbdd2e2f82f2a45a9f8fa4e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 26 Apr 2019 16:36:19 -0600 Subject: [PATCH 150/252] WIP encoding --- lib/bluecrypt-encoding.js | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 lib/bluecrypt-encoding.js diff --git a/lib/bluecrypt-encoding.js b/lib/bluecrypt-encoding.js new file mode 100644 index 0000000..d3f2292 --- /dev/null +++ b/lib/bluecrypt-encoding.js @@ -0,0 +1,135 @@ +(function (exports) { + +var Enc = exports.BluecryptEncoding = {}; + +Enc.bufToBin = function (buf) { + var bin = ''; + // cannot use .map() because Uint8Array would return only 0s + buf.forEach(function (ch) { + bin += String.fromCharCode(ch); + }); + return bin; +}; + +Enc.bufToHex = function toHex(u8) { + var hex = []; + var i, h; + var len = (u8.byteLength || u8.length); + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { h = '0' + h; } + hex.push(h); + } + + return hex.join('').toLowerCase(); +}; + +Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { + var r = str % 4; + if (2 === r) { + str += '=='; + } else if (3 === r) { + str += '='; + } + return str.replace(/-/g, '+').replace(/_/g, '/'); +}; + +Enc.base64ToBuf = function (b64) { + return Enc.binToBuf(atob(b64)); +}; +Enc.binToBuf = function (bin) { + var arr = bin.split('').map(function (ch) { + return ch.charCodeAt(0); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; +Enc.bufToHex = function (u8) { + var hex = []; + var i, h; + var len = (u8.byteLength || u8.length); + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { h = '0' + h; } + hex.push(h); + } + + return hex.join('').toLowerCase(); +}; +Enc.numToHex = function (d) { + d = d.toString(16); + if (d.length % 2) { + return '0' + d; + } + return d; +}; + +Enc.bufToUrlBase64 = function (u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; + +Enc.bufToBase64 = function (u8) { + var bin = ''; + u8.forEach(function (i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); +}; + +Enc.hexToBuf = function (hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function (h) { + arr.push(parseInt(h, 16)); + }); + return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; +}; + +Enc.numToHex = function (d) { + d = d.toString(16); + if (d.length % 2) { + return '0' + d; + } + return d; +}; + + +// +// JWK to SSH (tested working) +// +Enc.base64ToHex = function (b64) { + var bin = atob(Enc.urlBase64ToBase64(b64)); + return Enc.binToHex(bin); +}; + +Enc.binToHex = function (bin) { + return bin.split('').map(function (ch) { + var h = ch.charCodeAt(0).toString(16); + if (h.length % 2) { h = '0' + h; } + return h; + }).join(''); +}; + +Enc.hexToBase64 = function (hex) { + return btoa(Enc.hexToBin(hex)); +}; + +Enc.hexToBin = function (hex) { + return hex.match(/.{2}/g).map(function (h) { + return String.fromCharCode(parseInt(h, 16)); + }).join(''); +}; + +Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { + var r = str % 4; + if (2 === r) { + str += '=='; + } else if (3 === r) { + str += '='; + } + return str.replace(/-/g, '+').replace(/_/g, '/'); +}; + + +}('undefined' !== typeof exports ? module.exports : window )); -- 2.38.5 From 2e0549af5ac3f2469464420cec1b0eb89688a377 Mon Sep 17 00:00:00 2001 From: lastlink Date: Fri, 26 Apr 2019 18:50:10 -0400 Subject: [PATCH 151/252] setup for browser --- lib/x509.js | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 lib/x509.js diff --git a/lib/x509.js b/lib/x509.js new file mode 100644 index 0000000..63d1a7d --- /dev/null +++ b/lib/x509.js @@ -0,0 +1,173 @@ +'use strict'; +(function (exports) { + 'use strict'; + var x509 = exports.x509 = {}; + var ASN1 = exports.ASN1; + var Enc = exports.Enc; + + // 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(); + // 1.2.840.10045.2.1 + // ecPublicKey (ANSI X9.62 public key type) + var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase(); + + x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { + var index = 7; + var len = 32; + var olen = OBJ_ID_EC.length / 2; + + if ("P-384" === jwk.crv) { + olen = OBJ_ID_EC_384.length / 2; + index = 8; + len = 48; + } + if (len !== u8[index - 1]) { + throw new Error("Unexpected bitlength " + len); + } + + // private part is d + var d = u8.slice(index, index + len); + // compression bit index + var ci = index + len + 2 + olen + 2 + 3; + var c = u8[ci]; + var x, y; + + if (0x04 === c) { + y = u8.slice(ci + 1 + len, ci + 1 + len + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + x = u8.slice(ci + 1, ci + 1 + len); + + return { + kty: jwk.kty + , crv: jwk.crv + , d: Enc.bufToUrlBase64(d) + //, dh: Enc.bufToHex(d) + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + + x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { + var index = 24 + (OBJ_ID_EC.length / 2); + var len = 32; + if ("P-384" === jwk.crv) { + index = 24 + (OBJ_ID_EC_384.length / 2) + 2; + len = 48; + } + + //console.log(index, u8.slice(index)); + if (0x04 !== u8[index]) { + //console.log(jwk); + throw new Error("privkey not found"); + } + var d = u8.slice(index + 2, index + 2 + len); + var ci = index + 2 + len + 5; + var xi = ci + 1; + var x = u8.slice(xi, xi + len); + var yi = xi + len; + var y; + if (0x04 === u8[ci]) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== u8[ci]) { + throw new Error("invalid compression bit (expected 0x04 or 0x02)"); + } + + return { + kty: jwk.kty + , crv: jwk.crv + , d: Enc.bufToUrlBase64(d) + //, dh: Enc.bufToHex(d) + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + + x509.parseSpki = function parsePem(u8, jwk) { + var ci = 16 + OBJ_ID_EC.length / 2; + var len = 32; + + if ("P-384" === jwk.crv) { + ci = 16 + OBJ_ID_EC_384.length / 2; + len = 48; + } + + var c = u8[ci]; + var xi = ci + 1; + var x = u8.slice(xi, xi + len); + var yi = xi + len; + var y; + if (0x04 === c) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + + return { + kty: jwk.kty + , crv: jwk.crv + , x: Enc.bufToUrlBase64(x) + //, xh: Enc.bufToHex(x) + , y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + x509.parsePkix = x509.parseSpki; + + x509.packSec1 = function (jwk) { + var d = Enc.base64ToHex(jwk.d); + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToUint8( + ASN1('30' + , ASN1.UInt('01') + , ASN1('04', d) + , ASN1('A0', objId) + , ASN1('A1', ASN1.BitStr('04' + x + y))) + ); + }; + x509.packPkcs8 = function (jwk) { + var d = Enc.base64ToHex(jwk.d); + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToUint8( + ASN1('30' + , ASN1.UInt('00') + , ASN1('30' + , OBJ_ID_EC_PUB + , objId + ) + , ASN1('04' + , ASN1('30' + , ASN1.UInt('01') + , ASN1('04', d) + , ASN1('A1', ASN1.BitStr('04' + x + y))))) + ); + }; + x509.packSpki = function (jwk) { + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToUint8( + ASN1('30' + , ASN1('30' + , OBJ_ID_EC_PUB + , objId + ) + , ASN1.BitStr('04' + x + y)) + ); + }; + x509.packPkix = x509.packSpki; + +}('undefined' !== typeof module ? module.exports : window)); -- 2.38.5 From 1b01c2c4132f3db552f32ad56d92f4670d6365db Mon Sep 17 00:00:00 2001 From: lastlink Date: Fri, 26 Apr 2019 23:27:08 -0400 Subject: [PATCH 152/252] working der generation --- app.js | 3 +++ index.html | 4 +++- lib/bluecrypt-encoding.js | 2 +- lib/x509.js | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index fcacb77..1738a2c 100644 --- a/app.js +++ b/app.js @@ -43,6 +43,9 @@ function run() { }; console.log('opts', opts); Keypairs.generate(opts).then(function (results) { + var der = x509.packPkcs8(results.private); + console.log(der) + // Pem.encode(x509.packPkcs8(privateJwk)) $('.js-jwk').innerText = JSON.stringify(results, null, 2); // $('.js-loading').hidden = true; diff --git a/index.html b/index.html index da066a9..0d35e9a 100644 --- a/index.html +++ b/index.html @@ -66,8 +66,10 @@ ACME Account Response
- + + + diff --git a/lib/bluecrypt-encoding.js b/lib/bluecrypt-encoding.js index d3f2292..7dc1073 100644 --- a/lib/bluecrypt-encoding.js +++ b/lib/bluecrypt-encoding.js @@ -1,6 +1,6 @@ (function (exports) { -var Enc = exports.BluecryptEncoding = {}; +var Enc = exports.Enc = {}; Enc.bufToBin = function (buf) { var bin = ''; diff --git a/lib/x509.js b/lib/x509.js index 63d1a7d..114375d 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -128,7 +128,7 @@ var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1.UInt('01') , ASN1('04', d) @@ -141,7 +141,7 @@ var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1.UInt('00') , ASN1('30' @@ -159,7 +159,7 @@ var x = Enc.base64ToHex(jwk.x); var y = Enc.base64ToHex(jwk.y); var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToUint8( + return Enc.hexToBuf( ASN1('30' , ASN1('30' , OBJ_ID_EC_PUB -- 2.38.5 From 735ec948da2f15c9b0c269340270f82f3a3b66dd Mon Sep 17 00:00:00 2001 From: lastlink Date: Sat, 27 Apr 2019 00:02:57 -0400 Subject: [PATCH 153/252] working pem generation --- app.js | 8 ++++++-- index.html | 14 +++++++++++++ lib/ecdsa.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 1738a2c..097d710 100644 --- a/app.js +++ b/app.js @@ -44,15 +44,19 @@ function run() { console.log('opts', opts); Keypairs.generate(opts).then(function (results) { var der = x509.packPkcs8(results.private); - console.log(der) - // Pem.encode(x509.packPkcs8(privateJwk)) + var pem = Eckles.export({jwk:results.private}) + $('.js-jwk').innerText = JSON.stringify(results, null, 2); + $('.js-der').innerText = JSON.stringify(der, null, 2); + $('.js-input-pem').innerText = pem; // $('.js-loading').hidden = true; $('.js-jwk').hidden = false; $$('input').map(function ($el) { $el.disabled = false; }); $$('button').map(function ($el) { $el.disabled = false; }); $('.js-toc-jwk').hidden = false; + $('.js-toc-der').hidden = false; + $('.js-toc-pem').hidden = false; }); }); diff --git a/index.html b/index.html index 0d35e9a..d4fec55 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,12 @@ BlueCrypt +

BlueCrypt for the Browser

@@ -58,6 +64,14 @@ JWK Keypair
 
+ + -