From b3c6f5ee4accb30a842364deb9200c5882c4a9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 18:41:52 +0100 Subject: [PATCH 1/4] feat(core): add cloneable resource registry for structured clone support Adds infrastructure for custom JS objects to support structured cloning via `postMessage`/`MessageChannel`. This enables objects like `CryptoKey` and `X509Certificate` to be cloned across message ports. The mechanism works by: 1. Objects set `[core.hostObjectBrand]` to a serializer function that returns `{ type: "", ...data }` 2. Extensions register a deserializer via `core.registerCloneableResource(name, deserializerFn)` 3. During deserialization, the registry is consulted to reconstruct the original object Changes: - `libs/core/01_core.js`: Add `registerCloneableResource` / `getCloneableDeserializers` registry, auto-pass deserializers in `structuredClone` - `libs/core/ops_builtin_v8.rs`: Mark `op_deserialize` as reentrant so deserializer callbacks can invoke ops - `ext/web/13_message_port.js`: Pass cloneable deserializers during message deserialization Ref: https://github.com/denoland/deno/issues/12067 Ref: https://github.com/denoland/deno/issues/12734 Co-Authored-By: Claude Opus 4.6 --- ext/web/13_message_port.js | 6 ++++++ libs/core/01_core.js | 12 +++++++++++- libs/core/ops_builtin_v8.rs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ext/web/13_message_port.js b/ext/web/13_message_port.js index f6b3d74703aa3e..49c08e595828c4 100644 --- a/ext/web/13_message_port.js +++ b/ext/web/13_message_port.js @@ -409,6 +409,12 @@ function deserializeJsMessageData(messageData) { }; } + const deserializers = core.getCloneableDeserializers(); + if (!options) { + options = { deserializers }; + } else { + options.deserializers = deserializers; + } const data = core.deserialize(messageData.data, options); for (let i = 0; i < arrayBufferIdsInTransferables.length; ++i) { diff --git a/libs/core/01_core.js b/libs/core/01_core.js index 27f0f41ba4500d..45c4ccbcf40793 100755 --- a/libs/core/01_core.js +++ b/libs/core/01_core.js @@ -683,6 +683,14 @@ transferableResources[name] = { send, receive }; }; const getTransferableResource = (name) => transferableResources[name]; + const cloneableDeserializers = { __proto__: null }; + const registerCloneableResource = (name, deserialize) => { + if (cloneableDeserializers[name]) { + throw new Error(`${name} is already registered`); + } + cloneableDeserializers[name] = deserialize; + }; + const getCloneableDeserializers = () => cloneableDeserializers; // A helper function that will bind our own console implementation // with default implementation of Console from V8. This will cause @@ -1025,11 +1033,13 @@ hostObjectBrand, registerTransferableResource, getTransferableResource, + registerCloneableResource, + getCloneableDeserializers, encode: (text) => op_encode(text), encodeBinaryString: (buffer) => op_encode_binary_string(buffer), decode: (buffer) => op_decode(buffer), structuredClone: (value, deserializers) => - op_structured_clone(value, deserializers), + op_structured_clone(value, deserializers ?? cloneableDeserializers), serialize: ( value, options, diff --git a/libs/core/ops_builtin_v8.rs b/libs/core/ops_builtin_v8.rs index b0da9327213fe7..03496f7529063c 100644 --- a/libs/core/ops_builtin_v8.rs +++ b/libs/core/ops_builtin_v8.rs @@ -848,7 +848,7 @@ pub fn op_serialize<'s, 'i>( } } -#[op2] +#[op2(reentrant)] pub fn op_deserialize<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, #[buffer] zero_copy: JsBuffer, From 3cbaef31a968d540d43b7467787d1b7cc25b62de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 18:35:06 +0100 Subject: [PATCH 2/4] fix(ext/node): improve X509Certificate Node.js compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes multiple compatibility issues with `crypto.X509Certificate` to pass Node.js test-crypto-x509.js: - Add `isX509Certificate()` to `internal/crypto/x509` module - Fix `emailAddress` label in subject/issuer (was `Email`) - Fix `validFrom`/`validTo` timezone suffix (`+00:00` → `GMT`) - Add `signatureAlgorithm` and `signatureAlgorithmOid` getters - Fix `toLegacyObject()`: return Buffer for `raw`, null-prototype objects for `subject`/`issuer`/`infoAccess`, add `emailAddress` field and `infoAccess` as structured object - Fix RSA modulus to strip ASN.1 leading zero byte - Fix RSA exponent format to `0x`-prefixed hex - Add structured clone support for X509Certificate via MessagePort by introducing a cloneable resource registry in `Deno.core` and marking `op_deserialize` as reentrant Co-Authored-By: Claude Opus 4.6 --- ext/node/polyfills/internal/crypto/x509.ts | 43 +++++- ext/node_crypto/lib.rs | 2 + ext/node_crypto/x509.rs | 160 +++++++++++++++++++-- 3 files changed, 194 insertions(+), 11 deletions(-) diff --git a/ext/node/polyfills/internal/crypto/x509.ts b/ext/node/polyfills/internal/crypto/x509.ts index 6e2ffd09a92fbc..d232894887d9e4 100644 --- a/ext/node/polyfills/internal/crypto/x509.ts +++ b/ext/node/polyfills/internal/crypto/x509.ts @@ -18,6 +18,8 @@ import { op_node_x509_get_issuer, op_node_x509_get_raw, op_node_x509_get_serial_number, + op_node_x509_get_signature_algorithm_name, + op_node_x509_get_signature_algorithm_oid, op_node_x509_get_subject, op_node_x509_get_subject_alt_name, op_node_x509_get_valid_from, @@ -51,6 +53,8 @@ import { inspect } from "node:util"; import { customInspectSymbol as kInspect } from "ext:deno_node/internal/util.mjs"; import type { InspectOptions } from "node:util"; +const core = globalThis.Deno.core; + // deno-lint-ignore no-explicit-any export type PeerCertificate = any; @@ -134,6 +138,12 @@ export class X509Certificate { } this.#handle = op_node_x509_parse(buffer); + // deno-lint-ignore no-this-alias + const self = this; + this[core.hostObjectBrand] = () => ({ + type: "X509Certificate", + data: op_node_x509_get_raw(self.#handle), + }); } [kInspect](depth: number, options: InspectOptions) { @@ -282,6 +292,14 @@ export class X509Certificate { return op_node_x509_get_serial_number(this.#handle); } + get signatureAlgorithm(): string | undefined { + return op_node_x509_get_signature_algorithm_name(this.#handle) ?? undefined; + } + + get signatureAlgorithmOid(): string { + return op_node_x509_get_signature_algorithm_oid(this.#handle); + } + get subject(): string { return op_node_x509_get_subject(this.#handle) || undefined; } @@ -295,7 +313,20 @@ export class X509Certificate { } toLegacyObject(): PeerCertificate { - return op_node_x509_to_legacy_object(this.#handle); + const obj = op_node_x509_to_legacy_object(this.#handle); + if (obj.raw) { + obj.raw = Buffer.from(obj.raw); + } + if (obj.subject) { + obj.subject = Object.assign({ __proto__: null }, obj.subject); + } + if (obj.issuer) { + obj.issuer = Object.assign({ __proto__: null }, obj.issuer); + } + if (obj.infoAccess) { + obj.infoAccess = Object.assign({ __proto__: null }, obj.infoAccess); + } + return obj; } toString(): string { @@ -337,6 +368,16 @@ export class X509Certificate { } } +function isX509Certificate(value: unknown): value is X509Certificate { + return value instanceof X509Certificate; +} + +core.registerCloneableResource( + "X509Certificate", + (data: { data: ArrayBuffer }) => new X509Certificate(Buffer.from(data.data)), +); + export default { X509Certificate, + isX509Certificate, }; diff --git a/ext/node_crypto/lib.rs b/ext/node_crypto/lib.rs index 7eeee79ed0a423..ad393dc0ac5cee 100644 --- a/ext/node_crypto/lib.rs +++ b/ext/node_crypto/lib.rs @@ -173,6 +173,8 @@ deno_core::extension!( x509::op_node_x509_check_private_key, x509::op_node_x509_verify, x509::op_node_x509_get_info_access, + x509::op_node_x509_get_signature_algorithm_name, + x509::op_node_x509_get_signature_algorithm_oid, x509::op_node_x509_to_legacy_object, ], objects = [digest::Hasher,], diff --git a/ext/node_crypto/x509.rs b/ext/node_crypto/x509.rs index bfbf1051344daf..8d7911f49a5369 100644 --- a/ext/node_crypto/x509.rs +++ b/ext/node_crypto/x509.rs @@ -1,5 +1,6 @@ // Copyright 2018-2026 the Deno authors. MIT license. +use std::collections::HashMap; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; @@ -41,6 +42,8 @@ struct SubjectOrIssuer { ou: Option, #[serde(skip_serializing_if = "Option::is_none")] cn: Option, + #[serde(rename = "emailAddress", skip_serializing_if = "Option::is_none")] + email_address: Option, } #[derive(serde::Serialize)] @@ -57,6 +60,8 @@ pub struct CertificateObject { fingerprint256: String, fingerprint512: String, subjectaltname: String, + #[serde(rename = "infoAccess", skip_serializing_if = "Option::is_none")] + info_access: Option>>, // RSA key fields #[serde(skip_serializing_if = "Option::is_none")] bits: Option, @@ -143,8 +148,16 @@ impl Certificate { CertificateSources::Der(der) => der.to_vec(), }; - let valid_from = cert.validity().not_before.to_string(); - let valid_to = cert.validity().not_after.to_string(); + let valid_from = cert + .validity() + .not_before + .to_string() + .replace("+00:00", "GMT"); + let valid_to = cert + .validity() + .not_after + .to_string() + .replace("+00:00", "GMT"); let mut serial_number = cert.serial.to_str_radix(16); serial_number.make_ascii_uppercase(); @@ -154,6 +167,7 @@ impl Certificate { let fingerprint512 = self.fingerprint::().unwrap_or_default(); let subjectaltname = get_subject_alt_name(cert).unwrap_or_default(); + let info_access = get_info_access_object(cert); let subject = extract_subject_or_issuer(cert.subject()); let issuer = extract_subject_or_issuer(cert.issuer()); @@ -179,6 +193,7 @@ impl Certificate { fingerprint256, fingerprint512, subjectaltname, + info_access, bits, exponent, modulus, @@ -383,6 +398,9 @@ fn extract_subject_or_issuer(name: &X509Name) -> SubjectOrIssuer { oid if oid == &x509_parser::oid_registry::OID_X509_COMMON_NAME => { result.cn = Some(value_str); } + oid if oid == &x509_parser::oid_registry::OID_PKCS9_EMAIL_ADDRESS => { + result.email_address = Some(value_str); + } _ => {} } } @@ -435,9 +453,15 @@ fn x509name_to_string( let val_str = attribute_value_to_string(attr.attr_value(), attr.attr_type())?; // look ABBREV, and if not found, use shortname - let abbrev = match oid2abbrev(attr.attr_type(), oid_registry) { - Ok(s) => String::from(s), - _ => format!("{:?}", attr.attr_type()), + let abbrev = if *attr.attr_type() + == x509_parser::oid_registry::OID_PKCS9_EMAIL_ADDRESS + { + String::from("emailAddress") + } else { + match oid2abbrev(attr.attr_type(), oid_registry) { + Ok(s) => String::from(s), + _ => format!("{:?}", attr.attr_type()), + } }; let rdn = format!("{}={}", abbrev, val_str); match acc2.len() { @@ -456,14 +480,22 @@ fn x509name_to_string( #[string] pub fn op_node_x509_get_valid_from(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); - cert.validity().not_before.to_string() + cert + .validity() + .not_before + .to_string() + .replace("+00:00", "GMT") } #[op2] #[string] pub fn op_node_x509_get_valid_to(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); - cert.validity().not_after.to_string() + cert + .validity() + .not_after + .to_string() + .replace("+00:00", "GMT") } #[op2] @@ -509,9 +541,25 @@ fn extract_key_info(spki: &x509_parser::x509::SubjectPublicKeyInfo) -> KeyInfo { let modulus_bytes = key.modulus; let exponent_bytes = key.exponent; - let bits = Some((modulus_bytes.len() * 8) as u32); - let modulus = Some(data_encoding::HEXUPPER.encode(modulus_bytes)); - let exponent = Some(data_encoding::HEXUPPER.encode(exponent_bytes)); + // Strip leading zero byte used for ASN.1 positive integer encoding + let modulus_trimmed = if modulus_bytes.first() == Some(&0) { + &modulus_bytes[1..] + } else { + modulus_bytes + }; + let bits = Some((modulus_trimmed.len() * 8) as u32); + let modulus = Some(data_encoding::HEXUPPER.encode(modulus_trimmed)); + // Format exponent as "0x" + hex without leading zeros (e.g., "0x10001") + let exp_hex = data_encoding::HEXLOWER.encode(exponent_bytes); + let exp_trimmed = exp_hex.trim_start_matches('0'); + let exponent = Some(format!( + "0x{}", + if exp_trimmed.is_empty() { + "0" + } else { + exp_trimmed + } + )); let pubkey = Some(spki.raw.to_vec()); KeyInfo { @@ -643,6 +691,60 @@ fn get_subject_alt_name(cert: &X509Certificate) -> Option { } } +fn get_info_access_object( + cert: &X509Certificate, +) -> Option>> { + let oid_aia = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 1, 1]).ok()?; + let oid_ocsp = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 48, 1]).ok()?; + let oid_ca_issuers = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 48, 2]).ok()?; + + let ext = cert.extensions().iter().find(|e| e.oid == oid_aia)?; + + let data = ext.value; + let (_, seq) = + x509_parser::der_parser::asn1_rs::Sequence::from_der(data).ok()?; + + let mut result: HashMap> = HashMap::new(); + let mut remaining = seq.content.as_ref(); + + while !remaining.is_empty() { + let (rest, access_desc) = + x509_parser::der_parser::asn1_rs::Sequence::from_der(remaining).ok()?; + remaining = rest; + + let (general_name_data, method_oid) = + Oid::from_der(access_desc.content.as_ref()).ok()?; + + let method_name = if method_oid == oid_ocsp { + "OCSP - URI" + } else if method_oid == oid_ca_issuers { + "CA Issuers - URI" + } else { + continue; + }; + + if !general_name_data.is_empty() { + let (_, any) = + x509_parser::der_parser::asn1_rs::Any::from_der(general_name_data) + .ok()?; + if any.tag().0 == 6 + && let Ok(uri) = std::str::from_utf8(any.data) + { + result + .entry(method_name.to_string()) + .or_default() + .push(uri.to_string()); + } + } + } + + if result.is_empty() { + None + } else { + Some(result) + } +} + #[op2] #[string] pub fn op_node_x509_to_string(#[cppgc] cert: &Certificate) -> String { @@ -1087,6 +1189,44 @@ pub fn op_node_x509_verify( } } +/// Map well-known signature algorithm OIDs to their OpenSSL names. +fn sig_alg_oid_to_name(oid: &str) -> Option<&'static str> { + match oid { + "1.2.840.113549.1.1.4" => Some("md5WithRSAEncryption"), + "1.2.840.113549.1.1.5" => Some("sha1WithRSAEncryption"), + "1.2.840.113549.1.1.11" => Some("sha256WithRSAEncryption"), + "1.2.840.113549.1.1.12" => Some("sha384WithRSAEncryption"), + "1.2.840.113549.1.1.13" => Some("sha512WithRSAEncryption"), + "1.2.840.113549.1.1.10" => Some("rsassaPss"), + "1.2.840.10045.4.1" => Some("ecdsa-with-SHA1"), + "1.2.840.10045.4.3.2" => Some("ecdsa-with-SHA256"), + "1.2.840.10045.4.3.3" => Some("ecdsa-with-SHA384"), + "1.2.840.10045.4.3.4" => Some("ecdsa-with-SHA512"), + "1.3.101.112" => Some("ED25519"), + "1.3.101.113" => Some("ED448"), + _ => None, + } +} + +#[op2] +#[string] +pub fn op_node_x509_get_signature_algorithm_name( + #[cppgc] cert: &Certificate, +) -> Option { + let cert = cert.inner.get().deref(); + let oid = cert.signature_algorithm.algorithm.to_id_string(); + sig_alg_oid_to_name(&oid).map(|s| s.to_string()) +} + +#[op2] +#[string] +pub fn op_node_x509_get_signature_algorithm_oid( + #[cppgc] cert: &Certificate, +) -> String { + let cert = cert.inner.get().deref(); + cert.signature_algorithm.algorithm.to_id_string() +} + #[op2] #[string] pub fn op_node_x509_get_info_access( From 183b0e885ccd7edaba54e6e66c0b6a8118b0be3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:22:22 +0100 Subject: [PATCH 3/4] update test --- tests/unit_node/crypto/crypto_key_test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index 6d903abb2dbd86..16243f2e447b19 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -787,13 +787,19 @@ Deno.test("X509Certificate inspect", async function () { assertEquals( trimmedStdout, `X509Certificate { - subject: 'C=US\\nST=CA\\nL=SF\\nO=Joyent\\nOU=Node.js\\nCN=agent1\\nEmail=ry@tinyclouds.org', + subject: 'C=US\\n' + + 'ST=CA\\n' + + 'L=SF\\n' + + 'O=Joyent\\n' + + 'OU=Node.js\\n' + + 'CN=agent1\\n' + + 'emailAddress=ry@tinyclouds.org', subjectAltName: undefined, - issuer: 'C=US\\nST=CA\\nL=SF\\nO=Joyent\\nOU=Node.js\\nCN=ca1\\nEmail=ry@tinyclouds.org', + issuer: 'C=US\\nST=CA\\nL=SF\\nO=Joyent\\nOU=Node.js\\nCN=ca1\\nemailAddress=ry@tinyclouds.org', infoAccess: 'OCSP - URI:http://ocsp.nodejs.org/\\n' + 'CA Issuers - URI:http://ca.nodejs.org/ca.cert', - validFrom: 'Sep 3 21:40:37 2022 +00:00', - validTo: 'Jun 17 21:40:37 2296 +00:00', + validFrom: 'Sep 3 21:40:37 2022 GMT', + validTo: 'Jun 17 21:40:37 2296 GMT', validFromDate: 2022-09-03T21:40:37.000Z, validToDate: 2296-06-17T21:40:37.000Z, fingerprint: '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53', From 0dd6b1614629438bab823e038613bcbdd8d9d56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:29:11 +0100 Subject: [PATCH 4/4] fix: validate context-specific tag class in X509 GeneralName URI decoding Properly check that the ASN.1 tag class is ContextSpecific (not just tag number 6) when decoding uniformResourceIdentifier in AIA extensions. Co-Authored-By: Claude Opus 4.6 --- ext/node_crypto/x509.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ext/node_crypto/x509.rs b/ext/node_crypto/x509.rs index 8d7911f49a5369..6a426e806cab87 100644 --- a/ext/node_crypto/x509.rs +++ b/ext/node_crypto/x509.rs @@ -727,7 +727,8 @@ fn get_info_access_object( let (_, any) = x509_parser::der_parser::asn1_rs::Any::from_der(general_name_data) .ok()?; - if any.tag().0 == 6 + if any.class() == x509_parser::der_parser::asn1_rs::Class::ContextSpecific + && any.tag().0 == 6 && let Ok(uri) = std::str::from_utf8(any.data) { result @@ -1272,8 +1273,8 @@ pub fn op_node_x509_get_info_access( let (_, any) = x509_parser::der_parser::asn1_rs::Any::from_der(general_name_data) .ok()?; - // Tag 6 is context-specific for URI in GeneralName - if any.tag().0 == 6 + if any.class() == x509_parser::der_parser::asn1_rs::Class::ContextSpecific + && any.tag().0 == 6 && let Ok(uri) = std::str::from_utf8(any.data) { entries.push(format!("{}:{}", method_name, uri));