diff --git a/.gitignore b/.gitignore
index 052547a..8072530 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,32 +1,17 @@
+.env
+
*.pem
-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
diff --git a/README.md b/README.md
index 99e8924..b0c1f9a 100644
--- a/README.md
+++ b/README.md
@@ -2,28 +2,77 @@
| [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)
-|
-| A [Root](https://therootcompany.com) Project
+# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project
-# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js)
+A **Zero (External) Dependency**\* library for building
+Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates.
-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).
+The primary goal of this library is to make it easy to
+get Accounts and Certificates through Let's Encrypt.
-\* 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.
+# Features
+
+- [x] Let's Encrypt™ v2 / ACME Draft 12
+ - [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18
+ - [ ] (in-progress) StartTLS Everywhere™
+- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
+ - [x] **http-01** for single or multiple domains per certificate
+ - [x] **dns-01** for wildcards, localhost, private networks, etc
+- [x] VanillaJS
+ - [x] Zero External Dependencies
+ - [x] Safe, Efficient, Maintained
+ - [x] Works in Node v6+
+ - [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
+
+\* The only required dependencies were built by us, specifically for this and related libraries.
+There are some, truly optional, backwards-compatibility dependencies for node v6.
## Looking for Quick 'n' Easy™?
-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).
+If you want something that's more "batteries included" give
+[greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
+a try.
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
-- [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
+## v1.7+: Transitional v2 Support
+
+By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
+
+Although the draft 18 changes themselves don't requiring breaking the API,
+we've been keeping backwards compatibility for a long time and the API has become messy.
+
+We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
+the code with a fresh new release.
+
+As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js.
+We've been really good about backwards compatibility for
+
+## Recommended Example
+
+Due to the upcoming changes we've removed the old documentation.
+
+Instead we recommend that you take a look at the
+[Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
+
+- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
+
+That's not exactly the new API, but it's close.
+
+## Let's Encrypt v02 Directory URLs
+
+```
+# Production URL
+https://acme-v02.api.letsencrypt.org/directory
+```
+
+```
+# Staging URL
+https://acme-staging-v02.api.letsencrypt.org/directory
+```
+
+
-## le-acme-core-compatible API (recommended)
+## API
-Status: Stable, Locked, Bugfix-only
-
-See Full Documentation at
-
-```js
-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
-//
-```
-
-## Promise API (dev)
-
-Status: Almost stable, but **not semver locked**
+Status: Small, but breaking changes coming in v2
This API is a simple evolution of le-acme-core,
but tries to provide a better mapping to the new draft 11 APIs.
```js
-// Create Instance (Dependency Injection)
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'; }
-
-
- // 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
+ // 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,
+ skipDryRun: 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
});
-
// Discover Directory URLs
-ACME.init(acmeDirectoryUrl) // returns Promise
-
+ACME.init(acmeDirectoryUrl); // returns Promise
// Accounts
-ACME.accounts.create(options) // returns Promise registration data
-
- { email: '' // valid email (server checks MX records)
- , accountKeypair: { // privateKeyPem or privateKeyJwt
- privateKeyPem: ''
- }
- , agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl
- }
+ACME.accounts.create(options); // returns Promise registration data
+options = {
+ email: '', // valid email (server checks MX records)
+ accountKeypair: {
+ // privateKeyPem or privateKeyJwt
+ privateKeyPem: ''
+ },
+ agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
+};
// Registration
-ACME.certificates.create(options) // returns Promise
-
- { newAuthzUrl: '' // specify acmeUrls.newAuthz
- , newCertUrl: '' // specify acmeUrls.newCert
-
- , domainKeypair: {
- privateKeyPem: ''
- }
- , accountKeypair: {
- privateKeyPem: ''
- }
- , domains: [ 'example.com' ]
-
- , setChallenge: fn (hostname, key, val) // return Promise
- , removeChallenge: fn (hostname, key) // return Promise
- }
-```
-
-Helpers & Stuff
-
-```javascript
-// Constants
-ACME.challengePrefixes['http-01']; // '/.well-known/acme-challenge'
-ACME.challengePrefixes['dns-01']; // '_acme-challenge'
+ACME.certificates.create(options); // returns Promise
+
+options = {
+ domainKeypair: {
+ privateKeyPem: ''
+ },
+ accountKeypair: {
+ privateKeyPem: ''
+ },
+ domains: ['example.com'],
+
+ getZones: function(opts) {}, // should Promise an array of domain zone names
+ setChallenge: function(opts) {}, // should Promise the record id, or name
+ removeChallenge: function(opts) {} // should Promise null
+};
```
# Changelog
+- v1.8
+ - more transitional prepwork for new v2 API
+ - support newer (simpler) dns-01 and http-01 libraries
- v1.5
- perform full test challenge first (even before nonce)
- v1.3
diff --git a/examples/dns-01-digitalocean.js b/examples/dns-01-digitalocean.js
new file mode 100644
index 0000000..02112ce
--- /dev/null
+++ b/examples/dns-01-digitalocean.js
@@ -0,0 +1,69 @@
+(function(exports) {
+ 'use strict';
+
+ // node[0] ./test.js[1] jon.doe@gmail.com[2] example.com,*.example.com[3] xxxxxx[4]
+ var email = process.argv[2] || process.env.ACME_EMAIL;
+ var domains = (process.argv[3] || process.env.ACME_DOMAINS).split(/[,\s]+/);
+ var token = process.argv[4] || process.env.DIGITALOCEAN_API_KEY;
+
+ // git clone https://git.rootprojects.org/root/acme-dns-01-digitalocean.js node_modules/acme-dns-01-digitalocean
+ var dns01 = require('acme-dns-01-digitalocean').create({
+ //baseUrl: 'https://api.digitalocean.com/v2/domains',
+ token: token
+ });
+
+ // This will be replaced with Keypairs.js in the next version
+ var promisify = require('util').promisify;
+ var generateKeypair = promisify(require('rsa-compat').RSA.generateKeypair);
+
+ //var ACME = exports.ACME || require('acme').ACME;
+ var ACME = exports.ACME || require('../').ACME;
+ var acme = ACME.create({});
+ acme
+ .init({
+ //directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
+ })
+ .then(function() {
+ return generateKeypair(null).then(function(accountPair) {
+ return generateKeypair(null).then(function(serverPair) {
+ return acme.accounts
+ .create({
+ // valid email (server checks MX records)
+ email: email,
+ accountKeypair: accountPair,
+ agreeToTerms: function(tosUrl) {
+ // ask user (if user is the host)
+ return tosUrl;
+ }
+ })
+ .then(function(account) {
+ console.info('Created Account:');
+ console.info(account);
+
+ return acme.certificates
+ .create({
+ domains: domains,
+ challenges: { 'dns-01': dns01 },
+ domainKeypair: serverPair,
+ accountKeypair: accountPair,
+
+ // v2 will be directly compatible with the new ACME modules,
+ // whereas this version needs a shim
+ getZones: dns01.zones,
+ setChallenge: dns01.set,
+ removeChallenge: dns01.remove
+ })
+ .then(function(certs) {
+ console.info('Secured SSL Certificates');
+ console.info(certs);
+ });
+ });
+ });
+ });
+ })
+ .catch(function(e) {
+ console.error('Something went wrong:');
+ console.error(e);
+ process.exit(500);
+ });
+})('undefined' === typeof module ? window : module.exports);
diff --git a/examples/example.env b/examples/example.env
new file mode 100644
index 0000000..2df1d9a
--- /dev/null
+++ b/examples/example.env
@@ -0,0 +1,3 @@
+ACME_EMAIL=jon.doe@gmail.com
+ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
+DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/node.js b/node.js
index 67eb88d..1d4c045 100644
--- a/node.js
+++ b/node.js
@@ -276,7 +276,10 @@ ACME._registerAccount = function(me, options) {
}
if (1 === options.agreeToTerms.length) {
// newer promise API
- return options.agreeToTerms(me._tos).then(agree, reject);
+ return Promise.resolve(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) {
@@ -461,6 +464,58 @@ ACME._chooseChallenge = function(options, results) {
return challenge;
};
+ACME._getZones = function(me, options, dnsHosts) {
+ if ('function' !== typeof options.getZones) {
+ options.getZones = function() {
+ return Promise.resolve([]);
+ };
+ }
+ return new Promise(function(resolve, reject) {
+ try {
+ if (options.getZones.length <= 1) {
+ options
+ .getZones({ dnsHosts: dnsHosts })
+ .then(resolve)
+ .catch(reject);
+ } else if (2 === options.getZones.length) {
+ options.getZones({ dnsHosts: dnsHosts }, function(err, zonenames) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(zonenames);
+ }
+ });
+ } else {
+ throw new Error(
+ 'options.getZones should accept opts and Promise an array of zone names'
+ );
+ }
+ } catch (e) {
+ reject(e);
+ }
+ });
+};
+function newZoneRegExp(zonename) {
+ // (^|\.)example\.com$
+ // which matches:
+ // foo.example.com
+ // example.com
+ // but not:
+ // fooexample.com
+ return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$');
+}
+function pluckZone(zonenames, dnsHost) {
+ return zonenames
+ .filter(function(zonename) {
+ // the only character that needs to be escaped for regex
+ // and is allowed in a domain name is '.'
+ return newZoneRegExp(zonename).test(dnsHost);
+ })
+ .sort(function(a, b) {
+ // longest match first
+ return b.length - a.length;
+ })[0];
+}
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'];
@@ -490,6 +545,7 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
auth[key] = challenge[key];
});
+ var zone = pluckZone(options.zonenames || [], auth.identifier.value);
// 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
@@ -511,7 +567,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
.update(auth.keyAuthorization)
.digest('base64')
);
+ if (zone) {
+ auth.dnsZone = zone;
+ auth.dnsPrefix = auth.dnsHost
+ .replace(newZoneRegExp(zone), '')
+ .replace(/\.$/, '');
+ }
+ // for backwards compat
+ auth.challenge = auth;
return auth;
};
@@ -997,187 +1061,204 @@ ACME._getCertificate = function(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')
- );
-
+ var dnsHosts = options.domains.map(function(d) {
+ return (
+ require('crypto')
+ .randomBytes(2)
+ .toString('hex') + d
+ );
+ });
+ return ACME._getZones(me, options, dnsHosts).then(function(zonenames) {
+ options.zonenames = zonenames;
+ // Do a little dry-run / self-test
+ return ACME._testChallenges(me, options).then(function() {
if (me.debug) {
- console.debug('\n[DEBUG] newOrder\n');
+ console.debug('[acme-v2] certificates.create');
}
- 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);
+ 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"
+ };
- function setNext() {
- var authUrl = setAuths.shift();
- if (!authUrl) {
- return;
+ 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);
- return ACME._getChallenges(me, options, authUrl).then(function(
- results
- ) {
- // var domain = options.domains[i]; // results.identifier.value
+ function setNext() {
+ var authUrl = setAuths.shift();
+ if (!authUrl) {
+ return;
+ }
- // If it's already valid, we're golden it regardless
- if (
- results.challenges.some(function(ch) {
- return 'valid' === ch.status;
- })
+ return ACME._getChallenges(me, options, authUrl).then(function(
+ results
) {
- return setNext();
- }
+ // 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() +
+ "'."
+ )
+ );
+ }
- 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() +
- "'."
- )
+ var auth = ACME._challengeToAuth(
+ me,
+ options,
+ results,
+ challenge
);
- }
-
- var auth = ACME._challengeToAuth(me, options, results, challenge);
- auths.push(auth);
- return ACME._setChallenge(me, options, auth).then(setNext);
- });
- }
-
- function challengeNext() {
- var auth = auths.shift();
- if (!auth) {
- return;
+ auths.push(auth);
+ return ACME._setChallenge(me, options, auth).then(setNext);
+ });
}
- 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');
+ function challengeNext() {
+ var auth = auths.shift();
+ if (!auth) {
+ return;
}
- var validatedDomains = body.identifiers.map(function(ident) {
- return ident.value;
- });
+ return ACME._postChallenge(me, options, auth).then(challengeNext);
+ }
- 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;
+ // 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');
+ }
+ 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;
+ });
+ });
+ });
+ });
});
});
};
@@ -1190,7 +1271,7 @@ ACME.create = function create(me) {
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.request = me.request || require('@root/request');
me._dig = function(query) {
// TODO use digd.js
return new Promise(function(resolve, reject) {
@@ -1241,7 +1322,27 @@ ACME.create = function create(me) {
}
me.init = function(_directoryUrl) {
- me.directoryUrl = me.directoryUrl || _directoryUrl;
+ if (_directoryUrl) {
+ _directoryUrl = _directoryUrl.directoryUrl || _directoryUrl;
+ }
+ if ('string' === typeof _directoryUrl) {
+ me.directoryUrl = _directoryUrl;
+ }
+ if (!me.directoryUrl) {
+ me.directoryUrl =
+ 'https://acme-staging-v02.api.letsencrypt.org/directory';
+ console.warn();
+ console.warn(
+ "No ACME `directoryUrl` was specified. Using Let's Encrypt's staging environment as the default, which will issue invalid certs."
+ );
+ console.warn('\t' + me.directoryUrl);
+ console.warn();
+ console.warn(
+ "To get valid certificates you will need to switch to a production URL. You might like Let's Encrypt v2:"
+ );
+ console.warn('\t' + me.directoryUrl.replace('-staging', ''));
+ console.warn();
+ }
return ACME._directory(me).then(function(resp) {
me._directoryUrls = resp.body;
me._tos = me._directoryUrls.meta.termsOfService;
diff --git a/package-lock.json b/package-lock.json
index 07f4556..fef8f40 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,19 @@
{
"name": "acme-v2",
- "version": "1.7.6",
+ "version": "1.8.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
- "@coolaj86/urequest": {
- "version": "1.3.7",
- "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz",
- "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA=="
+ "@root/request": {
+ "version": "1.3.11",
+ "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
+ "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
+ },
+ "dotenv": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
+ "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
+ "dev": true
},
"eckles": {
"version": "1.4.1",
@@ -29,9 +35,9 @@
"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==",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
+ "integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
"requires": {
"keypairs": "^1.2.14"
}
diff --git a/package.json b/package.json
index 0c225a2..1ec026e 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "acme-v2",
- "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",
+ "version": "1.8.0",
+ "description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.",
"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
"main": "node.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "node ./test.js"
},
"repository": {
"type": "git",
@@ -23,10 +23,13 @@
"automated https",
"letsencrypt"
],
- "author": "AJ ONeal (https://coolaj86.com/)",
+ "author": "AJ ONeal (https://solderjs.com/)",
"license": "MPL-2.0",
"dependencies": {
- "@coolaj86/urequest": "^1.3.6",
- "rsa-compat": "^2.0.6"
+ "@root/request": "^1.3.11",
+ "rsa-compat": "^2.0.8"
+ },
+ "devDependencies": {
+ "dotenv": "^8.0.0"
}
}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..b8ff270
--- /dev/null
+++ b/test.js
@@ -0,0 +1,3 @@
+'use strict';
+require('dotenv').config();
+require('./examples/dns-01-digitalocean.js');