fix(ext/node): Support mTLS connections node compatibility (#28937)

Support for running Node.js code that requires mTLS connections on Deno.

Ref #21497 

Closes https://github.com/denoland/deno/issues/26472
Closes https://github.com/denoland/deno/issues/29148

---------

Co-authored-by: Satya Rohith <me@satyarohith.com>
Co-authored-by: Divy Srivastava <dj.srivastava23@gmail.com>
This commit is contained in:
nana4gonta 2025-05-06 17:55:18 +09:00 committed by GitHub
parent 2edc70e679
commit 730f48b170
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 139 additions and 5 deletions

View file

@ -187,7 +187,7 @@ async function startTls(
hostname,
caCerts,
alpnProtocols,
});
}, null);
return new TlsConn(rid, remoteAddr, localAddr);
}

View file

@ -196,7 +196,7 @@ pub fn op_tls_key_null() -> TlsKeysHolder {
TlsKeysHolder::from(TlsKeys::Null)
}
#[op2]
#[op2(reentrant)]
#[cppgc]
pub fn op_tls_key_static(
#[string] cert: &str,
@ -258,6 +258,7 @@ pub fn op_tls_cert_resolver_resolve_error(
pub fn op_tls_start<NP>(
state: Rc<RefCell<OpState>>,
#[serde] args: StartTlsArgs,
#[cppgc] key_pair: Option<&TlsKeysHolder>,
) -> Result<(ResourceId, IpAddr, IpAddr), NetError>
where
NP: NetPermissions + 'static,
@ -304,11 +305,13 @@ where
let local_addr = tcp_stream.local_addr()?;
let remote_addr = tcp_stream.peer_addr()?;
let tls_null = TlsKeysHolder::from(TlsKeys::Null);
let key_pair = key_pair.unwrap_or(&tls_null);
let mut tls_config = create_client_config(
root_cert_store,
ca_certs,
unsafely_ignore_certificate_errors,
TlsKeys::Null,
key_pair.take(),
SocketUse::GeneralSsl,
)?;

View file

@ -9,6 +9,8 @@ import {
op_node_http_await_response,
op_node_http_fetch_response_upgrade,
op_node_http_request_with_conn,
op_tls_key_null,
op_tls_key_static,
op_tls_start,
} from "ext:core/ops";
@ -515,12 +517,21 @@ class ClientRequest extends OutgoingMessage {
let baseConnRid = handle[kStreamBaseField][internalRidSymbol];
if (this._encrypted) {
const hasCaCerts = this.agent?.options?.ca !== undefined;
const caCerts = hasCaCerts
? [this.agent.options.ca.toString("UTF-8")]
: [];
const hasTlsKey = this.agent?.options?.key !== undefined &&
this.agent?.options?.cert !== undefined;
const keyPair = hasTlsKey
? op_tls_key_static(this.agent.options.cert, this.agent.options.key)
: op_tls_key_null();
[baseConnRid] = op_tls_start({
rid: baseConnRid,
hostname: parsedUrl.hostname,
caCerts: [],
caCerts: caCerts,
alpnProtocols: ["http/1.0", "http/1.1"],
});
}, keyPair);
}
this._req = await op_node_http_request_with_conn(

View file

@ -170,6 +170,27 @@ export function request(...args: any[]) {
}
options._defaultAgent = globalAgent;
if (options.agent === undefined) {
if (options.key !== undefined) {
options._defaultAgent.options.key = options.key;
}
if (options.cert !== undefined) {
options._defaultAgent.options.cert = options.cert;
}
if (options.ca !== undefined) {
options._defaultAgent.options.ca = options.ca;
}
} else {
if (options.key !== undefined) {
options.agent.options.key = options.key;
}
if (options.cert !== undefined) {
options.agent.options.cert = options.cert;
}
if (options.ca !== undefined) {
options.agent.options.ca = options.ca;
}
}
args.unshift(options);
return new HttpsClientRequest(args[0], args[1]);

View file

@ -75,6 +75,7 @@ util::unit_test_factory!(
fs_test,
fetch_test,
http_test,
http_no_cert_flag_test,
http2_test,
inspector_test,
_randomBytes_test = internal / _randomBytes_test,

View file

@ -0,0 +1,98 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import https from "node:https";
import net from "node:net";
import { assert, assertEquals } from "@std/assert";
Deno.test("[node/https] request directly with key and cert as arguments", async () => {
let body = "";
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
const server = https.createServer(
{
cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
},
(req, res) => {
// @ts-ignore: It exists on TLSSocket
assert(req.socket.encrypted);
res.end("success!");
},
);
server.listen(() => {
const req = https.request({
method: "GET",
hostname: "localhost",
port: (server.address() as net.AddressInfo).port,
key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
ca: Deno.readTextFileSync("tests/testdata/tls/RootCA.pem"),
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
deferred2.resolve();
server.close();
});
});
req.on("error", (e) => deferred2.reject(e));
req.end();
});
server.on("close", () => deferred1.resolve());
server.on("error", (e) => deferred1.reject(e));
await deferred1.promise;
await deferred2.promise;
assertEquals(body, "success!");
});
Deno.test("[node/https] request directly with key and cert as agent", async () => {
let body = "";
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
const server = https.createServer(
{
cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
},
(req, res) => {
// @ts-ignore: It exists on TLSSocket
assert(req.socket.encrypted);
res.end("success!");
},
);
server.listen(() => {
const req = https.request({
method: "GET",
hostname: "localhost",
port: (server.address() as never as net.AddressInfo).port,
agent: new https.Agent({
key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
ca: Deno.readTextFileSync("tests/testdata/tls/RootCA.pem"),
}),
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
deferred2.resolve();
server.close();
});
});
req.on("error", (e) => deferred2.reject(e));
req.end();
});
server.on("close", () => deferred1.resolve());
server.on("error", (e) => deferred1.reject(e));
await deferred1.promise;
await deferred2.promise;
assertEquals(body, "success!");
});