Skip to content

Commit a69dd06

Browse files
committed
Use 1-day certs for quicker expired issuance, where available
1 parent fa8305a commit a69dd06

1 file changed

Lines changed: 73 additions & 23 deletions

File tree

src/tls-certificates/acme.ts

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export class AcmeCA {
4141
});
4242
}
4343

44-
private pendingAcmeChallenges: { [token: string]: string | undefined } = {}
4544
private pendingCertRenewals: { [domain: string]: (Promise<AcmeGeneratedCertificate> & { id: string }) | undefined } = {};
4645

4746
getChallengeResponse(token: string) {
@@ -158,8 +157,13 @@ export class AcmeCA {
158157
return;
159158
}
160159

160+
// Only Google Trust Services supports short-lived certificates
161+
const requestCert = this.acmeProvider === 'google'
162+
? this.requestShortLivedCertificate(domain, { attemptId })
163+
: this.requestNewCertificate(domain, { attemptId });
164+
161165
const refreshPromise = Object.assign(
162-
this.requestNewCertificate(domain, { attemptId }).then((certData) => {
166+
requestCert.then((certData) => {
163167
delete this.pendingCertRenewals[cacheKey];
164168
this.certCache.cacheCert({ ...certData, domain: cacheKey });
165169
console.log(`Expired-mode cert issued for ${domain} (${attemptId}), will expire: ${new Date(certData.expiry).toISOString()}`);
@@ -270,33 +274,79 @@ export class AcmeCA {
270274
termsOfServiceAgreed: true,
271275
email: 'contact@' + domain,
272276
skipChallengeVerification: true,
273-
challengeCreateFn: async (_authz, challenge, keyAuth) => {
274-
if (challenge.type !== 'http-01') {
275-
throw new Error(`Unexpected ${challenge.type} challenge (${options.attemptId})`);
276-
}
277+
challengeCreateFn: async () => {
278+
// Challenge responses are stateless - getChallengeResponse() computes
279+
// the key authorization directly from the token
280+
},
281+
challengeRemoveFn: async () => {}
282+
});
277283

278-
console.log(`Preparing for ${challenge.type} ACME challenge ${challenge.token} (${options.attemptId})`);
284+
console.log(`Successfully ACMEd new certificate for ${domain} (${options.attemptId})`);
279285

280-
this.pendingAcmeChallenges[challenge.token] = keyAuth;
281-
},
282-
challengeRemoveFn: async (_authz, challenge) => {
283-
if (challenge.type !== 'http-01') {
284-
throw new Error(`Unexpected ${challenge.type} challenge (${options.attemptId})`);
285-
}
286+
return {
287+
key: key.toString(),
288+
cert,
289+
expiry: (new Date(ACME.crypto.readCertificateInfo(cert).notAfter)).valueOf()
290+
};
291+
}
286292

287-
console.log(`Removing ACME ${
288-
challenge.status
289-
} ${
290-
challenge.type
291-
} challenge ${
292-
JSON.stringify(challenge)
293-
}) (${options.attemptId})`);
293+
/**
294+
* Request a short-lived certificate (1 day validity) using the lower-level ACME API.
295+
* Google Trust Services supports validity periods as short as 1 day.
296+
*/
297+
private async requestShortLivedCertificate(domain: string, options: { attemptId: string }): Promise<AcmeGeneratedCertificate> {
298+
console.log(`Requesting short-lived certificate for ${domain} (${options.attemptId})`);
294299

295-
delete this.pendingAcmeChallenges[challenge.token];
296-
}
300+
const [key, csr] = await ACME.crypto.createCsr({
301+
commonName: domain
297302
});
298303

299-
console.log(`Successfully ACMEd new certificate for ${domain} (${options.attemptId})`);
304+
// Ensure account exists
305+
await this.acmeClient.createAccount({
306+
termsOfServiceAgreed: true,
307+
contact: [`mailto:contact@${domain}`]
308+
});
309+
310+
// Create order with 1-day validity
311+
const notBefore = new Date();
312+
const notAfter = new Date(Date.now() + ONE_DAY);
313+
314+
console.log(`Creating order with validity: ${notBefore.toISOString()} to ${notAfter.toISOString()} (${options.attemptId})`);
315+
316+
const order = await this.acmeClient.createOrder({
317+
identifiers: [{ type: 'dns', value: domain }],
318+
notBefore: notBefore.toISOString(),
319+
notAfter: notAfter.toISOString()
320+
});
321+
322+
// Get and complete authorizations
323+
const authorizations = await this.acmeClient.getAuthorizations(order);
324+
console.log(`Got ${authorizations.length} authorizations for ${domain} (${options.attemptId})`);
325+
326+
for (const authz of authorizations) {
327+
if (authz.status === 'valid') {
328+
console.log(`Authorization already valid for ${authz.identifier.value} (${options.attemptId})`);
329+
continue;
330+
}
331+
332+
// Find http-01 challenge
333+
const challenge = authz.challenges.find(c => c.type === 'http-01');
334+
if (!challenge) {
335+
throw new Error(`No http-01 challenge found for ${authz.identifier.value} (${options.attemptId})`);
336+
}
337+
338+
// Complete challenge - response is stateless via getChallengeResponse()
339+
console.log(`Completing http-01 challenge for ${authz.identifier.value} (${options.attemptId})`);
340+
await this.acmeClient.completeChallenge(challenge);
341+
await this.acmeClient.waitForValidStatus(challenge);
342+
}
343+
344+
// Finalize order and get certificate
345+
console.log(`Finalizing order for ${domain} (${options.attemptId})`);
346+
const finalized = await this.acmeClient.finalizeOrder(order, csr);
347+
const cert = await this.acmeClient.getCertificate(finalized);
348+
349+
console.log(`Successfully issued short-lived certificate for ${domain} (${options.attemptId})`);
300350

301351
return {
302352
key: key.toString(),

0 commit comments

Comments
 (0)