diff --git a/src/acme2.js b/src/acme2.js index 1631207fa..6e68fc744 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -21,6 +21,7 @@ const assert = require('assert'), promiseRetry = require('./promise-retry.js'), superagent = require('superagent'), safe = require('safetydance'), + users = require('./users.js'), _ = require('underscore'); const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory', @@ -30,16 +31,26 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory', // 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'); +function Acme2(fqdn, domainObject, email) { + assert.strictEqual(typeof fqdn, 'string'); + assert.strictEqual(typeof domainObject, 'object'); + assert.strictEqual(typeof email, 'string'); + this.fqdn = fqdn; this.accountKeyPem = null; // Buffer . - this.email = options.email; + this.email = email; this.keyId = null; - this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; + const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' + this.caDirectory = prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; this.directory = {}; - this.performHttpAuthorization = !!options.performHttpAuthorization; - this.wildcard = !!options.wildcard; + this.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null; + this.wildcard = !!domainObject.tlsConfig.wildcard; + this.domain = domainObject.domain; + + this.cn = fqdn !== this.domain && this.wildcard ? dns.makeWildcard(fqdn) : fqdn; // bare domain is not part of wildcard SAN + this.certName = this.cn.replace('*.', '_.'); + + debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.performHttpAuthorization}`); } // urlsafe base64 encoding (jose) @@ -173,23 +184,21 @@ Acme2.prototype.ensureAccount = async function () { await this.updateContact(result.headers.location); }; -Acme2.prototype.newOrder = async function (domain) { - assert.strictEqual(typeof domain, 'string'); - +Acme2.prototype.newOrder = async function () { const payload = { identifiers: [{ type: 'dns', - value: domain + value: this.cn }] }; - debug(`newOrder: ${domain}`); + debug(`newOrder: ${this.cn}`); 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.ACME_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`); - debug('newOrder: created order %s %j', domain, result.body); + debug(`newOrder: created order ${this.cn} %j`, result.body); const order = result.body, orderUrl = result.headers.location; @@ -276,8 +285,7 @@ Acme2.prototype.waitForChallenge = async function (challenge) { }; // https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits -Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDer) { - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.signCertificate = async function (finalizationUrl, csrDer) { assert.strictEqual(typeof finalizationUrl, 'string'); assert(Buffer.isBuffer(csrDer)); @@ -292,22 +300,28 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`); }; -Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) { - assert.strictEqual(typeof hostname, 'string'); - - 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(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, safe.error); - - debug('createKeyAndCsr: key file saved at %s', keyFilePath); +Acme2.prototype.ensureKey = async function () { + const key = await blobs.get(`${blobs.CERT_PREFIX}-${this.certName}.key`); + if (key) { + debug(`ensureKey: reuse existing key for ${this.cn}`); + return key; } + debug(`ensureKey: generating new key for ${this.cn}`); + const newKey = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves + if (!newKey) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); + return newKey; +}; + +Acme2.prototype.createCsr = async function (key) { + assert(Buffer.isBuffer(key)); + const [error, tmpdir] = await safe(fs.promises.mkdtemp(path.join(os.tmpdir(), 'acme-'))); if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating temporary directory for openssl config: ${error.message}`); + const keyFilePath = path.join(tmpdir, 'key'); + if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write key file: ${safe.error.message}`); + // 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 // we used to use -addext to the CLI to add these but that arg doesn't work on Ubuntu 16.04 @@ -315,47 +329,37 @@ Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFile const conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n' + '[req_distinguished_name]\n\n' + '[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n' - + `[alt_names]\nDNS.1 = ${hostname}\n`; + + `[alt_names]\nDNS.1 = ${this.cn}\n`; const opensslConfigFile = path.join(tmpdir, 'openssl.conf'); if (!safe.fs.writeFileSync(opensslConfigFile, conf)) throw new BoxError(BoxError.FS_ERROR, `Failed to write openssl config: ${safe.error.message}`); // while we pass the CN anyways, subjectAltName takes precedence - const csrDer = safe.child_process.execSync(`openssl req -new -key ${keyFilePath} -outform DER -subj /CN=${hostname} -config ${opensslConfigFile}`); + const csrDer = safe.child_process.execSync(`openssl req -new -key ${keyFilePath} -outform DER -subj /CN=${this.cn} -config ${opensslConfigFile}`); if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error); - 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 await safe(fs.promises.rm(tmpdir, { recursive: true, force: true })); - - debug('createKeyAndCsr: csr file (DER) saved at %s', csrFilePath); - - return csrDer; + debug(`createCsr: csr file created for ${this.cn}`); + return csrDer; // inspect with openssl req -text -noout -in hostname.csr -inform der }; -Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFilePath) { - assert.strictEqual(typeof hostname, 'string'); +Acme2.prototype.downloadCertificate = async function (certUrl) { assert.strictEqual(typeof certUrl, 'string'); - await promiseRetry({ times: 5, interval: 20000, debug }, async () => { - debug(`downloadCertificate: downloading certificate of ${hostname}`); + return await promiseRetry({ times: 5, interval: 20000, debug }, async () => { + debug(`downloadCertificate: downloading certificate of ${this.cn}`); const result = await this.postAsGet(certUrl); if (result.statusCode === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate'); if (result.statusCode !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`); const fullChainPem = result.body; // buffer - - if (!safe.fs.writeFileSync(certFilePath, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error); - - debug(`downloadCertificate: cert file for ${hostname} saved at ${certFilePath}`); + return fullChainPem; }); }; -Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authorization, acmeChallengesDir) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.prepareHttpChallenge = async function (authorization) { 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'; }); @@ -366,44 +370,39 @@ Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authori let keyAuthorization = this.getKeyAuthorization(challenge.token); - debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(acmeChallengesDir, challenge.token)); + debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token)); - if (!safe.fs.writeFileSync(path.join(acmeChallengesDir, challenge.token), keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`); + if (!safe.fs.writeFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`); return challenge; }; -Acme2.prototype.cleanupHttpChallenge = async function (hostname, domain, challenge, acmeChallengesDir) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.cleanupHttpChallenge = async function (challenge) { assert.strictEqual(typeof challenge, 'object'); - assert.strictEqual(typeof acmeChallengesDir, 'string'); - debug('cleanupHttpChallenge: unlinking %s', path.join(acmeChallengesDir, challenge.token)); + debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token)); - if (!safe.fs.unlinkSync(path.join(acmeChallengesDir, challenge.token))) throw new BoxError(BoxError.FS_ERROR, `Error unlinking challenge: ${safe.error.message}`); + if (!safe.fs.unlinkSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token))) throw new BoxError(BoxError.FS_ERROR, `Error unlinking challenge: ${safe.error.message}`); }; -function getChallengeSubdomain(hostname, domain) { +function getChallengeSubdomain(cn, domain) { let challengeSubdomain; - if (hostname === domain) { + if (cn === domain) { challengeSubdomain = '_acme-challenge'; - } else if (hostname.includes('*')) { // wildcard - let subdomain = hostname.slice(0, -domain.length - 1); + } else if (cn.includes('*')) { // wildcard + let subdomain = cn.slice(0, -domain.length - 1); challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge'; } else { - challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1); + challengeSubdomain = '_acme-challenge.' + cn.slice(0, -domain.length - 1); } - debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`); + debug(`getChallengeSubdomain: challenge subdomain for cn ${cn} at domain ${domain} is ${challengeSubdomain}`); return challengeSubdomain; } -Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authorization) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.prepareDnsChallenge = async function (authorization) { assert.strictEqual(typeof authorization, 'object'); debug('prepareDnsChallenge: challenges: %j', authorization); @@ -416,39 +415,34 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz shasum.update(keyAuthorization); const txtValue = urlBase64Encode(shasum.digest('base64')); - const challengeSubdomain = getChallengeSubdomain(hostname, domain); + const challengeSubdomain = getChallengeSubdomain(this.cn, this.domain); debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`); - await dns.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]); + await dns.upsertDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]); - await dns.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }); + await dns.waitForDnsRecord(challengeSubdomain, this.domain, 'TXT', txtValue, { times: 200 }); return challenge; }; -Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challenge) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.cleanupDnsChallenge = async function (challenge) { assert.strictEqual(typeof challenge, 'object'); const keyAuthorization = this.getKeyAuthorization(challenge.token); - let shasum = crypto.createHash('sha256'); + const shasum = crypto.createHash('sha256'); shasum.update(keyAuthorization); const txtValue = urlBase64Encode(shasum.digest('base64')); - let challengeSubdomain = getChallengeSubdomain(hostname, domain); + const challengeSubdomain = getChallengeSubdomain(this.cn, this.domain); debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`); - await dns.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]); + await dns.removeDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]); }; -Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl, acmeChallengesDir) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.prepareChallenge = async function (authorizationUrl) { assert.strictEqual(typeof authorizationUrl, 'string'); - assert.strictEqual(typeof acmeChallengesDir, 'string'); debug(`prepareChallenge: http: ${this.performHttpAuthorization}`); @@ -458,55 +452,49 @@ Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizati const authorization = response.body; if (this.performHttpAuthorization) { - return await this.prepareHttpChallenge(hostname, domain, authorization, acmeChallengesDir); + return await this.prepareHttpChallenge(authorization); } else { - return await this.prepareDnsChallenge(hostname, domain, authorization); + return await this.prepareDnsChallenge(authorization); } }; -Acme2.prototype.cleanupChallenge = async function (hostname, domain, challenge, acmeChallengesDir) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); +Acme2.prototype.cleanupChallenge = async function (challenge) { assert.strictEqual(typeof challenge, 'object'); - assert.strictEqual(typeof acmeChallengesDir, 'string'); debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`); if (this.performHttpAuthorization) { - await this.cleanupHttpChallenge(hostname, domain, challenge, acmeChallengesDir); + await this.cleanupHttpChallenge(challenge); } else { - await this.cleanupDnsChallenge(hostname, domain, challenge); + await this.cleanupDnsChallenge(challenge); } }; -Acme2.prototype.acmeFlow = async function (hostname, domain, acmeCertificatePaths) { - assert.strictEqual(typeof hostname, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof acmeCertificatePaths, 'object'); - - const { certFilePath, keyFilePath, csrFilePath } = acmeCertificatePaths; - +Acme2.prototype.acmeFlow = async function () { await this.ensureAccount(); - const { order, orderUrl } = await this.newOrder(hostname); + const { order, orderUrl } = await this.newOrder(); + + const certificates = []; 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, paths.ACME_CHALLENGES_DIR); + const challenge = await this.prepareChallenge(authorizationUrl); await this.notifyChallengeReady(challenge); await this.waitForChallenge(challenge); - const csrDer = await this.createKeyAndCsr(hostname, keyFilePath, csrFilePath); - await this.signCertificate(hostname, order.finalize, csrDer); + const key = await this.ensureKey(); + const csr = await this.createCsr(key); + await this.signCertificate(order.finalize, csr); const certUrl = await this.waitForOrder(orderUrl); - await this.downloadCertificate(hostname, certUrl, certFilePath); + const cert = await this.downloadCertificate(certUrl); - try { - await this.cleanupChallenge(hostname, domain, challenge, paths.ACME_CHALLENGES_DIR); - } catch (cleanupError) { - debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError); - } + await safe(this.cleanupChallenge(challenge), { debug }); + + certificates.push({ cert, key, csr }); } + + return certificates; }; Acme2.prototype.loadDirectory = async function () { @@ -523,32 +511,36 @@ Acme2.prototype.loadDirectory = async function () { }); }; -Acme2.prototype.getCertificate = async function (fqdn, domain, acmeCertificatePaths) { - assert.strictEqual(typeof fqdn, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof acmeCertificatePaths, 'object'); - - debug(`getCertificate: start acme flow for ${fqdn} from ${this.caDirectory}`); - - if (fqdn !== domain && this.wildcard) { // bare domain is not part of wildcard SAN - fqdn = dns.makeWildcard(fqdn); - debug(`getCertificate: will get wildcard cert for ${fqdn}`); - } +Acme2.prototype.getCertificate = async function () { + debug(`getCertificate: start acme flow for ${this.cn} from ${this.caDirectory}`); await this.loadDirectory(); - await this.acmeFlow(fqdn, domain, acmeCertificatePaths); + const result = await this.acmeFlow(); + + debug(`getCertificate: acme flow completed for ${this.cn}. result: ${result.length}`); + + await blobs.set(`${blobs.CERT_PREFIX}-${this.certName}.key`, result[0].key); + await blobs.set(`${blobs.CERT_PREFIX}-${this.certName}.cert`, result[0].cert); + await blobs.set(`${blobs.CERT_PREFIX}-${this.certName}.csr`, result[0].csr); + + return result[0]; }; -async function getCertificate(fqdn, domain, acmeCertificatePaths, options) { +async function getCertificate(fqdn, domainObject) { assert.strictEqual(typeof fqdn, 'string'); // this can also be a wildcard domain (for alias domains) - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof acmeCertificatePaths, 'object'); - assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof domainObject, 'object'); - await promiseRetry({ times: 3, interval: 0, debug }, async function () { - debug(`getCertificate: for fqdn ${fqdn} and domain ${domain}`); + // registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197) + // we cannot use admin@fqdn because the user might not have set it up. + // we simply update the account with the latest email we have each time when getting letsencrypt certs + // https://github.com/ietf-wg-acme/acme/issues/30 + const owner = await users.getOwner(); + const email = owner?.email || 'webmaster@cloudron.io'; // can error if not activated yet - const acme = new Acme2(options || { }); - return await acme.getCertificate(fqdn, domain, acmeCertificatePaths); + return await promiseRetry({ times: 3, interval: 0, debug }, async function () { + debug(`getCertificate: for fqdn ${fqdn} and domain ${domainObject.domain}`); + + const acme = new Acme2(fqdn, domainObject, email); + return await acme.getCertificate(); }); } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 01d4b9ae4..23f2bceff 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -348,29 +348,6 @@ async function writeAcmeCertificate(fqdn, domainObject) { return true; } -async function updateCertBlobs(fqdn, domainObject) { - assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains) - assert.strictEqual(typeof domainObject, 'object'); - - const { certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject); - - const privateKey = safe.fs.readFileSync(keyFilePath); - if (!privateKey) throw new BoxError(BoxError.FS_ERROR, `Failed to read private key: ${safe.error.message}`); - - const cert = safe.fs.readFileSync(certFilePath); - if (!cert) throw new BoxError(BoxError.FS_ERROR, `Failed to read cert: ${safe.error.message}`); - - const csr = safe.fs.readFileSync(csrFilePath); - if (!csr) throw new BoxError(BoxError.FS_ERROR, `Failed to read csr: ${safe.error.message}`); - - const certName = getAcmeCertificateNameSync(fqdn, domainObject); - await blobs.set(`${blobs.CERT_PREFIX}-${certName}.key`, privateKey); - await blobs.set(`${blobs.CERT_PREFIX}-${certName}.cert`, cert); - await blobs.set(`${blobs.CERT_PREFIX}-${certName}.csr`, csr); - - debug(`updateCertBlobs: cert of ${fqdn} was updated`); -} - async function needsRenewal(fqdn, domainObject) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof domainObject, 'object'); @@ -389,15 +366,18 @@ async function renewCert(fqdn, domainObject) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof domainObject, 'object'); - const apiOptions = await getAcmeApiOptions(domainObject); const acmePaths = getAcmeCertificatePathSync(fqdn, domainObject); - const [error] = await safe(acme2.getCertificate(fqdn, domainObject.domain, acmePaths, apiOptions)); + const [error, result] = await safe(acme2.getCertificate(fqdn, domainObject)); if (error) { // write the fallback cert to keep the nginx configs consistent fs.writeFileSync(acmePaths.certFilePath, domainObject.certificate.cert); fs.writeFileSync(acmePaths.keyFilePath, domainObject.certificate.key); } else { - await safe(updateCertBlobs(fqdn, domainObject)); + const { certFilePath, keyFilePath, csrFilePath } = getAcmeCertificatePathSync(fqdn, domainObject); + + if (!safe.fs.writeFileSync(keyFilePath, result.key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write private key: ${safe.error.message}`); + if (!safe.fs.writeFileSync(certFilePath, result.cert)) throw new BoxError(BoxError.FS_ERROR, `Failed to write cert: ${safe.error.message}`); + if (!safe.fs.writeFileSync(csrFilePath, result.csr)) throw new BoxError(BoxError.FS_ERROR, `Failed to write csr: ${safe.error.message}`); } if (domainObject.domain === settings.mailDomain() && getAcmeCertificatePathSync(settings.mailFqdn(), domainObject).certFilePath === acmePaths.certFilePath) {