diff --git a/src/broker-nats.ts b/src/broker-nats.ts index 148715c..a938076 100644 --- a/src/broker-nats.ts +++ b/src/broker-nats.ts @@ -19,7 +19,7 @@ import { createHash } from "node:crypto"; import net from "node:net"; import tls from "node:tls"; -import { connect as natsConnect, headers as natsHeaders, StringCodec, type JsMsg, type Subscription } from "nats"; +import { connect as natsConnect, headers as natsHeaders, StringCodec, type JsMsg, type Subscription, type TlsOptions } from "nats"; import type { Broker, Envelope, EventHeaders } from "@novox/mesh-sdk/messaging"; const sc = StringCodec(); @@ -298,14 +298,15 @@ function normalizeFingerprint(fingerprint: string): string { * A certificate authority is not consulted: the mesh issued this and knows its fingerprint, * which is stronger than trusting whoever a machine's trust store happens to contain. * - * **A constraint on the mesh, not a detail of this file.** Pinning the exact certificate makes - * hostname verification redundant in principle, but the NATS client exposes no hook to replace - * it — its TLS options are file paths and PEM strings, with no verify callback. So the - * certificate the mesh issues the bus **must carry a subject-alternative name matching the - * address nodes dial it by**. The fingerprint check below still happens and is still the real - * guarantee; what cannot be switched off is the check *beside* it. + * **The pin is the only check.** What comes back is handed to the client as its TLS options, and + * the client's transport spreads them into Node's own `tls.connect` — so the pinned certificate + * is the one authority the handshake accepts, and the hostname check beside it is replaced with + * one that always passes. Pinning the exact certificate makes verifying its name redundant, and + * the bus's certificate names the seat (`mesh-broker`), not the address a machine happens to + * dial it by: every module on the mesh met "does not match certificate's altnames" the first time + * it reached the handshake (2026-09-28). */ -async function pinnedTls(rawUrl: string, fingerprint: string): Promise<{ ca: string }> { +async function pinnedTls(rawUrl: string, fingerprint: string): Promise { const url = new URL(rawUrl.includes("://") ? rawUrl : `nats://${rawUrl}`); const port = url.port ? Number(url.port) : 4222; // **The bus speaks first, in the clear.** A NATS server sends its INFO line before TLS begins, @@ -342,7 +343,9 @@ async function pinnedTls(rawUrl: string, fingerprint: string): Promise<{ ca: str ); } const pem = `-----BEGIN CERTIFICATE-----\n${certificate.raw.toString("base64").replace(/(.{64})/g, "$1\n")}\n-----END CERTIFICATE-----\n`; - return { ca: pem }; + // Node's option, not the client's: the transport passes the whole object on. `undefined` from + // checkServerIdentity is "the name is fine"; the pin above already decided the rest. + return { ca: pem, checkServerIdentity: () => undefined } as TlsOptions; } /** The mesh's topic matching: `*` is one token, `#` the rest. This is the module's vocabulary —