mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Use BoringSSL 0.20261020.0 for shared-library builds and expose the package wrapper's version to the matrix updater. The new verifier no longer emits UNABLE_TO_VERIFY_LEAF_SIGNATURE after Node's verification callback accepts a missing-issuer error. Expect UNABLE_TO_GET_ISSUER_CERT_LOCALLY for the affected leaf certificates and suggest --use-system-ca for that error. Drop the SSL_get_server_tmp_key() fallback now that BoringSSL supplies SSL_get_peer_tmp_key() as a compatibility stub. Refs: https://boringssl-review.googlesource.com/c/boringssl/+/103849 Refs: https://github.com/electron/electron/pull/55110/changes/b457e70e339e8bb3066b210edfb6f96582cdb374 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66922 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
201 lines
6.5 KiB
JavaScript
201 lines
6.5 KiB
JavaScript
'use strict';
|
|
const common = require('../common');
|
|
if (!common.hasCrypto)
|
|
common.skip('missing crypto');
|
|
|
|
// Server-side client-certificate authorization must survive TLS session
|
|
// resumption. On a resumed handshake the client does not re-send its
|
|
// certificate, so the server has to report the same authorization state it
|
|
// derived from the original full handshake:
|
|
//
|
|
// - a trusted certificate stays authorized,
|
|
// - an untrusted certificate stays unauthorized with its verification error,
|
|
// - a missing certificate stays unauthorized (UNABLE_TO_GET_ISSUER_CERT).
|
|
//
|
|
// The missing-certificate case is special on TLS 1.3: ncrypto reports X509_V_OK
|
|
// for the resumed PSK handshake even though no certificate was presented, so
|
|
// the absence has to be detected explicitly (see onServerSocketSecure() in
|
|
// lib/internal/tls/wrap.js). The final case checks that such a certificate-less
|
|
// resumed session is rejected outright when rejectUnauthorized is set.
|
|
|
|
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const tls = require('tls');
|
|
const fixtures = require('../common/fixtures');
|
|
const { untrustedLeafErrorCode } = require('../common/tls');
|
|
const { once } = require('events');
|
|
|
|
const ca = fixtures.readKey('ca1-cert.pem');
|
|
const serverCert = {
|
|
key: fixtures.readKey('agent2-key.pem'),
|
|
cert: fixtures.readKey('agent2-cert.pem'),
|
|
};
|
|
|
|
// Client certificate variants, keyed by the peer state they produce.
|
|
const CLIENTS = {
|
|
trusted: { // Signed by ca1
|
|
creds: {
|
|
key: fixtures.readKey('agent1-key.pem'),
|
|
cert: fixtures.readKey('agent1-cert.pem'),
|
|
},
|
|
authorized: true,
|
|
authorizationError: null,
|
|
peerCN: 'agent1',
|
|
},
|
|
untrusted: { // Signed by ca2, not trusted
|
|
creds: {
|
|
key: fixtures.readKey('agent3-key.pem'),
|
|
cert: fixtures.readKey('agent3-cert.pem'),
|
|
},
|
|
authorized: false,
|
|
authorizationError: untrustedLeafErrorCode,
|
|
peerCN: 'agent3',
|
|
},
|
|
missing: { // No client certificate
|
|
creds: {},
|
|
authorized: false,
|
|
authorizationError: 'UNABLE_TO_GET_ISSUER_CERT',
|
|
peerCN: undefined,
|
|
},
|
|
};
|
|
|
|
async function handshake(options, captureSession) {
|
|
const socket = tls.connect(options);
|
|
const sessionPromise = captureSession ?
|
|
once(socket, 'session').then(([session]) => session) : null;
|
|
|
|
socket.resume();
|
|
await once(socket, 'secureConnect');
|
|
|
|
const closePromise = once(socket, 'close');
|
|
const session = sessionPromise ? await sessionPromise : undefined;
|
|
socket.end();
|
|
await closePromise;
|
|
return session;
|
|
}
|
|
|
|
// Test a single resumption configuration and expected result:
|
|
async function testResumption(version, name) {
|
|
const { creds, authorized, authorizationError, peerCN } = CLIENTS[name];
|
|
|
|
let connections = 0;
|
|
const server = tls.createServer({
|
|
...serverCert,
|
|
ca,
|
|
requestCert: true,
|
|
rejectUnauthorized: false,
|
|
minVersion: version,
|
|
maxVersion: version,
|
|
}, common.mustCall((socket) => {
|
|
// 2nd conn must resume:
|
|
const resumed = connections++ === 1;
|
|
const where = `${version} ${name} ${resumed ? 'resumed' : 'new'}`;
|
|
assert.strictEqual(socket.isSessionReused(), resumed, where);
|
|
|
|
// Both conns must report same expected auth state:
|
|
assert.strictEqual(socket.authorized, authorized, where);
|
|
assert.strictEqual(socket.authorizationError, authorizationError, where);
|
|
const peer = socket.getPeerCertificate();
|
|
if (peerCN === undefined)
|
|
assert.deepStrictEqual(peer, {}, where);
|
|
else
|
|
assert.strictEqual(peer.subject.CN, peerCN, where);
|
|
|
|
// N.b. BoringSSL only sends a ticket after a write:
|
|
socket.end('.');
|
|
}, 2));
|
|
|
|
server.listen(0);
|
|
await once(server, 'listening');
|
|
|
|
const options = {
|
|
port: server.address().port,
|
|
host: '127.0.0.1',
|
|
checkServerIdentity: () => undefined,
|
|
rejectUnauthorized: false,
|
|
minVersion: version,
|
|
maxVersion: version,
|
|
...creds,
|
|
};
|
|
|
|
try {
|
|
const session = await handshake(options, true);
|
|
assert(session);
|
|
await handshake({ ...options, session });
|
|
} finally {
|
|
server.close();
|
|
await once(server, 'close');
|
|
}
|
|
}
|
|
|
|
// Test the special case of resumption from rejectUnauthorized:false to
|
|
// rejectUnauthorized:true, which must be rejected even though the original
|
|
// session worked initially.
|
|
async function testRejectResumedWithoutCert() {
|
|
const options = {
|
|
...serverCert,
|
|
ca,
|
|
requestCert: true,
|
|
minVersion: 'TLSv1.3',
|
|
maxVersion: 'TLSv1.3',
|
|
ticketKeys: crypto.randomBytes(48),
|
|
};
|
|
const lenient = tls.createServer({ ...options, rejectUnauthorized: false });
|
|
lenient.on('secureConnection', common.mustCall((socket) => {
|
|
assert.strictEqual(socket.authorized, false);
|
|
assert.strictEqual(socket.authorizationError, 'UNABLE_TO_GET_ISSUER_CERT');
|
|
socket.end('.');
|
|
}));
|
|
|
|
const strict = tls.createServer({ ...options, rejectUnauthorized: true });
|
|
strict.on('secureConnection', common.mustNotCall());
|
|
|
|
const clientOptions = (port) => ({
|
|
port,
|
|
host: '127.0.0.1',
|
|
rejectUnauthorized: false,
|
|
checkServerIdentity: () => undefined,
|
|
minVersion: 'TLSv1.3',
|
|
maxVersion: 'TLSv1.3',
|
|
});
|
|
|
|
lenient.listen(0);
|
|
await once(lenient, 'listening');
|
|
const session = await handshake(clientOptions(lenient.address().port), true);
|
|
assert(session);
|
|
lenient.close();
|
|
await once(lenient, 'close');
|
|
|
|
strict.listen(0);
|
|
await once(strict, 'listening');
|
|
|
|
const resumed = tls.connect({ ...clientOptions(strict.address().port), session });
|
|
resumed.on('error', () => {}); // May observe the server's reset.
|
|
resumed.resume();
|
|
|
|
// The client completes the resumed handshake (it has the server's Finished)
|
|
// before the server's reset can arrive, so this asserts the strict server
|
|
// actually resumed rather than falling back to a rejected full handshake.
|
|
await once(resumed, 'secureConnect');
|
|
assert.strictEqual(resumed.isSessionReused(), true);
|
|
|
|
// Then the socket is destroyed during 'secure', which surfaces as a reset
|
|
// rather than a handshake failure.
|
|
const [err] = await once(strict, 'tlsClientError');
|
|
assert.strictEqual(err.code, 'ECONNRESET');
|
|
|
|
resumed.destroy();
|
|
strict.close();
|
|
await once(strict, 'close');
|
|
}
|
|
|
|
(async function() {
|
|
// Run the full matrix of configurations:
|
|
for (const version of ['TLSv1.2', 'TLSv1.3'])
|
|
for (const name of Object.keys(CLIENTS))
|
|
await testResumption(version, name);
|
|
|
|
// Validate the rejectUnauth:false->true case
|
|
await testRejectResumedWithoutCert();
|
|
})().then(common.mustCall());
|