Skip to content
Merged
73 changes: 73 additions & 0 deletions test-parity/node-suite/tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# `node:tls` granular parity coverage

This directory contains independent print-and-diff fixtures for the TLS API. The
Node oracle is Node.js 26.5.0. Network cases use only
`127.0.0.1`, bind port `0`, and use the fixed non-secret certificate material in
`fixtures/`. The localhost and SNI certificates share the committed test key and
cover only `localhost`/`127.0.0.1` and `api.local`; no case consults internet
endpoints or host trust stores.

## Upstream audit

The expansion was checked against these primary sources:

- Node.js 26.5.0 (`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`): the 241 TLS-named cases under [`test/parallel`](https://github.com/nodejs/node/tree/v26.5.0/test/parallel) and [`test/sequential`](https://github.com/nodejs/node/tree/v26.5.0/test/sequential), especially `test-tls-add-context.js`, `test-tls-alpn-server-client.js`, `test-tls-basic-validations.js`, `test-tls-ca-concat.js`, `test-tls-certificate-compression.js`, `test-tls-check-server-identity.js`, `test-tls-client-auth.js`, `test-tls-client-default-ciphers.js`, `test-tls-client-getephemeralkeyinfo.js`, `test-tls-connect-given-socket.js`, `test-tls-connect-simple.js`, `test-tls-connect-secure-context.js`, `test-tls-exportkeyingmaterial.js`, `test-tls-finished.js`, `test-tls-get-ca-certificates-default.js`, `test-tls-get-ca-certificates-error.js`, `test-tls-getcertificate-x509.js`, `test-tls-getcipher.js`, `test-tls-getprotocol.js`, `test-tls-ip-servername-forbidden.js`, `test-tls-multiple-cas-as-string.js`, `test-tls-no-cert-required.js`, `test-tls-options-boolean-check.js`, `test-tls-peer-certificate.js`, `test-tls-root-certificates.js`, `test-tls-secure-context-usage-order.js`, `test-tls-server-connection-server.js`, `test-tls-server-parent-constructor-options.js`, `test-tls-server-setkeycert.js`, `test-tls-set-default-ca-certificates-array-buffer.js`, `test-tls-set-default-ca-certificates-basic.js`, `test-tls-set-default-ca-certificates-error.js`, `test-tls-set-default-ca-certificates-mixed-types.js`, `test-tls-set-default-ca-certificates-recovery.js`, `test-tls-set-secure-context.js`, `test-tls-signal.js`, `test-tls-sni-case-insensitive.js`, `test-tls-sni-server-client.js`, `test-tls-snicallback-error.js`, `test-tls-socket-allow-half-open-option.js`, `test-tls-socket-constructor-alpn-options-parsing.js`, `test-tls-secure-session.js`, `test-tls-set-sigalgs.js`, and `test-tls-ticket-invalid-arg.js`.
- Deno `e6a0143641690dfb2723bab3ce97c394d5ea3ec3`: the current Node compatibility selections in [`tests/node_compat/config.jsonc`](https://github.com/denoland/deno/blob/e6a0143641690dfb2723bab3ce97c394d5ea3ec3/tests/node_compat/config.jsonc) and the implementation in [`ext/node/polyfills/tls_esm.ts`](https://github.com/denoland/deno/blob/e6a0143641690dfb2723bab3ce97c394d5ea3ec3/ext/node/polyfills/tls_esm.ts).
- Bun `4bbe0751a2e5436757768325c2cc7ed97dc8767c`: its current dedicated [`test/js/node/tls`](https://github.com/oven-sh/bun/tree/4bbe0751a2e5436757768325c2cc7ed97dc8767c/test/js/node/tls) cases and selected Node TLS tests under [`test/js/node/test/parallel`](https://github.com/oven-sh/bun/tree/4bbe0751a2e5436757768325c2cc7ed97dc8767c/test/js/node/test/parallel).

The local cases reduce those larger suites to deterministic semantic probes for
exports/classes/constants, ALPN wire conversion, root/default CA identity,
validation, mutation, connection application and explicit-CA precedence,
secure-context scalar/container material and algorithm validation, certificate
compression surface/validation, certificate and hostname identity including
custom acceptance/rejection callbacks, positional/context-backed/preconnected
loopback handshakes, required and optional client certificates, concatenated CA
inputs, missing-certificate and plaintext rejection, event cleanup, abort
signals, socket getters, mutable protocol/cipher defaults, `setKeyCert`, live
server-context rotation, shared signature algorithms and certificate views,
finished-message/exporter material, ALPN callback/protocol/key selection,
asynchronous and null-context SNI selection, case-insensitive wildcard SNI and
latest-`addContext` selection, TLS 1.2 session resumption, ticket keys, and
fragment-size controls.

## Build-mode diagnostic

A direct `node:tls` import currently exposes two separate build paths:

1. Auto-optimization attempts to rebuild `perry-stdlib` and fails at
`crates/perry-stdlib/src/tls.rs` because `rustls::crypto::ring` is referenced
while rustls's `ring` feature is not activated.
2. Without matched static fallback archives, compilation then reports missing
`libperry_runtime.a`. With `PERRY_RUNTIME_DIR=target/perry-dev` after building
`perry-runtime-static` and `perry-stdlib-static`, fixtures reach the behavioral
lane. Some socket cases then expose a separate link failure for the undefined
`_js_net_socket_once` symbol. Mixed `node:net` plus `node:tls` plaintext and
preconnected-socket cases expose another fallback-composition failure where
the TLS server symbols are absent from the selected archives.

These are feature/archive composition diagnostics, not changes made by this
suite. The fixtures intentionally remain present so later runtime/build work can
turn each category into behavioral results without rewriting coverage.

## Stopping boundary

The remaining upstream TLS cases were not copied when they depend on one or more
of the following:

- renegotiation, tracing/keylog output, PSK, OCSP, FIPS, engines/providers,
OpenSSL/rustls-specific cipher/security-level behavior, or legacy protocol
availability;
- system/extra CA environment flags, expired certificates, platform cipher
inventories, native certificate dumps, or absolute fixture paths;
- internet DNS/endpoints, named pipes, fixed ports, process CLI flags, cluster or
worker orchestration;
- large transfers, memory/GC pressure, concurrent stress, fault injection,
socket/kernel races, handshake timeouts, or session-ticket timing;
- certificate-compression wire ratios, decompression limits, proxy mutation, or
compression-bomb behavior, which require backend-aware transport harnesses;
- raw error strings, stack traces, cipher ordering, ephemeral port values, and
other backend- or environment-specific output.

Those categories need separate targeted runtime/build work or a purpose-built
harness. Within the portable boundary, further upstream cases are duplicates of
the semantic contracts already represented here.
35 changes: 35 additions & 0 deletions test-parity/node-suite/tls/alpn/callback-invalid-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
let serverError = "none";
let clientError = false;
let secure = false;
const server = tls.createServer({ key, cert, ALPNCallback: () => "unoffered" });
server.on("tlsClientError", (err: any, socket) => {
serverError = err.code;
socket.destroy();
});
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
ALPNProtocols: ["h2", "http/1.1"],
});
client.on("secureConnect", () => {
secure = true;
client.destroy();
});
client.on("error", () => {
clientError = true;
});
client.on("close", () =>
server.close(() => {
console.log("secure:", secure);
console.log("client error:", clientError);
console.log("server error:", serverError);
}));
});
32 changes: 32 additions & 0 deletions test-parity/node-suite/tls/alpn/callback-negotiation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
let offered = "none";
let serverProtocol: string | false | null = null;
const server = tls.createServer({
key,
cert,
ALPNCallback({ protocols }) {
offered = protocols.join(",");
return protocols.includes("h2") ? "h2" : undefined;
},
}, (socket) => {
serverProtocol = socket.alpnProtocol;
socket.end();
});
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
ALPNProtocols: ["http/1.1", "h2"],
});
client.on("secureConnect", () => console.log("client:", client.alpnProtocol));
client.on("close", () => server.close(() => {
console.log("server:", serverProtocol);
console.log("offered:", offered);
}));
});
21 changes: 21 additions & 0 deletions test-parity/node-suite/tls/alpn/conversion-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import tls from "node:tls";

