Skip to content

Commit 35e5ce7

Browse files
committed
test: update tls/crypto behaviour expectations when using BoringSSL
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent e5a038f commit 35e5ce7

47 files changed

Lines changed: 806 additions & 171 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

test/addons/openssl-get-ssl-ctx/binding.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ void GetSSLCtx(const v8::FunctionCallbackInfo<v8::Value>& args) {
1818
return;
1919
}
2020

21-
// Verify the pointer is a valid SSL_CTX by calling an OpenSSL function.
22-
const SSL_METHOD* method = SSL_CTX_get_ssl_method(ctx);
23-
if (method == nullptr) {
21+
// Verify the pointer is a valid SSL_CTX by calling a function available
22+
// across OpenSSL-compatible TLS backends and checking context-owned state.
23+
STACK_OF(SSL_CIPHER)* ciphers = SSL_CTX_get_ciphers(ctx);
24+
if (ciphers == nullptr) {
2425
isolate->ThrowException(v8::Exception::Error(
2526
v8::String::NewFromUtf8(isolate,
26-
"SSL_CTX_get_ssl_method returned nullptr")
27+
"SSL_CTX_get_ciphers returned nullptr")
2728
.ToLocalChecked()));
2829
return;
2930
}

test/common/boringssl.js

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
/* eslint-disable node-core/crypto-check */
2+
3+
'use strict';
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const fixtures = require('../common/fixtures');
7+
const tls = require('tls');
8+
9+
// This module is for BoringSSL-specific branches in tests whose original
10+
// OpenSSL coverage cannot run unchanged. Each helper should assert the
11+
// observable BoringSSL behavior that explains why the OpenSSL-specific
12+
// assertions are bypassed.
13+
14+
/**
15+
* BoringSSL exposes many removed or disabled TLS cipher suites as "no match"
16+
* at secure-context creation time. This is used for suites such as
17+
* finite-field DHE and anonymous ECDH that OpenSSL builds may still negotiate
18+
* in tests.
19+
* @param {Function} fn
20+
*/
21+
function assertNoCipherMatch(fn) {
22+
assert.throws(fn, {
23+
code: 'ERR_SSL_NO_CIPHER_MATCH',
24+
library: 'SSL routines',
25+
function: 'OPENSSL_internal',
26+
reason: 'NO_CIPHER_MATCH',
27+
});
28+
}
29+
30+
/**
31+
* BoringSSL does not parse OpenSSL cipher-string commands such as `@SECLEVEL`.
32+
* Those are OpenSSL policy directives, not cipher names.
33+
* @param {Function} fn
34+
*/
35+
function assertInvalidCommand(fn) {
36+
assert.throws(fn, {
37+
code: 'ERR_SSL_INVALID_COMMAND',
38+
library: 'SSL routines',
39+
function: 'OPENSSL_internal',
40+
reason: 'INVALID_COMMAND',
41+
});
42+
}
43+
44+
/**
45+
* Node's DHE tests exercise OpenSSL's finite-field DHE cipher support and DH
46+
* parameter-size policy. BoringSSL does not offer these DHE cipher suites on
47+
* this surface, so creating a server context with a DHE-only cipher list fails
48+
* before a handshake can test DH parameter behavior.
49+
*/
50+
function assertFiniteFieldDheUnsupported() {
51+
assertNoCipherMatch(() => {
52+
tls.createServer({
53+
key: fixtures.readKey('agent2-key.pem'),
54+
cert: fixtures.readKey('agent2-cert.pem'),
55+
ciphers: 'DHE-RSA-AES128-GCM-SHA256',
56+
});
57+
});
58+
}
59+
60+
/**
61+
* OpenSSL security levels reject small keys by policy and can be adjusted with
62+
* `@SECLEVEL` in the cipher string. BoringSSL does not implement those security
63+
* levels: the small-key server context is accepted, while the OpenSSL-specific
64+
* `@SECLEVEL` command is rejected as invalid cipher-string syntax.
65+
*/
66+
function assertOpenSSLSecurityLevelsUnsupported() {
67+
const options = {
68+
key: fixtures.readKey('agent11-key.pem'),
69+
cert: fixtures.readKey('agent11-cert.pem'),
70+
ciphers: 'DEFAULT',
71+
};
72+
73+
tls.createServer(options).close();
74+
75+
options.ciphers = 'DEFAULT:@SECLEVEL=0';
76+
assertInvalidCommand(() => tls.createServer(options));
77+
}
78+
79+
/**
80+
* Node's multi-key tests rely on OpenSSL accepting an array of private keys and
81+
* matching them with an array of certificates. BoringSSL rejects this mixed
82+
* EC/RSA identity configuration while configuring the certificate chain, before
83+
* a client can negotiate either identity.
84+
*/
85+
function assertMultiKeyUnsupported() {
86+
assert.throws(() => {
87+
tls.createServer({
88+
key: [
89+
fixtures.readKey('ec10-key.pem'),
90+
fixtures.readKey('agent1-key.pem'),
91+
],
92+
cert: [
93+
fixtures.readKey('agent1-cert.pem'),
94+
fixtures.readKey('ec10-cert.pem'),
95+
],
96+
});
97+
}, {
98+
code: 'ERR_OSSL_X509_KEY_TYPE_MISMATCH',
99+
library: 'X.509 certificate routines',
100+
function: 'OPENSSL_internal',
101+
reason: 'KEY_TYPE_MISMATCH',
102+
});
103+
}
104+
105+
/**
106+
* BoringSSL does not support caller-initiated renegotiation. Even on a TLS 1.2
107+
* connection, TLSSocket#renegotiate() returns false and the callback receives
108+
* Node's BoringSSL-specific unsupported-renegotiation error instead of
109+
* entering the native binding or exercising Node's renegotiation-limit logic.
110+
*/
111+
function testRenegotiationUnsupported() {
112+
const server = tls.createServer({
113+
key: fixtures.readKey('rsa_private.pem'),
114+
cert: fixtures.readKey('rsa_cert.crt'),
115+
maxVersion: 'TLSv1.2',
116+
}, (socket) => socket.resume());
117+
118+
server.listen(0, common.mustCall(() => {
119+
const client = tls.connect({
120+
port: server.address().port,
121+
rejectUnauthorized: false,
122+
maxVersion: 'TLSv1.2',
123+
}, common.mustCall(() => {
124+
const ok = client.renegotiate({}, common.mustCall((err) => {
125+
assert.throws(() => { throw err; }, {
126+
code: 'ERR_TLS_RENEGOTIATION_UNSUPPORTED',
127+
message: 'TLS session renegotiation is unsupported by this TLS ' +
128+
'implementation',
129+
});
130+
client.destroy();
131+
server.close();
132+
}));
133+
assert.strictEqual(ok, false);
134+
}));
135+
client.on('error', common.mustNotCall());
136+
}));
137+
}
138+
139+
/**
140+
* OpenSSL exposes the negotiated ephemeral key type, name, and size for TLS
141+
* clients. With BoringSSL the same ECDHE TLS 1.2 handshake succeeds, but
142+
* getEphemeralKeyInfo() returns null on the server side and an object whose
143+
* fields are undefined on the client side.
144+
*/
145+
function testEphemeralKeyInfoUnsupported() {
146+
const server = tls.createServer({
147+
key: fixtures.readKey('agent2-key.pem'),
148+
cert: fixtures.readKey('agent2-cert.pem'),
149+
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
150+
ecdhCurve: 'prime256v1',
151+
maxVersion: 'TLSv1.2',
152+
}, common.mustCall((socket) => {
153+
assert.strictEqual(socket.getEphemeralKeyInfo(), null);
154+
socket.end();
155+
}));
156+
157+
server.listen(0, common.mustCall(() => {
158+
const client = tls.connect({
159+
port: server.address().port,
160+
rejectUnauthorized: false,
161+
maxVersion: 'TLSv1.2',
162+
}, common.mustCall(() => {
163+
assert.deepStrictEqual(client.getEphemeralKeyInfo(), {
164+
type: undefined,
165+
name: undefined,
166+
size: undefined,
167+
});
168+
server.close();
169+
}));
170+
}));
171+
}
172+
173+
/**
174+
* The protocol matrix tests cover OpenSSL behavior for legacy TLS protocols.
175+
* For BoringSSL we only need to exhibit that a TLSv1-only client cannot connect
176+
* to a server whose minimum protocol is TLS 1.2; the client receives the
177+
* protocol-version alert instead of the OpenSSL version-specific matrix.
178+
*/
179+
function testLegacyProtocolUnsupported() {
180+
const server = tls.createServer({
181+
key: fixtures.readKey('agent2-key.pem'),
182+
cert: fixtures.readKey('agent2-cert.pem'),
183+
minVersion: 'TLSv1.2',
184+
}, common.mustNotCall());
185+
186+
server.on('tlsClientError', common.mustCall());
187+
server.listen(0, common.mustCall(() => {
188+
const client = tls.connect({
189+
port: server.address().port,
190+
rejectUnauthorized: false,
191+
secureProtocol: 'TLSv1_method',
192+
}, common.mustNotCall());
193+
client.on('error', common.mustCall((err) => {
194+
assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION');
195+
server.close();
196+
}));
197+
}));
198+
}
199+
200+
/**
201+
* BoringSSL can load a multi-PFX option well enough to serve the ECDSA
202+
* identity, but it does not provide the same OpenSSL multi-identity selection
203+
* behavior. After the ECDSA handshake succeeds, an RSA-only client fails with
204+
* no shared cipher instead of selecting the RSA identity from the same PFX list.
205+
*/
206+
function testMultiPfxSelectionDifference() {
207+
const server = tls.createServer({
208+
pfx: [
209+
{
210+
buf: fixtures.readKey('agent1.pfx'),
211+
passphrase: 'sample',
212+
},
213+
fixtures.readKey('ec.pfx'),
214+
],
215+
}, common.mustCallAtLeast((socket) => socket.end(), 1));
216+
217+
server.listen(0, common.mustCall(() => {
218+
const ecdsa = tls.connect(server.address().port, {
219+
ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
220+
maxVersion: 'TLSv1.2',
221+
rejectUnauthorized: false,
222+
}, common.mustCall(() => {
223+
assert.strictEqual(ecdsa.getCipher().name,
224+
'ECDHE-ECDSA-AES256-GCM-SHA384');
225+
ecdsa.end();
226+
227+
server.once('tlsClientError', common.mustCall((err) => {
228+
assert.strictEqual(err.code, 'ERR_SSL_NO_SHARED_CIPHER');
229+
}));
230+
const rsa = tls.connect(server.address().port, {
231+
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
232+
maxVersion: 'TLSv1.2',
233+
rejectUnauthorized: false,
234+
}, common.mustNotCall());
235+
rsa.on('error', common.mustCall((err) => {
236+
assert.strictEqual(err.code, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE');
237+
server.close();
238+
}));
239+
}));
240+
}));
241+
}
242+
243+
/**
244+
* PSK works for TLS 1.2 in BoringSSL, but Node's PSK tests also cover the
245+
* default TLS 1.3 path. In that path BoringSSL does not complete a certificate-
246+
* less PSK-only handshake through Node's current server setup: the server
247+
* reports NO_CERTIFICATE_SET and the client receives an internal-error alert.
248+
*/
249+
function testPskTls13Unsupported() {
250+
const key = Buffer.from('d731ef57be09e5204f0b205b60627028', 'hex');
251+
let gotClientError = false;
252+
let gotServerError = false;
253+
function maybeClose(server) {
254+
if (gotClientError && gotServerError)
255+
server.close();
256+
}
257+
258+
const server = tls.createServer({
259+
ciphers: 'PSK+HIGH',
260+
pskCallback() { return key; },
261+
}, common.mustNotCall());
262+
263+
server.once('tlsClientError', common.mustCall((err) => {
264+
assert.strictEqual(err.code, 'ERR_SSL_NO_CERTIFICATE_SET');
265+
gotServerError = true;
266+
maybeClose(server);
267+
}));
268+
269+
server.listen(0, common.mustCall(() => {
270+
const client = tls.connect({
271+
port: server.address().port,
272+
ciphers: 'PSK+HIGH',
273+
checkServerIdentity() {},
274+
pskCallback() {
275+
return { psk: key, identity: 'TestUser' };
276+
},
277+
}, common.mustNotCall());
278+
client.on('error', common.mustCall((err) => {
279+
assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR');
280+
gotClientError = true;
281+
maybeClose(server);
282+
}));
283+
}));
284+
}
285+
286+
/**
287+
* The OpenSSL ticket tests assume that once a TLS 1.3 session is reused, the
288+
* client will not necessarily receive a replacement session event before close.
289+
* BoringSSL emits new session tickets on both the initial and resumed TLS 1.3
290+
* connections, so the resumed connection still emits at least one 'session'
291+
* event while isSessionReused() is true.
292+
*/
293+
function testTls13SessionTicketSemanticsDiffer() {
294+
const server = tls.createServer({
295+
key: fixtures.readKey('agent1-key.pem'),
296+
cert: fixtures.readKey('agent1-cert.pem'),
297+
}, (socket) => socket.end());
298+
299+
let session;
300+
let secondSessionEvents = 0;
301+
302+
server.listen(0, common.mustCall(() => {
303+
const first = tls.connect({
304+
port: server.address().port,
305+
rejectUnauthorized: false,
306+
}, common.mustCall(() => {
307+
assert.strictEqual(first.isSessionReused(), false);
308+
}));
309+
first.on('session', common.mustCallAtLeast((sess) => {
310+
session = sess;
311+
}, 1));
312+
first.on('close', common.mustCall(() => {
313+
assert(Buffer.isBuffer(session));
314+
315+
const second = tls.connect({
316+
port: server.address().port,
317+
rejectUnauthorized: false,
318+
session,
319+
}, common.mustCall(() => {
320+
assert.strictEqual(second.isSessionReused(), true);
321+
}));
322+
second.on('session', common.mustCallAtLeast(() => {
323+
secondSessionEvents++;
324+
}, 1));
325+
second.on('close', common.mustCall(() => {
326+
assert(secondSessionEvents > 0);
327+
server.close();
328+
}));
329+
second.resume();
330+
}));
331+
first.resume();
332+
}));
333+
}
334+
335+
module.exports = {
336+
assertFiniteFieldDheUnsupported,
337+
assertMultiKeyUnsupported,
338+
assertNoCipherMatch,
339+
assertOpenSSLSecurityLevelsUnsupported,
340+
testEphemeralKeyInfoUnsupported,
341+
testLegacyProtocolUnsupported,
342+
testMultiPfxSelectionDifference,
343+
testPskTls13Unsupported,
344+
testRenegotiationUnsupported,
345+
testTls13SessionTicketSemanticsDiffer,
346+
};

0 commit comments

Comments
 (0)