acme: ARI support
ARI is a hint from the cert issuer about when to renew a cert. We will use it when the API is available. It provides a mechanim for CAs to revoke certs and signal to clients that cert should be renewed. https://www.rfc-editor.org/rfc/rfc9773.txt https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients
This commit is contained in:
@@ -155,13 +155,26 @@ function getAcmeCertificateNameSync(fqdn, domainObject) {
|
||||
}
|
||||
}
|
||||
|
||||
async function needsRenewal(cert, options) {
|
||||
async function needsRenewal(cert, renewalInfo, options) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof renewalInfo, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const { startDate, endDate } = await openssl.getCertificateDates(cert);
|
||||
const now = new Date();
|
||||
|
||||
// if we have ARI respect it. this allows us to be expempt from rate limit checks when "replaces" field is set
|
||||
// https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients#step-6-indicating-which-certificate-is-replaced-by-this-new-order
|
||||
if (renewalInfo) {
|
||||
const rt = new Date(renewalInfo.rt);
|
||||
let renew = false;
|
||||
if (now.getTime() >= rt.getTime()) renew = true; // renew immediately since now is in the past
|
||||
else if ((now.getTime() + (24*60*60*1000)) >= rt.getTime()) renew = true; // next cron run will be in the past
|
||||
debug(`needsRenewal: ${renew}. ARI ${JSON.stringify(renewalInfo)}`);
|
||||
return renew; // can wait
|
||||
}
|
||||
|
||||
// for CAs without ARI, fallback to 1 month
|
||||
let isExpiring;
|
||||
if (options.forceRenewal) {
|
||||
isExpiring = (now - startDate) > (65 * 60 * 1000); // was renewed 5 minutes ago. LE backdates issue date by 1 hour for clock skew
|
||||
@@ -307,6 +320,25 @@ async function getKey(certName) {
|
||||
return await openssl.generateKey('secp256r1');
|
||||
};
|
||||
|
||||
async function getRenewalInfo(cert, certName) {
|
||||
const renewalInfo = JSON.parse(await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.renewal`));
|
||||
if (!renewalInfo) return null;
|
||||
|
||||
if (Date.now() < (new Date(renewalInfo.valid)).getTime()) return renewalInfo; // still valid
|
||||
|
||||
debug(`getRenewalInfo: ${certName} refreshing`);
|
||||
const [error, result] = await safe(acme2.getRenewalInfo(cert, renewalInfo.url));
|
||||
if (error) {
|
||||
debug(`getRenewalInfo: ${certName} error getting renewal info`, error);
|
||||
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.renewal`);
|
||||
} else {
|
||||
debug(`getRenewalInfo: ${certName} updated: ${JSON.stringify(result)}`);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.renewal`, JSON.stringify(result));
|
||||
}
|
||||
|
||||
return error ? null : result;
|
||||
}
|
||||
|
||||
async function ensureCertificate(location, options, auditSource) {
|
||||
assert.strictEqual(typeof location, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -333,7 +365,9 @@ async function ensureCertificate(location, options, auditSource) {
|
||||
|
||||
if (key && cert) {
|
||||
const sameProvider = await providerMatches(domainObject, cert);
|
||||
const outdated = await needsRenewal(cert, options);
|
||||
const renewalInfo = await getRenewalInfo(cert, certName);
|
||||
const outdated = await needsRenewal(cert, renewalInfo, options);
|
||||
|
||||
if (sameProvider && !outdated) {
|
||||
debug(`ensureCertificate: ${fqdn} acme cert exists and is up to date`);
|
||||
return;
|
||||
@@ -347,10 +381,11 @@ async function ensureCertificate(location, options, auditSource) {
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.key`, key);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.cert`, result.cert);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.csr`, result.csr);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.renewal`, JSON.stringify(result.renewalInfo));
|
||||
|
||||
debug(`ensureCertificate: error: ${error ? error.message : 'null'}`);
|
||||
|
||||
await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: error?.message || '' }));
|
||||
await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: error?.message || '', renewalInfo: result.renewalInfo }));
|
||||
}
|
||||
|
||||
async function writeDashboardNginxConfig(vhost, certificatePath) {
|
||||
@@ -560,6 +595,7 @@ async function cleanupCerts(locations, auditSource, progressCallback) {
|
||||
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.key`);
|
||||
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.cert`);
|
||||
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.csr`);
|
||||
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.renewal`);
|
||||
|
||||
removedCertNames.push(certName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user