try {
const out: any = {};
console.log(
"return:",
tls.convertALPNProtocols(["h2", "http/1.1"], out) === undefined,
);
console.log("buffer:", Buffer.isBuffer(out.ALPNProtocols));
console.log("encoding:", out.ALPNProtocols.toString("hex"));

const empty: any = {};
tls.convertALPNProtocols([], empty);
console.log(
"empty:",
Buffer.isBuffer(empty.ALPNProtocols),
empty.ALPNProtocols.length,
);
} catch (err: any) {
console.log("error:", err instanceof TypeError, err.code ?? "none");
}
19 changes: 19 additions & 0 deletions test-parity/node-suite/tls/alpn/conversion-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import tls from "node:tls";

function probe(label: string, protocols: any) {
const out: any = { marker: true };
try {
tls.convertALPNProtocols(protocols, out);
console.log(label + ":", Object.keys(out).sort().join(","));
} catch (err: any) {
console.log(
label + ":",
err instanceof RangeError || err instanceof TypeError,
err.code ?? "none",
);
}
}
probe("too long", ["a".repeat(256)]);
probe("non string", [1]);
probe("boolean", true);
probe("null", null);
25 changes: 25 additions & 0 deletions test-parity/node-suite/tls/alpn/conversion-views.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import tls from "node:tls";

