Skip to content

Commit 7659f3c

Browse files
authored
fix(ext/node): normalize trailing dot in TLS servername (#32166)
Fixes #30170 Node.js strips trailing dots from FQDN server names before TLS certificate verification, but Deno was passing the servername with the trailing dot directly to rustls. This caused certificate validation failures when connecting with a trailing-dot servername like `example.local.` to a server whose certificate is valid for `example.local`. The error message was especially confusing because it said the certificate wasn't valid for `example.local.` when it was only valid for `example.local.` (same string): ``` certificate not valid for name "example.local."; certificate is only valid for DnsName("example.local.") ``` The fix strips the trailing dot from the hostname in the TLSSocket constructor before it's passed to the TLS layer. This matches Node.js behavior - DNS fully-qualified domain names end with a dot but TLS SNI extensions and certificate matching should work without it. Added a test that connects using `localhost.` as servername to verify the trailing dot is properly normalized.
1 parent a8f201f commit 7659f3c

File tree

2 files changed

+47
-1
lines changed

2 files changed

+47
-1
lines changed

ext/node/polyfills/_tls_wrap.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,14 @@ export class TLSSocket extends net.Socket {
8686
constructor(socket, opts = kEmptyObject) {
8787
const tlsOptions = { ...opts };
8888

89-
const hostname = opts.servername ?? opts.host ?? socket?._host ??
89+
let hostname = opts.servername ?? opts.host ?? socket?._host ??
9090
"localhost";
91+
// Strip trailing dot from hostname for SNI and certificate verification,
92+
// matching Node.js behavior. DNS FQDN trailing dots are valid but must
93+
// be normalized for TLS server name matching.
94+
if (hostname && hostname.endsWith(".")) {
95+
hostname = hostname.slice(0, -1);
96+
}
9197
tlsOptions.hostname = hostname;
9298

9399
const cert = tlsOptions?.secureContext?.cert;

tests/unit_node/tls_test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,43 @@ BnRlc3RDQTCB
479479
// deno-lint-ignore no-explicit-any
480480
(tls as any).setDefaultCACertificates([testCert]);
481481
});
482+
483+
// https://github.com/denoland/deno/issues/30170
484+
Deno.test("tls.connect strips trailing dot from servername", async () => {
485+
const listener = Deno.listenTls({
486+
port: 0,
487+
key,
488+
cert,
489+
});
490+
491+
const conn = tls.connect({
492+
host: "localhost",
493+
port: listener.addr.port,
494+
// Use trailing dot - should be normalized to "localhost"
495+
servername: "localhost.",
496+
secureContext: {
497+
ca: rootCaCert,
498+
// deno-lint-ignore no-explicit-any
499+
} as any,
500+
});
501+
502+
const serverConn = await listener.accept();
503+
504+
const { promise: connected, resolve: resolveConnected } = Promise
505+
.withResolvers<void>();
506+
conn.on("secureConnect", () => {
507+
assert(conn.authorized, "Connection should be authorized");
508+
resolveConnected();
509+
});
510+
511+
conn.on("error", (err: Error) => {
512+
// Should not get a certificate error with trailing dot
513+
throw err;
514+
});
515+
516+
await connected;
517+
conn.destroy();
518+
serverConn.close();
519+
listener.close();
520+
await new Promise((resolve) => conn.on("close", resolve));
521+
});

0 commit comments

Comments
 (0)