From 4478d4ba7e53922b525df092130d39b1fa4b4d8e Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 8 May 2026 11:12:29 +0200 Subject: [PATCH] quic: expose QUIC certificates as JS X509Certificate, not raw handles Signed-off-by: Tim Perry --- doc/api/quic.md | 18 ++++++----- lib/internal/quic/quic.js | 31 ++++++++++++++----- .../parallel/test-quic-session-properties.mjs | 15 +++++++-- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index ddf0c9331ef533..21a929a7a1e654 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -833,11 +833,12 @@ what this endpoint advertises to the peer as its own maximum. added: REPLACEME --> -* Type: {Object|undefined} +* Type: {crypto.X509Certificate|undefined} -The local certificate as an object with properties such as `subject`, -`issuer`, `valid_from`, `valid_to`, `fingerprint`, etc. Returns `undefined` -if the session is destroyed or no certificate is available. +The local certificate as a [`crypto.X509Certificate`][] instance. Server +sessions return the certificate configured for the negotiated SNI host. +Client sessions return `undefined` unless a client certificate was sent. +Returns `undefined` if the session is destroyed. ### `session.peerCertificate` @@ -845,11 +846,11 @@ if the session is destroyed or no certificate is available. added: REPLACEME --> -* Type: {Object|undefined} +* Type: {crypto.X509Certificate|undefined} -The peer's certificate as an object with properties such as `subject`, -`issuer`, `valid_from`, `valid_to`, `fingerprint`, etc. Returns `undefined` -if the session is destroyed or the peer did not present a certificate. +The peer's certificate as a [`crypto.X509Certificate`][] instance. Returns +`undefined` if the peer did not present a certificate or the session is +destroyed. ### `session.ephemeralKeyInfo` @@ -3499,6 +3500,7 @@ throughput issues caused by flow control. [`application.enableConnectProtocol`]: #sessionoptionsapplication [`application.enableDatagrams`]: #sessionoptionsapplication [`application.qpackMaxDTableCapacity`]: #sessionoptionsapplication +[`crypto.X509Certificate`]: crypto.md#class-x509certificate [`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost [`endpoint.maxConnectionsTotal`]: #endpointmaxconnectionstotal [`error.errorCode`]: #errorerrorcode diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index b9cbc8feb62e20..b06360c2c43228 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -146,6 +146,10 @@ const { isKeyObject, } = require('internal/crypto/keys'); +const { + InternalX509Certificate, +} = require('internal/crypto/x509'); + const { FileHandle, kHandle: kFileHandle, @@ -2989,24 +2993,37 @@ class QuicSession { } /** - * The local certificate as an object, or undefined if not available. - * @type {object|undefined} + * The local certificate as a {@link crypto.X509Certificate}, or undefined + * if no local certificate is available. Server sessions return their + * configured certificate; client sessions return undefined unless a + * client certificate was sent. + * @type {crypto.X509Certificate|undefined} */ get certificate() { QuicSession.#assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#certificate ??= this.#handle.getCertificate(); + if (this.#certificate === undefined) { + const handle = this.#handle.getCertificate(); + this.#certificate = handle ? new InternalX509Certificate(handle) : null; + } + return this.#certificate ?? undefined; } /** - * The peer's certificate as an object, or undefined if the peer did - * not present a certificate or the session is destroyed. - * @type {object|undefined} + * The peer's certificate as a {@link crypto.X509Certificate}, or undefined + * if the peer did not present a certificate or the session is destroyed. + * @type {crypto.X509Certificate|undefined} */ get peerCertificate() { QuicSession.#assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#peerCertificate ??= this.#handle.getPeerCertificate(); + if (this.#peerCertificate === undefined) { + const handle = this.#handle.getPeerCertificate(); + this.#peerCertificate = handle ? + new InternalX509Certificate(handle) : + null; + } + return this.#peerCertificate ?? undefined; } /** diff --git a/test/parallel/test-quic-session-properties.mjs b/test/parallel/test-quic-session-properties.mjs index ced11a7bdd7244..f5c0cc1c30988c 100644 --- a/test/parallel/test-quic-session-properties.mjs +++ b/test/parallel/test-quic-session-properties.mjs @@ -12,10 +12,15 @@ // All three return undefined after destroy. import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; import assert from 'node:assert'; +import { X509Certificate } from 'node:crypto'; const { ok, strictEqual } = assert; +// The QUIC test helpers configure both sides with the agent1 fixture cert, +const expectedCert = new X509Certificate(fixtures.readKey('agent1-cert.pem')); + if (!hasQuic) { skip('QUIC is not enabled'); } @@ -38,7 +43,10 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { // Own certificate. const cert = serverSession.certificate; - ok(cert); + ok(cert instanceof X509Certificate); + strictEqual(cert.subject, expectedCert.subject); + strictEqual(cert.issuer, expectedCert.issuer); + strictEqual(cert.fingerprint256, expectedCert.fingerprint256); // Peer certificate (client's cert — not set in this // test since we don't use verifyClient, so it's undefined). @@ -65,7 +73,10 @@ strictEqual(clientSession.path, path); // Peer certificate (server's cert). const peerCert = clientSession.peerCertificate; -ok(peerCert); +ok(peerCert instanceof X509Certificate); +strictEqual(peerCert.subject, expectedCert.subject); +strictEqual(peerCert.issuer, expectedCert.issuer); +strictEqual(peerCert.fingerprint256, expectedCert.fingerprint256); // Ephemeral key info (client only). const keyInfo = clientSession.ephemeralKeyInfo;