From c4db0d746df5fd5bbd1b18ded305250224ee063d Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Tue, 16 Nov 2021 22:56:35 -0800 Subject: [PATCH] acme: if account key was revoked, generate new account key the plan was to migrate only specific keys but this allows us the flexibility to revoke keys after the release (since we have not gotten response from DO about access to old 1-click images so far). --- ...211116080624-regenerate-le-account-keys.js | 28 ---------------- src/acme2.js | 33 +++++++++++++++---- src/blobs.js | 4 --- src/reverseproxy.js | 10 ++---- 4 files changed, 29 insertions(+), 46 deletions(-) delete mode 100644 migrations/20211116080624-regenerate-le-account-keys.js diff --git a/migrations/20211116080624-regenerate-le-account-keys.js b/migrations/20211116080624-regenerate-le-account-keys.js deleted file mode 100644 index 972643acb..000000000 --- a/migrations/20211116080624-regenerate-le-account-keys.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const child_process = require('child_process'); - -// DO 1-click images ended up having the same ACME key. We regenerate the shared ones. -// https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441 -exports.up = function(db, callback) { - db.all('SELECT SHA2(value, 256) AS sha256key FROM blobs WHERE id = ?', [ 'acme_account_key' ], function (error, result) { - if (error) return callback(error); - if (result.length === 0) return callback(); - - const BAD_KEYS = [ - '37d6c4c4bf8aaceed57cb9097edb38a6641eae4324ca383774d732ea3b81c269', /* 7.0.3 DO */ - '6498621a2ba9106fc1f1d903df802d82ca3570004a59776ca63920ad3a7837a0', /* 6.3.5 Vultr */ - 'd47957b86cbf66279ba9fb09cfd3d96e753ed84ed18060b1f0488e5d4ca4d4ce', /* 6.3.6 Vultr */ - '195a4335b95f3f183b85c995d4d4a1ddaae6ff04d03e1ceb5537b00b062fd6c1', /* 7.0.3 Vultr */ - ]; - - if (!BAD_KEYS.includes(result[0].sha256key)) return callback(); - - const acmeAccountKey = child_process.execSync('openssl genrsa 4096'); - db.runSql('UPDATE blobs SET value = ? WHERE id = ?', [ acmeAccountKey, 'acme_account_key' ], callback); - }); -}; - -exports.down = function(db, callback) { - callback(); -}; diff --git a/src/acme2.js b/src/acme2.js index 89c5b6686..1a2f28c5b 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -9,6 +9,7 @@ exports = module.exports = { }; const assert = require('assert'), + blobs = require('./blobs.js'), BoxError = require('./boxerror.js'), crypto = require('crypto'), debug = require('debug')('box:cert/acme2'), @@ -31,7 +32,7 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory', function Acme2(options) { assert.strictEqual(typeof options, 'object'); - this.accountKeyPem = options.accountKeyPem; // Buffer + this.accountKeyPem = null; // Buffer . this.email = options.email; this.keyId = null; this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; @@ -133,18 +134,38 @@ Acme2.prototype.updateContact = async function (registrationUri) { debug(`updateContact: contact of user updated to ${this.email}`); }; -Acme2.prototype.registerUser = async function () { +async function generateAccountKey() { + const acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096'); + if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`); + return acmeAccountKey; +} + +Acme2.prototype.ensureAccount = async function () { const payload = { termsOfServiceAgreed: true }; - debug('registerUser: registering user'); + debug('ensureAccount: registering user'); + + this.accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY); + if (!this.accountKeyPem) { + debug('ensureAccount: generating new account keys'); + this.accountKeyPem = await generateAccountKey(); + await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem); + } + + let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload)); + if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') { + debug(`ensureAccount: key was revoked. ${result.status} ${JSON.stringify(result.body)}. generating new account key`); + this.accountKeyPem = await generateAccountKey(); + await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem); + result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload)); + } - 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) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`); - debug(`registerUser: user registered keyid: ${result.headers.location}`); + debug(`ensureAccount: user registered keyid: ${result.headers.location}`); this.keyId = result.headers.location; @@ -464,7 +485,7 @@ Acme2.prototype.acmeFlow = async function (hostname, domain, paths) { const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths; - await this.registerUser(); + await this.ensureAccount(); const { order, orderUrl } = await this.newOrder(hostname); for (let i = 0; i < order.authorizations.length; i++) { diff --git a/src/blobs.js b/src/blobs.js index 056ca40bc..bd6347804 100644 --- a/src/blobs.js +++ b/src/blobs.js @@ -54,10 +54,6 @@ async function clear() { } async function generateSecrets() { - const acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096'); - if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`); - await set(exports.ACME_ACCOUNT_KEY, acmeAccountKey); - debug('generateSecrets: generating dhparams.pem'); // https://security.stackexchange.com/questions/95178/diffie-hellman-parameters-still-calculating-after-24-hours const dhparams = safe.child_process.execSync('openssl dhparam -dsaparam 2048'); diff --git a/src/reverseproxy.js b/src/reverseproxy.js index be8aa7818..dadb216a6 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -51,8 +51,7 @@ const acme2 = require('./acme2.js'), shell = require('./shell.js'), sysinfo = require('./sysinfo.js'), users = require('./users.js'), - util = require('util'), - _ = require('underscore'); + util = require('util'); const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); @@ -82,11 +81,6 @@ async function getAcmeApi(domainObject) { const [error, owner] = await safe(users.getOwner()); apiOptions.email = (error || !owner) ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet - const accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY); - if (!accountKeyPem) throw new BoxError(BoxError.NOT_FOUND, 'acme account key not found'); - - apiOptions.accountKeyPem = accountKeyPem; - return { acmeApi, apiOptions }; } @@ -412,7 +406,7 @@ async function ensureCertificate(vhost, domain, auditSource) { debug(`ensureCertificate: ${vhost} cert does not exist`); } - debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); + debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions); const acmePaths = getAcmeCertificatePathSync(vhost, domainObject); let [error] = await safe(acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions));