Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
test: add snicallback prioritization test
Depending on the content of a cert, it seems that OpenSSL
will prioritize the root cert over the context returned by
SNICallback. This behavior is unexpected. This PR adds a
test that demonstrates the behavior. Ideally there is a fix,
or a explanation for this behavior we can add to the docs.

Fixes: #54235
PR-URL: #54251
  • Loading branch information
Ethan-Arrowood committed Aug 14, 2025
commit fd1228ff16706cb745cd07643a6fe7186c96b2ad
94 changes: 94 additions & 0 deletions test/parallel/test-https-snicallback-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

const common = require('../common');
const { it, describe } = require('node:test');
const assert = require('node:assert');
const tls = require('node:tls');
const https = require('node:https');
const dns = require('node:dns');
const events = require('node:events');

const fixtures = require('../common/fixtures');
const crypto = require('node:crypto');

const root = {
cert: fixtures.readKey('ca5-cert.pem'),
key: fixtures.readKey('ca5-key.pem')
};

const agent1 = {
cert: fixtures.readKey('agent1-cert.pem'),
key: fixtures.readKey('agent1-key.pem')
};

const PORT = 8443;

const sni = {
'ca5.com': {
cert: new crypto.X509Certificate(root.cert),
secureContext: tls.createSecureContext(root)
},
'agent1.com': {
cert: new crypto.X509Certificate(agent1.cert),
context: tls.createSecureContext(agent1),
},
};

const agent = new https.Agent({
lookup: (hostname, options, cb) => {
if (Object.keys(sni).includes(hostname))
hostname = 'localhost';
return dns.lookup(hostname, options, cb);
}
});

function makeRequest(url, expectedCert) {
return new Promise((resolve, reject) => {
https.get(url, { rejectUnauthorized: false, agent }, common.mustCall((response) => {
const actualCert = response.socket.getPeerX509Certificate();

assert.deepStrictEqual(actualCert.subject, expectedCert.subject);

response.on('data', common.mustCall((chunk) => {
assert.strictEqual(chunk.toString(), 'Hello, World!');
resolve();
}));

response.on('error', reject);
})).on('error', reject);
});
}

describe('Regression test for SNICallback / Certification prioritization issue', () => {
it('should use certificates from SNICallback', async (t) => {
let snicbCount = 0;
const server = https.createServer({
cert: root.cert,
key: root.key,
SNICallback: (servername, cb) => {
snicbCount++;
// This returns the secure context generated from the respective certificate
cb(null, sni[servername].context);
}
}, (req, res) => {
res.writeHead(200);
res.end('Hello, World!');
}).on('error', common.mustNotCall);

server.listen(PORT);
await events.once(server, 'listening');

// Assert that raw IP address gets the root cert
await makeRequest(`https://127.0.0.1:${PORT}`, sni['ca5.com'].cert);

for (const [hostname, { cert: expectedCert }] of Object.entries(sni)) {
// Currently, the agent1 request will fail as it receives the root cert (ca5) instead.
await makeRequest(`https://${hostname}:${PORT}`, expectedCert);
}

// SNICallback should only be called for the hostname requests, not the IP one
assert.strictEqual(snicbCount, 2);

server.close();
});
});