diff --git a/test-parity/node-suite/tls/README.md b/test-parity/node-suite/tls/README.md new file mode 100644 index 0000000000..65536d819a --- /dev/null +++ b/test-parity/node-suite/tls/README.md @@ -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. diff --git a/test-parity/node-suite/tls/alpn/callback-invalid-result.ts b/test-parity/node-suite/tls/alpn/callback-invalid-result.ts new file mode 100644 index 0000000000..b85b3a0bb0 --- /dev/null +++ b/test-parity/node-suite/tls/alpn/callback-invalid-result.ts @@ -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); + })); +}); diff --git a/test-parity/node-suite/tls/alpn/callback-negotiation.ts b/test-parity/node-suite/tls/alpn/callback-negotiation.ts new file mode 100644 index 0000000000..1ba4608276 --- /dev/null +++ b/test-parity/node-suite/tls/alpn/callback-negotiation.ts @@ -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); + })); +}); diff --git a/test-parity/node-suite/tls/alpn/conversion-array.ts b/test-parity/node-suite/tls/alpn/conversion-array.ts new file mode 100644 index 0000000000..9c218ba65f --- /dev/null +++ b/test-parity/node-suite/tls/alpn/conversion-array.ts @@ -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"); +} diff --git a/test-parity/node-suite/tls/alpn/conversion-validation.ts b/test-parity/node-suite/tls/alpn/conversion-validation.ts new file mode 100644 index 0000000000..ba09073f13 --- /dev/null +++ b/test-parity/node-suite/tls/alpn/conversion-validation.ts @@ -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); diff --git a/test-parity/node-suite/tls/alpn/conversion-views.ts b/test-parity/node-suite/tls/alpn/conversion-views.ts new file mode 100644 index 0000000000..675981fd7e --- /dev/null +++ b/test-parity/node-suite/tls/alpn/conversion-views.ts @@ -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; + } +} diff --git a/test-parity/node-suite/tls/alpn/negotiation.ts b/test-parity/node-suite/tls/alpn/negotiation.ts new file mode 100644 index 0000000000..e15893b033 --- /dev/null +++ b/test-parity/node-suite/tls/alpn/negotiation.ts @@ -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))); +}); diff --git a/test-parity/node-suite/tls/alpn/no-overlap.ts b/test-parity/node-suite/tls/alpn/no-overlap.ts new file mode 100644 index 0000000000..577059f4eb --- /dev/null +++ b/test-parity/node-suite/tls/alpn/no-overlap.ts @@ -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); + })); +}); diff --git a/test-parity/node-suite/tls/alpn/one-sided-negotiation.ts b/test-parity/node-suite/tls/alpn/one-sided-negotiation.ts new file mode 100644 index 0000000000..64f59ff013 --- /dev/null +++ b/test-parity/node-suite/tls/alpn/one-sided-negotiation.ts @@ -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(); + })); + }); +} diff --git a/test-parity/node-suite/tls/alpn/set-key-cert-selection.ts b/test-parity/node-suite/tls/alpn/set-key-cert-selection.ts new file mode 100644 index 0000000000..414eca1f3d --- /dev/null +++ b/test-parity/node-suite/tls/alpn/set-key-cert-selection.ts @@ -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))); + }); +} diff --git a/test-parity/node-suite/tls/context/algorithm-validation.ts b/test-parity/node-suite/tls/context/algorithm-validation.ts new file mode 100644 index 0000000000..4003089d2e --- /dev/null +++ b/test-parity/node-suite/tls/context/algorithm-validation.ts @@ -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" }); diff --git a/test-parity/node-suite/tls/context/certificate-compression-validation.ts b/test-parity/node-suite/tls/context/certificate-compression-validation.ts new file mode 100644 index 0000000000..006e8992ec --- /dev/null +++ b/test-parity/node-suite/tls/context/certificate-compression-validation.ts @@ -0,0 +1,31 @@ +import tls from "node:tls"; + +const getAlgorithms = (tls as any).getCertificateCompressionAlgorithms; +const supported: string[] = typeof getAlgorithms === "function" + ? getAlgorithms() + : []; +function probe(label: string, options: any) { + try { + console.log( + label + ":", + tls.createSecureContext(options) instanceof tls.SecureContext, + ); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code ?? "none"); + } +} +probe("default", {}); +probe("undefined", { certificateCompression: undefined }); +probe("null", { certificateCompression: null }); +probe("empty", { certificateCompression: [] }); +probe("boolean", { certificateCompression: true }); +probe("string", { certificateCompression: "zlib" }); +probe("invalid", { certificateCompression: ["invalid"] }); +probe("non string", { certificateCompression: [1] }); +if (supported.length > 0) { + probe("supported", { certificateCompression: supported }); + probe("tls12 conflict", { + maxVersion: "TLSv1.2", + certificateCompression: [supported[0]], + }); +} diff --git a/test-parity/node-suite/tls/context/cipher-validation.ts b/test-parity/node-suite/tls/context/cipher-validation.ts new file mode 100644 index 0000000000..a89d65ed4f --- /dev/null +++ b/test-parity/node-suite/tls/context/cipher-validation.ts @@ -0,0 +1,15 @@ +import tls from "node:tls"; + +function probe(label: string, options: any) { + try { + tls.createSecureContext(options); + console.log(label + ": ok"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +probe("default", {}); +probe("tls13", { ciphers: "TLS_AES_128_GCM_SHA256" }); +probe("empty", { ciphers: "" }); +probe("number", { ciphers: 1 }); +probe("unknown", { ciphers: "NOT_A_CIPHER" }); diff --git a/test-parity/node-suite/tls/context/falsy-material-properties.ts b/test-parity/node-suite/tls/context/falsy-material-properties.ts new file mode 100644 index 0000000000..bfe8114f8a --- /dev/null +++ b/test-parity/node-suite/tls/context/falsy-material-properties.ts @@ -0,0 +1,16 @@ +import tls from "node:tls"; + +for (const [label, value] of [ + ["null", null], + ["false", false], + ["undefined", undefined], + ["empty string", ""], + ["zero", 0], +] as const) { + try { + const context = tls.createSecureContext({ key: value, cert: value, ca: value }); + console.log(label + ":", context instanceof tls.SecureContext); + } catch (err: any) { + console.log(label + ":", err.name, err.code); + } +} diff --git a/test-parity/node-suite/tls/context/material-container-validation.ts b/test-parity/node-suite/tls/context/material-container-validation.ts new file mode 100644 index 0000000000..9c685dd00b --- /dev/null +++ b/test-parity/node-suite/tls/context/material-container-validation.ts @@ -0,0 +1,21 @@ +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)); +for ( + const [label, options] of [ + ["key object", { key: { pem: key }, cert }], + ["key array item", { key: [key, true], cert: [cert, cert] }], + ["cert array item", { key: [key, key], cert: [cert, true] }], + ["ca array item", { key, cert, ca: [cert, true] }], + ] as const +) { + try { + tls.createSecureContext(options as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} diff --git a/test-parity/node-suite/tls/context/material-containers.ts b/test-parity/node-suite/tls/context/material-containers.ts new file mode 100644 index 0000000000..dd23f06f87 --- /dev/null +++ b/test-parity/node-suite/tls/context/material-containers.ts @@ -0,0 +1,30 @@ +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)); +for ( + const [label, options] of [ + ["buffer arrays", { key: [key], cert: [cert], ca: [cert] }], + ["string arrays", { + key: [key.toString()], + cert: [cert.toString()], + ca: [cert.toString()], + }], + ["key object buffer", { key: [{ pem: key }], cert }], + ["key object string", { + key: [{ pem: key.toString() }], + cert: cert.toString(), + }], + ] as const +) { + try { + console.log( + label + ":", + tls.createSecureContext(options as any) instanceof tls.SecureContext, + ); + } catch (err: any) { + console.log(label + ":", false, err.code ?? err.name); + } +} diff --git a/test-parity/node-suite/tls/context/material-type-validation.ts b/test-parity/node-suite/tls/context/material-type-validation.ts new file mode 100644 index 0000000000..3cd0c6264e --- /dev/null +++ b/test-parity/node-suite/tls/context/material-type-validation.ts @@ -0,0 +1,15 @@ +import tls from "node:tls"; + +function probe(label: string, options: any) { + try { + tls.createSecureContext(options); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +probe("key boolean", { key: true }); +probe("cert boolean", { cert: true }); +probe("ca object", { ca: {} }); +probe("passphrase number", { key: "bad", passphrase: 1 }); +probe("pfx object", { pfx: {} }); diff --git a/test-parity/node-suite/tls/context/secure-context-empty-options.ts b/test-parity/node-suite/tls/context/secure-context-empty-options.ts new file mode 100644 index 0000000000..b331cb34e4 --- /dev/null +++ b/test-parity/node-suite/tls/context/secure-context-empty-options.ts @@ -0,0 +1,10 @@ +import tls from "node:tls"; + +for (const value of [undefined, null, false, 0, ""] as any[]) { + try { + const context = tls.createSecureContext(value); + console.log(typeof value + ":", context instanceof tls.SecureContext, Object.keys(context).join(",")); + } catch (err: any) { + console.log(typeof value + ":", err.name, err.code); + } +} diff --git a/test-parity/node-suite/tls/context/secure-context-fixed-certificate.ts b/test-parity/node-suite/tls/context/secure-context-fixed-certificate.ts new file mode 100644 index 0000000000..16b1c1ae05 --- /dev/null +++ b/test-parity/node-suite/tls/context/secure-context-fixed-certificate.ts @@ -0,0 +1,14 @@ +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)); +for (const material of [ + { key, cert }, + { key: key.toString(), cert: cert.toString() }, + { key, cert, ca: cert }, +]) { + const context = tls.createSecureContext(material); + console.log("context:", context instanceof tls.SecureContext, typeof context.context); +} diff --git a/test-parity/node-suite/tls/context/server-context-rotation.ts b/test-parity/node-suite/tls/context/server-context-rotation.ts new file mode 100644 index 0000000000..3a46664999 --- /dev/null +++ b/test-parity/node-suite/tls/context/server-context-rotation.ts @@ -0,0 +1,55 @@ +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 localhostCert = readFileSync(new URL("localhost-cert.pem", fixture)); +const apiCert = readFileSync(new URL("api-local-cert.pem", fixture)); +let firstServerSocket: tls.TLSSocket | undefined; +let connection = 0; +const server = tls.createServer({ key, cert: localhostCert }, (socket) => { + if (++connection === 1) { + firstServerSocket = socket; + socket.write("before|"); + } else { + socket.end(); + } +}); +server.listen(0, "127.0.0.1", () => { + let firstData = ""; + let rotated = false; + const first = connect((client) => { + console.log( + "first cert:", + (client.getPeerCertificate() as any).subject?.CN, + ); + }); + first.on("data", (chunk) => { + firstData += chunk.toString(); + if (!rotated && firstData === "before|") { + rotated = true; + server.setSecureContext({ key, cert: apiCert }); + const second = connect((client) => { + console.log( + "second cert:", + (client.getPeerCertificate() as any).subject?.CN, + ); + client.end(); + }); + second.on("close", () => firstServerSocket?.end("after")); + } + }); + first.on("end", () => console.log("first data:", firstData)); + first.on("close", () => server.close()); +}); + +function connect(onSecure: (client: tls.TLSSocket) => void) { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + rejectUnauthorized: false, + }); + client.on("secureConnect", () => onSecure(client)); + client.on("error", (err: any) => console.log("client error:", err.code)); + return client; +} diff --git a/test-parity/node-suite/tls/context/server-set-secure-context.ts b/test-parity/node-suite/tls/context/server-set-secure-context.ts new file mode 100644 index 0000000000..a8885407a7 --- /dev/null +++ b/test-parity/node-suite/tls/context/server-set-secure-context.ts @@ -0,0 +1,17 @@ +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)); +const server = tls.createServer(); +console.log("valid return:", server.setSecureContext({ key, cert }) === undefined); +console.log("empty return:", server.setSecureContext({}) === undefined); +for (const [label, value] of [["null", null], ["number", 1]] as const) { + try { + server.setSecureContext(value as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} diff --git a/test-parity/node-suite/tls/context/typed-material.ts b/test-parity/node-suite/tls/context/typed-material.ts new file mode 100644 index 0000000000..c670fe2188 --- /dev/null +++ b/test-parity/node-suite/tls/context/typed-material.ts @@ -0,0 +1,18 @@ +import tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const fixture = new URL("../fixtures/", import.meta.url); +const key = new Uint8Array(readFileSync(new URL("localhost-key.pem", fixture))); +const cert = new Uint8Array(readFileSync(new URL("localhost-cert.pem", fixture))); +const cases: Array<[string, any, any]> = [ + ["uint8", key, cert], + ["dataview", new DataView(key.buffer, key.byteOffset, key.byteLength), new DataView(cert.buffer, cert.byteOffset, cert.byteLength)], + ["arraybuffer", key.buffer, cert.buffer], +]; +for (const [label, keyValue, certValue] of cases) { + try { + console.log(label + ":", tls.createSecureContext({ key: keyValue, cert: certValue }) instanceof tls.SecureContext); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} diff --git a/test-parity/node-suite/tls/context/version-validation.ts b/test-parity/node-suite/tls/context/version-validation.ts new file mode 100644 index 0000000000..39f18c8d08 --- /dev/null +++ b/test-parity/node-suite/tls/context/version-validation.ts @@ -0,0 +1,16 @@ +import tls from "node:tls"; + +function probe(label: string, options: any) { + try { + const context = tls.createSecureContext(options); + console.log(label + ":", context instanceof tls.SecureContext); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +probe("range", { minVersion: "TLSv1.2", maxVersion: "TLSv1.3" }); +probe("invalid minimum", { minVersion: "TLSv9" }); +probe("invalid maximum", { maxVersion: "TLSv9" }); +probe("minimum conflict", { secureProtocol: "TLSv1_2_method", minVersion: "TLSv1.2" }); +probe("maximum conflict", { secureProtocol: "TLSv1_2_method", maxVersion: "TLSv1.2" }); +probe("invalid method", { secureProtocol: "not-a-method" }); diff --git a/test-parity/node-suite/tls/fixtures/api-local-cert.pem b/test-parity/node-suite/tls/fixtures/api-local-cert.pem new file mode 100644 index 0000000000..c93df558fa --- /dev/null +++ b/test-parity/node-suite/tls/fixtures/api-local-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgIUJp6us08mvPkb7xXvvgYpDBR5F3owDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJYXBpLmxvY2FsMB4XDTI2MDcxNjEwMTg0M1oXDTM2MDcx +MzEwMTg0M1owFDESMBAGA1UEAwwJYXBpLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAjekpyhiK0q4H8TQo01JTA564FZpOitgwvIYMe3qhf0dF +lo2CbjxJcx5GOQ57k6vcNlLfIL2yV8f7hJNuFlfLAFvtm9pm45BvbsPvduW1AuSI +3oA/fpfsQ5K1VgAPbLZFhdndCjoGW3/ZO8PbUC5DTge5luCfXoV0zFzZATbJxziy +QZ9nYspc58Se6Xj0KhM3XCy2S7V5wVPRXo2nIW5ho83yHfKyVyKEew7nloxhrNAY +iRwHzVzBvCotdgZK/lBm1qsugHs31LR6T75izQGooIN1wz2V9kiHCW+s3CmgFCy2 +5AbN547xLwn4djh5Tz4lJhA4Rh0D0F/vzceL5ToJBwIDAQABo2kwZzAdBgNVHQ4E +FgQU1s+brNmcdkCqkncnW6rNlJpdiP0wHwYDVR0jBBgwFoAU1s+brNmcdkCqkncn +W6rNlJpdiP0wDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTALgglhcGkubG9jYWww +DQYJKoZIhvcNAQELBQADggEBACe3jZjwb9a0yZvHMBUTOThRxH0AVRfHqr41IamP +lZf9nOMokjTUAPp5+5FkIkIsyase72mm8fCmWrpDDT9SdiIpHY7b7N2ryJNmVA1c +sIdnexJedazm4F/DPGGxEb7TLIcanSRocyFuuFVclCEy7P0hCiQzoyPOPelRP8JN +XT5DRzvmd6dg+BU7PS6/WRdk8LsaZ3lnN1zry49SGnS3ibrZX00oWjVyPcd0nJUa +TItZKt+YXFg0loZMsIXkAVw4Jan9RZ3Nh0RHWf1MCDNloI4IlqSGkeOevpIuJQzW +7F/+Ti8/2S7wCCBE2v2ZvzxuwSFkDOSbcrzfMLh5E8YTajY= +-----END CERTIFICATE----- diff --git a/test-parity/node-suite/tls/fixtures/localhost-cert.pem b/test-parity/node-suite/tls/fixtures/localhost-cert.pem new file mode 100644 index 0000000000..d46ed9697d --- /dev/null +++ b/test-parity/node-suite/tls/fixtures/localhost-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAg2gAwIBAgIUZF3wbyk6BduDu+lEeegKd2ULMK8wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDUyNDE3NDI1NloXDTM2MDUy +MTE3NDI1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAjekpyhiK0q4H8TQo01JTA564FZpOitgwvIYMe3qhf0dF +lo2CbjxJcx5GOQ57k6vcNlLfIL2yV8f7hJNuFlfLAFvtm9pm45BvbsPvduW1AuSI +3oA/fpfsQ5K1VgAPbLZFhdndCjoGW3/ZO8PbUC5DTge5luCfXoV0zFzZATbJxziy +QZ9nYspc58Se6Xj0KhM3XCy2S7V5wVPRXo2nIW5ho83yHfKyVyKEew7nloxhrNAY +iRwHzVzBvCotdgZK/lBm1qsugHs31LR6T75izQGooIN1wz2V9kiHCW+s3CmgFCy2 +5AbN547xLwn4djh5Tz4lJhA4Rh0D0F/vzceL5ToJBwIDAQABo28wbTAdBgNVHQ4E +FgQU1s+brNmcdkCqkncnW6rNlJpdiP0wHwYDVR0jBBgwFoAU1s+brNmcdkCqkncn +W6rNlJpdiP0wDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH +BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAHFmvSxFCTHcqiocEHF3i0seBmNwWq40 +TtyVf9qyZYUZVqM/Z7tGDsNfNOhM+YscLs1ZTs8XzdpdYBEVyCLDYGjb4Cv6r5gS +hr+E0NQBnPuker6Rw64nzahfWYjf/Eo+7nwUbCahTbXHAs43c4m0bmL02r1NxVmv +BKGQKO/uR9Dy+3TKykNQkacKJ6oDxdTDovMUKlbwU/HlyzwK/HTm762cJfgZiMYM +uru8x9wmqogCQSAz2q6a6q/CZfn1o7S5KiWd0FzinP+50g5cSL/ob0GJ8Jge1oI5 +5rap/3DFfnTn0zfJ60U52+BVFnOIqkYT7/g5N4laGrza73tYXq7FV4s= +-----END CERTIFICATE----- diff --git a/test-parity/node-suite/tls/fixtures/localhost-key.pem b/test-parity/node-suite/tls/fixtures/localhost-key.pem new file mode 100644 index 0000000000..d1a37ff9e4 --- /dev/null +++ b/test-parity/node-suite/tls/fixtures/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN6SnKGIrSrgfx +NCjTUlMDnrgVmk6K2DC8hgx7eqF/R0WWjYJuPElzHkY5DnuTq9w2Ut8gvbJXx/uE +k24WV8sAW+2b2mbjkG9uw+925bUC5IjegD9+l+xDkrVWAA9stkWF2d0KOgZbf9k7 +w9tQLkNOB7mW4J9ehXTMXNkBNsnHOLJBn2diylznxJ7pePQqEzdcLLZLtXnBU9Fe +jachbmGjzfId8rJXIoR7DueWjGGs0BiJHAfNXMG8Ki12Bkr+UGbWqy6AezfUtHpP +vmLNAaigg3XDPZX2SIcJb6zcKaAULLbkBs3njvEvCfh2OHlPPiUmEDhGHQPQX+/N +x4vlOgkHAgMBAAECggEACFfV8iDBQKOkqeSkJdBoOwVA01xQE8+kBeFnqHbMOdxp +1fEZ4vs+Yjs8a6xTTZpEBxmWLqmYa5rBSckVJtEgiTPeY1RSyjw6oOt6D6Zvnuzq +sxIdKYcrB8n/SUAVqBGLQtRNL4W7y/NXRTE9mpgtss+3dIxeMkNsW3t18qFS+Zhg +TP8q984k+zl3QOz6sc5T39Unuk1g98LC2sjCXwKANzZRMBMigoGDnWgk9t86cEXM +YWmyStS89HKEDmxWQMRIc/6zw5YC9Jo0cF2OJxGtN/O+LLeeNoJcdnlSAcyYRU1Q +asJhtNkMwfMRrTVH0kQfF5X3a/aJfusiJnQBcvlVeQKBgQDF8rI8dlaQ7jNOfSoZ +FhphZe1DriFaulRA9PUwrxEb/qvRstre0Egu967ILmqoqKfufyNT4W5JnWngliN6 +S7D9cvxpW0RsUQHZXMqZp7s6kt4hAdziuyC2Wx2y6+zFHkbOwJcaULYrSNHJCPOj +cMu5TIplum+hnO9rMHKEpE0fAwKBgQC3h17rEy4uFbWPQD3fNjAi9QzIKX9wm8eD +SYekgZaHpAjrLCa8oNR6qMxU5Cpn7I3o2HegSUe29jDAr8GMp47JYRTGMHUl1Zwa +KtSGEH19sRhVUqIVW2h2/tysuaYpK1hFjPWM+KpKQFNzgt2EPf5057zE7gOHLcAL +UccMgP1crQKBgQCy4h1SaHrYZHq3LoNRwli6thrRc9YuoH4taXD+uuaSTvZE/gWv +H7hrwWcQ/mli229PJ1PspKc/HWMmE2giR669jCEwsMrHu/kYzjNE4oBfcYQNfhp4 +RzVLtlHDdFM226KPixnCLThDK35x14YdqHxiixnyzqW8/g6a5mBHIBeVswKBgQCT +y79DndGdqTvqHbj1zWScci0V8F1BqSHVd1x1vSolF5NbF9YmJ3qVQOQ0JP6FbHmn +ntNPUFQhYkdGlQNQKwuQ3s5lAFcG3ev1IrK9OABnPTu0UnRWsKMC2SGLM4I9Ozu9 +3tNL8GDqpLzPk/6h5W7KZGifSnGq5cv3EaczSZk/jQKBgAcaLGi25ozeFgK1qvuQ +WFTjLYV6KaMrGd5+NF+2a/NQsDGTZSF1egKUvE5QH5YNf37xWkqwvR3rsbenxLAG +aNYjvX+bUs4Mc/bgNkO51P9sH6YoKsuFzTTx4eR5ZS+dtfoiZMfzKkRBK4Baggrv +7S9Q3thVBhvBcz19oFN2Rmvf +-----END PRIVATE KEY----- diff --git a/test-parity/node-suite/tls/handshake/allow-untrusted.ts b/test-parity/node-suite/tls/handshake/allow-untrusted.ts new file mode 100644 index 0000000000..c3de91753d --- /dev/null +++ b/test-parity/node-suite/tls/handshake/allow-untrusted.ts @@ -0,0 +1,20 @@ +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)); +const server = tls.createServer({ key, cert }, (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, + servername: "localhost", + rejectUnauthorized: false, + }); + client.on("secureConnect", () => { + console.log("connected:", client.encrypted); + console.log("authorization:", client.authorized, client.authorizationError); + }); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/handshake/authorized-loopback.ts b/test-parity/node-suite/tls/handshake/authorized-loopback.ts new file mode 100644 index 0000000000..356ebc11fd --- /dev/null +++ b/test-parity/node-suite/tls/handshake/authorized-loopback.ts @@ -0,0 +1,18 @@ +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)); +const server = tls.createServer({ key, cert }, (socket) => socket.end("ok")); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, servername: "localhost", ca: cert }); + client.once("secureConnect", () => { + const peer: any = client.getPeerCertificate(); + console.log("authorization:", client.authorized, client.authorizationError); + console.log("peer identity:", peer.subject?.CN, peer.subjectaltname?.includes("DNS:localhost"), peer.subjectaltname?.includes("IP Address:127.0.0.1")); + }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); + client.resume(); +}); diff --git a/test-parity/node-suite/tls/handshake/basic-loopback.ts b/test-parity/node-suite/tls/handshake/basic-loopback.ts new file mode 100644 index 0000000000..2de0850bc2 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/basic-loopback.ts @@ -0,0 +1,30 @@ +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)); +const events: string[] = []; +const server = tls.createServer({ key, cert }, (socket) => { + events.push("server:secureConnection"); + socket.on("data", (chunk) => { + events.push("server:data:" + chunk.toString()); + socket.end("pong"); + }); +}); +server.on("error", (err: any) => events.push("server:error:" + err.code)); +server.listen(0, "127.0.0.1", () => { + events.push("server:listening"); + const address: any = server.address(); + const client = tls.connect({ host: "127.0.0.1", port: address.port, rejectUnauthorized: false }); + client.on("secureConnect", () => { + events.push("client:secureConnect"); + client.write("ping"); + }); + client.on("data", (chunk) => events.push("client:data:" + chunk.toString())); + client.on("error", (err: any) => events.push("client:error:" + err.code)); + client.on("close", () => server.close(() => { + events.push("server:close"); + console.log(events.join("|")); + })); +}); diff --git a/test-parity/node-suite/tls/handshake/client-certificate-optional.ts b/test-parity/node-suite/tls/handshake/client-certificate-optional.ts new file mode 100644 index 0000000000..50547a3f4b --- /dev/null +++ b/test-parity/node-suite/tls/handshake/client-certificate-optional.ts @@ -0,0 +1,45 @@ +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 serverState = "none"; +function authorizationCode(value: unknown) { + if (typeof value === "string") return value; + if (value && typeof value === "object") { + const error = value as { code?: unknown; name?: unknown }; + return String(error.code ?? error.name ?? "error"); + } + return "none"; +} +const server = tls.createServer({ + key, + cert, + ca: cert, + requestCert: true, + rejectUnauthorized: false, +}, (socket) => { + serverState = [ + socket.authorized, + authorizationCode(socket.authorizationError), + Object.keys(socket.getPeerCertificate()).length, + ].join("/"); + 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, + }); + client.on( + "secureConnect", + () => console.log("client secure:", client.encrypted), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on( + "close", + () => server.close(() => console.log("server:", serverState)), + ); +}); diff --git a/test-parity/node-suite/tls/handshake/client-certificate-required.ts b/test-parity/node-suite/tls/handshake/client-certificate-required.ts new file mode 100644 index 0000000000..dfdf04eb60 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/client-certificate-required.ts @@ -0,0 +1,51 @@ +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 = false; +let serverError = false; +let serverAccepted = false; +const options = { + minVersion: "TLSv1.2" as const, + maxVersion: "TLSv1.2" as const, +}; +const server = tls.createServer({ + key, + cert, + ca: cert, + requestCert: true, + rejectUnauthorized: true, + ...options, +}, (socket) => { + serverAccepted = true; + socket.destroy(); +}); +server.on("tlsClientError", (_err, socket) => { + serverError = true; + 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, + ...options, + }); + client.on("secureConnect", () => { + secure = true; + client.destroy(); + }); + client.on("error", () => { + clientError = true; + }); + client.on( + "close", + () => + server.close(() => + console.log("result:", secure, clientError, serverError, serverAccepted) + ), + ); +}); diff --git a/test-parity/node-suite/tls/handshake/close-ordering.ts b/test-parity/node-suite/tls/handshake/close-ordering.ts new file mode 100644 index 0000000000..11ed2bd094 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/close-ordering.ts @@ -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)); +const events: string[] = []; +let clientClosed = false; +let serverSocketClosed = false; +let serverClosed = false; +function report() { + if (clientClosed && serverSocketClosed && serverClosed) console.log(events.join("|")); +} +const server = tls.createServer({ key, cert }, (socket) => { + socket.on("end", () => events.push("server:end")); + socket.on("close", () => { events.push("server:socket-close"); serverSocketClosed = true; report(); }); + 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 }); + client.on("secureConnect", () => events.push("client:secureConnect")); + client.on("end", () => events.push("client:end")); + client.on("close", () => { + events.push("client:close"); + clientClosed = true; + server.close(() => { + events.push("server:close"); + serverClosed = true; + report(); + }); + }); +}); diff --git a/test-parity/node-suite/tls/handshake/concatenated-ca.ts b/test-parity/node-suite/tls/handshake/concatenated-ca.ts new file mode 100644 index 0000000000..13a77cf618 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/concatenated-ca.ts @@ -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)); +const unrelated = readFileSync(new URL("api-local-cert.pem", fixture)); +const cases: Array<[string, string | Buffer[]]> = [ + ["array", [unrelated, cert]], + ["string", unrelated.toString() + "\n" + cert.toString()], +]; +run(0); + +function run(index: number) { + if (index === cases.length) return; + const [label, ca] = cases[index]; + const server = tls.createServer({ key, cert }, (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, + servername: "localhost", + ca, + }); + client.on( + "secureConnect", + () => console.log(label + ":", client.authorized), + ); + client.on("error", (err: any) => console.log(label + " error:", err.code)); + client.on("close", () => server.close(() => run(index + 1))); + }); +} diff --git a/test-parity/node-suite/tls/handshake/custom-identity-accept.ts b/test-parity/node-suite/tls/handshake/custom-identity-accept.ts new file mode 100644 index 0000000000..6a51422f56 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/custom-identity-accept.ts @@ -0,0 +1,26 @@ +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 checked = "none"; +const server = tls.createServer({ key, cert }, (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, + servername: "mismatch.api.local", + ca: cert, + checkServerIdentity(hostname, peer) { + checked = hostname + "/" + peer.subject.CN; + return undefined; + }, + }); + client.on( + "secureConnect", + () => console.log("accepted:", client.authorized, checked), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/handshake/custom-identity-reject.ts b/test-parity/node-suite/tls/handshake/custom-identity-reject.ts new file mode 100644 index 0000000000..51970c78b8 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/custom-identity-reject.ts @@ -0,0 +1,33 @@ +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 errorCode = "none"; +const server = tls.createServer({ key, cert }, (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, + servername: "localhost", + ca: cert, + checkServerIdentity() { + return Object.assign(new Error("identity rejected"), { + code: "CUSTOM_IDENTITY", + }); + }, + }); + client.on("secureConnect", () => { + secure = true; + client.destroy(); + }); + client.on("error", (err: any) => { + errorCode = err.code; + }); + client.on( + "close", + () => server.close(() => console.log("result:", secure, errorCode)), + ); +}); diff --git a/test-parity/node-suite/tls/handshake/default-ca-client-certificate.ts b/test-parity/node-suite/tls/handshake/default-ca-client-certificate.ts new file mode 100644 index 0000000000..70cc5733ec --- /dev/null +++ b/test-parity/node-suite/tls/handshake/default-ca-client-certificate.ts @@ -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)); +tls.setDefaultCACertificates([cert]); +let serverAuthorization: string | boolean = "none"; +const server = tls.createServer({ + key, + cert, + requestCert: true, + rejectUnauthorized: true, +}, (socket) => { + serverAuthorization = socket.authorized || socket.authorizationError || + "none"; + socket.end(); +}); +server.on("tlsClientError", (err: any) => { + serverAuthorization = err.code; +}); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "localhost", + key, + cert, + rejectUnauthorized: false, + }); + client.on( + "secureConnect", + () => console.log("client secure:", client.encrypted), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on( + "close", + () => + server.close(() => + console.log("server authorized:", serverAuthorization) + ), + ); +}); diff --git a/test-parity/node-suite/tls/handshake/default-ca-client-trust.ts b/test-parity/node-suite/tls/handshake/default-ca-client-trust.ts new file mode 100644 index 0000000000..1e3b86d444 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/default-ca-client-trust.ts @@ -0,0 +1,22 @@ +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)); +tls.setDefaultCACertificates([cert]); +const server = tls.createServer({ key, cert }, (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, + servername: "localhost", + }); + client.on( + "secureConnect", + () => + console.log("authorized:", client.authorized, client.authorizationError), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/handshake/default-version-mutation.ts b/test-parity/node-suite/tls/handshake/default-version-mutation.ts new file mode 100644 index 0000000000..15551ed69b --- /dev/null +++ b/test-parity/node-suite/tls/handshake/default-version-mutation.ts @@ -0,0 +1,45 @@ +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)); +const originalMin = tls.DEFAULT_MIN_VERSION; +const originalMax = tls.DEFAULT_MAX_VERSION; +(tls as any).DEFAULT_MIN_VERSION = "TLSv1.2"; +(tls as any).DEFAULT_MAX_VERSION = "TLSv1.2"; +let restored = false; +function restoreDefaults() { + if (restored) return; + restored = true; + (tls as any).DEFAULT_MIN_VERSION = originalMin; + (tls as any).DEFAULT_MAX_VERSION = originalMax; +} +process.once("exit", restoreDefaults); +let serverProtocol: string | null = null; +const server = tls.createServer({ key, cert }, (socket) => { + serverProtocol = socket.getProtocol(); + 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, + }); + client.on( + "secureConnect", + () => console.log("client:", client.getProtocol()), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => + server.close(() => { + console.log("server:", serverProtocol); + restoreDefaults(); + console.log( + "restored:", + tls.DEFAULT_MIN_VERSION === originalMin, + tls.DEFAULT_MAX_VERSION === originalMax, + ); + })); +}); diff --git a/test-parity/node-suite/tls/handshake/existing-net-socket.ts b/test-parity/node-suite/tls/handshake/existing-net-socket.ts new file mode 100644 index 0000000000..1a8c4ea233 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/existing-net-socket.ts @@ -0,0 +1,31 @@ +import tls from "node:tls"; +import net from "node:net"; +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)); +const server = tls.createServer( + { key, cert }, + (socket) => socket.end("wrapped"), +); +server.listen(0, "127.0.0.1", () => { + const raw = net.connect((server.address() as any).port, "127.0.0.1", () => { + const client = tls.connect({ socket: raw, rejectUnauthorized: false }); + let data = ""; + client.on( + "secureConnect", + () => console.log("secure:", client.encrypted, client.authorized), + ); + client.on("data", (chunk) => { + data += chunk.toString(); + }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("end", () => console.log("data:", data)); + client.on("close", () => server.close()); + }); + raw.on("error", (err: any) => { + console.log("raw error:", err.code); + server.close(); + }); +}); diff --git a/test-parity/node-suite/tls/handshake/explicit-empty-ca-precedence.ts b/test-parity/node-suite/tls/handshake/explicit-empty-ca-precedence.ts new file mode 100644 index 0000000000..a6604ab74f --- /dev/null +++ b/test-parity/node-suite/tls/handshake/explicit-empty-ca-precedence.ts @@ -0,0 +1,46 @@ +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)); +tls.setDefaultCACertificates([cert]); +const server = tls.createServer({ key, cert }, (socket) => socket.end()); +server.listen( + 0, + "127.0.0.1", + () => + connect( + "implicit", + undefined, + () => connect("empty", [], () => server.close()), + ), +); + +function connect(label: string, ca: Buffer[] | undefined, done: () => void) { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "localhost", + rejectUnauthorized: false, + ...(ca === undefined ? {} : { ca }), + }); + client.on( + "secureConnect", + () => + console.log( + label + ":", + client.authorized, + authorizationCode(client.authorizationError), + ), + ); + client.on("error", (err: any) => console.log(label + " error:", err.code)); + client.on("close", done); +} + +function authorizationCode(value: unknown) { + if (value === null || value === undefined) return "none"; + if (typeof value === "string") return value; + const error = value as { code?: string; name?: string }; + return error.code ?? error.name ?? "error"; +} diff --git a/test-parity/node-suite/tls/handshake/mutual-authentication.ts b/test-parity/node-suite/tls/handshake/mutual-authentication.ts new file mode 100644 index 0000000000..392a5b30ca --- /dev/null +++ b/test-parity/node-suite/tls/handshake/mutual-authentication.ts @@ -0,0 +1,31 @@ +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 serverAuthorized = false; +let serverPeer = "none"; +let clientAuthorized = false; +const server = tls.createServer({ key, cert, ca: cert, requestCert: true, rejectUnauthorized: true }, (socket) => { + serverAuthorized = socket.authorized; + serverPeer = (socket.getPeerCertificate() as any).subject?.CN ?? "missing"; + socket.end(); +}); +server.on("tlsClientError", (err: any) => { serverPeer = "error:" + err.code; }); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "localhost", + key, + cert, + ca: cert, + }); + client.on("secureConnect", () => { clientAuthorized = client.authorized; }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close(() => { + console.log("authorized:", clientAuthorized, serverAuthorized); + console.log("server peer:", serverPeer); + })); +}); diff --git a/test-parity/node-suite/tls/handshake/no-certificate.ts b/test-parity/node-suite/tls/handshake/no-certificate.ts new file mode 100644 index 0000000000..4f9696b97c --- /dev/null +++ b/test-parity/node-suite/tls/handshake/no-certificate.ts @@ -0,0 +1,29 @@ +import tls from "node:tls"; + +let secure = false; +let clientError = false; +let serverError = false; +const server = tls.createServer(); +server.on("tlsClientError", (_err, socket) => { + serverError = true; + 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, + }); + client.on("secureConnect", () => { + secure = true; + client.destroy(); + }); + client.on("error", () => { + clientError = true; + }); + client.on("close", () => + server.close(() => { + console.log("secure:", secure); + console.log("errors:", clientError, serverError); + })); +}); diff --git a/test-parity/node-suite/tls/handshake/plaintext-rejection.ts b/test-parity/node-suite/tls/handshake/plaintext-rejection.ts new file mode 100644 index 0000000000..37bf537299 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/plaintext-rejection.ts @@ -0,0 +1,18 @@ +import tls from "node:tls"; +import net from "node:net"; +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 tlsError = false; +let secureConnection = false; +const server = tls.createServer({ key, cert }, () => { secureConnection = true; }); +server.on("tlsClientError", (err) => { tlsError = err instanceof Error; }); +server.listen(0, "127.0.0.1", () => { + const client = net.connect((server.address() as any).port, "127.0.0.1", () => { + client.end("GET / HTTP/1.0\r\n\r\n"); + }); + client.on("error", () => {}); + client.on("close", () => server.close(() => console.log("rejected:", tlsError, secureConnection))); +}); diff --git a/test-parity/node-suite/tls/handshake/positional-connect-overload.ts b/test-parity/node-suite/tls/handshake/positional-connect-overload.ts new file mode 100644 index 0000000000..3cc9db59c5 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/positional-connect-overload.ts @@ -0,0 +1,19 @@ +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)); +const server = tls.createServer({ key, cert }, (socket) => socket.end()); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect( + (server.address() as any).port, + "127.0.0.1", + { rejectUnauthorized: false }, + function () { + console.log("callback receiver:", this === client); + console.log("socket class:", client instanceof tls.TLSSocket, client.encrypted); + }, + ); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/handshake/reject-untrusted.ts b/test-parity/node-suite/tls/handshake/reject-untrusted.ts new file mode 100644 index 0000000000..b81b75fe58 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/reject-untrusted.ts @@ -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 secure = false; +let errorCode = "none"; +const server = tls.createServer({ key, cert }); +server.on("tlsClientError", () => {}); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, servername: "localhost" }); + client.on("secureConnect", () => { secure = true; }); + client.on("error", (err: any) => { errorCode = err.code; }); + client.on("close", () => server.close(() => console.log("rejected:", !secure, errorCode))); +}); diff --git a/test-parity/node-suite/tls/handshake/secure-context-option.ts b/test-parity/node-suite/tls/handshake/secure-context-option.ts new file mode 100644 index 0000000000..addb9ebe29 --- /dev/null +++ b/test-parity/node-suite/tls/handshake/secure-context-option.ts @@ -0,0 +1,22 @@ +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)); +const secureContext = tls.createSecureContext({ ca: cert, minVersion: "TLSv1.2" }); +const server = tls.createServer({ key, cert }, (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, + servername: "localhost", + secureContext, + }); + client.on("secureConnect", () => { + console.log("authorized:", client.authorized, client.authorizationError); + console.log("protocol:", ["TLSv1.2", "TLSv1.3"].includes(client.getProtocol() as string)); + }); + client.on("error", (err: any) => console.log("error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/helpers/ca-argument-validation.ts b/test-parity/node-suite/tls/helpers/ca-argument-validation.ts new file mode 100644 index 0000000000..8404d26007 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/ca-argument-validation.ts @@ -0,0 +1,23 @@ +import tls from "node:tls"; + +for ( + const [label, value] of [ + ["number", 1], + ["null", null], + ["function", () => {}], + ["boolean", true], + ] as const +) { + try { + tls.getCACertificates(value as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +try { + tls.getCACertificates("test" as any); + console.log("value: no throw"); +} catch (err: any) { + console.log("value:", err instanceof TypeError, err.code); +} diff --git a/test-parity/node-suite/tls/helpers/ca-cache-identity.ts b/test-parity/node-suite/tls/helpers/ca-cache-identity.ts new file mode 100644 index 0000000000..cf3a008a36 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/ca-cache-identity.ts @@ -0,0 +1,11 @@ +import tls from "node:tls"; + +const implicit = tls.getCACertificates(); +const explicit = tls.getCACertificates("default"); +console.log("default identity:", implicit === explicit, explicit === tls.getCACertificates("default")); +console.log("bundled identity:", tls.getCACertificates("bundled") === tls.rootCertificates); +for (const type of ["default", "bundled", "system", "extra"] as const) { + const first = tls.getCACertificates(type); + const second = tls.getCACertificates(type); + console.log(type + ":", first === second, Object.isFrozen(first)); +} diff --git a/test-parity/node-suite/tls/helpers/default-ciphers-forwarding.ts b/test-parity/node-suite/tls/helpers/default-ciphers-forwarding.ts new file mode 100644 index 0000000000..f0183c0db5 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/default-ciphers-forwarding.ts @@ -0,0 +1,18 @@ +import tls from "node:tls"; + +class Stop extends Error {} +let seen: unknown; +const original = tls.createSecureContext; +(tls as any).createSecureContext = (options: any) => { + seen = options.ciphers; + throw new Stop(); +}; +try { + tls.connect(); + console.log("stopped: false"); +} catch (err) { + console.log("stopped:", err instanceof Stop); +} finally { + (tls as any).createSecureContext = original; +} +console.log("default forwarded:", seen === tls.DEFAULT_CIPHERS); diff --git a/test-parity/node-suite/tls/helpers/default-ciphers-mutation.ts b/test-parity/node-suite/tls/helpers/default-ciphers-mutation.ts new file mode 100644 index 0000000000..6a684b7651 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/default-ciphers-mutation.ts @@ -0,0 +1,23 @@ +import tls from "node:tls"; + +class Stop extends Error {} +const originalCiphers = tls.DEFAULT_CIPHERS; +const originalCreateContext = tls.createSecureContext; +let seen = "none"; +try { + (tls as any).DEFAULT_CIPHERS = "DEFAULT"; + (tls as any).createSecureContext = (options: any) => { + seen = options.ciphers; + throw new Stop(); + }; + try { + tls.connect(); + } catch (err) { + console.log("stopped:", err instanceof Stop); + } + console.log("mutated:", tls.DEFAULT_CIPHERS, seen); +} finally { + (tls as any).DEFAULT_CIPHERS = originalCiphers; + (tls as any).createSecureContext = originalCreateContext; +} +console.log("restored:", tls.DEFAULT_CIPHERS === originalCiphers); diff --git a/test-parity/node-suite/tls/helpers/root-certificates-immutability.ts b/test-parity/node-suite/tls/helpers/root-certificates-immutability.ts new file mode 100644 index 0000000000..f2444f57f8 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/root-certificates-immutability.ts @@ -0,0 +1,19 @@ +import tls from "node:tls"; + +const roots = tls.rootCertificates; +console.log("identity:", roots === tls.rootCertificates); +console.log("shape:", Array.isArray(roots), Object.isFrozen(roots), roots.length > 1); +console.log("unique:", roots.length === new Set(roots).size); +console.log("pem:", roots.every((cert) => cert.startsWith("-----BEGIN CERTIFICATE-----\n"))); +for (const [label, mutate] of [ + ["element", () => { (roots as any)[0] = "changed"; }], + ["sort", () => { (roots as any).sort(); }], + ["property", () => { (tls as any).rootCertificates = []; }], +] as const) { + try { + mutate(); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError); + } +} diff --git a/test-parity/node-suite/tls/helpers/set-default-ca-argument-validation.ts b/test-parity/node-suite/tls/helpers/set-default-ca-argument-validation.ts new file mode 100644 index 0000000000..3579ce5726 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/set-default-ca-argument-validation.ts @@ -0,0 +1,54 @@ +import tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync( + new URL("../fixtures/localhost-cert.pem", import.meta.url), +).toString(); +tls.setDefaultCACertificates([cert]); + +function unchanged() { + const actual = tls.getCACertificates("default"); + return actual.length === 1 && actual[0] === cert; +} +for ( + const [label, value] of [ + ["null", null], + ["undefined", undefined], + ["string", "string"], + ["number", 42], + ["object", {}], + ["boolean", true], + ] as const +) { + try { + tls.setDefaultCACertificates(value as any); + console.log(label + ": no throw", unchanged()); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code, unchanged()); + } +} +for ( + const [label, value] of [["null", null], ["number", 42], ["object", {}], [ + "boolean", + true, + ]] as const +) { + for ( + const [position, input] of [["first", [value]], ["second", [ + cert, + value, + ]]] as const + ) { + try { + tls.setDefaultCACertificates(input as any); + console.log(label + " " + position + ": no throw", unchanged()); + } catch (err: any) { + console.log( + label + " " + position + ":", + err instanceof TypeError, + err.code, + unchanged(), + ); + } + } +} diff --git a/test-parity/node-suite/tls/helpers/set-default-ca-mixed-dedup.ts b/test-parity/node-suite/tls/helpers/set-default-ca-mixed-dedup.ts new file mode 100644 index 0000000000..00646057ea --- /dev/null +++ b/test-parity/node-suite/tls/helpers/set-default-ca-mixed-dedup.ts @@ -0,0 +1,11 @@ +import tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync(new URL("../fixtures/localhost-cert.pem", import.meta.url)); +const bundled = tls.getCACertificates("bundled"); +const system = tls.getCACertificates("system"); +tls.setDefaultCACertificates([cert.toString(), Buffer.from(cert), new Uint8Array(cert)]); +const actual = tls.getCACertificates("default"); +console.log("deduplicated:", actual.length, actual[0] === cert.toString()); +console.log("implicit cache:", actual === tls.getCACertificates()); +console.log("other stores:", bundled === tls.getCACertificates("bundled"), system === tls.getCACertificates("system")); diff --git a/test-parity/node-suite/tls/helpers/set-default-ca-recovery.ts b/test-parity/node-suite/tls/helpers/set-default-ca-recovery.ts new file mode 100644 index 0000000000..9531e12f90 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/set-default-ca-recovery.ts @@ -0,0 +1,23 @@ +import tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync(new URL("../fixtures/localhost-cert.pem", import.meta.url)).toString(); +tls.setDefaultCACertificates([cert]); +try { + tls.setDefaultCACertificates(["not a valid certificate"]); + console.log("invalid only: no throw"); +} catch (err: any) { + console.log("invalid only:", err instanceof Error, err.code); +} +let actual = tls.getCACertificates("default"); +console.log("invalid only recovery:", actual.length, actual[0] === cert); + +const malformedPem = "-----BEGIN CERTIFICATE-----\nvalid cert content\n-----END CERTIFICATE-----"; +try { + tls.setDefaultCACertificates([cert, malformedPem]); + console.log("mixed invalid: no throw"); +} catch (err: any) { + console.log("mixed invalid:", err instanceof Error, typeof err.code === "string"); +} +actual = tls.getCACertificates("default"); +console.log("mixed invalid recovery:", actual.length, actual[0] === cert); diff --git a/test-parity/node-suite/tls/helpers/set-default-ca-typed-views.ts b/test-parity/node-suite/tls/helpers/set-default-ca-typed-views.ts new file mode 100644 index 0000000000..1d7a200562 --- /dev/null +++ b/test-parity/node-suite/tls/helpers/set-default-ca-typed-views.ts @@ -0,0 +1,15 @@ +import tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync(new URL("../fixtures/localhost-cert.pem", import.meta.url)); +const uint8 = new Uint8Array(cert); +const inputs: Array<[string, ArrayBufferView]> = [ + ["buffer", Buffer.from(cert)], + ["uint8", uint8], + ["dataview", new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength)], +]; +for (const [label, input] of inputs) { + console.log(label + " return:", tls.setDefaultCACertificates([input]) === undefined); + const actual = tls.getCACertificates("default"); + console.log(label + " value:", actual.length, actual[0] === cert.toString()); +} diff --git a/test-parity/node-suite/tls/identity/dns-patterns.ts b/test-parity/node-suite/tls/identity/dns-patterns.ts new file mode 100644 index 0000000000..e4586c47ae --- /dev/null +++ b/test-parity/node-suite/tls/identity/dns-patterns.ts @@ -0,0 +1,12 @@ +import tls from "node:tls"; + +function ok(host: string, cert: any) { + return tls.checkServerIdentity(host, cert) === undefined; +} +console.log("case insensitive:", ok("a.example", { subject: { CN: "A.EXAMPLE" } })); +console.log("trailing dot:", ok("a.example", { subject: { CN: "a.example." } })); +console.log("single wildcard:", ok("api.example.com", { subject: { CN: "*.example.com" } })); +console.log("wildcard depth:", ok("deep.api.example.com", { subject: { CN: "*.example.com" } })); +console.log("partial wildcard:", ok("api.example", { subject: { CN: "a*i.example" } })); +console.log("multiple cn:", ok("second.example", { subject: { CN: ["first.example", "second.example"] } })); +console.log("san overrides cn:", ok("cn.example", { subjectaltname: "DNS:san.example", subject: { CN: "cn.example" } })); diff --git a/test-parity/node-suite/tls/identity/error-shape.ts b/test-parity/node-suite/tls/identity/error-shape.ts new file mode 100644 index 0000000000..9738597e09 --- /dev/null +++ b/test-parity/node-suite/tls/identity/error-shape.ts @@ -0,0 +1,9 @@ +import tls from "node:tls"; + +const cert: any = { subjectaltname: "DNS:good.example", subject: { CN: "fallback.example" } }; +const err: any = tls.checkServerIdentity("bad.example", cert); +console.log("class:", err instanceof Error, err.name); +console.log("code:", err.code); +console.log("fields:", err.host, err.cert === cert, typeof err.reason); +console.log("enumerable:", Object.keys(err).sort().join(",")); +console.log("message semantic:", err.message.includes("bad.example"), err.message.includes("good.example")); diff --git a/test-parity/node-suite/tls/identity/falsy-host-and-empty-cert.ts b/test-parity/node-suite/tls/identity/falsy-host-and-empty-cert.ts new file mode 100644 index 0000000000..c7a597e42e --- /dev/null +++ b/test-parity/node-suite/tls/identity/falsy-host-and-empty-cert.ts @@ -0,0 +1,32 @@ +import tls from "node:tls"; + +for ( + const [label, host] of [["false", false], ["null", null], [ + "undefined", + undefined, + ]] as const +) { + const cert = { subject: { CN: "a.example" } } as any; + const err: any = tls.checkServerIdentity(host as any, cert); + console.log( + label + ":", + err instanceof Error, + err?.code, + err?.host === String(host), + err?.cert === cert, + ); +} +for ( + const [label, cert] of [["empty", {}], ["empty subject", { + subject: {}, + }]] as const +) { + const err: any = tls.checkServerIdentity("a.example", cert as any); + console.log( + label + ":", + err instanceof Error, + err?.code, + err?.cert === cert, + typeof err?.reason, + ); +} diff --git a/test-parity/node-suite/tls/identity/fixed-certificate.ts b/test-parity/node-suite/tls/identity/fixed-certificate.ts new file mode 100644 index 0000000000..8dff0e85e2 --- /dev/null +++ b/test-parity/node-suite/tls/identity/fixed-certificate.ts @@ -0,0 +1,11 @@ +import tls from "node:tls"; +import { X509Certificate } from "node:crypto"; +import { readFileSync } from "node:fs"; + +const pem = readFileSync(new URL("../fixtures/localhost-cert.pem", import.meta.url)); +const cert = new X509Certificate(pem); +const legacy = cert.toLegacyObject(); +console.log("localhost:", tls.checkServerIdentity("localhost", legacy) === undefined); +console.log("ipv4:", tls.checkServerIdentity("127.0.0.1", legacy) === undefined); +const mismatch: any = tls.checkServerIdentity("other.local", legacy); +console.log("mismatch:", mismatch instanceof Error, mismatch.code, mismatch.host, mismatch.cert === legacy); diff --git a/test-parity/node-suite/tls/identity/idna-and-invalid-labels.ts b/test-parity/node-suite/tls/identity/idna-and-invalid-labels.ts new file mode 100644 index 0000000000..a34176eeef --- /dev/null +++ b/test-parity/node-suite/tls/identity/idna-and-invalid-labels.ts @@ -0,0 +1,14 @@ +import tls from "node:tls"; + +function ok(host: string, name: string) { + return tls.checkServerIdentity(host, { subject: { CN: name } } as any) === undefined; +} +function code(host: string, name: string) { + return (tls.checkServerIdentity(host, { subject: { CN: name } } as any) as any)?.code; +} +console.log("idna wildcard:", ok("xn--bcher-kva.example.com", "*.example.com")); +console.log("idna embedded wildcard:", code("xn--bcher-kva.example.com", "xn--*.example.com")); +console.log("empty label:", code("bad.x.example.com", "bad..example.com")); +console.log("space label:", code("x.example.com", "bad label.com")); +console.log("unicode label:", code("x.example.com", "café.example.com")); +console.log("unicode separator:", code("foo。bar.example.com", "*.example.com")); diff --git a/test-parity/node-suite/tls/identity/ip-addresses.ts b/test-parity/node-suite/tls/identity/ip-addresses.ts new file mode 100644 index 0000000000..c7a499620c --- /dev/null +++ b/test-parity/node-suite/tls/identity/ip-addresses.ts @@ -0,0 +1,11 @@ +import tls from "node:tls"; + +function result(host: string, san?: string, cn?: string) { + const err = tls.checkServerIdentity(host, { subjectaltname: san, subject: cn ? { CN: cn } : {} } as any); + return err ? [err.code, err.host, err.reason].join("/") : "ok"; +} +console.log("ipv4 san:", result("127.0.0.1", "IP Address:127.0.0.1")); +console.log("ipv4 dns rejected:", result("127.0.0.1", "DNS:127.0.0.1", "127.0.0.1")); +console.log("ipv4 cn rejected:", result("127.0.0.1", undefined, "127.0.0.1")); +console.log("ipv4 mismatch:", result("127.0.0.2", "IP Address:127.0.0.1")); +console.log("ipv6 canonical:", result("::1", "IP Address:0:0:0:0:0:0:0:1")); diff --git a/test-parity/node-suite/tls/identity/san-kinds-and-wildcards.ts b/test-parity/node-suite/tls/identity/san-kinds-and-wildcards.ts new file mode 100644 index 0000000000..215caa0a0a --- /dev/null +++ b/test-parity/node-suite/tls/identity/san-kinds-and-wildcards.ts @@ -0,0 +1,19 @@ +import tls from "node:tls"; + +function result(host: string, subjectaltname: string) { + return (tls.checkServerIdentity( + host, + { subjectaltname, subject: {} } as any, + ) as any)?.code ?? "ok"; +} +console.log("uri ignored:", result("a.b.example", "URI:http://a.b.example/")); +console.log("ip cidr rejected:", result("8.8.8.8", "IP Address:8.8.8.0/24")); +console.log("bare wildcard rejected:", result("a.com", "DNS:*")); +console.log("top-level wildcard rejected:", result("a.com", "DNS:*.com")); +console.log("multi-label suffix:", result("a.co.uk", "DNS:*.co.uk")); +console.log("partial leftmost:", result("a-cb.a.com", "DNS:*b.a.com")); +console.log("partial crosses label:", result("a.b.a.com", "DNS:*b.a.com")); +console.log( + "later san matches:", + result("a.b.a.com", "DNS:*b.a.com, DNS:a.b.a.com"), +); diff --git a/test-parity/node-suite/tls/options/alpn-callback-validation.ts b/test-parity/node-suite/tls/options/alpn-callback-validation.ts new file mode 100644 index 0000000000..231e8e7f04 --- /dev/null +++ b/test-parity/node-suite/tls/options/alpn-callback-validation.ts @@ -0,0 +1,10 @@ +import tls from "node:tls"; + +try { + tls.createServer({ ALPNCallback: () => "h2", ALPNProtocols: ["h2"] }); + console.log("conflict: no throw"); +} catch (err: any) { + console.log("conflict:", err instanceof TypeError, err.code); +} +console.log("callback only:", tls.createServer({ ALPNCallback: () => "h2" }) instanceof tls.Server); +console.log("null callback:", tls.createServer({ ALPNCallback: null as any, ALPNProtocols: ["h2"] }) instanceof tls.Server); diff --git a/test-parity/node-suite/tls/options/alpn-validation.ts b/test-parity/node-suite/tls/options/alpn-validation.ts new file mode 100644 index 0000000000..caba1139b6 --- /dev/null +++ b/test-parity/node-suite/tls/options/alpn-validation.ts @@ -0,0 +1,17 @@ +import tls from "node:tls"; + +function probe(label: string, protocols: any) { + try { + const socket = tls.connect({ port: 1, ALPNProtocols: protocols }); + socket.on("error", () => {}); + socket.destroy(); + console.log(label + ": ok"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError || err instanceof RangeError, err.code); + } +} +probe("array", ["h2", "http/1.1"]); +probe("buffer", Buffer.from([2, 104, 50])); +probe("long name", ["a".repeat(256)]); +probe("non string", [1]); +probe("boolean", true); diff --git a/test-parity/node-suite/tls/options/connect-validation.ts b/test-parity/node-suite/tls/options/connect-validation.ts new file mode 100644 index 0000000000..663cf94bc1 --- /dev/null +++ b/test-parity/node-suite/tls/options/connect-validation.ts @@ -0,0 +1,24 @@ +import tls from "node:tls"; + +function probe(label: string, options: any) { + try { + const socket = tls.connect(options); + socket.on("error", () => {}); + socket.destroy(); + console.log(label + ": no throw"); + } catch (err: any) { + console.log( + label + ":", + err instanceof TypeError || err instanceof RangeError, + err.code, + ); + } +} +probe("missing port", {}); +probe("port type", { port: "bad" }); +probe("port range", { port: 70000 }); +probe("identity null", { port: 1, checkServerIdentity: null }); +probe("identity number", { port: 1, checkServerIdentity: 1 }); +probe("identity undefined", { port: 1, checkServerIdentity: undefined }); +probe("identity boolean", { port: 1, checkServerIdentity: true }); +probe("alpn type", { port: 1, ALPNProtocols: true }); diff --git a/test-parity/node-suite/tls/options/create-server-options-type.ts b/test-parity/node-suite/tls/options/create-server-options-type.ts new file mode 100644 index 0000000000..4b0cb3439b --- /dev/null +++ b/test-parity/node-suite/tls/options/create-server-options-type.ts @@ -0,0 +1,26 @@ +import tls from "node:tls"; + +for ( + const [label, value] of [ + ["undefined", undefined], + ["null", null], + ["function", () => {}], + ["string", "bad"], + ["number", 1], + ["boolean", true], + ["array", []], + ] as const +) { + try { + const server = value === undefined + ? tls.createServer() + : tls.createServer(value as any); + console.log( + label + ":", + server instanceof tls.Server, + server.listenerCount("secureConnection"), + ); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} diff --git a/test-parity/node-suite/tls/options/create-server-validation.ts b/test-parity/node-suite/tls/options/create-server-validation.ts new file mode 100644 index 0000000000..9fe64cd227 --- /dev/null +++ b/test-parity/node-suite/tls/options/create-server-validation.ts @@ -0,0 +1,17 @@ +import tls from "node:tls"; + +function probe(label: string, options: any) { + try { + const server = tls.createServer(options); + console.log(label + ":", server instanceof tls.Server); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +probe("empty", {}); +probe("ciphers", { ciphers: 1 }); +probe("curve", { ecdhCurve: 1 }); +probe("handshake timeout", { handshakeTimeout: "1" }); +probe("session timeout", { sessionTimeout: "1" }); +probe("ticket keys type", { ticketKeys: "bad" }); +probe("ticket keys length", { ticketKeys: Buffer.alloc(0) }); diff --git a/test-parity/node-suite/tls/options/ip-servername-validation.ts b/test-parity/node-suite/tls/options/ip-servername-validation.ts new file mode 100644 index 0000000000..8d5749f703 --- /dev/null +++ b/test-parity/node-suite/tls/options/ip-servername-validation.ts @@ -0,0 +1,12 @@ +import tls from "node:tls"; + +for (const servername of ["127.0.0.1", "::1"]) { + try { + const socket = tls.connect({ port: 1, servername }); + socket.on("error", () => {}); + socket.destroy(); + console.log(servername + ": no throw"); + } catch (err: any) { + console.log(servername + ":", err instanceof TypeError, err.code); + } +} diff --git a/test-parity/node-suite/tls/options/pre-aborted-signal.ts b/test-parity/node-suite/tls/options/pre-aborted-signal.ts new file mode 100644 index 0000000000..d9fb1728af --- /dev/null +++ b/test-parity/node-suite/tls/options/pre-aborted-signal.ts @@ -0,0 +1,27 @@ +import tls from "node:tls"; + +const controller = new AbortController(); +controller.abort(); +let secure = false; +let abortError = false; +let reported = false; +const socket = tls.connect({ + host: "127.0.0.1", + port: 1, + signal: controller.signal, +}); +socket.on("secureConnect", () => { + secure = true; + socket.destroy(); +}); +socket.on( + "error", + (err: any) => { + abortError ||= err.name === "AbortError" && err.code === "ABORT_ERR"; + }, +); +socket.on("close", () => { + if (reported) return; + reported = true; + console.log("result:", secure, abortError); +}); diff --git a/test-parity/node-suite/tls/options/server-controls-validation.ts b/test-parity/node-suite/tls/options/server-controls-validation.ts new file mode 100644 index 0000000000..de384bcc77 --- /dev/null +++ b/test-parity/node-suite/tls/options/server-controls-validation.ts @@ -0,0 +1,47 @@ +import tls from "node:tls"; + +const server = tls.createServer(); +console.log( + "state:", + server instanceof tls.Server, + server.listening, + server.address(), +); +console.log( + "events:", + server.listenerCount("secureConnection"), + server.eventNames().length, +); +for ( + const [label, value] of [["short", Buffer.alloc(47)], [ + "long", + Buffer.alloc(49), + ], ["string", "bad"]] as const +) { + try { + server.setTicketKeys(value as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log( + label + ":", + err instanceof TypeError || err instanceof RangeError, + ); + } +} +for ( + const [label, value] of [ + ["null", null], + ["undefined", undefined], + ["number", 1], + ["uint8", new Uint8Array(48)], + ["dataview", new DataView(new ArrayBuffer(48))], + ] as const +) { + try { + server.setTicketKeys(value as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code ?? "none"); + } +} +console.log("ticket length:", server.getTicketKeys().length); diff --git a/test-parity/node-suite/tls/options/server-parent-options.ts b/test-parity/node-suite/tls/options/server-parent-options.ts new file mode 100644 index 0000000000..57af06a61f --- /dev/null +++ b/test-parity/node-suite/tls/options/server-parent-options.ts @@ -0,0 +1,7 @@ +import tls from "node:tls"; + +const defaults = tls.createServer(); +console.log("defaults:", (defaults as any).allowHalfOpen, (defaults as any).pauseOnConnect); +const configured = tls.createServer({ allowHalfOpen: true, pauseOnConnect: true }); +console.log("configured:", (configured as any).allowHalfOpen, (configured as any).pauseOnConnect); +console.log("instances:", defaults instanceof tls.Server, configured instanceof tls.Server); diff --git a/test-parity/node-suite/tls/options/sni-callback-validation.ts b/test-parity/node-suite/tls/options/sni-callback-validation.ts new file mode 100644 index 0000000000..8c476d932c --- /dev/null +++ b/test-parity/node-suite/tls/options/sni-callback-validation.ts @@ -0,0 +1,14 @@ +import tls from "node:tls"; + +function probe(label: string, value: any) { + try { + const server = tls.createServer({ SNICallback: value }); + console.log(label + ":", server instanceof tls.Server); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } +} +probe("function", (_name: string, callback: Function) => callback(null, null)); +probe("null", null); +probe("number", 1); +probe("string", "callback"); diff --git a/test-parity/node-suite/tls/options/tls-socket-allow-half-open.ts b/test-parity/node-suite/tls/options/tls-socket-allow-half-open.ts new file mode 100644 index 0000000000..dedccde776 --- /dev/null +++ b/test-parity/node-suite/tls/options/tls-socket-allow-half-open.ts @@ -0,0 +1,20 @@ +import tls from "node:tls"; +import net from "node:net"; +import { Duplex } from "node:stream"; + +const netBacked = new tls.TLSSocket(new net.Socket(), { allowHalfOpen: true }); +console.log("net backed:", netBacked.allowHalfOpen); +netBacked.destroy(); + +const duplex = new Duplex({ + allowHalfOpen: false, + read() {}, + write(_chunk, _encoding, callback) { callback(); }, +}); +const duplexBacked = new tls.TLSSocket(duplex, { allowHalfOpen: true }); +console.log("duplex backed:", duplexBacked.allowHalfOpen); +duplexBacked.destroy(); + +const standalone = new tls.TLSSocket(undefined, { allowHalfOpen: true }); +console.log("standalone:", standalone.allowHalfOpen); +standalone.destroy(); diff --git a/test-parity/node-suite/tls/options/tls-socket-alpn-construction.ts b/test-parity/node-suite/tls/options/tls-socket-alpn-construction.ts new file mode 100644 index 0000000000..239651304f --- /dev/null +++ b/test-parity/node-suite/tls/options/tls-socket-alpn-construction.ts @@ -0,0 +1,43 @@ +import tls from "node:tls"; + +for ( + const [label, protocols] of [ + ["array", ["http/1.1"]], + ["buffer", Buffer.from([2, 104, 50])], + ["uint8", new Uint8Array([2, 104, 50])], + ] as const +) { + try { + const socket = new tls.TLSSocket(null as any, { + ALPNProtocols: protocols as any, + }); + console.log(label + ":", socket instanceof tls.TLSSocket); + try { + socket.destroy(); + } catch {} + } catch (err: any) { + console.log(label + ":", false, err.code ?? err.name); + } +} +for ( + const [label, protocols] of [["long", ["a".repeat(256)]], [ + "boolean", + true, + ]] as const +) { + try { + const socket = new tls.TLSSocket(null as any, { + ALPNProtocols: protocols as any, + }); + console.log(label + ": no throw"); + try { + socket.destroy(); + } catch {} + } catch (err: any) { + console.log( + label + ":", + err instanceof TypeError || err instanceof RangeError, + err.code, + ); + } +} diff --git a/test-parity/node-suite/tls/options/tls-socket-default-state.ts b/test-parity/node-suite/tls/options/tls-socket-default-state.ts new file mode 100644 index 0000000000..1c4c11d205 --- /dev/null +++ b/test-parity/node-suite/tls/options/tls-socket-default-state.ts @@ -0,0 +1,10 @@ +import tls from "node:tls"; +import net from "node:net"; + +const socket = new tls.TLSSocket(new net.Socket()); +console.log("class:", socket instanceof tls.TLSSocket, socket instanceof net.Socket); +console.log("flags:", socket.encrypted, socket.authorized, socket.authorizationError); +console.log("negotiation:", socket.alpnProtocol, socket.servername, socket.getProtocol()); +console.log("session:", socket.getSession(), socket.isSessionReused()); +console.log("peer:", Object.keys(socket.getPeerCertificate()).length); +socket.destroy(); diff --git a/test-parity/node-suite/tls/server/add-context-validation.ts b/test-parity/node-suite/tls/server/add-context-validation.ts new file mode 100644 index 0000000000..10301729cb --- /dev/null +++ b/test-parity/node-suite/tls/server/add-context-validation.ts @@ -0,0 +1,21 @@ +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)); +const server = tls.createServer({ key, cert }); +console.log("options:", server.addContext("options.local", { key, cert }) === undefined); +console.log("context:", server.addContext("context.local", tls.createSecureContext({ key, cert })) === undefined); +for (const [label, hostname, context] of [ + ["empty hostname", "", { key, cert }], + ["hostname type", 1, { key, cert }], + ["context type", "bad.local", 1], +] as const) { + try { + server.addContext(hostname as any, context as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError || err instanceof Error, err.code ?? "none"); + } +} diff --git a/test-parity/node-suite/tls/server/connection-server-reference.ts b/test-parity/node-suite/tls/server/connection-server-reference.ts new file mode 100644 index 0000000000..8058da4000 --- /dev/null +++ b/test-parity/node-suite/tls/server/connection-server-reference.ts @@ -0,0 +1,17 @@ +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 rawReference = false; +let secureReference = false; +const server = tls.createServer({ key, cert }, (socket: any) => { + secureReference = socket.server === server; + socket.end(); +}); +server.on("connection", (socket: any) => { rawReference = socket.server === server; }); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, rejectUnauthorized: false }); + client.on("close", () => server.close(() => console.log("references:", rawReference, secureReference))); +}); diff --git a/test-parity/node-suite/tls/server/constructor-handshake.ts b/test-parity/node-suite/tls/server/constructor-handshake.ts new file mode 100644 index 0000000000..4930ef9f6d --- /dev/null +++ b/test-parity/node-suite/tls/server/constructor-handshake.ts @@ -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 accepted = false; +const server = new tls.Server({ key, cert }, (socket) => { + accepted = socket instanceof tls.TLSSocket; + socket.end(); +}); +console.log("server class:", server instanceof tls.Server); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, rejectUnauthorized: false }); + client.on("close", () => server.close(() => console.log("accepted:", accepted))); +}); diff --git a/test-parity/node-suite/tls/server/ticket-keys-roundtrip.ts b/test-parity/node-suite/tls/server/ticket-keys-roundtrip.ts new file mode 100644 index 0000000000..55b2ddc9a7 --- /dev/null +++ b/test-parity/node-suite/tls/server/ticket-keys-roundtrip.ts @@ -0,0 +1,11 @@ +import tls from "node:tls"; + +const server = tls.createServer(); +const original = server.getTicketKeys(); +const replacement = Buffer.alloc(48, 7); +console.log("original:", Buffer.isBuffer(original), original.length); +console.log("set return:", server.setTicketKeys(replacement) === undefined); +const actual = server.getTicketKeys(); +console.log("roundtrip:", actual.equals(replacement), actual !== replacement); +replacement[0] = 9; +console.log("copied:", actual[0] === 7); diff --git a/test-parity/node-suite/tls/session/tls12-resumption.ts b/test-parity/node-suite/tls/session/tls12-resumption.ts new file mode 100644 index 0000000000..4515d186da --- /dev/null +++ b/test-parity/node-suite/tls/session/tls12-resumption.ts @@ -0,0 +1,26 @@ +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)); +const server = tls.createServer({ key, cert, minVersion: "TLSv1.2", maxVersion: "TLSv1.2" }, (socket) => socket.end()); +server.listen(0, "127.0.0.1", () => connectOnce()); +function connectOnce(session?: Buffer) { + const client = tls.connect({ host: "127.0.0.1", port: (server.address() as any).port, rejectUnauthorized: false, minVersion: "TLSv1.2", maxVersion: "TLSv1.2", session }); + client.on("secureConnect", () => { + if (!session) { + const first = client.getSession(); + console.log("first:", client.isSessionReused(), Buffer.isBuffer(first), (first?.length ?? 0) > 0); + client.once("close", () => connectOnce(first)); + } else { + console.log("second:", client.isSessionReused()); + client.once("close", () => server.close()); + } + client.end(); + }); + client.on("error", (err: any) => { + console.log("error:", err.code); + server.close(); + }); +} diff --git a/test-parity/node-suite/tls/sni/add-context-selection.ts b/test-parity/node-suite/tls/sni/add-context-selection.ts new file mode 100644 index 0000000000..bb90932cd9 --- /dev/null +++ b/test-parity/node-suite/tls/sni/add-context-selection.ts @@ -0,0 +1,27 @@ +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 serverNames: string[] = []; +const server = tls.createServer({ key, cert: defaultCert }, (socket) => { + serverNames.push(socket.servername); + socket.end(); +}); +server.addContext("api.local", { key, cert: apiCert }); +server.listen(0, "127.0.0.1", () => connect("API.LOCAL", () => connect("other.local", () => { + server.close(() => console.log("server names:", serverNames.join(","))); +}))); +function connect(servername: string, done: () => void) { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername, + rejectUnauthorized: false, + }); + client.on("secureConnect", () => console.log(servername + ":", (client.getPeerCertificate() as any).subject?.CN)); + client.on("error", (err: any) => console.log(servername + " error:", err.code)); + client.on("close", done); +} diff --git a/test-parity/node-suite/tls/sni/async-callback.ts b/test-parity/node-suite/tls/sni/async-callback.ts new file mode 100644 index 0000000000..6f14ee7ae0 --- /dev/null +++ b/test-parity/node-suite/tls/sni/async-callback.ts @@ -0,0 +1,36 @@ +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 context = tls.createSecureContext({ key, cert: apiCert }); +let requested = "none"; +const server = tls.createServer({ + key, + cert: defaultCert, + SNICallback(name, callback) { + requested = name; + queueMicrotask(() => callback(null, context)); + }, +}, (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, + servername: "async.api.local", + rejectUnauthorized: false, + }); + client.on( + "secureConnect", + () => + console.log( + "selected:", + requested, + (client.getPeerCertificate() as any).subject?.CN, + ), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/sni/callback-error.ts b/test-parity/node-suite/tls/sni/callback-error.ts new file mode 100644 index 0000000000..cbeda31497 --- /dev/null +++ b/test-parity/node-suite/tls/sni/callback-error.ts @@ -0,0 +1,37 @@ +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 serverMessage = "none"; +let serverName = "none"; +let clientError = false; +const server = tls.createServer({ + key, + cert, + SNICallback(name, callback) { + callback(new Error("selection failed")); + }, +}); +server.on("tlsClientError", (err, socket) => { + serverMessage = err.message; + serverName = socket.servername; +}); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "failed.api.local", + rejectUnauthorized: false, + }); + client.on("secureConnect", () => client.destroy()); + client.on("error", () => { + clientError = true; + }); + client.on("close", () => + server.close(() => { + console.log("client error:", clientError); + console.log("server:", serverMessage, serverName); + })); +}); diff --git a/test-parity/node-suite/tls/sni/context-selection.ts b/test-parity/node-suite/tls/sni/context-selection.ts new file mode 100644 index 0000000000..9e7d0f399c --- /dev/null +++ b/test-parity/node-suite/tls/sni/context-selection.ts @@ -0,0 +1,17 @@ +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)); +const context = tls.createSecureContext({ key, cert }); +let requested = "none"; +const server = tls.createServer({ key, cert, SNICallback(name, callback) { + requested = name; + callback(null, context); +} }, (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, servername: "api.local", rejectUnauthorized: false }); + client.on("secureConnect", () => console.log("client servername:", client.servername)); + client.on("close", () => server.close(() => console.log("callback name:", requested))); +}); diff --git a/test-parity/node-suite/tls/sni/latest-context-precedence.ts b/test-parity/node-suite/tls/sni/latest-context-precedence.ts new file mode 100644 index 0000000000..81595c033e --- /dev/null +++ b/test-parity/node-suite/tls/sni/latest-context-precedence.ts @@ -0,0 +1,27 @@ +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 localhostCert = readFileSync(new URL("localhost-cert.pem", fixture)); +const apiCert = readFileSync(new URL("api-local-cert.pem", fixture)); +const server = tls.createServer( + { key, cert: apiCert }, + (socket) => socket.end(), +); +server.addContext("*.example.local", { key, cert: apiCert }); +server.addContext("*.example.local", { key, cert: localhostCert }); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "one.example.local", + rejectUnauthorized: false, + }); + client.on("secureConnect", () => { + console.log("selected:", (client.getPeerCertificate() as any).subject?.CN); + client.end(); + }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/sni/null-context-fallback.ts b/test-parity/node-suite/tls/sni/null-context-fallback.ts new file mode 100644 index 0000000000..94b5a2d836 --- /dev/null +++ b/test-parity/node-suite/tls/sni/null-context-fallback.ts @@ -0,0 +1,38 @@ +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 serverName = "none"; +const server = tls.createServer({ + key, + cert, + SNICallback(name, callback) { + callback(null, null as any); + }, +}, (socket) => { + serverName = socket.servername; + socket.end(); +}); +server.listen(0, "127.0.0.1", () => { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername: "fallback.api.local", + rejectUnauthorized: false, + }); + client.on( + "secureConnect", + () => + console.log( + "certificate:", + (client.getPeerCertificate() as any).subject?.CN, + ), + ); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on( + "close", + () => server.close(() => console.log("servername:", serverName)), + ); +}); diff --git a/test-parity/node-suite/tls/sni/wildcard-case-and-depth.ts b/test-parity/node-suite/tls/sni/wildcard-case-and-depth.ts new file mode 100644 index 0000000000..5360a8f8f7 --- /dev/null +++ b/test-parity/node-suite/tls/sni/wildcard-case-and-depth.ts @@ -0,0 +1,42 @@ +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 server = tls.createServer( + { key, cert: defaultCert }, + (socket) => socket.end(), +); +server.addContext("*.api.local", { key, cert: apiCert }); +server.listen( + 0, + "127.0.0.1", + () => + connect( + "SERVICE.API.LOCAL", + () => connect("deep.service.api.local", () => server.close()), + ), +); + +function connect(servername: string, done: () => void) { + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as any).port, + servername, + rejectUnauthorized: false, + }); + client.on("secureConnect", () => { + console.log( + servername + ":", + (client.getPeerCertificate() as any).subject?.CN, + ); + client.end(); + }); + client.on( + "error", + (err: any) => console.log(servername + " error:", err.code), + ); + client.on("close", done); +} diff --git a/test-parity/node-suite/tls/state/detailed-peer-certificate.ts b/test-parity/node-suite/tls/state/detailed-peer-certificate.ts new file mode 100644 index 0000000000..5aaf6271fe --- /dev/null +++ b/test-parity/node-suite/tls/state/detailed-peer-certificate.ts @@ -0,0 +1,18 @@ +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)); +const server = tls.createServer({ key, cert }, (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 }); + client.on("secureConnect", () => { + const abbreviated: any = client.getPeerCertificate(false); + const detailed: any = client.getPeerCertificate(true); + console.log("identity:", abbreviated.subject?.CN, detailed.subject?.CN); + console.log("same raw:", abbreviated.raw.equals(detailed.raw)); + console.log("issuer:", detailed.issuerCertificate?.subject?.CN, detailed.issuerCertificate === detailed); + }); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/state/ephemeral-key-info.ts b/test-parity/node-suite/tls/state/ephemeral-key-info.ts new file mode 100644 index 0000000000..f6b6b2372b --- /dev/null +++ b/test-parity/node-suite/tls/state/ephemeral-key-info.ts @@ -0,0 +1,42 @@ +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 serverInfoIsNull = false; +const cipher = "ECDHE-RSA-AES128-GCM-SHA256"; +const server = tls.createServer({ + key, + cert, + ciphers: cipher, + minVersion: "TLSv1.2", + maxVersion: "TLSv1.2", +}, (socket) => { + serverInfoIsNull = socket.getEphemeralKeyInfo() === null; + 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, + ciphers: cipher, + minVersion: "TLSv1.2", + maxVersion: "TLSv1.2", + }); + client.on("secureConnect", () => { + const info: any = client.getEphemeralKeyInfo(); + console.log( + "client shape:", + typeof info?.type, + typeof info?.name, + typeof info?.size, + info?.size > 0, + ); + }); + client.on( + "close", + () => server.close(() => console.log("server null:", serverInfoIsNull)), + ); +}); diff --git a/test-parity/node-suite/tls/state/export-keying-material-validation.ts b/test-parity/node-suite/tls/state/export-keying-material-validation.ts new file mode 100644 index 0000000000..ba61beb45a --- /dev/null +++ b/test-parity/node-suite/tls/state/export-keying-material-validation.ts @@ -0,0 +1,30 @@ +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)); +const server = tls.createServer({ key, cert }, (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 }); + client.on("secureConnect", () => { + const probes: Array<[string, () => unknown]> = [ + ["context string", () => client.exportKeyingMaterial(8, "label", "bad" as any)], + ["label null", () => client.exportKeyingMaterial(8, null as any)], + ["length string", () => client.exportKeyingMaterial("8" as any, "label")], + ["negative length", () => client.exportKeyingMaterial(-1, "label")], + ["zero length", () => client.exportKeyingMaterial(0, "label")], + ]; + for (const [label, probe] of probes) { + try { + probe(); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError || err instanceof RangeError, err.code); + } + } + const emptyContext = client.exportKeyingMaterial(8, "label", Buffer.alloc(0)); + console.log("empty context:", Buffer.isBuffer(emptyContext), emptyContext.length); + }); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/state/export-keying-material.ts b/test-parity/node-suite/tls/state/export-keying-material.ts new file mode 100644 index 0000000000..986519b201 --- /dev/null +++ b/test-parity/node-suite/tls/state/export-keying-material.ts @@ -0,0 +1,27 @@ +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)); +const label = "perry-node-suite"; +let serverMaterial: Buffer | undefined; +let clientMaterial: Buffer | undefined; +let clientWithContext: Buffer | undefined; +const server = tls.createServer({ key, cert }, (socket) => { + serverMaterial = socket.exportKeyingMaterial(32, label); + 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 }); + client.on("secureConnect", () => { + clientMaterial = client.exportKeyingMaterial(32, label); + clientWithContext = client.exportKeyingMaterial(32, label, Buffer.from([0, 1, 2, 3])); + }); + client.on("close", () => server.close(() => { + console.log("buffers:", [serverMaterial, clientMaterial, clientWithContext].every(Buffer.isBuffer)); + console.log("lengths:", serverMaterial?.length, clientMaterial?.length, clientWithContext?.length); + console.log("agreement:", serverMaterial?.equals(clientMaterial as Buffer)); + console.log("context differs:", !clientMaterial?.equals(clientWithContext as Buffer)); + })); +}); diff --git a/test-parity/node-suite/tls/state/finished-messages.ts b/test-parity/node-suite/tls/state/finished-messages.ts new file mode 100644 index 0000000000..8b4531c116 --- /dev/null +++ b/test-parity/node-suite/tls/state/finished-messages.ts @@ -0,0 +1,27 @@ +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 serverFinished: Buffer | undefined; +let serverPeerFinished: Buffer | undefined; +let clientFinished: Buffer | undefined; +let clientPeerFinished: Buffer | undefined; +const server = tls.createServer({ key, cert }, (socket) => { + serverFinished = socket.getFinished(); + serverPeerFinished = socket.getPeerFinished(); + 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 }); + client.on("secureConnect", () => { + clientFinished = client.getFinished(); + clientPeerFinished = client.getPeerFinished(); + }); + client.on("close", () => server.close(() => { + console.log("buffers:", [serverFinished, serverPeerFinished, clientFinished, clientPeerFinished].every(Buffer.isBuffer)); + console.log("nonempty:", [serverFinished, serverPeerFinished, clientFinished, clientPeerFinished].every((value) => (value?.length ?? 0) > 0)); + console.log("cross match:", serverFinished?.equals(clientPeerFinished as Buffer), serverPeerFinished?.equals(clientFinished as Buffer)); + })); +}); diff --git a/test-parity/node-suite/tls/state/max-send-fragment.ts b/test-parity/node-suite/tls/state/max-send-fragment.ts new file mode 100644 index 0000000000..05440d387b --- /dev/null +++ b/test-parity/node-suite/tls/state/max-send-fragment.ts @@ -0,0 +1,12 @@ +import tls from "node:tls"; +import net from "node:net"; + +const socket = new tls.TLSSocket(new net.Socket()); +for (const value of [511, 512, 16384, 16385, "1024"] as any[]) { + try { + console.log(String(value) + ":", socket.setMaxSendFragment(value)); + } catch (err: any) { + console.log(String(value) + ":", err instanceof TypeError || err instanceof RangeError, err.code); + } +} +socket.destroy(); diff --git a/test-parity/node-suite/tls/state/negotiated-getters.ts b/test-parity/node-suite/tls/state/negotiated-getters.ts new file mode 100644 index 0000000000..4de16d45c8 --- /dev/null +++ b/test-parity/node-suite/tls/state/negotiated-getters.ts @@ -0,0 +1,21 @@ +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)); +const server = tls.createServer({ key, cert }, (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 }); + client.once("secureConnect", () => { + const cipher: any = client.getCipher(); + const peer: any = client.getPeerCertificate(); + const session = client.getSession(); + console.log("protocol:", ["TLSv1.2", "TLSv1.3"].includes(client.getProtocol() as string)); + console.log("cipher:", typeof cipher.name, typeof cipher.standardName, typeof cipher.version); + console.log("peer:", peer.subject?.CN, typeof peer.raw?.byteLength === "number"); + console.log("session:", Buffer.isBuffer(session), (session?.length ?? 0) > 0, client.isSessionReused()); + }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on("close", () => server.close()); +}); diff --git a/test-parity/node-suite/tls/state/protocol-after-close.ts b/test-parity/node-suite/tls/state/protocol-after-close.ts new file mode 100644 index 0000000000..7e0e073019 --- /dev/null +++ b/test-parity/node-suite/tls/state/protocol-after-close.ts @@ -0,0 +1,15 @@ +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)); +const server = tls.createServer({ key, cert }, (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 }); + client.on("secureConnect", () => console.log("during:", ["TLSv1.2", "TLSv1.3"].includes(client.getProtocol() as string))); + client.on("close", () => { + console.log("after:", client.getProtocol()); + server.close(); + }); +}); diff --git a/test-parity/node-suite/tls/state/set-key-cert.ts b/test-parity/node-suite/tls/state/set-key-cert.ts new file mode 100644 index 0000000000..e078370734 --- /dev/null +++ b/test-parity/node-suite/tls/state/set-key-cert.ts @@ -0,0 +1,36 @@ +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)); +for ( + const [label, value] of [ + ["context", tls.createSecureContext({ key, cert })], + ["options", { key, cert }], + ] as const +) { + const socket = new tls.TLSSocket(); + try { + console.log(label + ":", socket.setKeyCert(value) === undefined); + } catch (err: any) { + console.log(label + ":", false, err.code ?? err.name); + } finally { + try { + socket.destroy(); + } catch {} + } +} +for (const [label, value] of [["null", null], ["number", 1]] as const) { + const socket = new tls.TLSSocket(); + try { + socket.setKeyCert(value as any); + console.log(label + ": no throw"); + } catch (err: any) { + console.log(label + ":", err instanceof TypeError, err.code); + } finally { + try { + socket.destroy(); + } catch {} + } +} diff --git a/test-parity/node-suite/tls/state/shared-signature-algorithms.ts b/test-parity/node-suite/tls/state/shared-signature-algorithms.ts new file mode 100644 index 0000000000..796d899319 --- /dev/null +++ b/test-parity/node-suite/tls/state/shared-signature-algorithms.ts @@ -0,0 +1,33 @@ +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 serverShape = false; +const server = tls.createServer({ key, cert }, (socket) => { + const algorithms = socket.getSharedSigalgs(); + serverShape = Array.isArray(algorithms) && + algorithms.every((value) => typeof value === "string"); + 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, + }); + client.on("secureConnect", () => { + const algorithms = client.getSharedSigalgs(); + console.log( + "client:", + Array.isArray(algorithms), + algorithms.every((value) => typeof value === "string"), + ); + }); + client.on("error", (err: any) => console.log("client error:", err.code)); + client.on( + "close", + () => server.close(() => console.log("server:", serverShape)), + ); +}); diff --git a/test-parity/node-suite/tls/state/x509-certificate-views.ts b/test-parity/node-suite/tls/state/x509-certificate-views.ts new file mode 100644 index 0000000000..eb52a75e91 --- /dev/null +++ b/test-parity/node-suite/tls/state/x509-certificate-views.ts @@ -0,0 +1,25 @@ +import tls from "node:tls"; +import { X509Certificate } from "node:crypto"; +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 serverOwn = false; +let serverPeerMissing = false; +const server = tls.createServer({ key, cert }, (socket) => { + serverOwn = socket.getX509Certificate() instanceof X509Certificate; + serverPeerMissing = socket.getPeerX509Certificate() === undefined; + 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 }); + client.on("secureConnect", () => { + const legacy: any = client.getPeerCertificate(); + const x509 = client.getPeerX509Certificate(); + console.log("client peer:", x509 instanceof X509Certificate, x509?.subject.includes("CN=localhost")); + console.log("same raw:", Buffer.isBuffer(legacy.raw), x509?.raw.equals(legacy.raw)); + console.log("client own missing:", client.getX509Certificate() === undefined); + }); + client.on("close", () => server.close(() => console.log("server views:", serverOwn, serverPeerMissing))); +}); diff --git a/test-parity/node-suite/tls/surface/certificate-compression.ts b/test-parity/node-suite/tls/surface/certificate-compression.ts new file mode 100644 index 0000000000..2c02a27f4c --- /dev/null +++ b/test-parity/node-suite/tls/surface/certificate-compression.ts @@ -0,0 +1,18 @@ +import tls from "node:tls"; + +const getAlgorithms = (tls as any).getCertificateCompressionAlgorithms; +console.log("export:", typeof getAlgorithms === "function"); +if (typeof getAlgorithms === "function") { + const first = getAlgorithms(); + const second = getAlgorithms(); + const allowed = new Set(["zlib", "brotli", "zstd"]); + console.log( + "shape:", + Array.isArray(first), + first.every((value: unknown) => + typeof value === "string" && allowed.has(value) + ), + first.length === new Set(first).size, + ); + console.log("fresh:", first !== second, first.join(",") === second.join(",")); +} diff --git a/test-parity/node-suite/tls/surface/exports-and-classes.ts b/test-parity/node-suite/tls/surface/exports-and-classes.ts new file mode 100644 index 0000000000..d4ecdaa446 --- /dev/null +++ b/test-parity/node-suite/tls/surface/exports-and-classes.ts @@ -0,0 +1,13 @@ +import tls from "node:tls"; +import { EventEmitter } from "node:events"; +import { Duplex } from "node:stream"; + +const functions = [ + "checkServerIdentity", "connect", "createSecureContext", "createServer", + "getCACertificates", "getCiphers", "setDefaultCACertificates", +]; +console.log("functions:", functions.every((name) => typeof (tls as any)[name] === "function")); +console.log("classes:", typeof tls.SecureContext === "function", typeof tls.Server === "function", typeof tls.TLSSocket === "function"); +console.log("inheritance:", tls.Server.prototype instanceof EventEmitter, tls.TLSSocket.prototype instanceof Duplex); +console.log("aliases:", tls.createServer !== tls.Server, tls.createSecureContext !== tls.SecureContext); +console.log("constants:", tls.DEFAULT_MIN_VERSION, tls.DEFAULT_MAX_VERSION, tls.CLIENT_RENEG_LIMIT, tls.CLIENT_RENEG_WINDOW); diff --git a/test-parity/node-suite/tls/surface/extended-api.ts b/test-parity/node-suite/tls/surface/extended-api.ts new file mode 100644 index 0000000000..a1f49afa9e --- /dev/null +++ b/test-parity/node-suite/tls/surface/extended-api.ts @@ -0,0 +1,21 @@ +import tls from "node:tls"; + +console.log("alpn converter:", typeof tls.convertALPNProtocols === "function"); +console.log( + "set key cert:", + typeof tls.TLSSocket.prototype.setKeyCert === "function", +); +console.log( + "shared sigalgs:", + typeof tls.TLSSocket.prototype.getSharedSigalgs === "function", +); +console.log( + "x509 getters:", + typeof tls.TLSSocket.prototype.getX509Certificate === "function", + typeof tls.TLSSocket.prototype.getPeerX509Certificate === "function", +); +console.log( + "legacy pair removed:", + !("SecurePair" in tls), + !("createSecurePair" in tls), +); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 56a4fa4e88..4d16921a7b 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -178,8 +178,8 @@ "total": 90 }, "tls": { - "pass": 3, - "total": 3 + "pass": 9, + "total": 100 }, "trace_events": { "pass": 6,