consolidate acme paths in the reverseproxy code
This commit is contained in:
+39
-64
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user