node/test/common/boringssl.js
Filip Skokan bbf51ad24c test: update tls/crypto behaviour expectations when using BoringSSL
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63161
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
2026-05-08 19:27:18 +00:00

346 lines
11 KiB
JavaScript

/* eslint-disable node-core/crypto-check */
'use strict';
const common = require('../common');
const assert = require('assert');
const fixtures = require('../common/fixtures');
const tls = require('tls');
// This module is for BoringSSL-specific branches in tests whose original
// OpenSSL coverage cannot run unchanged. Each helper should assert the
// observable BoringSSL behavior that explains why the OpenSSL-specific
// assertions are bypassed.
/**
* BoringSSL exposes many removed or disabled TLS cipher suites as "no match"
* at secure-context creation time. This is used for suites such as
* finite-field DHE and anonymous ECDH that OpenSSL builds may still negotiate
* in tests.
* @param {Function} fn
*/
function assertNoCipherMatch(fn) {
assert.throws(fn, {
code: 'ERR_SSL_NO_CIPHER_MATCH',
library: 'SSL routines',
function: 'OPENSSL_internal',
reason: 'NO_CIPHER_MATCH',
});
}
/**
* BoringSSL does not parse OpenSSL cipher-string commands such as `@SECLEVEL`.
* Those are OpenSSL policy directives, not cipher names.
* @param {Function} fn
*/
function assertInvalidCommand(fn) {
assert.throws(fn, {
code: 'ERR_SSL_INVALID_COMMAND',
library: 'SSL routines',
function: 'OPENSSL_internal',
reason: 'INVALID_COMMAND',
});
}
/**
* Node's DHE tests exercise OpenSSL's finite-field DHE cipher support and DH
* parameter-size policy. BoringSSL does not offer these DHE cipher suites on
* this surface, so creating a server context with a DHE-only cipher list fails
* before a handshake can test DH parameter behavior.
*/
function assertFiniteFieldDheUnsupported() {
assertNoCipherMatch(() => {
tls.createServer({
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
ciphers: 'DHE-RSA-AES128-GCM-SHA256',
});
});
}
/**
* OpenSSL security levels reject small keys by policy and can be adjusted with
* `@SECLEVEL` in the cipher string. BoringSSL does not implement those security
* levels: the small-key server context is accepted, while the OpenSSL-specific
* `@SECLEVEL` command is rejected as invalid cipher-string syntax.
*/
function assertOpenSSLSecurityLevelsUnsupported() {
const options = {
key: fixtures.readKey('agent11-key.pem'),
cert: fixtures.readKey('agent11-cert.pem'),
ciphers: 'DEFAULT',
};
tls.createServer(options).close();
options.ciphers = 'DEFAULT:@SECLEVEL=0';
assertInvalidCommand(() => tls.createServer(options));
}
/**
* Node's multi-key tests rely on OpenSSL accepting an array of private keys and
* matching them with an array of certificates. BoringSSL rejects this mixed
* EC/RSA identity configuration while configuring the certificate chain, before
* a client can negotiate either identity.
*/
function assertMultiKeyUnsupported() {
assert.throws(() => {
tls.createServer({
key: [
fixtures.readKey('ec10-key.pem'),
fixtures.readKey('agent1-key.pem'),
],
cert: [
fixtures.readKey('agent1-cert.pem'),
fixtures.readKey('ec10-cert.pem'),
],
});
}, {
code: 'ERR_OSSL_X509_KEY_TYPE_MISMATCH',
library: 'X.509 certificate routines',
function: 'OPENSSL_internal',
reason: 'KEY_TYPE_MISMATCH',
});
}
/**
* BoringSSL does not support caller-initiated renegotiation. Even on a TLS 1.2
* connection, TLSSocket#renegotiate() returns false and the callback receives
* Node's BoringSSL-specific unsupported-renegotiation error instead of
* entering the native binding or exercising Node's renegotiation-limit logic.
*/
function testRenegotiationUnsupported() {
const server = tls.createServer({
key: fixtures.readKey('rsa_private.pem'),
cert: fixtures.readKey('rsa_cert.crt'),
maxVersion: 'TLSv1.2',
}, (socket) => socket.resume());
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
maxVersion: 'TLSv1.2',
}, common.mustCall(() => {
const ok = client.renegotiate({}, common.mustCall((err) => {
assert.throws(() => { throw err; }, {
code: 'ERR_TLS_RENEGOTIATION_UNSUPPORTED',
message: 'TLS session renegotiation is unsupported by this TLS ' +
'implementation',
});
client.destroy();
server.close();
}));
assert.strictEqual(ok, false);
}));
client.on('error', common.mustNotCall());
}));
}
/**
* OpenSSL exposes the negotiated ephemeral key type, name, and size for TLS
* clients. With BoringSSL the same ECDHE TLS 1.2 handshake succeeds, but
* getEphemeralKeyInfo() returns null on the server side and an object whose
* fields are undefined on the client side.
*/
function testEphemeralKeyInfoUnsupported() {
const server = tls.createServer({
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
ecdhCurve: 'prime256v1',
maxVersion: 'TLSv1.2',
}, common.mustCall((socket) => {
assert.strictEqual(socket.getEphemeralKeyInfo(), null);
socket.end();
}));
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
maxVersion: 'TLSv1.2',
}, common.mustCall(() => {
assert.deepStrictEqual(client.getEphemeralKeyInfo(), {
type: undefined,
name: undefined,
size: undefined,
});
server.close();
}));
}));
}
/**
* The protocol matrix tests cover OpenSSL behavior for legacy TLS protocols.
* For BoringSSL we only need to exhibit that a TLSv1-only client cannot connect
* to a server whose minimum protocol is TLS 1.2; the client receives the
* protocol-version alert instead of the OpenSSL version-specific matrix.
*/
function testLegacyProtocolUnsupported() {
const server = tls.createServer({
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
minVersion: 'TLSv1.2',
}, common.mustNotCall());
server.on('tlsClientError', common.mustCall());
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
secureProtocol: 'TLSv1_method',
}, common.mustNotCall());
client.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION');
server.close();
}));
}));
}
/**
* BoringSSL can load a multi-PFX option well enough to serve the ECDSA
* identity, but it does not provide the same OpenSSL multi-identity selection
* behavior. After the ECDSA handshake succeeds, an RSA-only client fails with
* no shared cipher instead of selecting the RSA identity from the same PFX list.
*/
function testMultiPfxSelectionDifference() {
const server = tls.createServer({
pfx: [
{
buf: fixtures.readKey('agent1.pfx'),
passphrase: 'sample',
},
fixtures.readKey('ec.pfx'),
],
}, common.mustCallAtLeast((socket) => socket.end(), 1));
server.listen(0, common.mustCall(() => {
const ecdsa = tls.connect(server.address().port, {
ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384',
maxVersion: 'TLSv1.2',
rejectUnauthorized: false,
}, common.mustCall(() => {
assert.strictEqual(ecdsa.getCipher().name,
'ECDHE-ECDSA-AES256-GCM-SHA384');
ecdsa.end();
server.once('tlsClientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_SSL_NO_SHARED_CIPHER');
}));
const rsa = tls.connect(server.address().port, {
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
maxVersion: 'TLSv1.2',
rejectUnauthorized: false,
}, common.mustNotCall());
rsa.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE');
server.close();
}));
}));
}));
}
/**
* PSK works for TLS 1.2 in BoringSSL, but Node's PSK tests also cover the
* default TLS 1.3 path. In that path BoringSSL does not complete a certificate-
* less PSK-only handshake through Node's current server setup: the server
* reports NO_CERTIFICATE_SET and the client receives an internal-error alert.
*/
function testPskTls13Unsupported() {
const key = Buffer.from('d731ef57be09e5204f0b205b60627028', 'hex');
let gotClientError = false;
let gotServerError = false;
function maybeClose(server) {
if (gotClientError && gotServerError)
server.close();
}
const server = tls.createServer({
ciphers: 'PSK+HIGH',
pskCallback() { return key; },
}, common.mustNotCall());
server.once('tlsClientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_SSL_NO_CERTIFICATE_SET');
gotServerError = true;
maybeClose(server);
}));
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
ciphers: 'PSK+HIGH',
checkServerIdentity() {},
pskCallback() {
return { psk: key, identity: 'TestUser' };
},
}, common.mustNotCall());
client.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR');
gotClientError = true;
maybeClose(server);
}));
}));
}
/**
* The OpenSSL ticket tests assume that once a TLS 1.3 session is reused, the
* client will not necessarily receive a replacement session event before close.
* BoringSSL emits new session tickets on both the initial and resumed TLS 1.3
* connections, so the resumed connection still emits at least one 'session'
* event while isSessionReused() is true.
*/
function testTls13SessionTicketSemanticsDiffer() {
const server = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
}, (socket) => socket.end());
let session;
let secondSessionEvents = 0;
server.listen(0, common.mustCall(() => {
const first = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
}, common.mustCall(() => {
assert.strictEqual(first.isSessionReused(), false);
}));
first.on('session', common.mustCallAtLeast((sess) => {
session = sess;
}, 1));
first.on('close', common.mustCall(() => {
assert(Buffer.isBuffer(session));
const second = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
session,
}, common.mustCall(() => {
assert.strictEqual(second.isSessionReused(), true);
}));
second.on('session', common.mustCallAtLeast(() => {
secondSessionEvents++;
}, 1));
second.on('close', common.mustCall(() => {
assert(secondSessionEvents > 0);
server.close();
}));
second.resume();
}));
first.resume();
}));
}
module.exports = {
assertFiniteFieldDheUnsupported,
assertMultiKeyUnsupported,
assertNoCipherMatch,
assertOpenSSLSecurityLevelsUnsupported,
testEphemeralKeyInfoUnsupported,
testLegacyProtocolUnsupported,
testMultiPfxSelectionDifference,
testPskTls13Unsupported,
testRenegotiationUnsupported,
testTls13SessionTicketSemanticsDiffer,
};