consolidate acme paths in the reverseproxy code

This commit is contained in:
Girish Ramakrishnan
2021-05-07 22:43:30 -07:00
parent dea31109e2
commit 302ea60b8d
2 changed files with 69 additions and 70 deletions
+39 -64
View File
@@ -2,14 +2,12 @@
const assert = require('assert'),
async = require('async'),
blobs = require('../blobs.js'),
BoxError = require('../boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
promiseRetry = require('../promise-retry.js'),
superagent = require('superagent'),
safe = require('safetydance'),
@@ -272,36 +270,17 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.createKeyAndCsr = async function (hostname) {
Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) {
assert.strictEqual(typeof hostname, 'string');
const outdir = paths.NGINX_CERT_DIR;
const certName = hostname.replace('*.', '_.');
const csrFile = path.join(outdir, `${certName}.csr`);
const privateKeyFile = path.join(outdir, `${certName}.key`);
let privateKey = await blobs.get(`${blobs.CERT_PREFIX}-${certName}.key`);
if (!privateKeyFile) {
debug(`createKeyAndCsr: reuse the key for renewal at ${privateKeyFile}`);
} else {
debug('createKeyAndCsr: create new key');
privateKey = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!privateKey) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.key`, privateKey);
}
if (!safe.fs.writeFileSync(privateKeyFile, privateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not write private key: ${safe.error.message}`);
debug(`createKeyAndCsr: key file saved at ${privateKeyFile}`);
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);
if (safe.fs.existsSync(keyFilePath)) {
debug('createKeyAndCsr: reuse the key for renewal at %s', keyFilePath);
} else {
let key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!key) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(privateKeyFile, key)) throw new BoxError(BoxError.FS_ERROR, safe.error);
if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, safe.error);
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
debug('createKeyAndCsr: key file saved at %s', keyFilePath);
}
// 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/)
@@ -310,22 +289,19 @@ Acme2.prototype.createKeyAndCsr = async function (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}`);
const csrDer = safe.child_process.execSync(`openssl req -new -key ${keyFilePath} -outform DER -subj /CN=${hostname} ${extensionArgs}`);
if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csrDer);
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
if (!safe.fs.writeFileSync(csrFilePath, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFilePath);
return csrDer;
};
Acme2.prototype.downloadCertificate = async function (hostname, certUrl) {
Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFilePath) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
const outdir = paths.NGINX_CERT_DIR;
await promiseRetry({ times: 5, interval: 20000 }, async () => {
debug('downloadCertificate: downloading certificate');
@@ -335,19 +311,17 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl) {
const fullChainPem = result.body; // buffer
const certName = hostname.replace('*.', '_.');
const certificateFile = path.join(outdir, `${certName}.cert`);
await blobs.set(`${blobs.CERT_PREFIX}-${certName}.cert`, fullChainPem);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error);
if (!safe.fs.writeFileSync(certFilePath, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error);
debug(`downloadCertificate: cert file for ${hostname} saved at ${certificateFile}`);
debug(`downloadCertificate: cert file for ${hostname} saved at ${certFilePath}`);
});
};
Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authorization) {
Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authorization, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug('prepareHttpChallenge: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
@@ -358,21 +332,22 @@ Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authori
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(acmeChallengesDir, challenge.token));
if (!fs.writeFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, safe.error);
if (!fs.writeFileSync(path.join(acmeChallengesDir, challenge.token), keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, safe.error);
return challenge;
};
Acme2.prototype.cleanupHttpChallenge = async function (hostname, domain, challenge) {
Acme2.prototype.cleanupHttpChallenge = async function (hostname, domain, challenge, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
debug('cleanupHttpChallenge: unlinking %s', path.join(acmeChallengesDir, challenge.token));
await fs.promises.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
await fs.promises.unlink(path.join(acmeChallengesDir, challenge.token));
};
function getChallengeSubdomain(hostname, domain) {
@@ -447,10 +422,11 @@ Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challeng
});
};
Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl) {
Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
@@ -460,29 +436,33 @@ Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizati
const authorization = response.body;
if (this.performHttpAuthorization) {
return await this.prepareHttpChallenge(hostname, domain, authorization);
return await this.prepareHttpChallenge(hostname, domain, authorization, acmeChallengesDir);
} else {
return await this.prepareDnsChallenge(hostname, domain, authorization);
}
};
Acme2.prototype.cleanupChallenge = async function (hostname, domain, challenge) {
Acme2.prototype.cleanupChallenge = async function (hostname, domain, challenge, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
if (this.performHttpAuthorization) {
await this.cleanupHttpChallenge(hostname, domain, challenge);
await this.cleanupHttpChallenge(hostname, domain, challenge, acmeChallengesDir);
} else {
await this.cleanupDnsChallenge(hostname, domain, challenge);
}
};
Acme2.prototype.acmeFlow = async function (hostname, domain) {
Acme2.prototype.acmeFlow = async function (hostname, domain, paths) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths;
await this.registerUser();
const { order, orderUrl } = await this.newOrder(hostname);
@@ -491,16 +471,16 @@ Acme2.prototype.acmeFlow = async function (hostname, domain) {
const authorizationUrl = order.authorizations[i];
debug(`acmeFlow: authorizing ${authorizationUrl}`);
const challenge = await this.prepareChallenge(hostname, domain, authorizationUrl);
const challenge = await this.prepareChallenge(hostname, domain, authorizationUrl, acmeChallengesDir);
await this.notifyChallengeReady(challenge);
await this.waitForChallenge(challenge);
const csrDer = await this.createKeyAndCsr(hostname);
const csrDer = await this.createKeyAndCsr(hostname, keyFilePath, csrFilePath);
await this.signCertificate(hostname, order.finalize, csrDer);
const certUrl = await this.waitForOrder(orderUrl);
await this.downloadCertificate(hostname, certUrl);
await this.downloadCertificate(hostname, certUrl, certFilePath);
try {
await this.cleanupChallenge(hostname, domain, challenge);
await this.cleanupChallenge(hostname, domain, challenge, acmeChallengesDir);
} catch (cleanupError) {
debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
}
@@ -521,9 +501,10 @@ Acme2.prototype.loadDirectory = async function () {
});
};
Acme2.prototype.getCertificate = async function (vhost, domain) {
Acme2.prototype.getCertificate = async function (vhost, domain, paths) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
@@ -533,17 +514,13 @@ Acme2.prototype.getCertificate = async function (vhost, domain) {
}
await this.loadDirectory();
await this.acmeFlow(vhost, domain);
const outdir = paths.NGINX_CERT_DIR;
const certName = vhost.replace('*.', '_.');
return { certFilePath: path.join(outdir, `${certName}.cert`), keyFilePath: path.join(outdir, `${certName}.key`) };
await this.acmeFlow(vhost, domain, paths);
};
function getCertificate(vhost, domain, options, callback) {
function getCertificate(vhost, domain, paths, options, callback) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -552,8 +529,6 @@ function getCertificate(vhost, domain, options, callback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(vhost, domain).then((result) => {
callback(null, result.certFilePath, result.keyFilePath);
}).catch(retryCallback);
acme.getCertificate(vhost, domain, paths).then(callback).catch(retryCallback);
}, callback);
}