From fced1469280cff49ed53483b9a24f1550956b8e2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 14 Jun 2019 01:32:54 -0600 Subject: [PATCH] v1.8: transitional support for v2.0 --- .gitignore | 19 +- README.md | 224 ++++++++-------- examples/dns-01-digitalocean.js | 69 +++++ examples/example.env | 3 + node.js | 451 +++++++++++++++++++------------- package-lock.json | 22 +- package.json | 15 +- test.js | 3 + 8 files changed, 482 insertions(+), 324 deletions(-) create mode 100644 examples/dns-01-digitalocean.js create mode 100644 examples/example.env create mode 100644 test.js 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 +``` + + -``` -# Production URL -https://acme-v02.api.letsencrypt.org/directory -``` +## API -``` -# 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 - -```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 + // used for overriding the default user-agent + userAgent: 'My custom UA String', + getUserAgentString: function(deps) { + return 'My custom UA String'; + }, - // other overrides -, request: require('request') -, promisify: require('util').promisify + // don't try to validate challenges locally + skipChallengeTest: false, + skipDryRun: false, - // 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 + // 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 +ACME.certificates.create(options); // returns Promise - { newAuthzUrl: '' // specify acmeUrls.newAuthz - , newCertUrl: '' // specify acmeUrls.newCert +options = { + domainKeypair: { + privateKeyPem: '' + }, + accountKeypair: { + privateKeyPem: '' + }, + domains: ['example.com'], - , 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' + 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); - - 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(); + 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; } - - 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 (options.subject === a) { + return -1; } - - 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; - } - 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'); + if (options.subject === b) { + return 1; } - var validatedDomains = body.identifiers.map(function(ident) { - return ident.value; - }); - - return ACME._finalizeOrder(me, options, validatedDomains); + return 0; }) - .then(function(order) { - if (me.debug) { - console.debug('acme-v2: order was finalized'); + .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 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 || '') + + 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() + + "'." + ) ); - // 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; + } + + 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; + } + 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.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');