Author SHA1 Message Date
jschoubben d4a2802342 The pin is the only check: the runtime stops verifying the bus's name
Every module on the new runtime reached the handshake and failed on 'does not match
certificate's altnames': the bus's certificate names the seat, not the address a machine dials
it by, and pinning the exact certificate already decides everything a name check could. The
client's transport spreads the TLS options into Node's tls.connect, so the hostname check is
replaced with one that passes and the pinned certificate is the one authority accepted.
2026-09-28 04:03:43 +02:00
mesh-admin 8acfa7a07d Merge pull request 'One bus: the runtime pins the certificate after the server speaks, and the old transport goes' (#15) from feat/one-bus into main 2026-09-28 01:09:01 +00:00
+12 -9
View File
@@ -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<TlsOptions> {
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 —