From a38b1ac055a7a8e6149f18906ed916749abd7a9b Mon Sep 17 00:00:00 2001 From: Shlomo <78599753+ShlomoCode@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:49:28 +0000 Subject: [PATCH] feat: load full certificate bundles from `NODE_EXTRA_CA_CERTS` --- .../bun-usockets/src/crypto/root_certs.cpp | 100 ++++--- src/deps/uws.zig | 4 + test/harness.ts | 5 + .../tls/node-tls-cert-extra-ca.fixture.ts | 18 ++ test/js/node/tls/node-tls-cert.test.ts | 281 +++++++++++++----- test/js/web/fetch/fetch.tls.test.ts | 81 ++++- 6 files changed, 367 insertions(+), 122 deletions(-) create mode 100644 test/js/node/tls/node-tls-cert-extra-ca.fixture.ts diff --git a/packages/bun-usockets/src/crypto/root_certs.cpp b/packages/bun-usockets/src/crypto/root_certs.cpp index 0e8c8541d9a8a3..3140395e5d0678 100644 --- a/packages/bun-usockets/src/crypto/root_certs.cpp +++ b/packages/bun-usockets/src/crypto/root_certs.cpp @@ -8,6 +8,8 @@ #include static const int root_certs_size = sizeof(root_certs) / sizeof(root_certs[0]); +extern "C" void bun_log_warn(const char* message); + // This callback is used to avoid the default passphrase callback in OpenSSL // which will typically prompt for the passphrase. The prompting is designed // for the OpenSSL CLI, but works poorly for this case because it involves @@ -27,57 +29,83 @@ 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)); + char msg[512]; + snprintf(msg, sizeof(msg), "ignoring extra certs from `%s`, load failed: %s", filename, buf); + bun_log_warn(msg); + 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; @@ -92,13 +120,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); } } @@ -122,9 +146,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); @@ -138,8 +161,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; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index a56e2cbf3847fa..2eea6728f57dcb 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -4608,3 +4608,7 @@ pub fn onThreadExit() void { extern fn uws_app_clear_routes(ssl_flag: c_int, app: *uws_app_t) void; pub extern fn us_socket_upgrade_to_tls(s: *Socket, new_context: *SocketContext, sni: ?[*:0]const u8) ?*Socket; + +pub export fn bun_log_warn(message: [*c]const u8) void { + bun.Output.warn("{s}", .{message}); +} diff --git a/test/harness.ts b/test/harness.ts index 5619b1095caa9e..2c48f2b72e6b13 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -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 { diff --git a/test/js/node/tls/node-tls-cert-extra-ca.fixture.ts b/test/js/node/tls/node-tls-cert-extra-ca.fixture.ts new file mode 100644 index 00000000000000..61b8b94c58f031 --- /dev/null +++ b/test/js/node/tls/node-tls-cert-extra-ca.fixture.ts @@ -0,0 +1,18 @@ +import tls from "node:tls"; + +tls + .connect( + { + host: "localhost", + port: Number(process.env.SERVER_PORT), + rejectUnauthorized: true, + }, + () => { + console.log("Connected Successfully"); + process.exit(0); + }, + ) + .on("error", err => { + console.error("Failed to connect:", err); + process.exit(1); + }); diff --git a/test/js/node/tls/node-tls-cert.test.ts b/test/js/node/tls/node-tls-cert.test.ts index 4c2d508a7a6c49..191a684e398e0c 100644 --- a/test/js/node/tls/node-tls-cert.test.ts +++ b/test/js/node/tls/node-tls-cert.test.ts @@ -4,13 +4,14 @@ import type { AddressInfo } from "node:net"; import type { Server, TLSSocket } from "node:tls"; import { join } from "path"; import tls from "tls"; +import { bunEnv, bunExe, tmpdirSync, invalidTls } from "harness"; -const client = { +const clientTls = { key: readFileSync(join(import.meta.dir, "fixtures", "ec10-key.pem"), "utf8"), cert: readFileSync(join(import.meta.dir, "fixtures", "ec10-cert.pem"), "utf8"), ca: readFileSync(join(import.meta.dir, "fixtures", "ca5-cert.pem"), "utf8"), }; -const server = { +const serverTls = { key: readFileSync(join(import.meta.dir, "fixtures", "agent10-key.pem"), "utf8"), cert: readFileSync(join(import.meta.dir, "fixtures", "agent10-cert.pem"), "utf8"), ca: readFileSync(join(import.meta.dir, "fixtures", "ca2-cert.pem"), "utf8"), @@ -23,8 +24,8 @@ function split(file: any, into: any) { } // Split out the single end-entity cert and the subordinate CA for later use. -split(client.cert, client); -split(server.cert, server); +split(clientTls.cert, clientTls); +split(serverTls.cert, serverTls); // The certificates aren't for "localhost", so override the identity check. function checkServerIdentity(hostname: string, cert: any) { @@ -109,15 +110,15 @@ function connect(options: any) { it("complete cert chains sent to peer.", async () => { await connect({ client: { - key: client.key, - cert: client.cert, - ca: server.ca, + key: clientTls.key, + cert: clientTls.cert, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, requestCert: true, }, }); @@ -126,13 +127,13 @@ it("complete cert chains sent to peer.", async () => { it("complete cert chains sent to peer, but without requesting client's cert.", async () => { await connect({ client: { - ca: server.ca, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, }, }); }); @@ -143,13 +144,13 @@ it.todo("Request cert from TLS1.2 client that doesn't have one.", async () => { await connect({ client: { maxVersion: "TLSv1.2", - ca: server.ca, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, requestCert: true, }, }); @@ -162,15 +163,15 @@ it.todo("Request cert from TLS1.2 client that doesn't have one.", async () => { it("Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer.", async () => { await connect({ client: { - key: client.key, - cert: client.single, - ca: [server.ca, server.subca], + key: clientTls.key, + cert: clientTls.single, + ca: [serverTls.ca, serverTls.subca], checkServerIdentity, }, server: { - key: server.key, - cert: server.single, - ca: [client.ca, client.subca], + key: serverTls.key, + cert: serverTls.single, + ca: [clientTls.ca, clientTls.subca], requestCert: true, }, }); @@ -179,15 +180,15 @@ it("Typical configuration error, incomplete cert chains sent, we have to know th it("Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer. But using multi-PEM", async () => { await connect({ client: { - key: client.key, - cert: client.single, - ca: server.ca + "\n" + server.subca, + key: clientTls.key, + cert: clientTls.single, + ca: serverTls.ca + "\n" + serverTls.subca, checkServerIdentity, }, server: { - key: server.key, - cert: server.single, - ca: client.ca + "\n" + client.subca, + key: serverTls.key, + cert: serverTls.single, + ca: clientTls.ca + "\n" + clientTls.subca, requestCert: true, }, }); @@ -196,15 +197,15 @@ it("Typical configuration error, incomplete cert chains sent, we have to know th it("Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer. But using multi-PEM in an array", async () => { await connect({ client: { - key: client.key, - cert: client.single, - ca: [server.ca + "\n" + server.subca], + key: clientTls.key, + cert: clientTls.single, + ca: [serverTls.ca + "\n" + serverTls.subca], checkServerIdentity, }, server: { - key: server.key, - cert: server.single, - ca: [client.ca + "\n" + client.subca], + key: serverTls.key, + cert: serverTls.single, + ca: [clientTls.ca + "\n" + clientTls.subca], requestCert: true, }, }); @@ -214,12 +215,12 @@ it("Fail to complete server's chain", async () => { try { await connect({ client: { - ca: server.ca, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.single, + key: serverTls.key, + cert: serverTls.single, }, }); expect.unreachable(); @@ -232,15 +233,15 @@ it("Fail to complete client's chain.", async () => { try { await connect({ client: { - key: client.key, - cert: client.single, - ca: server.ca, + key: clientTls.key, + cert: clientTls.single, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, requestCert: true, }, }); @@ -257,8 +258,8 @@ it("Fail to find CA for server.", async () => { checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, + key: serverTls.key, + cert: serverTls.cert, }, }); expect.unreachable(); @@ -274,8 +275,8 @@ it("Server sent their CA, but CA cannot be trusted if it is not locally known.", checkServerIdentity, }, server: { - key: server.key, - cert: server.cert + "\n" + server.ca, + key: serverTls.key, + cert: serverTls.cert + "\n" + serverTls.ca, }, }); expect.unreachable(); @@ -288,11 +289,11 @@ it("Server sent their CA, wrongly, but its OK since we know the CA locally.", as await connect({ client: { checkServerIdentity, - ca: server.ca, + ca: serverTls.ca, }, server: { - key: server.key, - cert: server.cert + "\n" + server.ca, + key: serverTls.key, + cert: serverTls.cert + "\n" + serverTls.ca, }, }); }); @@ -300,15 +301,15 @@ it("Server sent their CA, wrongly, but its OK since we know the CA locally.", as it.todo('Confirm client support for "BEGIN TRUSTED CERTIFICATE".', async () => { await connect({ client: { - key: client.key, - cert: client.cert, - ca: server.ca.replace(/CERTIFICATE/g, "TRUSTED CERTIFICATE"), + key: clientTls.key, + cert: clientTls.cert, + ca: serverTls.ca.replace(/CERTIFICATE/g, "TRUSTED CERTIFICATE"), checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, requestCert: true, }, }); @@ -317,15 +318,15 @@ it.todo('Confirm client support for "BEGIN TRUSTED CERTIFICATE".', async () => { it.todo('Confirm server support for "BEGIN TRUSTED CERTIFICATE".', async () => { await connect({ client: { - key: client.key, - cert: client.cert, - ca: server.ca, + key: clientTls.key, + cert: clientTls.cert, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca.replace(/CERTIFICATE/g, "TRUSTED CERTIFICATE"), + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca.replace(/CERTIFICATE/g, "TRUSTED CERTIFICATE"), requestCert: true, }, }); @@ -334,15 +335,15 @@ it.todo('Confirm server support for "BEGIN TRUSTED CERTIFICATE".', async () => { it('Confirm client support for "BEGIN X509 CERTIFICATE".', async () => { await connect({ client: { - key: client.key, - cert: client.cert, - ca: server.ca.replace(/CERTIFICATE/g, "X509 CERTIFICATE"), + key: clientTls.key, + cert: clientTls.cert, + ca: serverTls.ca.replace(/CERTIFICATE/g, "X509 CERTIFICATE"), checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca, + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca, requestCert: true, }, }); @@ -351,15 +352,15 @@ it('Confirm client support for "BEGIN X509 CERTIFICATE".', async () => { it('Confirm server support for "BEGIN X509 CERTIFICATE".', async () => { await connect({ client: { - key: client.key, - cert: client.cert, - ca: server.ca, + key: clientTls.key, + cert: clientTls.cert, + ca: serverTls.ca, checkServerIdentity, }, server: { - key: server.key, - cert: server.cert, - ca: client.ca.replace(/CERTIFICATE/g, "X509 CERTIFICATE"), + key: serverTls.key, + cert: serverTls.cert, + ca: clientTls.ca.replace(/CERTIFICATE/g, "X509 CERTIFICATE"), requestCert: true, }, }); @@ -439,3 +440,131 @@ it("should not accept untrusted certificates", async () => { server?.close(); } }); + +async function createTLSServer(options: tls.TlsOptions) { + const server = await new Promise((resolve, reject) => { + const server = tls + .createServer(options) + .on("error", reject) + .listen(0, () => resolve(server)); + }); + + const address = server.address() as AddressInfo; + + return { + server, + address, + [Symbol.dispose]() { + server.close(); + }, + }; +} + +it("should load extra CA from NODE_EXTRA_CA_CERTS", async () => { + const caPath = join(tmpdirSync(), "ca.pem"); + await Bun.write(caPath, serverTls.ca); + + await using server = await createTLSServer({ + key: serverTls.key, + cert: serverTls.cert, + passphrase: "123123123", + }); + + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: caPath, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + + expect(await proc.exited).toBe(0); +}); + +it("should use NODE_EXTRA_CA_CERTS even if the used CA is not first in bundle", async () => { + const bundlePath = join(tmpdirSync(), "bundle.pem"); + const bundleContent = `${clientTls.cert}\n${serverTls.ca}`; + await Bun.write(bundlePath, bundleContent); + + await using server = await createTLSServer({ + key: serverTls.key, + cert: serverTls.cert, + passphrase: "123123123", + }); + + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: bundlePath, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + + expect(await proc.exited).toBe(0); +}); + +it("should ignore invalid NODE_EXTRA_CA_CERTS", async () => { + await using server = await createTLSServer({ + key: serverTls.key, + cert: serverTls.cert, + passphrase: "123123123", + }); + + for (const invalid of ["not-exist.pem", "", " "]) { + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: invalid, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + + expect(await proc.exited).toBe(1); + const stderr = await Bun.readableStreamToText(proc.stderr); + expect(stderr).toContain("UNABLE_TO_GET_ISSUER_CERT_LOCALLY"); + } +}); + +it("should ignore NODE_EXTRA_CA_CERTS if it contains invalid cert", async () => { + const mixedValidAndInvalidCertsBundlePath = join(tmpdirSync(), "mixed-valid-and-invalid-certs-bundle.pem"); + await Bun.write(mixedValidAndInvalidCertsBundlePath, `${invalidTls.cert}\n${serverTls.ca}`); + + const mixedInvalidAndValidCertsBundlePath = join(tmpdirSync(), "mixed-invalid-and-valid-certs-bundle.pem"); + await Bun.write(mixedInvalidAndValidCertsBundlePath, `${serverTls.ca}\n${invalidTls.cert}`); + + await using server = await createTLSServer({ + key: serverTls.key, + cert: serverTls.cert, + passphrase: "123123123", + }); + + for (const invalid of [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath]) { + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: invalid, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + + expect(await proc.exited).toBe(1); + const stderr = await Bun.readableStreamToText(proc.stderr); + expect(stderr).toContain("ignoring extra certs"); + } +}); diff --git a/test/js/web/fetch/fetch.tls.test.ts b/test/js/web/fetch/fetch.tls.test.ts index 2151c6f6992fbd..e7ec2c690804fe 100644 --- a/test/js/web/fetch/fetch.tls.test.ts +++ b/test/js/web/fetch/fetch.tls.test.ts @@ -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. @@ -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, @@ -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) { @@ -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: { @@ -315,15 +315,44 @@ it("fetch should use NODE_EXTRA_CA_CERTS", async () => { expect(await proc.exited).toBe(0); }); +it("fetch should use NODE_EXTRA_CA_CERTS even if the used CA is not 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, @@ -340,3 +369,37 @@ it("fetch should ignore invalid NODE_EXTRA_CA_CERTS", async () => { 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, + 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); + const stderr = await Bun.readableStreamToText(proc.stderr); + expect(stderr).toContain("ignoring extra certs"); + } +});