Description
Version
v16.5.0, v18.13.0, v19.4.0
Platform
21.6.0 Darwin Kernel Version 21.6.0: Sun Nov 6 23:31:09 PST 2022; root:xnu-8020.240.14~1/RELEASE_ARM64_T8110 arm64
Subsystem
tls
What steps will reproduce the bug?
As per the documentation, the req.socket.getPeerCertificate(true)
function should return the full certificate chain with the issuerCertificate
property containing an object representing its issuer's certificate.
This works as expected when the rootCA from which the key
and cert
were generated is added in the ca
attribute of the tls options directly on creation of the https server. However, when the rootCA of interest added via an SNICallback, only the peer certificate (excluding the full certificate chain, i.e. what would be generated by req.socket.getPeerCertificate(false)
) is returned. This is demonstrated in the mutual TLS example below.
Prerequisites: A rootCA and child server and client certs and keys should be generated (these are represented by
*.actual.*
in the example). I also generated another rootCA and child server cert and key which are not actually used for validation (these are represented by*.placeholder.*
in the example). I followed this gist for their generation.
A request can be made to the https server using curl
see below:
curl --cert client.actual.crt --key client.actual.key --cacert rootCA.actual.crt https://localhost:8000
With the code as it is below (rootCA.actual.crt
added in SNI callback) the req.socket.getPeerCertificate(true).issuerCertificate
will log undefined
to the server's console. However, uncommenting the line that adds the rootCA.actual.crt
to the server configuration options, the req.socket.getPeerCertificate(true).issuerCertificate
will log the full issuer certificate chain as one would expect.
const https = require("node:https");
const tls = require("node:tls");
const fs = require("node:fs");
function sniCallback(serverName, callback) {
callback(
null,
new tls.createSecureContext({
cert: fs.readFileSync("certs/server.actual.crt"),
key: fs.readFileSync("certs/server.actual.key"),
ca: fs.readFileSync("certs/rootCA.actual.crt"),
})
);
}
const options = {
requestCert: true,
rejectUnauthorized: false,
key: fs.readFileSync("certs/server.placeholder.key"),
cert: fs.readFileSync("certs/server.placeholder.crt"),
ca: [
fs.readFileSync("certs/rootCA.placeholder.crt"),
// Uncomment the certificate read below to get full certificate chain.
// fs.readFileSync("certs/rootCA.actual.crt"),
],
SNICallback: sniCallback,
};
https
.createServer(options, (req, res) => {
// This will log `undefined` if the required rootCA is only added in the
// SNI Callback.
console.log(
"Issuer cert: ",
req.socket.getPeerCertificate(true).issuerCertificate
);
res.writeHead(200);
res.end("certificate check\n");
})
.listen(8000);
How often does it reproduce? Is there a required condition?
It reproduces every time with no special conditions that I have determined.
What is the expected behavior?
req.socket.getPeerCertificate(true)
should return the full certificate chain, including issuerCertificate
What do you see instead?
req.socket.getPeerCertificate(true)
is only returning just the peer's certificate, excluding issuerCertificate
. Thus the issuerCertificate
attribute is undefined
.
Additional information
No response