diff --git a/Cargo.lock b/Cargo.lock index d548b2a583..8650129115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1859,6 +1859,7 @@ dependencies = [ "thiserror", "tokio", "url", + "webpki-root-certs", "winapi", "windows-sys 0.52.0", "x25519-dalek", @@ -5974,9 +5975,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-tokio-stream" @@ -8115,6 +8116,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d93b773107ba49bc84dd3b241e019c702d886fd5c457defe2ea8b1123a5dcd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.1" diff --git a/Cargo.toml b/Cargo.toml index 2794d0a785..2c90088b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,6 +191,7 @@ twox-hash = "=1.6.3" # Upgrading past 2.4.1 may cause WPT failures url = { version = "< 2.5.0", features = ["serde", "expose_internals"] } uuid = { version = "1.3.0", features = ["v4"] } +webpki-root-certs = "0.26.5" webpki-roots = "0.26" which = "4.2.5" yoke = { version = "0.7.4", features = ["derive"] } diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 452f7420b1..24e7ecf2ed 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -94,6 +94,7 @@ stable_deref_trait = "1.2.0" thiserror.workspace = true tokio.workspace = true url.workspace = true +webpki-root-certs.workspace = true winapi.workspace = true x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } x509-parser = "0.15.0" diff --git a/ext/node/lib.rs b/ext/node/lib.rs index dab9cc7bd1..af14e3e854 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -407,6 +407,7 @@ deno_core::extension!(deno_node, ops::ipc::op_node_ipc_unref, ops::process::op_node_process_kill, ops::process::op_process_abort, + ops::tls::op_get_root_certificates, ], esm_entry_point = "ext:deno_node/02_init.js", esm = [ diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index d11cc7461b..b562261f39 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -11,6 +11,7 @@ pub mod ipc; pub mod os; pub mod process; pub mod require; +pub mod tls; pub mod util; pub mod v8; pub mod vm; diff --git a/ext/node/ops/tls.rs b/ext/node/ops/tls.rs new file mode 100644 index 0000000000..86b1779601 --- /dev/null +++ b/ext/node/ops/tls.rs @@ -0,0 +1,29 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use base64::Engine; +use deno_core::op2; +use webpki_root_certs; + +#[op2] +#[serde] +pub fn op_get_root_certificates() -> Vec { + let certs = webpki_root_certs::TLS_SERVER_ROOT_CERTS + .iter() + .map(|cert| { + let b64 = base64::engine::general_purpose::STANDARD.encode(cert); + let pem_lines = b64 + .chars() + .collect::>() + // Node uses 72 characters per line, so we need to follow node even though + // it's not spec compliant https://datatracker.ietf.org/doc/html/rfc7468#section-2 + .chunks(72) + .map(|c| c.iter().collect::()) + .collect::>() + .join("\n"); + let pem = format!( + "-----BEGIN CERTIFICATE-----\n{pem_lines}\n-----END CERTIFICATE-----\n", + ); + pem + }) + .collect::>(); + certs +} diff --git a/ext/node/polyfills/tls.ts b/ext/node/polyfills/tls.ts index 7d00bc6e51..4cfe9ebd63 100644 --- a/ext/node/polyfills/tls.ts +++ b/ext/node/polyfills/tls.ts @@ -7,6 +7,10 @@ import { notImplemented } from "ext:deno_node/_utils.ts"; import tlsCommon from "node:_tls_common"; import tlsWrap from "node:_tls_wrap"; +import { op_get_root_certificates } from "ext:core/ops"; +import { primordials } from "ext:core/mod.js"; + +const { ObjectFreeze } = primordials; // openssl -> rustls const cipherMap = { @@ -30,7 +34,58 @@ export function getCiphers() { return Object.keys(cipherMap).map((name) => name.toLowerCase()); } -export const rootCertificates = undefined; +let lazyRootCertificates: string[] | null = null; +function ensureLazyRootCertificates(target: string[]) { + if (lazyRootCertificates === null) { + lazyRootCertificates = op_get_root_certificates() as string[]; + lazyRootCertificates.forEach((v) => target.push(v)); + ObjectFreeze(target); + } +} +export const rootCertificates = new Proxy([] as string[], { + // @ts-ignore __proto__ is not in the types + __proto__: null, + get(target, prop) { + ensureLazyRootCertificates(target); + return Reflect.get(target, prop); + }, + ownKeys(target) { + ensureLazyRootCertificates(target); + return Reflect.ownKeys(target); + }, + has(target, prop) { + ensureLazyRootCertificates(target); + return Reflect.has(target, prop); + }, + getOwnPropertyDescriptor(target, prop) { + ensureLazyRootCertificates(target); + return Reflect.getOwnPropertyDescriptor(target, prop); + }, + set(target, prop, value) { + ensureLazyRootCertificates(target); + return Reflect.set(target, prop, value); + }, + defineProperty(target, prop, descriptor) { + ensureLazyRootCertificates(target); + return Reflect.defineProperty(target, prop, descriptor); + }, + deleteProperty(target, prop) { + ensureLazyRootCertificates(target); + return Reflect.deleteProperty(target, prop); + }, + isExtensible(target) { + ensureLazyRootCertificates(target); + return Reflect.isExtensible(target); + }, + preventExtensions(target) { + ensureLazyRootCertificates(target); + return Reflect.preventExtensions(target); + }, + setPrototypeOf() { + return false; + }, +}); + export const DEFAULT_ECDH_CURVE = "auto"; export const DEFAULT_MAX_VERSION = "TLSv1.3"; export const DEFAULT_MIN_VERSION = "TLSv1.2"; diff --git a/tests/unit_node/tls_test.ts b/tests/unit_node/tls_test.ts index 6826ab84c4..7daa544c74 100644 --- a/tests/unit_node/tls_test.ts +++ b/tests/unit_node/tls_test.ts @@ -1,9 +1,11 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { + assert, assertEquals, assertInstanceOf, assertStringIncludes, + assertThrows, } from "@std/assert"; import { delay } from "@std/async/delay"; import { fromFileUrl, join } from "@std/path"; @@ -215,3 +217,13 @@ Deno.test("tls.connect() throws InvalidData when there's error in certificate", "InvalidData: invalid peer certificate: UnknownIssuer", ); }); + +Deno.test("tls.rootCertificates is not empty", () => { + assert(tls.rootCertificates.length > 0); + assert(Object.isFrozen(tls.rootCertificates)); + assert(tls.rootCertificates instanceof Array); + assert(tls.rootCertificates.every((cert) => typeof cert === "string")); + assertThrows(() => { + (tls.rootCertificates as string[]).push("new cert"); + }, TypeError); +});