abi
abi14mo ago

HTTPS w/ client certificate – works in Node.js, not in Deno

I am making an HTTPS call with a client certificate. It works great in Node.js, but when I try to do the equivalent in Deno, I get a TLS handshake error. Any ideas on what I'm doing wrong?
1 Reply
abi
abi14mo ago
Node.js version:
$ cat node.mjs
import https, { Agent } from "https";
import fs from "fs";

const key = fs.readFileSync("my_key.pem");
const cert = fs.readFileSync("my_cert.pem");
const ca = fs.readFileSync("my_ca.pem");

https.get(process.env.SECRET_URL, {
agent: new Agent({
key: key,
cert: cert,
ca: ca,
}),
}, (resp) => {
console.log(`status: ${resp.statusCode}`);
});

$ node --version
v20.1.0

$ node node.mjs
status: 403 # <-- this means it worked
$ cat node.mjs
import https, { Agent } from "https";
import fs from "fs";

const key = fs.readFileSync("my_key.pem");
const cert = fs.readFileSync("my_cert.pem");
const ca = fs.readFileSync("my_ca.pem");

https.get(process.env.SECRET_URL, {
agent: new Agent({
key: key,
cert: cert,
ca: ca,
}),
}, (resp) => {
console.log(`status: ${resp.statusCode}`);
});

$ node --version
v20.1.0

$ node node.mjs
status: 403 # <-- this means it worked
Deno version:
$ cat deno.ts
const caCert = await Deno.readTextFile("my_ca.pem");
const certChain = await Deno.readTextFile("my_cert.pem");
const privateKey = await Deno.readTextFile("my_key.pem");

const httpClient = Deno.createHttpClient({
caCerts: [caCert],
certChain: certChain,
privateKey: privateKey,
});

const resp = await fetch(Deno.env.get("SECRET_URL")!, {
client: httpClient,
});

console.log(`status: ${resp.status}`);

$ deno --version
deno 1.33.4 (release, aarch64-apple-darwin)
v8 11.4.183.2
typescript 5.0.4

$ deno run --unstable -A deno.ts
TLS alert received: AlertMessagePayload {
level: Fatal,
description: HandshakeFailure,
}
error: Uncaught TypeError: error sending request for url (<redacted>): error trying to connect: received fatal alert: HandshakeFailure
const resp = await fetch(Deno.env.get("SECRET_URL")!, {
^
at async mainFetch (ext:deno_fetch/26_fetch.js:266:12)
at async fetch (ext:deno_fetch/26_fetch.js:490:7)
at async .../deno.ts:11:14
$ cat deno.ts
const caCert = await Deno.readTextFile("my_ca.pem");
const certChain = await Deno.readTextFile("my_cert.pem");
const privateKey = await Deno.readTextFile("my_key.pem");

const httpClient = Deno.createHttpClient({
caCerts: [caCert],
certChain: certChain,
privateKey: privateKey,
});

const resp = await fetch(Deno.env.get("SECRET_URL")!, {
client: httpClient,
});

console.log(`status: ${resp.status}`);

$ deno --version
deno 1.33.4 (release, aarch64-apple-darwin)
v8 11.4.183.2
typescript 5.0.4

$ deno run --unstable -A deno.ts
TLS alert received: AlertMessagePayload {
level: Fatal,
description: HandshakeFailure,
}
error: Uncaught TypeError: error sending request for url (<redacted>): error trying to connect: received fatal alert: HandshakeFailure
const resp = await fetch(Deno.env.get("SECRET_URL")!, {
^
at async mainFetch (ext:deno_fetch/26_fetch.js:266:12)
at async fetch (ext:deno_fetch/26_fetch.js:490:7)
at async .../deno.ts:11:14
Alright, I've been trying to debug this for many hours, and it seems that the issue might be that rustls, which is backing Deno's TLS support, simply doesn't support TLS 1.2 and/or certain ciphers. This sucks. Is anyone else able to tell me if this is supported by rustls or not? I can't seem to find a straight answer.
New, TLSv1/SSLv3, Cipher is AES256-GCM-SHA384
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : AES256-GCM-SHA384
New, TLSv1/SSLv3, Cipher is AES256-GCM-SHA384
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : AES256-GCM-SHA384
Okay, so it seems that rustls will not support any cipher without "forward secrecy", whatever that is. And apparently the server I'm communicating with does not speak any ciphers that support forward secrecy. Dammit...