Skip to content

Commit

Permalink
feat: load full certificate bundles from NODE_EXTRA_CA_CERTS
Browse files Browse the repository at this point in the history
  • Loading branch information
ShlomoCode committed Feb 5, 2025
1 parent 0861c03 commit 8b38e9f
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 46 deletions.
96 changes: 59 additions & 37 deletions packages/bun-usockets/src/crypto/root_certs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,57 +27,81 @@ us_ssl_ctx_get_X509_without_callback_from(struct us_cert_string_t content) {
in = BIO_new_mem_buf(content.str, content.len);
if (in == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_BUF_LIB);
goto end;
}
} else {
x = PEM_read_bio_X509(in, NULL, us_no_password_callback, NULL);
if (x == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_PEM_LIB);
}

x = PEM_read_bio_X509(in, NULL, us_no_password_callback, NULL);
if (x == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_PEM_LIB);
goto end;
// NOTE: PEM_read_bio_X509 allocates, so input BIO must be freed.
BIO_free(in);
}

// NOTE: PEM_read_bio_X509 allocates, so input BIO must be freed.
BIO_free(in);
return x;
end:
X509_free(x);
BIO_free(in);
return NULL;
}

static X509 *
us_ssl_ctx_get_X509_without_callback_from_file(const char *filename) {
static STACK_OF(X509) *us_ssl_ctx_load_all_certs_from_file(const char *filename) {
BIO *in = NULL;
STACK_OF(X509) *certs = NULL;
X509 *x = NULL;
BIO *in;
unsigned long last_err;

ERR_clear_error(); // clear error stack for SSL_CTX_use_certificate()

in = BIO_new(BIO_s_file());
in = BIO_new_file(filename, "r");
if (in == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_BUF_LIB);
OPENSSL_PUT_ERROR(SSL, ERR_R_SYS_LIB);
goto end;
}

if (BIO_read_filename(in, filename) <= 0) {
OPENSSL_PUT_ERROR(SSL, ERR_R_SYS_LIB);
certs = sk_X509_new_null();
if (certs == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
goto end;
}

x = PEM_read_bio_X509(in, NULL, us_no_password_callback, NULL);
if (x == NULL) {
while ((x = PEM_read_bio_X509(in, NULL, us_no_password_callback, NULL))) {
if (!sk_X509_push(certs, x)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
X509_free(x);
goto end;
}
}

last_err = ERR_peek_last_error();
// Ignore error if its EOF/no start line found.
if (ERR_GET_LIB(last_err) == ERR_LIB_PEM && ERR_GET_REASON(last_err) == PEM_R_NO_START_LINE) {
ERR_clear_error();
} else {
goto end;
}

if (sk_X509_num(certs) == 0) {
OPENSSL_PUT_ERROR(SSL, ERR_R_PEM_LIB);
goto end;
}
return x;

BIO_free(in);
return certs;

end:
X509_free(x);
BIO_free(in);
if (certs) {
sk_X509_pop_free(certs, X509_free);
}

char buf[256];
ERR_error_string_n(ERR_peek_last_error(), buf, sizeof(buf));
fprintf(stderr, "Warning: Error loading certs from file: %s\n", buf);

return NULL;
}

static void us_internal_init_root_certs(X509 *root_cert_instances[sizeof(root_certs) / sizeof(root_certs[0])], X509 *&root_extra_cert_instances) {
static void us_internal_init_root_certs(
X509 *root_cert_instances[root_certs_size],
STACK_OF(X509) *&root_extra_cert_instances) {
static std::atomic_flag root_cert_instances_lock = ATOMIC_FLAG_INIT;
static std::atomic_bool root_cert_instances_initialized = 0;

if (std::atomic_load(&root_cert_instances_initialized) == 1)
return;

Expand All @@ -92,13 +116,9 @@ static void us_internal_init_root_certs(X509 *root_cert_instances[sizeof(root_ce
}

// get extra cert option from environment variable
const char *extra_cert = getenv("NODE_EXTRA_CA_CERTS");
if (extra_cert) {
size_t length = strlen(extra_cert);
if (length > 0) {
root_extra_cert_instances =
us_ssl_ctx_get_X509_without_callback_from_file(extra_cert);
}
const char *extra_certs = getenv("NODE_EXTRA_CA_CERTS");
if (extra_certs && extra_certs[0]) {
root_extra_cert_instances = us_ssl_ctx_load_all_certs_from_file(extra_certs);
}
}

Expand All @@ -122,9 +142,8 @@ extern "C" X509_STORE *us_get_default_ca_store() {
return NULL;
}

static X509 *root_cert_instances[sizeof(root_certs) / sizeof(root_certs[0])] = {
NULL};
static X509 *root_extra_cert_instances = NULL;
static X509 *root_cert_instances[root_certs_size] = {NULL};
static STACK_OF(X509) *root_extra_cert_instances = NULL;

us_internal_init_root_certs(root_cert_instances, root_extra_cert_instances);

Expand All @@ -138,8 +157,11 @@ extern "C" X509_STORE *us_get_default_ca_store() {
}

if (root_extra_cert_instances) {
X509_up_ref(root_extra_cert_instances);
X509_STORE_add_cert(store, root_extra_cert_instances);
for (int i = 0; i < sk_X509_num(root_extra_cert_instances); i++) {
X509 *cert = sk_X509_value(root_extra_cert_instances, i);
X509_up_ref(cert);
X509_STORE_add_cert(store, cert);
}
}

return store;
Expand Down
5 changes: 5 additions & 0 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,11 @@ export const tls = Object.freeze({
key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+7odzr3yIYewR\nNRGIubF5hzT7Bym2dDab4yhaKf5drL+rcA0J15BM8QJ9iSmL1ovg7x35Q2MBKw3r\nl/Yyy3aJS8whZTUze522El72iZbdNbS+oH6GxB2gcZB6hmUehPjHIUH4icwPdwVU\neR6fB7vkfDddLXe0Tb4qsO1EK8H0mr5PiQSXfj39Yc1QHY7/gZ/xeSrt/6yn0oH9\nHbjF2XLSL2j6cQPKEayartHN0SwzwLi0eWSzcziVPSQV7c6Lg9UuIHbKlgOFzDpc\np1p1lRqv2yrT25im/dS6oy9XX+p7EfZxqeqpXX2fr5WKxgnzxI3sW93PG8FUIDHt\nnUsoHX3RAgMBAAECggEAAckMqkn+ER3c7YMsKRLc5bUE9ELe+ftUwfA6G+oXVorn\nE+uWCXGdNqI+TOZkQpurQBWn9IzTwv19QY+H740cxo0ozZVSPE4v4czIilv9XlVw\n3YCNa2uMxeqp76WMbz1xEhaFEgn6ASTVf3hxYJYKM0ljhPX8Vb8wWwlLONxr4w4X\nOnQAB5QE7i7LVRsQIpWKnGsALePeQjzhzUZDhz0UnTyGU6GfC+V+hN3RkC34A8oK\njR3/Wsjahev0Rpb+9Pbu3SgTrZTtQ+srlRrEsDG0wVqxkIk9ueSMOHlEtQ7zYZsk\nlX59Bb8LHNGQD5o+H1EDaC6OCsgzUAAJtDRZsPiZEQKBgQDs+YtVsc9RDMoC0x2y\nlVnP6IUDXt+2UXndZfJI3YS+wsfxiEkgK7G3AhjgB+C+DKEJzptVxP+212hHnXgr\n1gfW/x4g7OWBu4IxFmZ2J/Ojor+prhHJdCvD0VqnMzauzqLTe92aexiexXQGm+WW\nwRl3YZLmkft3rzs3ZPhc1G2X9QKBgQDOQq3rrxcvxSYaDZAb+6B/H7ZE4natMCiz\nLx/cWT8n+/CrJI2v3kDfdPl9yyXIOGrsqFgR3uhiUJnz+oeZFFHfYpslb8KvimHx\nKI+qcVDcprmYyXj2Lrf3fvj4pKorc+8TgOBDUpXIFhFDyM+0DmHLfq+7UqvjU9Hs\nkjER7baQ7QKBgQDTh508jU/FxWi9RL4Jnw9gaunwrEt9bxUc79dp+3J25V+c1k6Q\nDPDBr3mM4PtYKeXF30sBMKwiBf3rj0CpwI+W9ntqYIwtVbdNIfWsGtV8h9YWHG98\nJ9q5HLOS9EAnogPuS27walj7wL1k+NvjydJ1of+DGWQi3aQ6OkMIegap0QKBgBlR\nzCHLa5A8plG6an9U4z3Xubs5BZJ6//QHC+Uzu3IAFmob4Zy+Lr5/kITlpCyw6EdG\n3xDKiUJQXKW7kluzR92hMCRnVMHRvfYpoYEtydxcRxo/WS73SzQBjTSQmicdYzLE\ntkLtZ1+ZfeMRSpXy0gR198KKAnm0d2eQBqAJy0h9AoGBAM80zkd+LehBKq87Zoh7\ndtREVWslRD1C5HvFcAxYxBybcKzVpL89jIRGKB8SoZkF7edzhqvVzAMP0FFsEgCh\naClYGtO+uo+B91+5v2CCqowRJUGfbFOtCuSPR7+B3LDK8pkjK2SQ0mFPUfRA5z0z\nNVWtC0EYNBTRkqhYtqr3ZpUc\n-----END PRIVATE KEY-----\n",
});

export const invalidTls = Object.freeze({
cert: "-----BEGIN CERTIFICATE-----\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh\nbmNpc2NvMQ0wCwYDVQQKDARPdmVuMREwDwYDVQQLDAhUZWFtIEJ1bjETMBEGA1UE\nAwwKc2VydmVyLWJ1bjAeFw0yNTAyMDQwNDUyNTdaFw0yNzAyMDQwNDUyNTdaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj\nbzENMAsGA1UECgwET3ZlbjERMA8GA1UECwwIVGVhbSBCdW4xEzARBgNVBAMMCnNl\ncnZlci1idW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1rZqCnASs\nHPzPjs/mls+z3qTl6OsCNI+kTsA23/+ZkvtBe7EI+9LfV1Sy4MF66ZovR0UgeJUB\nlL7ExadXkfZJS0N6LEAIyEMQI0cpILv3i6sJCcRwHV7X7N55lkUdsJtQ3fSKsyn9\nPDWJGVdwtRjdod3XyevYcx5NLGZOF/4KJmR4eNkX8ycG8zvW/srPHHE95/+k/5Wo\n/RrS+OLl+bgVznxmXtnFMdbYvJ1RLyipCED2P569NWXAgCzYESX2tqLr20R8ca8Q\niTcXXijY1Wq+pVR5NhIckt+zyZlUQ5IT3DvAQn4aW30wA514k1AKDKQjtxdRzVmV\nGDOTOzAlpmeZAgMBAAGjTzBNMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQ\nAAAAAAAAAAAAAAAAAAAAATAdBgNVHQ4EFgQUkgeZUw9BZc/9mxAym4BjaVYhHoow\nDQYJKoZIhvcNAQELBQADggEBAJGQomt68rO1wuhHaG355kGaIsTsoUJgs7VAKNI2\n0/vtMKODX2Zo2BHhiI1wSH751IySqWbGYCvXl6QrsV5tD/jdIYKvyXLFmV0KgQSY\nkZ91sde4jIiiqL5h03GJSUydCl4SE1A1H8Ht41NYoyVaWVPzv5lprt654vpRPbPq\nYBQTWSFcYkmGnza/tRALUftM5U3yKOTQ8sKH/eKGC9KU0DI5pZ2XAxrIyvrJZMm1\n0WwWTrO0KlXN8N9v8tVCVm7g6mYug4HEADQ4kymyfwM6mPY1EmsGy36KOqCRUtUR\n+jmAZr9m+l+27GxR9zjxoLWHkARuWZM/hL//u90cNfNDRgQ=\n-----END CERTIFICATE-----\n",
key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1rZqCnASsHPzP\njs/mls+z3qTl6OsCNI+kTsA23/+ZkvtBe7EI+9LfV1Sy4MF66ZovR0UgeJUBlL7E\nxadXkfZJS0N6LEAIyEMQI0cpILv3i6sJCcRwHV7X7N55lkUdsJtQ3fSKsyn9PDWJ\nGVdwtRjdod3XyevYcx5NLGZOF/4KJmR4eNkX8ycG8zvW/srPHHE95/+k/5Wo/RrS\n+OLl+bgVznxmXtnFMdbYvJ1RLyipCED2P569NWXAgCzYESX2tqLr20R8ca8QiTcX\nXijY1Wq+pVR5NhIckt+zyZlUQ5IT3DvAQn4aW30wA514k1AKDKQjtxdRzVmVGDOT\nOzAlpmeZAgMBAAECggEANW6F2zTckOwDlF2pmmUvV/S6pZ1/hIoF1uqMUHdHmpim\nSaeBtSUu6x2pnORKMwaCILaCx55/IFRpWMDSywf0GbFHeqaJ/Ks9QgFGG/vzHEZY\n+pMDUX/p1XJmKfc+g5Fd1IY6thIkXsR28EfiNhUk54YEE0NhGCsfNc5BlmUrAzuw\nSevCkbChsZzLoasskt5hgWOb1wT757xDrOOss3LXvwaFkMXANQHiaGWxSpmyXTVf\nmtIX4wpN2K5BQxRBV6xmRaBBp7fWJlbqvV67wwh2cxIAyvQ68VVVHTbfv44TUw62\nyCKle6hSLi/OnMr1FJv16Ez+K3lUIkYE0nTYIvQkYwKBgQD34Nwy0U8WcHcOSbU1\nMIREfvPNYfl/hH94wmLyKZWhpqOfqgrUg+19+n9TXw4DGdO7lwAzz+ud7TImbGDI\n+1cb9/FxTK5cRwICTLC+Pse6pVkKUvPdil/TfHZBJP1jeIMGMDVi0fcGv97LxrHV\nJGQwA5x1nHGHl0JrENRqm3M2NwKBgQC7oXkWb0s2C8ANI14gz+q/2EmTSJxxrzXR\nz5EQk87PmPfkY4I1T8vKFcaiJynyReLwpYTip2WYGqc7qAO9oLwmA+d/NMOBI2sg\noEn154Q9zvr3jqIgu9/AapEgEDlA+v18veoIz3bae6wu57lpGvGtCoQLBS6q2UZg\n3zFI3BJorwKBgDz4WjFFuqZSU3Z4OtIydNZEQ8Oo7a2n8ZLKfXwDLoLsciK7uJ49\nNRVfoCHpp5CrsaDaq3oTEmluBn/c+JF3AR4oBoNP0TNxY9Uc9/xThN0r/pLDhKhh\neOCUJKIxbwIgilnjUb5U1uYaG7sTzHoY0Wvd94YWTPaFBhk/sn/mbJhRAoGAA+/E\nWZsmKdEfS2dFj0ytcS75hDSOy7fQWkGPmphvS127Pbh0v+eXr/q6+yX1NFcRBtmC\nKzs133YXsiG5Sl439Fg6oCmcPHZgxgN26cjctmtESrNcZXFrpV7XAqQ0f0+Ex/w4\nD81Cghz8JNPJyRG+plHFKXIHY6BBYMDuCMhNPpMCgYEA1BVG5scNtmBE3SaRns2G\npKgWiwmzPDTqwf3R0rgkHQroZUIz616jLgKXIVMBPaq/771uq+hzJZro9sNcNL8p\n9PkLRr4V4KtUSqkjvitU68vMM1qxtO9NVwCI5u3wicVC5mMqcH8FN+sO/5/jIPBl\nO/qEOVDlCuYtURcnh/Oz1cE=\n-----END PRIVATE KEY-----\n",
});

export function disableAggressiveGCScope() {
const gc = Bun.unsafe.gcAggressionLevel(0);
return {
Expand Down
80 changes: 71 additions & 9 deletions test/js/web/fetch/fetch.tls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ type TLSOptions = {
passphrase?: string;
};

import { tls as cert1, expiredTls as cert2 } from "harness";
import { tls as validTls, expiredTls, invalidTls } from "harness";

const CERT_LOCALHOST_IP = { ...cert1 };
const CERT_EXPIRED = { ...cert2 };
const CERT_LOCALHOST_IP = { ...validTls };
const CERT_EXPIRED = { ...expiredTls };

// Note: Do not use bun.sh as the example domain
// Cloudflare sometimes blocks automated requests to it.
Expand Down Expand Up @@ -228,7 +228,7 @@ it("fetch should respect rejectUnauthorized env", async () => {

it("fetch timeout works on tls", async () => {
using server = Bun.serve({
tls: cert1,
tls: validTls,
hostname: "localhost",
port: 0,
rejectUnauthorized: false,
Expand All @@ -248,7 +248,7 @@ it("fetch timeout works on tls", async () => {
try {
await fetch(server.url, {
signal: AbortSignal.timeout(TIMEOUT),
tls: { ca: cert1.cert },
tls: { ca: validTls.cert },
}).then(res => res.text());
expect.unreachable();
} catch (e) {
Expand Down Expand Up @@ -292,13 +292,13 @@ for (const timeout of [0, 1, 10, 20, 100, 300]) {
it("fetch should use NODE_EXTRA_CA_CERTS", async () => {
using server = Bun.serve({
port: 0,
tls: cert1,
tls: validTls,
fetch() {
return new Response("OK");
},
});
const cert_path = join(tmpdirSync(), "cert.pem");
await Bun.write(cert_path, cert1.cert);
await Bun.write(cert_path, validTls.cert);

const proc = Bun.spawn({
env: {
Expand All @@ -315,15 +315,77 @@ it("fetch should use NODE_EXTRA_CA_CERTS", async () => {
expect(await proc.exited).toBe(0);
});

it("fetch should load certificate even if it's not the first in bundle", async () => {
using server = Bun.serve({
port: 0,
tls: validTls,
fetch() {
return new Response("OK");
},
});

const bundlePath = join(tmpdirSync(), "bundle.pem");
const bundleContent = `${expiredTls.cert}\n${validTls.cert}`;
await Bun.write(bundlePath, bundleContent);

const proc = Bun.spawn({
env: {
...bunEnv,
SERVER: server.url,
NODE_EXTRA_CA_CERTS: bundlePath,
},
stderr: "inherit",
stdout: "inherit",
stdin: "inherit",
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
});

expect(await proc.exited).toBe(0);
});

it("fetch should ignore invalid NODE_EXTRA_CA_CERTS", async () => {
using server = Bun.serve({
port: 0,
tls: cert1,
tls: validTls,
fetch() {
return new Response("OK");
},
});
for (const invalid of ["invalid.pem", "", " "]) {

for (const invalid of ["not-exist.pem", "", " "]) {
const proc = Bun.spawn({
env: {
...bunEnv,
SERVER: server.url,
NODE_EXTRA_CA_CERTS: invalid,
},
stderr: "pipe",
stdout: "inherit",
stdin: "inherit",
cmd: [bunExe(), join(import.meta.dir, "fetch.tls.extra-cert.fixture.js")],
});

expect(await proc.exited).toBe(1);
expect(await Bun.readableStreamToText(proc.stderr)).toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
}
});

it("fetch should ignore NODE_EXTRA_CA_CERTS if it's contains invalid cert", async () => {
using server = Bun.serve({
port: 0,
tls: validTls,
fetch() {
return new Response("OK");
},
});

const mixedValidAndInvalidCertsBundlePath = join(tmpdirSync(), "mixed-valid-and-invalid-certs-bundle.pem");
await Bun.write(mixedValidAndInvalidCertsBundlePath, `${invalidTls.cert}\n${validTls.cert}`);

const mixedInvalidAndValidCertsBundlePath = join(tmpdirSync(), "mixed-invalid-and-valid-certs-bundle.pem");
await Bun.write(mixedInvalidAndValidCertsBundlePath, `${validTls.cert}\n${invalidTls.cert}`);

for (const invalid of [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath]) {
const proc = Bun.spawn({
env: {
...bunEnv,
Expand Down

0 comments on commit 8b38e9f

Please sign in to comment.