Skip to content

Commit ea019fa

Browse files
committed
Add cipher endpoints
1 parent 21faf81 commit ea019fa

4 files changed

Lines changed: 165 additions & 1 deletion

File tree

src/endpoints/groups.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,15 @@ export const tlsVersions: EndpointGroup = {
128128
description: 'Endpoints that only accept specific TLS versions. These can be used together, to simulate a server supporting any specific combination of versions.'
129129
};
130130

131+
export const tlsCiphers: EndpointGroup = {
132+
id: 'ciphers',
133+
name: 'Cipher Suites',
134+
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.'
135+
};
136+
131137
export const tlsGroupOrder: EndpointGroup[] = [
132138
tlsCertificateModes,
133139
tlsProtocolNegotiation,
134-
tlsVersions
140+
tlsVersions,
141+
tlsCiphers
135142
];

src/endpoints/tls-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface TlsEndpoint {
2323

2424
export * from './tls/alpn-specifiers.js';
2525
export * from './tls/cert-modes.js';
26+
export * from './tls/ciphers.js';
2627
export * from './tls/example.js';
2728
export * from './tls/no-tls.js';
2829
export * from './tls/tls-versions.js';

src/endpoints/tls/ciphers.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { TlsEndpoint } from '../tls-index.js';
2+
import { tlsCiphers } from '../groups.js';
3+
4+
// Each of these offers only a specific weak/legacy cipher, capped at TLS 1.2. The cap matters:
5+
// none of these ciphers (static RSA, CBC, NULL, weak DH) exist in TLS 1.3, which only has AEAD
6+
// suites with ephemeral key exchange. `@SECLEVEL=0` drops OpenSSL's security-level so the weak
7+
// suites can be used. A client only connects if it's willing to negotiate the weak cipher.
8+
9+
// A deliberately weak 1024-bit DH group for the Logjam-style weak-DH test, pre-generated
10+
// because generating one per process is slow. It's intentionally weak - nothing to protect.
11+
const WEAK_DH_PARAMS = `-----BEGIN DH PARAMETERS-----
12+
MIGHAoGBAPwIcVZU2Dt7WtCI8hhI8wECGgMidZpXASdKXwAQReA+739EGI4HUmM4
13+
qkm2vyhAReLHc4UALQI8SKV5G7WDHKIAZi0sDofR3qitV2Z44aVk0u4Z3M8S1NA9
14+
aSgcv6Iz0BQPADQkEq28bRT0i3/A+K6DuHbC98RtnhqF+OuaHXJ/AgEC
15+
-----END DH PARAMETERS-----`;
16+
17+
export const staticRsa: TlsEndpoint = {
18+
sniPart: 'static-rsa',
19+
configureTlsOptions() {
20+
return {
21+
ciphers: 'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA:AES256-SHA@SECLEVEL=0',
22+
maxVersion: 'TLSv1.2'
23+
};
24+
},
25+
meta: {
26+
path: 'static-rsa',
27+
description: 'Uses RSA key exchange only, so the connection has no forward secrecy.',
28+
examples: ['https://static-rsa.testserver.host/'],
29+
group: tlsCiphers
30+
}
31+
};
32+
33+
export const cbc: TlsEndpoint = {
34+
sniPart: 'cbc',
35+
configureTlsOptions() {
36+
return {
37+
ciphers: 'ECDHE-RSA-AES128-SHA:AES128-SHA@SECLEVEL=0',
38+
maxVersion: 'TLSv1.2'
39+
};
40+
},
41+
meta: {
42+
path: 'cbc',
43+
description: 'Uses a CBC-mode cipher suite (as opposed to an AEAD suite like AES-GCM).',
44+
examples: ['https://cbc.testserver.host/'],
45+
group: tlsCiphers
46+
}
47+
};
48+
49+
export const nullCipher: TlsEndpoint = {
50+
sniPart: 'null-cipher',
51+
configureTlsOptions() {
52+
return {
53+
ciphers: 'NULL-SHA:NULL-SHA256@SECLEVEL=0',
54+
maxVersion: 'TLSv1.2'
55+
};
56+
},
57+
meta: {
58+
path: 'null-cipher',
59+
description: 'Uses a NULL cipher. The handshake is authenticated, but application data is unencrypted.',
60+
examples: ['https://null-cipher.testserver.host/'],
61+
group: tlsCiphers
62+
}
63+
};
64+
65+
export const weakDh: TlsEndpoint = {
66+
sniPart: 'weak-dh',
67+
configureTlsOptions() {
68+
return {
69+
ciphers: 'DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA@SECLEVEL=0',
70+
maxVersion: 'TLSv1.2',
71+
dhparam: WEAK_DH_PARAMS
72+
};
73+
},
74+
meta: {
75+
path: 'weak-dh',
76+
description: 'Uses ephemeral Diffie-Hellman key exchange with a weak 1024-bit group.',
77+
examples: ['https://weak-dh.testserver.host/'],
78+
group: tlsCiphers
79+
}
80+
};

test/tls-ciphers.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as net from 'net';
2+
import * as tls from 'tls';
3+
4+
import { expect } from 'chai';
5+
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
6+
7+
import { createTestServer } from './test-helpers.js';
8+
9+
describe("TLS cipher endpoints", () => {
10+
11+
let server: DestroyableServer;
12+
let port: number;
13+
14+
beforeEach(async () => {
15+
server = makeDestroyable(await createTestServer());
16+
await new Promise<void>((resolve) => server.listen(resolve));
17+
port = (server.address() as net.AddressInfo).port;
18+
});
19+
afterEach(async () => { await server.destroy(); });
20+
21+
// Offers the given legacy cipher (and, by not capping the version, TLS 1.3 too) so we can
22+
// check both that the endpoint negotiates the weak cipher AND that it caps at TLS 1.2.
23+
const negotiate = (servername: string, ciphers: string) =>
24+
new Promise<{ name: string, version: string | null }>((resolve, reject) => {
25+
const conn = tls.connect({
26+
port, host: '127.0.0.1', servername, rejectUnauthorized: false,
27+
ciphers: `${ciphers}@SECLEVEL=0`
28+
});
29+
conn.on('secureConnect', () => {
30+
const info = { name: conn.getCipher().name, version: conn.getProtocol() };
31+
conn.destroy();
32+
resolve(info);
33+
});
34+
conn.on('error', reject);
35+
});
36+
37+
it("static-rsa negotiates an RSA-key-exchange suite over TLS 1.2", async () => {
38+
const { name, version } = await negotiate('static-rsa.localhost', 'AES128-GCM-SHA256:AES128-SHA');
39+
expect(version).to.equal('TLSv1.2');
40+
expect(name).to.match(/^AES(128|256)-/); // no ECDHE-/DHE- prefix => static RSA
41+
});
42+
43+
it("cbc negotiates a CBC-mode suite over TLS 1.2", async () => {
44+
const { name, version } = await negotiate('cbc.localhost', 'ECDHE-RSA-AES128-SHA');
45+
expect(version).to.equal('TLSv1.2');
46+
expect(name).to.equal('ECDHE-RSA-AES128-SHA'); // -SHA (not -SHA256/-GCM) => CBC
47+
});
48+
49+
it("null-cipher negotiates a NULL cipher over TLS 1.2", async () => {
50+
const { name, version } = await negotiate('null-cipher.localhost', 'NULL-SHA');
51+
expect(version).to.equal('TLSv1.2');
52+
expect(name).to.match(/NULL/);
53+
});
54+
55+
it("weak-dh negotiates ephemeral DH over TLS 1.2", async () => {
56+
const { name, version } = await negotiate('weak-dh.localhost', 'DHE-RSA-AES128-SHA');
57+
expect(version).to.equal('TLSv1.2');
58+
expect(name).to.match(/^DHE-/);
59+
});
60+
61+
it("refuses a client that won't offer the weak cipher", async () => {
62+
// A default modern client offers no NULL cipher, so it can't connect to null-cipher.
63+
const outcome = await new Promise<'connected' | 'failed'>((resolve) => {
64+
const conn = tls.connect({ port, host: '127.0.0.1', servername: 'null-cipher.localhost', rejectUnauthorized: false });
65+
conn.on('secureConnect', () => { conn.destroy(); resolve('connected'); });
66+
conn.on('error', () => resolve('failed'));
67+
});
68+
expect(outcome).to.equal('failed');
69+
});
70+
71+
it("treats two cipher endpoints as a conflict", async () => {
72+
let rejected = false;
73+
await negotiate('static-rsa--null-cipher.localhost', 'AES128-SHA').catch(() => { rejected = true; });
74+
expect(rejected).to.equal(true);
75+
});
76+
});

0 commit comments

Comments
 (0)