v1.8: transitional support for v2.0

This commit is contained in:
AJ ONeal 2019-06-14 01:32:54 -06:00
parent f2b6772f5c
commit fced146928
8 changed files with 482 additions and 324 deletions

19
.gitignore vendored
View File

@ -1,32 +1,17 @@
.env
*.pem *.pem
letsencrypt.work
letsencrypt.logs
letsencrypt.config
# Logs # Logs
logs logs
*.log *.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage 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 # Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules node_modules

224
README.md
View File

@ -2,28 +2,77 @@
| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js)
| [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) | [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 The primary goal of this library is to make it easy to
Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. get Accounts and Certificates through Let's Encrypt.
Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8).
\* <small>although `node-forge` and `ursa` are included as `optionalDependencies` # Features
for backwards compatibility with older versions of node, there are no other
dependencies except those that I wrote for this (and related) projects.</small> - [x] Let's Encrypt&trade; v2 / ACME Draft 12
- [ ] (in-progress) Let's Encrypt&trade; v2.1 / ACME Draft 18
- [ ] (in-progress) StartTLS Everywhere&trade;
- [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))
\* <small>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.</small>
## Looking for Quick 'n' Easy&trade;? ## Looking for Quick 'n' Easy&trade;?
If you're looking to _build a webserver_, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). If you want something that's more "batteries included" give
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)
a 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)
## 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
```
<!--
## How to build ACME clients ## 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
@ -63,136 +112,75 @@ examples/https-server.js
examples/http-server.js examples/http-server.js
``` ```
## Let's Encrypt Directory URLs -->
``` ## API
# Production URL
https://acme-v02.api.letsencrypt.org/directory
```
``` Status: Small, but breaking changes coming in v2
# Staging URL
https://acme-staging-v02.api.letsencrypt.org/directory
```
## 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
See Full Documentation at <https://git.coolaj86.com/coolaj86/le-acme-core.js>
```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**
This API is a simple evolution of le-acme-core, This API is a simple evolution of le-acme-core,
but tries to provide a better mapping to the new draft 11 APIs. but tries to provide a better mapping to the new draft 11 APIs.
```js ```js
// Create Instance (Dependency Injection)
var ACME = require('acme-v2').ACME.create({ var ACME = require('acme-v2').ACME.create({
RSA: require('rsa-compat').RSA // used for overriding the default user-agent
userAgent: 'My custom UA String',
getUserAgentString: function(deps) {
return 'My custom UA String';
},
// other overrides // don't try to validate challenges locally
, request: require('request') skipChallengeTest: false,
, promisify: require('util').promisify skipDryRun: false,
// used for constructing user-agent // ask if the certificate can be issued up to 10 times before failing
, os: require('os') retryPoll: 8,
, process: require('process') // ask if the certificate has been validated up to 6 times before cancelling
retryPending: 4,
// used for overriding the default user-agent // Wait 1000ms between retries
, userAgent: 'My custom UA String' retryInterval: 1000,
, getUserAgentString: function (deps) { return 'My custom UA String'; } // Wait 10,000ms after deauthorizing a challenge before retrying
deauthWait: 10 * 1000
// 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
}); });
// Discover Directory URLs // Discover Directory URLs
ACME.init(acmeDirectoryUrl) // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
// Accounts // Accounts
ACME.accounts.create(options) // returns Promise<regr> registration data ACME.accounts.create(options); // returns Promise<regr> registration data
{ email: '<email>' // valid email (server checks MX records)
, accountKeypair: { // privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>'
}
, agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl
}
options = {
email: '<email>', // valid email (server checks MX records)
accountKeypair: {
// privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>'
},
agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
};
// Registration // Registration
ACME.certificates.create(options) // returns Promise<pems={ privkey (key), cert, chain (ca) }> ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
{ newAuthzUrl: '<url>' // specify acmeUrls.newAuthz options = {
, newCertUrl: '<url>' // specify acmeUrls.newCert domainKeypair: {
privateKeyPem: '<ASCII PEM>'
},
accountKeypair: {
privateKeyPem: '<ASCII PEM>'
},
domains: ['example.com'],
, domainKeypair: { getZones: function(opts) {}, // should Promise an array of domain zone names
privateKeyPem: '<ASCII PEM>' setChallenge: function(opts) {}, // should Promise the record id, or name
} removeChallenge: function(opts) {} // should Promise null
, accountKeypair: { };
privateKeyPem: '<ASCII PEM>'
}
, 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'
``` ```
# Changelog # Changelog
- v1.8
- more transitional prepwork for new v2 API
- support newer (simpler) dns-01 and http-01 libraries
- v1.5 - v1.5
- perform full test challenge first (even before nonce) - perform full test challenge first (even before nonce)
- v1.3 - v1.3

View File

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

3
examples/example.env Normal file
View File

@ -0,0 +1,3 @@
ACME_EMAIL=jon.doe@gmail.com
ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

451
node.js
View File

