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..6a426e806cab87 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,61 @@ 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.class() == x509_parser::der_parser::asn1_rs::Class::ContextSpecific + && 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 +1190,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( @@ -1132,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)); 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',