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:
49
src/acme2.js
49
src/acme2.js
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user