Skip to content

Commit 73ebb09

Browse files
committed
Add client cert endpoints
1 parent 7d33f46 commit 73ebb09

9 files changed

Lines changed: 258 additions & 54 deletions

File tree

src/endpoints/endpoint-config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ export function getEndpointConfig(serverNameParts: string[]) {
5656

5757
resolveEnabledVersions(tlsOptions);
5858

59-
// Pull out our non-OpenSSL marker so what's left is a clean SecureContextOptions.
59+
// Pull out our non-OpenSSL markers so what's left is a clean SecureContextOptions.
6060
const rejectTls = tlsOptions.rejectTls === true;
6161
delete tlsOptions.rejectTls;
62+
const requireClientCert = tlsOptions.requireClientCert === true;
63+
delete tlsOptions.requireClientCert;
6264

6365
if (rejectTls && serverNameParts.length > 1) {
6466
throw new Error(`'no-tls' can't be combined with other endpoints in '${serverNameParts.join('--')}'`);
@@ -68,7 +70,8 @@ export function getEndpointConfig(serverNameParts: string[]) {
6870
certOptions: certOptions as CertOptions,
6971
tlsOptions: tlsOptions as tls.SecureContextOptions,
7072
alpnPreferences,
71-
rejectTls
73+
rejectTls,
74+
requireClientCert
7275
};
7376
}
7477

src/endpoints/groups.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,16 @@ export const tlsCiphers: EndpointGroup = {
134134
description: 'Endpoints that offer only a specific weak or legacy cipher suite (over TLS 1.2), to test how your client handles it. A well-configured client should refuse to connect.'
135135
};
136136

137+
export const tlsClientAuth: EndpointGroup = {
138+
id: 'client-auth',
139+
name: 'Client Authentication',
140+
description: 'Endpoints that require the client to present a certificate (mutual TLS).'
141+
};
142+
137143
export const tlsGroupOrder: EndpointGroup[] = [
138144
tlsCertificateModes,
139145
tlsProtocolNegotiation,
140146
tlsVersions,
141-
tlsCiphers
147+
tlsCiphers,
148+
tlsClientAuth
142149
];

src/endpoints/http/tls-certs.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { httpTls } from '../groups.js';
33

44
const PREFIX = '/tls/certs/';
55

6-
const CERT_NAMES = ['untrusted-root', 'intermediate', 'self-signed'] as const;
6+
const CERT_NAMES = ['untrusted-root', 'intermediate', 'self-signed', 'client-cert'] as const;
77
type CertName = typeof CERT_NAMES[number];
88

99
// The downloadable certificates, keyed by name. Registered as lazy providers at server
@@ -35,15 +35,21 @@ export const tlsCertificates: HttpEndpoint = {
3535

3636
const pem = await (resolved[name] ??= provider());
3737

38+
// client-cert is a certificate + private key bundle, not a bare CA cert.
39+
const contentType = name === 'client-cert'
40+
? 'application/x-pem-file'
41+
: 'application/x-x509-ca-cert';
42+
3843
res.writeHead(200, {
39-
'content-type': 'application/x-x509-ca-cert',
44+
'content-type': contentType,
4045
'content-disposition': `attachment; filename="testserver-${name}.pem"`
4146
});
4247
res.end(pem);
4348
},
4449
meta: {
4550
path: '/tls/certs/{name}',
46-
description: 'Download the TLS certificates used by the local CA configuration, in PEM format.',
51+
description: 'Download the TLS certificates used by the local CA configuration, in PEM ' +
52+
'format. client-cert is a certificate + key bundle for the mTLS client-cert endpoint.',
4753
examples: CERT_NAMES.map(name => `${PREFIX}${name}`),
4854
group: httpTls
4955
}

src/endpoints/tls-index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type { EndpointMeta, EndpointGroup };
99
export interface TlsOptionContribution extends Partial<tls.SecureContextOptions> {
1010
enabledVersions?: tls.SecureVersion[];
1111
rejectTls?: boolean;
12+
requireClientCert?: boolean;
1213
}
1314

1415
export interface TlsEndpoint {
@@ -24,6 +25,7 @@ export interface TlsEndpoint {
2425
export * from './tls/alpn-specifiers.js';
2526
export * from './tls/cert-modes.js';
2627
export * from './tls/ciphers.js';
28+
export * from './tls/client-cert.js';
2729
export * from './tls/example.js';
2830
export * from './tls/no-tls.js';
2931
export * from './tls/tls-versions.js';

src/endpoints/tls/client-cert.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
import { tlsClientAuth } from '../groups.js';
3+
4+
export const clientCert: TlsEndpoint = {
5+
sniPart: 'client-cert',
6+
configureTlsOptions() {
7+
return { requireClientCert: true };
8+
},
9+
meta: {
10+
path: 'client-cert',
11+
description: 'Requires the client to present a certificate (mutual TLS). The handshake ' +
12+
"is rejected unless the client presents a certificate issued by this server's " +
13+
'dedicated client-auth CA. Download one to use from /tls/certs/client-cert.',
14+
examples: ['https://client-cert.testserver.host/'],
15+
group: tlsClientAuth
16+
}
17+
};

src/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ const createTcpHandler = async (options: ServerOptions = {}) => {
234234
'intermediate': () => tlsConfig.localCA.getIntermediateCertificatePem(),
235235
'self-signed': () => tlsConfig.localCA
236236
.generateCertificate(tlsConfig.rootDomain, { selfSigned: true })
237-
.then((cert) => cert.cert)
237+
.then((cert) => cert.cert),
238+
'client-cert': () => tlsConfig.localCA.getClientCertificate()
239+
.then((client) => `${client.cert.trimEnd()}\n${client.key.trimEnd()}\n`)
238240
});
239241
const tlsHandler = await createTlsHandler(tlsConfig, connProcessor);
240242

0 commit comments

Comments
 (0)