const source = Buffer.from([9, 2, 104, 50, 9]);
for (
const [label, view] of [
["buffer", source.subarray(1, 4)],
["uint8", new Uint8Array(source.buffer, source.byteOffset + 1, 3)],
["dataview", new DataView(source.buffer, source.byteOffset + 1, 3)],
] as const
) {
const out: any = {};
try {
tls.convertALPNProtocols(view, out);
source[2] = 120;
console.log(
label + ":",
Buffer.isBuffer(out.ALPNProtocols),
out.ALPNProtocols.toString("hex"),
);
} catch (err: any) {
console.log(label + ":", err instanceof TypeError, err.code ?? "none");
} finally {
source[2] = 104;
}
}
16 changes: 16 additions & 0 deletions test-parity/node-suite/tls/alpn/negotiation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
let serverProtocol: string | false | null = null;
const server = tls.createServer({ key, cert, ALPNProtocols: ["h2", "http/1.1"] }, (socket) => {
serverProtocol = socket.alpnProtocol;
socket.end();
});
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, rejectUnauthorized: false, ALPNProtocols: ["http/1.1", "h2"] });
client.on("secureConnect", () => console.log("client:", client.alpnProtocol));
client.on("close", () => server.close(() => console.log("server:", serverProtocol)));
});
28 changes: 28 additions & 0 deletions test-parity/node-suite/tls/alpn/no-overlap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
let secure = false;
let clientError = "none";
let serverError = "none";
const server = tls.createServer({ key, cert, ALPNProtocols: ["h2"] });
server.on("tlsClientError", (err: any) => { serverError = err.code; });
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
ALPNProtocols: ["http/1.1"],
});
client.on("secureConnect", () => {
secure = true;
client.destroy();
});
client.on("error", (err: any) => { clientError = err.code; });
client.on("close", () => server.close(() => {
console.log("secure:", secure);
console.log("errors:", clientError, serverError);
}));
});
43 changes: 43 additions & 0 deletions test-parity/node-suite/tls/alpn/one-sided-negotiation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const cert = readFileSync(new URL("localhost-cert.pem", fixture));
run(
"server only",
{ ALPNProtocols: ["h2"] },
{},
() => run("client only", {}, { ALPNProtocols: ["h2"] }, () => {}),
);

function run(
label: string,
serverOptions: any,
clientOptions: any,
done: () => void,
) {
let serverProtocol: string | false | null = null;
const server = tls.createServer({ key, cert, ...serverOptions }, (socket) => {
serverProtocol = socket.alpnProtocol;
socket.end();
});
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
...clientOptions,
});
client.on(
"secureConnect",
() => console.log(label + " client:", client.alpnProtocol),
);
client.on("error", (err: any) => console.log(label + " error:", err.code));
client.on("close", () =>
server.close(() => {
console.log(label + " server:", serverProtocol);
done();
}));
});
}
52 changes: 52 additions & 0 deletions test-parity/node-suite/tls/alpn/set-key-cert-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import tls from "node:tls";
import { readFileSync } from "node:fs";

const fixture = new URL("../fixtures/", import.meta.url);
const key = readFileSync(new URL("localhost-key.pem", fixture));
const defaultCert = readFileSync(new URL("localhost-cert.pem", fixture));
const apiCert = readFileSync(new URL("api-local-cert.pem", fixture));
const values = [
{ label: "options", value: { key, cert: apiCert } },
{
label: "context",
value: tls.createSecureContext({ key, cert: apiCert }),
},
];
run(0);

function run(index: number) {
if (index === values.length) return;
const current = values[index];
let callbackSocket = false;
const server = tls.createServer({
key,
cert: defaultCert,
ALPNCallback({ protocols }) {
callbackSocket = this instanceof tls.TLSSocket;
this.setKeyCert(current.value);
return protocols[0];
},
}, (socket) => socket.end());
server.listen(0, "127.0.0.1", () => {
const client = tls.connect({
host: "127.0.0.1",
port: (server.address() as any).port,
rejectUnauthorized: false,
ALPNProtocols: ["acme-tls/1"],
});
client.on("secureConnect", () => {
console.log(
current.label + ":",
callbackSocket,
client.alpnProtocol,
(client.getPeerCertificate() as any).subject?.CN,
);
client.end();
});
client.on(
"error",
(err: any) => console.log(current.label + " error:", err.code),
);
client.on("close", () => server.close(() => run(index + 1)));
});
}
15 changes: 15 additions & 0 deletions test-parity/node-suite/tls/context/algorithm-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import tls from "node:tls";

function probe(label: string, options: any) {
try {
console.log(label + ":", tls.createSecureContext(options) instanceof tls.SecureContext);
} catch (err: any) {
console.log(label + ":", err instanceof Error, err instanceof TypeError, err.code ?? "none");
}
}
probe("valid sigalgs", { sigalgs: "rsa_pss_rsae_sha256" });
probe("sigalgs type", { sigalgs: 1 });
probe("sigalgs value", { sigalgs: "not-an-algorithm" });
probe("valid curve", { ecdhCurve: "prime256v1" });
probe("curve type", { ecdhCurve: 1 });
probe("curve value", { ecdhCurve: "not-a-curve" });
Loading
Loading