Skip to content

tlsSocket.getPeerCertificate(true) not returning full certificate chain when using SNICallback. #46163

Open
@sidrubs

Description

@sidrubs

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    tlsIssues and PRs related to the tls subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions