Files
cloudron-box/src/cert/acme2.js

556 lines
23 KiB
JavaScript
Raw Normal View History

2018-09-10 15:19:10 -07:00
'use strict';
2021-05-06 22:29:34 -07:00
const assert = require('assert'),
2018-09-10 15:19:10 -07:00
async = require('async'),
2019-09-25 14:13:10 -07:00
BoxError = require('../boxerror.js'),
2018-09-10 15:19:10 -07:00
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
2018-09-10 20:50:36 -07:00
domains = require('../domains.js'),
2018-09-10 15:19:10 -07:00
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
2021-05-06 22:29:34 -07:00
promiseRetry = require('../promise-retry.js'),
superagent = require('superagent'),
2018-09-10 15:19:10 -07:00
safe = require('safetydance'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
2021-04-29 15:25:14 -07:00
getCertificate,
2018-09-10 15:19:10 -07:00
// testing
2018-10-31 15:41:02 -07:00
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
2018-09-10 15:19:10 -07:00
};
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = options.accountKeyPem; // Buffer
2018-09-10 15:19:10 -07:00
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
2018-09-10 20:50:36 -07:00
this.performHttpAuthorization = !!options.performHttpAuthorization;
2018-09-11 22:46:17 -07:00
this.wildcard = !!options.wildcard;
2018-09-10 15:19:10 -07:00
}
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
2021-04-29 15:37:32 -07:00
var buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
2018-09-10 15:19:10 -07:00
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
2021-04-29 15:37:32 -07:00
assert(Buffer.isBuffer(pem));
2018-09-10 15:19:10 -07:00
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
2018-09-10 15:19:10 -07:00
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
2021-05-06 22:29:34 -07:00
Acme2.prototype.sendSignedRequest = async function (url, payload) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
2021-04-29 15:25:14 -07:00
assert(Buffer.isBuffer(this.accountKeyPem));
2018-09-10 15:19:10 -07:00
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
2021-05-06 22:29:34 -07:00
const payload64 = b64(payload);
2021-05-06 22:29:34 -07:00
const response = await safe(superagent.get(this.directory.newNonce).timeout(30000).ok(() => true));
if (!response) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${safe.error.message}`);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response');
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
const signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const data = {
protected: protected64,
payload: payload64,
signature: signature64
};
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const response2 = await safe(superagent.post(url).send(data).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').timeout(30000).ok(() => true));
if (!response2) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${safe.error.message}`);
2021-05-06 22:29:34 -07:00
return response2;
2018-09-10 15:19:10 -07:00
};
// https://tools.ietf.org/html/rfc8555#section-6.3
2021-05-06 22:29:34 -07:00
Acme2.prototype.postAsGet = async function (url) {
return await this.sendSignedRequest(url, '');
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.updateContact = async function (registrationUri) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof registrationUri, 'string');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
2021-05-06 22:29:34 -07:00
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug(`updateContact: contact of user updated to ${this.email}`);
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.registerUser = async function () {
const payload = {
2018-09-10 15:19:10 -07:00
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
2021-05-06 22:29:34 -07:00
const result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
// 200 if already exists. 201 for new accounts
if (result.status !== 200 && result.status !== 201) return new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug(`registerUser: user registered keyid: ${result.headers.location}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
this.keyId = result.headers.location;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
await this.updateContact(result.headers.location);
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.newOrder = async function (domain) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof domain, 'string');
2021-05-06 22:29:34 -07:00
const payload = {
2018-09-10 15:19:10 -07:00
identifiers: [{
type: 'dns',
value: domain
}]
};
2021-05-06 22:29:34 -07:00
debug(`newOrder: ${domain}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
if (result.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug('newOrder: created order %s %j', domain, result.body);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const order = result.body, orderUrl = result.headers.location;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header');
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
return { order, orderUrl };
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.waitForOrder = async function (orderUrl) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof orderUrl, 'string');
debug(`waitForOrder: ${orderUrl}`);
2021-05-06 22:29:34 -07:00
return await promiseRetry({ times: 15, interval: 20000 }, async () => {
2018-09-10 15:19:10 -07:00
debug('waitForOrder: getting status');
2021-05-06 22:29:34 -07:00
const result = await safe(this.postAsGet(orderUrl));
if (!result) {
debug(`waitForOrder: network error getting uri ${orderUrl}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `Network error: ${safe.error.message}`);
}
if (result.status !== 200) {
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response code: ${result.status}`);
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`);
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status or invalid response: ${JSON.stringify(result.body)}`);
});
2018-09-10 15:19:10 -07:00
};
2018-09-10 20:50:36 -07:00
Acme2.prototype.getKeyAuthorization = function (token) {
2021-04-29 15:25:14 -07:00
assert(Buffer.isBuffer(this.accountKeyPem));
2018-09-10 15:19:10 -07:00
2018-09-10 20:50:36 -07:00
let jwk = {
2018-09-10 15:19:10 -07:00
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
2018-09-10 20:50:36 -07:00
let shasum = crypto.createHash('sha256');
2018-09-10 15:19:10 -07:00
shasum.update(JSON.stringify(jwk));
2018-09-10 20:50:36 -07:00
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.notifyChallengeReady = async function (challenge) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
debug('notifyChallengeReady: %s was met', challenge.url);
2018-09-10 20:50:36 -07:00
const keyAuthorization = this.getKeyAuthorization(challenge.token);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const payload = {
2018-09-10 15:19:10 -07:00
resource: 'challenge',
keyAuthorization: keyAuthorization
};
2021-05-06 22:29:34 -07:00
const result = await this.sendSignedRequest(challenge.url, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.waitForChallenge = async function (challenge) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof challenge, 'object');
debug('waitingForChallenge: %j', challenge);
2021-05-06 22:29:34 -07:00
await promiseRetry({ times: 15, interval: 20000 }, async () => {
2018-09-10 15:19:10 -07:00
debug('waitingForChallenge: getting status');
2021-05-06 22:29:34 -07:00
const result = await this.postAsGet(challenge.url);
if (!result) {
debug(`waitForChallenge: network error getting uri ${challenge.url}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `network error: ${safe.error.message}`);
}
if (result.status !== 200) {
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode);
}
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
if (result.body.status === 'pending') throw new BoxError(BoxError.TRY_AGAIN);
else if (result.body.status === 'valid') return;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status: ${result.body.status}`);
2018-09-10 15:19:10 -07:00
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
2021-05-06 22:29:34 -07:00
Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDer) {
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
2021-04-29 15:37:32 -07:00
assert(Buffer.isBuffer(csrDer));
2018-09-10 15:19:10 -07:00
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
2021-05-06 22:29:34 -07:00
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
// 429 means we reached the cert limit for this domain
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.createKeyAndCsr = async function (hostname) {
2018-09-11 22:46:17 -07:00
assert.strictEqual(typeof hostname, 'string');
2018-09-10 15:19:10 -07:00
const outdir = paths.APP_CERTS_DIR;
2018-09-11 22:46:17 -07:00
const certName = hostname.replace('*.', '_.');
const csrFile = path.join(outdir, `${certName}.csr`);
const privateKeyFile = path.join(outdir, `${certName}.key`);
2018-09-10 15:19:10 -07:00
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
let key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
2021-05-06 22:29:34 -07:00
if (!key) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(privateKeyFile, key)) throw new BoxError(BoxError.FS_ERROR, safe.error);
2018-09-10 15:19:10 -07:00
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
// OCSP must-staple is currently disabled because nginx does not provide staple on the first request (https://forum.cloudron.io/topic/4917/ocsp-stapling-for-tls-ssl/)
// ' -addext "tlsfeature = status_request"'; // this adds OCSP must-staple
const extensionArgs = `-addext "subjectAltName = DNS:${hostname}"`
+ ' -addext "basicConstraints = CA:FALSE"' // this is not for a CA cert. cannot sign other certs with this
+ ' -addext "keyUsage = nonRepudiation, digitalSignature, keyEncipherment"';
const csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname} ${extensionArgs}`);
2021-05-06 22:29:34 -07:00
if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(csrFile, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
2018-09-10 15:19:10 -07:00
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
2021-05-06 22:29:34 -07:00
return csrDer;
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.downloadCertificate = async function (hostname, certUrl) {
2018-09-11 22:46:17 -07:00
assert.strictEqual(typeof hostname, 'string');
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof certUrl, 'string');
2021-05-06 22:29:34 -07:00
const outdir = paths.APP_CERTS_DIR;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
await promiseRetry({ times: 5, interval: 20000 }, async () => {
2019-10-03 14:37:12 -07:00
debug('downloadCertificate: downloading certificate');
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const result = await safe(this.postAsGet(certUrl));
if (!result) throw new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${safe.error.message}`);
if (result.statusCode === 202) throw new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const fullChainPem = result.body; // buffer
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const certName = hostname.replace('*.', '_.');
const certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug(`downloadCertificate: cert file for ${hostname} saved at ${certificateFile}`);
});
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authorization) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
2021-04-29 15:25:14 -07:00
debug('prepareHttpChallenge: challenges: %j', authorization);
2018-09-10 20:50:36 -07:00
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
2021-05-06 22:29:34 -07:00
if (httpChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges');
2018-09-10 20:50:36 -07:00
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
2021-05-06 22:29:34 -07:00
await safe(fs.promises.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization));
if (safe.error) throw new BoxError(BoxError.FS_ERROR, safe.error);
return challenge;
2018-09-10 20:50:36 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.cleanupHttpChallenge = async function (hostname, domain, challenge) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
2018-09-28 17:05:53 -07:00
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
2021-05-06 22:29:34 -07:00
await fs.promises.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
2018-09-10 20:50:36 -07:00
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
2018-10-31 15:41:02 -07:00
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
2018-10-31 15:41:02 -07:00
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
2021-05-06 22:29:34 -07:00
Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authorization) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
2021-04-29 15:25:14 -07:00
debug('prepareDnsChallenge: challenges: %j', authorization);
2021-05-06 22:29:34 -07:00
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges');
const challenge = dnsChallenges[0];
2018-09-10 20:50:36 -07:00
const keyAuthorization = this.getKeyAuthorization(challenge.token);
2021-05-06 22:29:34 -07:00
const shasum = crypto.createHash('sha256');
2018-09-10 20:50:36 -07:00
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
2021-05-06 22:29:34 -07:00
const challengeSubdomain = getChallengeSubdomain(hostname, domain);
2018-09-10 20:50:36 -07:00
2018-09-11 22:46:17 -07:00
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
return new Promise((resolve, reject) => {
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return reject(error);
2018-09-11 19:23:10 -07:00
2021-05-06 22:29:34 -07:00
resolve(challenge);
});
2018-09-11 19:23:10 -07:00
});
2018-09-10 20:50:36 -07:00
});
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challenge) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
2018-09-10 20:50:36 -07:00
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
return new Promise((resolve, reject) => {
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
resolve(null);
});
2018-09-10 20:50:36 -07:00
});
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
2019-10-03 10:36:57 -07:00
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
2021-05-06 22:29:34 -07:00
const response = await this.postAsGet(authorizationUrl);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code getting authorization : ${response.status}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const authorization = response.body;
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
if (this.performHttpAuthorization) {
return await this.prepareHttpChallenge(hostname, domain, authorization);
} else {
return await this.prepareDnsChallenge(hostname, domain, authorization);
}
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.cleanupChallenge = async function (hostname, domain, challenge) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
2019-10-03 10:36:57 -07:00
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
2018-09-10 20:50:36 -07:00
if (this.performHttpAuthorization) {
2021-05-06 22:29:34 -07:00
await this.cleanupHttpChallenge(hostname, domain, challenge);
2018-09-10 20:50:36 -07:00
} else {
2021-05-06 22:29:34 -07:00
await this.cleanupDnsChallenge(hostname, domain, challenge);
2018-09-10 20:50:36 -07:00
}
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.acmeFlow = async function (hostname, domain) {
2018-09-10 20:50:36 -07:00
assert.strictEqual(typeof hostname, 'string');
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof domain, 'string');
2021-05-06 22:29:34 -07:00
await this.registerUser();
const { order, orderUrl } = await this.newOrder(hostname);
for (let i = 0; i < order.authorizations.length; i++) {
const authorizationUrl = order.authorizations[i];
debug(`acmeFlow: authorizing ${authorizationUrl}`);
const challenge = await this.prepareChallenge(hostname, domain, authorizationUrl);
await this.notifyChallengeReady(challenge);
await this.waitForChallenge(challenge);
const csrDer = await this.createKeyAndCsr(hostname);
await this.signCertificate(hostname, order.finalize, csrDer);
const certUrl = await this.waitForOrder(orderUrl);
await this.downloadCertificate(hostname, certUrl);
try {
await this.cleanupChallenge(hostname, domain, challenge);
} catch (cleanupError) {
debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
}
}
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.loadDirectory = async function () {
await promiseRetry({ times: 3, interval: 20000 }, async () => {
const response = await safe(superagent.get(this.caDirectory).timeout(30000).ok(() => true));
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
if (!response) throw new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${safe.error.message}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching directory : ${response.status}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
this.directory = response.body;
});
2018-09-10 15:19:10 -07:00
};
2021-05-06 22:29:34 -07:00
Acme2.prototype.getCertificate = async function (vhost, domain) {
assert.strictEqual(typeof vhost, 'string');
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof domain, 'string');
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
2018-09-10 15:19:10 -07:00
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
vhost = domains.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
2018-09-11 22:46:17 -07:00
}
2021-05-06 22:29:34 -07:00
await this.loadDirectory();
await this.acmeFlow(vhost, domain);
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const outdir = paths.APP_CERTS_DIR;
const certName = vhost.replace('*.', '_.');
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
return { certFilePath: path.join(outdir, `${certName}.cert`), keyFilePath: path.join(outdir, `${certName}.key`) };
2018-09-10 15:19:10 -07:00
};
function getCertificate(vhost, domain, options, callback) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
2018-09-10 15:19:10 -07:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
2019-10-03 14:47:18 -07:00
let attempt = 1;
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
2021-05-06 22:29:34 -07:00
acme.getCertificate(vhost, domain).then((result) => {
callback(null, result.certFilePath, result.keyFilePath);
}).catch(retryCallback);
2019-10-03 14:47:18 -07:00
}, callback);
2018-09-10 15:19:10 -07:00
}