@ -276,7 +276,10 @@ ACME._registerAccount = function(me, options) {
} }
if (1 === options.agreeToTerms.length) { if (1 === options.agreeToTerms.length) {
// newer promise API // 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) { } else if (2 === options.agreeToTerms.length) {
// backwards compat cb API // backwards compat cb API
return options.agreeToTerms(me._tos, function(err, tosUrl) { return options.agreeToTerms(me._tos, function(err, tosUrl) {
@ -461,6 +464,58 @@ ACME._chooseChallenge = function(options, results) {
return challenge; 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) { ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
// we don't poison the dns cache with our dummy request // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
@ -490,6 +545,7 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
auth[key] = challenge[key]; auth[key] = challenge[key];
}); });
var zone = pluckZone(options.zonenames || [], auth.identifier.value);
// batteries-included helpers // batteries-included helpers
auth.hostname = auth.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 // 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) .update(auth.keyAuthorization)
.digest('base64') .digest('base64')
); );
if (zone) {
auth.dnsZone = zone;
auth.dnsPrefix = auth.dnsHost
.replace(newZoneRegExp(zone), '')
.replace(/\.$/, '');
}
// for backwards compat
auth.challenge = auth;
return auth; return auth;
}; };
@ -997,187 +1061,204 @@ ACME._getCertificate = function(me, options) {
} }
} }
// Do a little dry-run / self-test var dnsHosts = options.domains.map(function(d) {
return ACME._testChallenges(me, options).then(function() { return (
if (me.debug) { require('crypto')
console.debug('[acme-v2] certificates.create'); .randomBytes(2)
} .toString('hex') + d
return ACME._getNonce(me).then(function() { );
var body = { });
// raw wildcard syntax MUST be used here return ACME._getZones(me, options, dnsHosts).then(function(zonenames) {
identifiers: options.domains options.zonenames = zonenames;
.sort(function(a, b) { // Do a little dry-run / self-test
// the first in the list will be the subject of the certificate, I believe (and hope) return ACME._testChallenges(me, options).then(function() {
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) { if (me.debug) {
console.debug('\n[DEBUG] newOrder\n'); console.debug('[acme-v2] certificates.create');
} }
me._nonce = null; return ACME._getNonce(me).then(function() {
return me var body = {
._request({ // raw wildcard syntax MUST be used here
method: 'POST', identifiers: options.domains
url: me._directoryUrls.newOrder, .sort(function(a, b) {
headers: { 'Content-Type': 'application/jose+json' }, // the first in the list will be the subject of the certificate, I believe (and hope)
json: jws if (!options.subject) {
}) return 0;
.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();
} }
if (options.subject === a) {
var challenge = ACME._chooseChallenge(options, results); return -1;
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 (options.subject === b) {
var auth = ACME._challengeToAuth(me, options, results, challenge); return 1;
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 0;
return ident.value;
});
return ACME._finalizeOrder(me, options, validatedDomains);
}) })
.then(function(order) { .map(function(hostname) {
if (me.debug) { return { type: 'dns', value: hostname };
console.debug('acme-v2: order was finalized'); })
//, "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 me
._request({ method: 'GET', url: me._certificate, json: true }) return ACME._getChallenges(me, options, authUrl).then(function(
.then(function(resp) { results
if (me.debug) { ) {
console.debug('acme-v2: csr submitted and cert received:'); // var domain = options.domains[i]; // results.identifier.value
}
// https://github.com/certbot/certbot/issues/5721 // If it's already valid, we're golden it regardless
var certsarr = ACME.splitPemChain( if (
ACME.formatPemChain(resp.body || '') 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() +
"'."
)
); );
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ }
var certs = {
expires: order.expires, var auth = ACME._challengeToAuth(
identifiers: order.identifiers, me,
//, authorizations: order.authorizations options,
cert: certsarr.shift(), results,
//, privkey: privkeyPem challenge
chain: certsarr.join('\n') );
}; auths.push(auth);
if (me.debug) { return ACME._setChallenge(me, options, auth).then(setNext);
console.debug(certs); });
} }
return certs;
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');
}
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.challengePrefixes = ACME.challengePrefixes;
me.RSA = me.RSA || require('rsa-compat').RSA; me.RSA = me.RSA || require('rsa-compat').RSA;
//me.Keypairs = me.Keypairs || require('keypairs'); //me.Keypairs = me.Keypairs || require('keypairs');
me.request = me.request || require('@coolaj86/urequest'); me.request = me.request || require('@root/request');
me._dig = function(query) { me._dig = function(query) {
// TODO use digd.js // TODO use digd.js
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -1241,7 +1322,27 @@ ACME.create = function create(me) {
} }
me.init = function(_directoryUrl) { 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) { return ACME._directory(me).then(function(resp) {
me._directoryUrls = resp.body; me._directoryUrls = resp.body;
me._tos = me._directoryUrls.meta.termsOfService; me._tos = me._directoryUrls.meta.termsOfService;

22
package-lock.json generated
View File

@ -1,13 +1,19 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.7.6", "version": "1.8.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@coolaj86/urequest": { "@root/request": {
"version": "1.3.7", "version": "1.3.11",
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" "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": { "eckles": {
"version": "1.4.1", "version": "1.4.1",
@ -29,9 +35,9 @@
"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="
}, },
"rsa-compat": { "rsa-compat": {
"version": "2.0.6", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
"integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", "integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
"requires": { "requires": {
"keypairs": "^1.2.14" "keypairs": "^1.2.14"
} }

View File

@ -1,11 +1,11 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.7.7", "version": "1.8.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", "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", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
"main": "node.js", "main": "node.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "node ./test.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -23,10 +23,13 @@
"automated https", "automated https",
"letsencrypt" "letsencrypt"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@coolaj86/urequest": "^1.3.6", "@root/request": "^1.3.11",
"rsa-compat": "^2.0.6" "rsa-compat": "^2.0.8"
},
"devDependencies": {
"dotenv": "^8.0.0"
} }
} }

3
test.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
require('dotenv').config();
require('./examples/dns-01-digitalocean.js');