Skip to content

Commit 35c9794

Browse files
committed
Add support for self-signed TLS endpoint
1 parent 76d917b commit 35c9794

4 files changed

Lines changed: 116 additions & 6 deletions

File tree

src/server.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as net from 'net';
22

33
import { createHttp1Handler, createHttp2Handler } from './http-handler.js';
4-
import { createTlsHandler } from './tls-handler.js';
4+
import { createTlsHandler, CertMode } from './tls-handler.js';
55
import { ConnectionProcessor } from './process-connection.js';
66

77
import { AcmeCA, AcmeProvider, ExternalAccessBindingConfig } from './tls-certificates/acme.js';
@@ -45,7 +45,12 @@ async function generateTlsConfig(options: ServerOptions) {
4545
key: defaultCert.key,
4646
cert: defaultCert.cert,
4747
ca: caCert.cert,
48-
generateCertificate: (domain: string) => ca.generateCertificate(domain),
48+
generateCertificate: (domain: string, mode?: CertMode) => {
49+
if (mode === 'self-signed') {
50+
return ca.generateSelfSignedCertificate(domain);
51+
}
52+
return ca.generateCertificate(domain);
53+
},
4954
acmeChallenge: () => undefined // Not supported
5055
};
5156
}
@@ -68,7 +73,11 @@ async function generateTlsConfig(options: ServerOptions) {
6873
key: defaultCert.key,
6974
cert: defaultCert.cert,
7075
ca: caCert.cert,
71-
generateCertificate: (domain: string) => {
76+
generateCertificate: (domain: string, mode?: CertMode) => {
77+
if (mode === 'self-signed') {
78+
return ca.generateSelfSignedCertificate(domain);
79+
}
80+
7281
if (domain === rootDomain || domain.endsWith('.' + rootDomain)) {
7382
const cert = acmeCA.tryGetCertificateSync(domain);
7483
if (cert) return cert;

src/tls-certificates/local-ca.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,60 @@ export class LocalCA {
253253

254254
return generatedCertificate;
255255
}
256+
257+
generateSelfSignedCertificate(domain: string) {
258+
const cacheKey = `${domain}:self-signed`;
259+
260+
const cachedCert = this.certInMemoryCache[cacheKey];
261+
if (cachedCert) return cachedCert;
262+
263+
const cert = pki.createCertificate();
264+
265+
cert.publicKey = KEY_PAIR!.publicKey;
266+
cert.serialNumber = generateSerialNumber();
267+
268+
cert.validity.notBefore = new Date();
269+
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
270+
271+
cert.validity.notAfter = new Date();
272+
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
273+
274+
const subject = [
275+
{ name: 'commonName', value: domain },
276+
{ name: 'countryName', value: this.options?.countryName ?? 'XX' },
277+
{ name: 'localityName', value: this.options?.localityName ?? 'Unknown' },
278+
{ name: 'organizationName', value: this.options?.organizationName ?? 'Testserver Test Cert' }
279+
];
280+
281+
cert.setSubject(subject);
282+
cert.setIssuer(subject); // Self-signed: issuer = subject
283+
284+
cert.setExtensions([
285+
{ name: 'basicConstraints', cA: false, critical: true },
286+
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true },
287+
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
288+
{
289+
name: 'subjectAltName',
290+
altNames: [{ type: 2, value: domain }]
291+
},
292+
{ name: 'subjectKeyIdentifier' }
293+
]);
294+
295+
cert.sign(KEY_PAIR!.privateKey, md.sha256.create()); // Self-signed: sign with own key
296+
297+
const certPem = pki.certificateToPem(cert);
298+
const generatedCertificate = {
299+
key: pki.privateKeyToPem(KEY_PAIR!.privateKey),
300+
cert: certPem,
301+
ca: certPem // Self-signed: cert is its own CA
302+
};
303+
304+
this.certInMemoryCache[cacheKey] = generatedCertificate;
305+
306+
setTimeout(() => {
307+
delete this.certInMemoryCache[cacheKey];
308+
}, 1000 * 60 * 60 * 24).unref();
309+
310+
return generatedCertificate;
311+
}
256312
}

src/tls-handler.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import * as tls from 'tls';
22

33
import { ConnectionProcessor } from './process-connection.js';
44

5-
export const CERT_MODES = ['wrong-host'] as const;
5+
export const CERT_MODES = ['wrong-host', 'self-signed'] as const;
66
export type CertMode = typeof CERT_MODES[number];
77

8-
export type CertGenerator = (domain: string) => {
8+
// Modes that require special certificate generation (vs just domain remapping)
9+
const CERT_GENERATION_MODES = new Set<CertMode>(['self-signed']);
10+
11+
export type CertGenerator = (domain: string, mode?: CertMode) => {
912
key: string,
1013
cert: string,
1114
ca?: string
@@ -117,8 +120,10 @@ export async function createTlsHandler(
117120
certDomain = `example.${tlsConfig.rootDomain}`;
118121
}
119122

123+
const generationMode = certModeParts.find(mode => CERT_GENERATION_MODES.has(mode));
124+
120125
try {
121-
const generatedCert = tlsConfig.generateCertificate(certDomain);
126+
const generatedCert = tlsConfig.generateCertificate(certDomain, generationMode);
122127
cb(null, tls.createSecureContext({
123128
key: generatedCert.key,
124129
cert: generatedCert.cert,

test/https.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,44 @@ Connection: keep-alive
194194

195195
});
196196

197+
describe("self-signed certificates", () => {
198+
199+
it("returns a self-signed certificate where issuer equals subject", async () => {
200+
const conn = tls.connect({
201+
port: serverPort,
202+
servername: 'self-signed.localhost',
203+
rejectUnauthorized: false
204+
});
205+
206+
const cert = await new Promise<tls.PeerCertificate>((resolve, reject) => {
207+
conn.on('secureConnect', () => resolve(conn.getPeerCertificate()));
208+
conn.on('error', reject);
209+
});
210+
conn.destroy();
211+
212+
expect(cert.subject.CN).to.equal('self-signed.localhost');
213+
expect(cert.issuer.CN).to.equal('self-signed.localhost');
214+
expect(cert.subject.O).to.equal(cert.issuer.O);
215+
});
216+
217+
it("can combine self-signed with protocol preferences", async () => {
218+
const conn = tls.connect({
219+
port: serverPort,
220+
servername: 'http2.self-signed.localhost',
221+
ALPNProtocols: ['http/1.1', 'h2'],
222+
rejectUnauthorized: false
223+
});
224+
225+
const [cert, protocol] = await new Promise<[tls.PeerCertificate, string | false]>((resolve, reject) => {
226+
conn.on('secureConnect', () => resolve([conn.getPeerCertificate(), conn.alpnProtocol]));
227+
conn.on('error', reject);
228+
});
229+
conn.destroy();
230+
231+
expect(cert.subject.CN).to.equal(cert.issuer.CN);
232+
expect(protocol).to.equal('h2');
233+
});
234+
235+
});
236+
197237
});

0 commit comments

Comments
 (0)