acme: handle LE validation type cache logic

LE stores the validation type for 60 days. So, if we authorized via http previously,
we won't get a DNS challenge for that duration.

There are two ways to fix this:
* Deactivate the challenges - https://community.letsencrypt.org/t/authorization-deactivation/19860 and https://community.letsencrypt.org/t/deactivate-authorization/189526
* Just be able to handle dns or http challenge, whatever is asked. This is what this commit does. It prefers DNS challenge when possible

Other relevant threads:

https://community.letsencrypt.org/t/flush-of-authorization-cache/188043
https://community.letsencrypt.org/t/let-s-encrypt-s-vulnerability-as-a-feature-authz-reuse-and-eternal-account-key/21687
https://community.letsencrypt.org/t/http-01-validation-cache/22529
This commit is contained in:
Girish Ramakrishnan
2023-05-02 23:01:14 +02:00
parent 1a32ea511e
commit 15e0f11bb9

View File

@@ -43,7 +43,7 @@ function Acme2(fqdn, domainObject, email) {
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 = domainObject.provider.match(/noop|manual|wildcard/) !== null;
this.forceHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
this.wildcard = !!domainObject.tlsConfig.wildcard;
this.domain = domainObject.domain;
@@ -58,7 +58,7 @@ function Acme2(fqdn, domainObject, email) {
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}`);
debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.forceHttpAuthorization}`);
}
// urlsafe base64 encoding (jose)
@@ -370,13 +370,8 @@ Acme2.prototype.downloadCertificate = async function (certUrl) {
});
};
Acme2.prototype.prepareHttpChallenge = async function (authorization) {
assert.strictEqual(typeof authorization, 'object');
debug(`prepareHttpChallenge: challenges: ${JSON.stringify(authorization)}`);
const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
const challenge = httpChallenges[0];
Acme2.prototype.prepareHttpChallenge = async function (challenge) {
assert.strictEqual(typeof challenge, 'object');
debug(`prepareHttpChallenge: preparing for challenge ${JSON.stringify(challenge)}`);
@@ -386,8 +381,6 @@ Acme2.prototype.prepareHttpChallenge = async function (authorization) {
debug(`prepareHttpChallenge: writing ${keyAuthorization} to ${challengeFilePath}`);
if (!safe.fs.writeFileSync(challengeFilePath, keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`);
return challenge;
};
Acme2.prototype.cleanupHttpChallenge = async function (challenge) {
@@ -416,13 +409,10 @@ function getChallengeSubdomain(cn, domain) {
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) {
assert.strictEqual(typeof authorization, 'object');
Acme2.prototype.prepareDnsChallenge = async function (cn, challenge) {
assert.strictEqual(typeof challenge, 'object');
debug(`prepareDnsChallenge: challenges: ${JSON.stringify(authorization)}`);
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges');
const challenge = dnsChallenges[0];
debug(`prepareDnsChallenge: preparing for challenge: ${JSON.stringify(challenge)}`);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
const shasum = crypto.createHash('sha256');
@@ -436,8 +426,6 @@ Acme2.prototype.prepareDnsChallenge = async function (cn, authorization) {
await dns.upsertDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]);
await dns.waitForDnsRecord(challengeSubdomain, this.domain, 'TXT', txtValue, { times: 200 });
return challenge;
};
Acme2.prototype.cleanupDnsChallenge = async function (cn, challenge) {
@@ -460,22 +448,31 @@ Acme2.prototype.prepareChallenge = async function (cn, authorization) {
assert.strictEqual(typeof cn, 'string');
assert.strictEqual(typeof authorization, 'object');
debug(`prepareChallenge: http: ${this.performHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
debug(`prepareChallenge: http: ${this.forceHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
if (this.performHttpAuthorization) {
return await this.prepareHttpChallenge(authorization);
} else {
return await this.prepareDnsChallenge(cn, authorization);
// validation is cached by LE for 60 days or so. if a user switches from non-wildcard DNS (http challenge) to programmatic DNS (dns challenge), then
// LE remembers the challenge type and won't give us a dns challenge for 60 days!
// https://letsencrypt.org/docs/faq/#i-successfully-renewed-a-certificate-but-validation-didn-t-happen-this-time-how-is-that-possible
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
const httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (this.forceHttpAuthorization || dnsChallenges.length === 0) {
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
await this.prepareHttpChallenge(httpChallenges[0]);
return httpChallenges[0];
}
await this.prepareDnsChallenge(cn, dnsChallenges[0]);
return dnsChallenges[0];
};
Acme2.prototype.cleanupChallenge = async function (cn, challenge) {
assert.strictEqual(typeof cn, 'string');
assert.strictEqual(typeof challenge, 'object');
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
debug(`cleanupChallenge: http: ${this.forceHttpAuthorization}`);
if (this.performHttpAuthorization) {
if (this.forceHttpAuthorization) {
await this.cleanupHttpChallenge(challenge);
} else {
await this.cleanupDnsChallenge(cn, challenge);