Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/runtime/socket/socket_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,8 +1685,17 @@ impl<const SSL: bool> NewSocket<SSL> {
}
}

// `socket.authorized` must reflect peer-certificate verification, not
// just handshake completion. The callback's `success` arg stays raw:
// node:tls treats a false second argument as "TLS never established".
let authorized_flag = if SSL {
authorized && ssl_error.error_no == 0
} else {
authorized
};

this.update_flags(|f| {
f.set(Flags::AUTHORIZED, authorized);
f.set(Flags::AUTHORIZED, authorized_flag);
f.set(Flags::HOSTNAME_MISMATCH, hostname_mismatch);
});

Expand Down
264 changes: 264 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createSocketPair } from "bun:internal-for-testing";
import { describe, expect, it, jest } from "bun:test";
import { closeSync } from "fs";
import { bunEnv, bunExe, expectMaxObjectTypeCount, getMaxFD, isWindows, tempDir, tls } from "harness";
import nodeTls from "node:tls";
describe.concurrent("socket", () => {
it("should throw when a socket from a file descriptor has a bad file descriptor", async () => {
const open = jest.fn();
Expand Down Expand Up @@ -1776,3 +1777,266 @@ it.concurrent("setTypeOfService validates its argument instead of asserting", as
});
void stderr;
});

// https://github.com/oven-sh/bun/issues/33754
// A `Bun.listen` TLS server with `requestCert` must report `socket.authorized`
// based on whether the client certificate actually verified, not merely that
// the TLS handshake completed. Certs below were generated once with openssl:
// a CA, a "localhost" server cert signed by it, and a client cert signed by a
// completely different rogue CA the server never trusts.
describe("Bun.listen TLS client-certificate authorization", () => {
const CA_CRT = `-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAgIURCg2v+DXlydKS0oySTGIE3z/hy4wDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UEAwwNTGVnaXQtVGVzdC1DQTAeFw0yNjA3MDgwNjU2MTRaFw0z
NjA3MDUwNjU2MTRaMBgxFjAUBgNVBAMMDUxlZ2l0LVRlc3QtQ0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtvs7EHLwTOT/folLzxbSsCI2BaSC7/AQq
xtLXHd9ZyW942v9XzDahUUqmeLtKQONEb3b0r1aZlQlAgQlGRxPNodbmwg43TFxn
EdrzRrj0++5R+J/PV19+wrHfVVBGEVjC24iN1wz7beqyIr9DiCc8/7FqZZIgMHcd
RJi6tAOToVxyqUZZXYIYAa2VEKRtM6DqepKEyxjqrAgwYh2kyfxFFXETqKwnGnZb
ORD8AMGe6nxvC5GwJvOQAy/k8vwkYENgPfnMuiiWLQeStquswe6ufHRJq7Pu3SCz
j0uswLSNB5QgPm+rAie2Q2FGOcCFTmx2lLXiXAHAxuR7uQ3Mrh33AgMBAAGjUzBR
MB0GA1UdDgQWBBQyNc02kiPUTqWS1JvGP59zZa60PDAfBgNVHSMEGDAWgBQyNc02
kiPUTqWS1JvGP59zZa60PDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4IBAQAaf/ZPTCgJez9JkjNnGqTTrZRJf1oB1KKJSefU7lQio+tOMHEwK/6fRk6B
GYTSfXEOTVpRHp1ITTUx8XRc56C/gcEoiQzsrIcxNLEw4c3lvvFFClij3JMASRw4
d6BaoRHbecckClHFB0xLyYGXXM+Nr3AH25mdoiDQ1fyUY44gUFqay88plkZTOHJ7
Bn1c1bSzbCH2Le2jtVW9sntw/5BmR/Wu6fzynXERzVZuBaMjh0SKrlfXJrolMn+j
xDilF8VcHP0B/EhvfVBLQbMbGPtH/vcb7yZHA5LR4FDYe4GUAn1rTocxNNGWR6qI
hfy/JaAe+cCaj/lhbnn3ntyu4Hup
-----END CERTIFICATE-----`;

const SERVER_KEY = `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfzMHr8zfqb38y
W4YsFxPg+9atUKeHlgmpdlYHRsDZoP/ytPEVzlaBQdtt01g67SbkY/C+luXHLy7l
vfM2fTlhG86cGs5Wc+bRMSxVo9ev6xxX96dRTRp67JSHU71hEkXV324nDJIM58HO
M0NkK5DJqj4L9Yruu92Fvero0Q7rqLwxclAkmZQVu1TIyRiaAn3kt+d1geOblZJW
Fq0OvyI8Y2oW7w21Ftu184H7CY97T7AfyhyAH5bNGjOBaGrSYQClOlvka/NSVCvw
nZ1yPOgcVc5kHDfkAV7adDfxbizuHqrHdIg15cjxmIdWx5ygm18B4oyrQJePZLk9
hyw19dV5AgMBAAECggEAICajSUlUpBpGdK6PVPAvy+eCDL4Eg0b7tluoujzg4aLu
giiaZd6RsNoMMvfkufpt1uvAnzDAa+AMZbbnJNHSl2/OO8DiGatm4nClBNyX1M76
8GxEnjpsbnJkWBigoTxlyfbiNTvqE26L+hqFOPPFRiNt7Hvm7KsShO8muzGlahc7
w3CLhod0yjDOmXUe0fui4P8EVkESk/yDdRyJ+Kr/CqHa0WwVR6UC0UpBPAylefA0
JVJUkXnX0fRM/5Hy2L9LGolWyHbPD0OO5J4gFikCKFTd98G2n5kBaWJxeIdor3Fb
TV/a9VXtjf+cnQqNk6mnhciTFgd2mJVYjXwg4ymjvQKBgQDxU9726kJMAal16htE
JTqf6xQh369gj0bSgqHurAgJy8k0rvZtoKxM5/wAvnsOUQ4JFGjyisFBlaMXkfHx
gQyAXES1wdzwxWL779h5Cy8yRyrXf2ATFtfVfONr/GA3FlQxGIzXrmMHOIBaf/65
i5J0cn6KNP7WQX1C7fYo3G7TKwKBgQDtaBONrTjRwRhLFNm1RAP2Uh72ryciP2xt
8B11nielPj+D+sa2HJDGe5q3T3wOFAzmfj9CmTVLTC5nERJmpV9hpVBuC8ixvOq2
PQ0ft86lglE2qnJkcj8uGmGm7KDyea7UUSW6xmflNXW/rY8k/IeeocTWzfV+2V3Y
t+fZk7N36wKBgQDqFLF0DZxK/12xe2gBPJ9V2P8JMGB7p95JeN67lHCjl+DN0lxD
0BLw8iCVVC4mn+aeVgbKJF76T7wHs8/bspI+u8EGEEpP3RZ7S5VNK9UWzsM2jl3R
hlnKwb4S05U8OdNmX+rVlliF+388yWR583EWtKwbQPZjOtjWn90im1aASwKBgQDT
fNmeZmetg8Subf6bWeHltrZaryG/gpyHO1YjBybuL4vJeMc4SC44grgLAMXUjMwJ
MQINxAoT7+OOcUjhJATaKbiCsACzRUYZ3j0oukdebb8HYcPR82yRF3NSjo26M+go
v7lKr6CyMXOZs3VHT6dJC3ccnBFMVTsi6oGh88/2zQKBgBQPAsMBGjALPiGrNZwW
CFDYMBz5VKZ8J6GVH7vwn4KQZd4jw/VOLF/NfX+ijn3KniBmIzpakRbCc2DSx9jw
8cNB7ZDonWPCu3bPWAQvfdEZPgt1KEz9u5ylEXYP7PZNJR+Qs3BnJUzE7UJq1WcX
BF2nr2uDK3QfASTFcqKlZmZN
-----END PRIVATE KEY-----`;

const SERVER_CRT = `-----BEGIN CERTIFICATE-----
MIIC/DCCAeSgAwIBAgIUJ8/XSQwF1luGKteqE6Hljj5MVM4wDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UEAwwNTGVnaXQtVGVzdC1DQTAeFw0yNjA3MDgwNjU2MTVaFw0z
NjA3MDUwNjU2MTVaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAN/MwevzN+pvfzJbhiwXE+D71q1Qp4eWCal2VgdG
wNmg//K08RXOVoFB223TWDrtJuRj8L6W5ccvLuW98zZ9OWEbzpwazlZz5tExLFWj
16/rHFf3p1FNGnrslIdTvWESRdXfbicMkgznwc4zQ2QrkMmqPgv1iu673YW96ujR
DuuovDFyUCSZlBW7VMjJGJoCfeS353WB45uVklYWrQ6/IjxjahbvDbUW27XzgfsJ
j3tPsB/KHIAfls0aM4FoatJhAKU6W+Rr81JUK/CdnXI86BxVzmQcN+QBXtp0N/Fu
LO4eqsd0iDXlyPGYh1bHnKCbXwHijKtAl49kuT2HLDX11XkCAwEAAaNCMEAwHQYD
VR0OBBYEFNa6d063ZGCj98LvLSL8I4ENeFOcMB8GA1UdIwQYMBaAFDI1zTaSI9RO
pZLUm8Y/n3NlrrQ8MA0GCSqGSIb3DQEBCwUAA4IBAQChmWJZ6m2xZAF0zDJEE9+C
Q5soyhl34/Jxg2cG+jEmoYBlA4/R3TrtCvbq+NnwOlp85nA++jndnBumRu31eDUH
mTVViNhX0r21EsTXWaUckKw8YbQBgeoFQkMiIhpb1rOBU0ZrwOj19e5Up8xzoEzo
VbyJVjjto67oY8pHev1GGWDMQlAgJhz5nOfU+xgtJfZWH6VNgG96GNG18CmjM3EE
HIyPp44YI9gKb+aQ93VH3tzPhIblIEavGz8iRRqZmI2MwYjAvj7c+iY2M8EeNTmA
zdQ+vTy4ZvC4FbTBzY5tkBlPF/TuOMBpPDcnZUHXvSWtsgPBrEd8SM3eODZNhDMw
-----END CERTIFICATE-----`;

const ATTACKER_KEY = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUkAH8c0Z0W+/H
ZB3HYgjJAMTJTM92SLDFkK9DigNYTe1XgTCP1oH5jsirAcAObv7G/sfZBeR0Jifr
ieOou+bH6Vv5BC6VBg+GvFTtQyNFwClxDCpUMtXbL91n57MyYdM3F2QzIENzCKJp
pAYqVgdgpXtfU7dsKp5GfHHpEkPa6GR6zYAlGBX79BUIQNjvwOdTSV1s4rg9N83b
QGgebrzPxTnt6ugsVBVDI29FGCz0fpY5AQ2Ymz1JCljoZstq5iU6/hPY/76CbzHk
3msOsWKzAoTaiiQ3PThtV4rvhwcMowNsHuQPpr2pC9iqICaGvLkJveiVi4/G+zbn
flmDCfsZAgMBAAECggEABcQHB7Sznc15o1vr2fKgk6h/eq6L+LO+hJRmcPEYyyMP
HI/LgjGZjsVvXU+ulADTy4ZU3EPhx245q2cJGmHchZK1epcida8sCBckFqc95bWd
BiGqPOVSLnUPmzo/Vqu8Rk2WmnNcmsjRgNcYVClNlP8nKB54Jm8LAIJQ63JFhufh
4Eztk3j8KXJN371G8o/gKqV3/wIoG9B7wFhNsUl3g6oEf1fRRYntYzDBcYZb1j7E
ZzR8symIXz5l5FTM0hgScxNXW4JVNc7PLQpLKSXbsWZql65zyyoDS+TcwFbonrQD
woN9/veudfWLiSzT6cQF7Nq2AwGDG9NluZQowjdzeQKBgQDPBhdM0rNL5LoZ1Uwl
bK7I7k0u8OwyEXmqcaOuJyGmtA/ld2SmG8pjKRavdcM8CWQUnD0mMNBoUM8uf1Wn
psrExYVyqJPFl477ZwJXYPSHkokxPDqNWnW8BL+lVYIShCFF0YqxTPsUnlX+FSsM
LDuHPDUwNhdffokHcCmnVhDNtQKBgQC3tVkQARA7IEnNGPkpSYR7pK6eDj2SAABo
rRd2uTwyhz+N/2ziD2kkra/jKOD1FbfBRoU1mC5R2M0xe6rqaCzVwAXL1DVZLH8w
auP1Iz5Bgsv1wO32aqm/jPY9jF2HuICwA23zZ/fbzhGlXeqThnG5yqbgwvFVcE1k
qVCJEru2VQKBgAx9f5Zg9/RSPnAkkE2JuxngVsPIQVfb+g0wggGV1s/p+TOM+oOT
FajZ58Z6Qmcy4djkfEP7mfdROM7DW/WbeIxapmx+gzveov/D/T4sWVR8oM5Zpea1
WHkZiD5/ZDOdySwfMlD0Jgnea63CtTAs0wKbvVHFUa+vQLE0MS8pYCYpAoGAWeyb
PApJN6gGeC7RSDSEdUGXeCbgXKdDi/mukp75qIIrygZN9ho1DY83mapY8589443x
htqHUekeCCrQ7w0vZTIppCysMIpnd/vauhQWVVsBG7rkwMpVbT5DCr26ysS1uXL0
T0GFQkMMwDXIrY4R/TAFn9/M4lWmjK+UjIRu/kECgYEAo4lgcaUTtGhxPXE3A8fd
IudOPh7oZzeoEg2YgfCFaSmAO+RJASs5GU8JrhMK09AwB8uOGen6ELMtnZiYRKpK
tha0aOPTR4W4gBGeUN9iXGvSvPVcjoMIYConUr2G7rMLT/c/bbpkiLwyLkYxedqR
mC+O9upMXi0bT4s+yu4UHzk=
-----END PRIVATE KEY-----`;

const ATTACKER_CRT = `-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIUML8e9nqsrHuvFN1/5i1U30fTs6swDQYJKoZIhvcNAQEL
BQAwHDEaMBgGA1UEAwwRUm9ndWUtQXR0YWNrZXItQ0EwHhcNMjYwNzA4MDY1NjE1
WhcNMzYwNzA1MDY1NjE1WjATMREwDwYDVQQDDAhhdHRhY2tlcjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAJSQAfxzRnRb78dkHcdiCMkAxMlMz3ZIsMWQ
r0OKA1hN7VeBMI/WgfmOyKsBwA5u/sb+x9kF5HQmJ+uJ46i75sfpW/kELpUGD4a8
VO1DI0XAKXEMKlQy1dsv3WfnszJh0zcXZDMgQ3MIommkBipWB2Cle19Tt2wqnkZ8
cekSQ9roZHrNgCUYFfv0FQhA2O/A51NJXWziuD03zdtAaB5uvM/FOe3q6CxUFUMj
b0UYLPR+ljkBDZibPUkKWOhmy2rmJTr+E9j/voJvMeTeaw6xYrMChNqKJDc9OG1X
iu+HBwyjA2we5A+mvakL2KogJoa8uQm96JWLj8b7Nud+WYMJ+xkCAwEAAaNCMEAw
HQYDVR0OBBYEFLIHghpukEjtpVlMBWpuqqynyeypMB8GA1UdIwQYMBaAFHRJlVcz
2ggsHzqULpfY5tqj6bK4MA0GCSqGSIb3DQEBCwUAA4IBAQA0hTvMyg/la6WHtVqQ
raikcRdfgZPJMqULtxNP8h5HN8G2N1J61Yrqs+hZv8tpsgJ69kSAVq2rlGlpe4XZ
BTj5g8+zEVy+/oP/720EQLXwp8xhB9yn/dKEPTpuQHc0yhNU7L4F/EsfCuBdOY09
UN9EB9KAnNGzKLt5QhZRFz/Ik/Eg9iTLblaHTsyK8XaEncOQGSIP/UyXThJ/xE2g
RL/yKf/oq1zXajZAPCFTlt9OcxZLDS0sHlpybthTuN6rrFQzoIhlGmecp3i6B+oH
H13FrrYXfkdKAdTYPoIw1P2rvMfDcVXf2ELcyN9vjHq6YSoplEam/pXnPVOHRgT5
NMTg
-----END CERTIFICATE-----`;

async function runHandshake(serverTlsExtra: Record<string, unknown>, clientCert?: { key: string; cert: string }) {
const { promise, resolve, reject } = Promise.withResolvers<{
success: boolean;
authorized: boolean;
callbackError: string | null;
getterError: string | null;
}>();

using server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
tls: {
key: SERVER_KEY,
cert: SERVER_CRT,
...serverTlsExtra,
// rejectUnauthorized left at its documented default (true)
},
socket: {
open() {},
handshake(socket, success, authorizationError) {
resolve({
success,
authorized: socket.authorized,
callbackError: authorizationError?.message ?? null,
getterError: socket.getAuthorizationError()?.message ?? null,
});
},
Comment thread
robobun marked this conversation as resolved.
data() {},
// Surface a server-side teardown before handshake instead of hanging
// until the runner timeout.
close() {
reject(new Error("server socket closed before handshake"));
},
error(_socket, err) {
reject(err);
},
},
});

const client = nodeTls.connect({
host: "127.0.0.1",
port: server.port,
...(clientCert ? { key: clientCert.key, cert: clientCert.cert } : {}),
ca: CA_CRT,
servername: "localhost",
rejectUnauthorized: false,
});
client.on("error", reject);
client.on("close", () => reject(new Error("client closed before server handshake")));

try {
return await promise;
} finally {
client.destroy();
}
}

it("reports authorized=false for a client cert signed by an untrusted CA", async () => {
const result = await runHandshake({ ca: CA_CRT, requestCert: true }, { key: ATTACKER_KEY, cert: ATTACKER_CRT });
expect(result.authorized).toBe(false);
expect(result.callbackError).not.toBeNull();
expect(result.getterError).not.toBeNull();
// The handshake itself completed; only certificate verification failed. The
// callback's `success` arg must stay true so node:tls's server handler does
// not misread a verify failure as "the TLS session was never established".
expect(result.success).toBe(true);
});

it("reports authorized=true for a client cert signed by the trusted CA", async () => {
// SERVER_CRT/SERVER_KEY chains to CA_CRT, which the server trusts.
const result = await runHandshake({ ca: CA_CRT, requestCert: true }, { key: SERVER_KEY, cert: SERVER_CRT });
expect(result.authorized).toBe(true);
expect(result.callbackError).toBeNull();
expect(result.getterError).toBeNull();
expect(result.success).toBe(true);
});

it("reports authorized=false on a plain server that never requested a client cert", async () => {
// No ca, no requestCert: no client certificate is verified, so the
// connection is not authorized even though the handshake completes. Matches
// Node's tls.TLSSocket#authorized (false unless requestCert + verified).
const result = await runHandshake({});
expect(result.success).toBe(true);
expect(result.authorized).toBe(false);
});

it("reports authorized=false on a Bun.connect client when the server cert does not verify", async () => {
// The client trusts no CA that issued the server's cert, but the hostname
// (CN=localhost) matches, so the sole reason authorized is false is the
// failed chain verification, matching node:tls's client-side authorized.
const { promise, resolve, reject } = Promise.withResolvers<{ authorized: boolean; getterError: string | null }>();
using server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
tls: { key: SERVER_KEY, cert: SERVER_CRT },
socket: {
open() {},
data() {},
close() {
reject(new Error("server socket closed before client handshake"));
},
error(_socket, err) {
reject(err);
},
},
});
const client = await Bun.connect({
hostname: "127.0.0.1",
port: server.port,
tls: { serverName: "localhost", rejectUnauthorized: false },
socket: {
open() {},
handshake(socket) {
resolve({ authorized: socket.authorized, getterError: socket.getAuthorizationError()?.message ?? null });
},
data() {},
close() {
reject(new Error("client socket closed before handshake"));
},
error(_s, err) {
reject(err);
},
connectError(_s, err) {
reject(err);
},
},
});
try {
const result = await promise;
expect(result.authorized).toBe(false);
expect(result.getterError).not.toBeNull();
// The failure is the chain, not the hostname.
expect(result.getterError).not.toBe("Hostname mismatch");
} finally {
client.end();
}
});
});
Comment thread
robobun marked this conversation as resolved.
Loading