Skip to content

Commit 7fccd7e

Browse files
committed
Add ACME-based just-wait expired TLS endpoint
For now this will be ultra-slow, since we use ZeroSSL who only support 90 days certs. In future this will merely be moderately slow, once we're using Let's Encrypt 6 day certs or Google 1+ day certs.
1 parent d3f4897 commit 7fccd7e

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

src/server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ async function generateTlsConfig(options: ServerOptions) {
7474
ca: caCert.cert,
7575
generateCertificate: (domain: string, mode?: CertMode) => {
7676
if (mode === 'self-signed') return ca.generateSelfSignedCertificate(domain);
77-
if (mode === 'expired') return ca.generateExpiredCertificate(domain);
77+
78+
if (mode === 'expired') {
79+
// Try to get an actually-expired ACME cert; fall back to LocalCA if not expired yet
80+
const expiredAcmeCert = acmeCA.tryGetExpiredCertificateSync(rootDomain);
81+
if (expiredAcmeCert) return expiredAcmeCert;
82+
return ca.generateExpiredCertificate(domain);
83+
}
7884

7985
if (domain === rootDomain || domain.endsWith('.' + rootDomain)) {
8086
const cert = acmeCA.tryGetCertificateSync(domain);

src/tls-certificates/acme.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,54 @@ export class AcmeCA {
7171
return cachedCert;
7272
}
7373

74+
// Returns an ACME cert only if it has actually expired. Issues once, never renews.
75+
// Returns undefined if no expired ACME cert is available (caller should use LocalCA fallback).
76+
tryGetExpiredCertificateSync(domain: string) {
77+
const cacheKey = `${domain}:expired`;
78+
const cachedCert = this.certCache.getCert(cacheKey);
79+
80+
if (cachedCert) {
81+
const isExpired = cachedCert.expiry <= Date.now();
82+
console.log(`Found cached expired-mode cert for ${domain} (expiry: ${new Date(cachedCert.expiry).toISOString()}, actually expired: ${isExpired})`);
83+
84+
if (isExpired) {
85+
return cachedCert;
86+
}
87+
// Not yet expired - caller should use LocalCA fallback
88+
return undefined;
89+
}
90+
91+
// No cached cert - issue one in the background (will be expired eventually)
92+
const attemptId = Math.random().toString(16).slice(2);
93+
console.log(`No expired-mode cert cached for ${domain}, issuing new one (${attemptId})`);
94+
this.issueExpiredModeCertificate(domain, cacheKey, attemptId);
95+
96+
return undefined;
97+
}
98+
99+
private async issueExpiredModeCertificate(domain: string, cacheKey: string, attemptId: string) {
100+
if (this.pendingCertRenewals[cacheKey]) {
101+
console.log(`Expired-mode cert already being issued for ${domain} (${attemptId})`);
102+
return;
103+
}
104+
105+
const refreshPromise = Object.assign(
106+
this.requestNewCertificate(domain, { attemptId }).then((certData) => {
107+
delete this.pendingCertRenewals[cacheKey];
108+
this.certCache.cacheCert({ ...certData, domain: cacheKey });
109+
console.log(`Expired-mode cert issued for ${domain} (${attemptId}), will expire: ${new Date(certData.expiry).toISOString()}`);
110+
return certData;
111+
}).catch((e) => {
112+
delete this.pendingCertRenewals[cacheKey];
113+
console.log(`Expired-mode cert generation failed for ${domain} (${attemptId}):`, e.message);
114+
throw e;
115+
}),
116+
{ id: attemptId }
117+
);
118+
119+
this.pendingCertRenewals[cacheKey] = refreshPromise;
120+
}
121+
74122
private async getCertificate(
75123
domain: string,
76124
options: { forceRegenerate?: boolean, attemptId: string }

0 commit comments

Comments
 (0)