Skip to content

Commit 3cbaef3

Browse files
bartlomiejuclaude
andcommitted
fix(ext/node): improve X509Certificate Node.js compatibility
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 <noreply@anthropic.com>
1 parent b3c6f5e commit 3cbaef3

File tree

3 files changed

+194
-11
lines changed

3 files changed

+194
-11
lines changed

ext/node/polyfills/internal/crypto/x509.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
op_node_x509_get_issuer,
1919
op_node_x509_get_raw,
2020
op_node_x509_get_serial_number,
21+
op_node_x509_get_signature_algorithm_name,
22+
op_node_x509_get_signature_algorithm_oid,
2123
op_node_x509_get_subject,
2224
op_node_x509_get_subject_alt_name,
2325
op_node_x509_get_valid_from,
@@ -51,6 +53,8 @@ import { inspect } from "node:util";
5153
import { customInspectSymbol as kInspect } from "ext:deno_node/internal/util.mjs";
5254
import type { InspectOptions } from "node:util";
5355

56+
const core = globalThis.Deno.core;
57+
5458
// deno-lint-ignore no-explicit-any
5559
export type PeerCertificate = any;
5660

@@ -134,6 +138,12 @@ export class X509Certificate {
134138
}
135139

136140
this.#handle = op_node_x509_parse(buffer);
141+
// deno-lint-ignore no-this-alias
142+
const self = this;
143+
this[core.hostObjectBrand] = () => ({
144+
type: "X509Certificate",
145+
data: op_node_x509_get_raw(self.#handle),
146+
});
137147
}
138148

139149
[kInspect](depth: number, options: InspectOptions) {
@@ -282,6 +292,14 @@ export class X509Certificate {
282292
return op_node_x509_get_serial_number(this.#handle);
283293
}
284294

295+
get signatureAlgorithm(): string | undefined {
296+
return op_node_x509_get_signature_algorithm_name(this.#handle) ?? undefined;
297+
}
298+
299+
get signatureAlgorithmOid(): string {
300+
return op_node_x509_get_signature_algorithm_oid(this.#handle);
301+
}
302+
285303
get subject(): string {
286304
return op_node_x509_get_subject(this.#handle) || undefined;
287305
}
@@ -295,7 +313,20 @@ export class X509Certificate {
295313
}
296314

297315
toLegacyObject(): PeerCertificate {
298-
return op_node_x509_to_legacy_object(this.#handle);
316+
const obj = op_node_x509_to_legacy_object(this.#handle);
317+
if (obj.raw) {
318+
obj.raw = Buffer.from(obj.raw);
319+
}
320+
if (obj.subject) {
321+
obj.subject = Object.assign({ __proto__: null }, obj.subject);
322+
}
323+
if (obj.issuer) {
324+
obj.issuer = Object.assign({ __proto__: null }, obj.issuer);
325+
}
326+
if (obj.infoAccess) {
327+
obj.infoAccess = Object.assign({ __proto__: null }, obj.infoAccess);
328+
}
329+
return obj;
299330
}
300331

301332
toString(): string {
@@ -337,6 +368,16 @@ export class X509Certificate {
337368
}
338369
}
339370

371+
function isX509Certificate(value: unknown): value is X509Certificate {
372+
return value instanceof X509Certificate;
373+
}
374+
375+
core.registerCloneableResource(
376+
"X509Certificate",
377+
(data: { data: ArrayBuffer }) => new X509Certificate(Buffer.from(data.data)),
378+
);
379+
340380
export default {
341381
X509Certificate,
382+
isX509Certificate,
342383
};

ext/node_crypto/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ deno_core::extension!(
173173
x509::op_node_x509_check_private_key,
174174
x509::op_node_x509_verify,
175175
x509::op_node_x509_get_info_access,
176+
x509::op_node_x509_get_signature_algorithm_name,
177+
x509::op_node_x509_get_signature_algorithm_oid,
176178
x509::op_node_x509_to_legacy_object,
177179
],
178180
objects = [digest::Hasher,],

ext/node_crypto/x509.rs

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright 2018-2026 the Deno authors. MIT license.
22

3+
use std::collections::HashMap;
34
use std::net::IpAddr;
45
use std::net::Ipv4Addr;
56
use std::net::Ipv6Addr;
@@ -41,6 +42,8 @@ struct SubjectOrIssuer {
4142
ou: Option<String>,
4243
#[serde(skip_serializing_if = "Option::is_none")]
4344
cn: Option<String>,
45+
#[serde(rename = "emailAddress", skip_serializing_if = "Option::is_none")]
46+
email_address: Option<String>,
4447
}
4548

4649
#[derive(serde::Serialize)]
@@ -57,6 +60,8 @@ pub struct CertificateObject {
5760
fingerprint256: String,
5861
fingerprint512: String,
5962
subjectaltname: String,
63+
#[serde(rename = "infoAccess", skip_serializing_if = "Option::is_none")]
64+
info_access: Option<HashMap<String, Vec<String>>>,
6065
// RSA key fields
6166
#[serde(skip_serializing_if = "Option::is_none")]
6267
bits: Option<u32>,
@@ -143,8 +148,16 @@ impl Certificate {
143148
CertificateSources::Der(der) => der.to_vec(),
144149
};
145150

146-
let valid_from = cert.validity().not_before.to_string();
147-
let valid_to = cert.validity().not_after.to_string();
151+
let valid_from = cert
152+
.validity()
153+
.not_before
154+
.to_string()
155+
.replace("+00:00", "GMT");
156+
let valid_to = cert
157+
.validity()
158+
.not_after
159+
.to_string()
160+
.replace("+00:00", "GMT");
148161

149162
let mut serial_number = cert.serial.to_str_radix(16);
150163
serial_number.make_ascii_uppercase();
@@ -154,6 +167,7 @@ impl Certificate {
154167
let fingerprint512 = self.fingerprint::<sha2::Sha512>().unwrap_or_default();
155168

156169
let subjectaltname = get_subject_alt_name(cert).unwrap_or_default();
170+
let info_access = get_info_access_object(cert);
157171

158172
let subject = extract_subject_or_issuer(cert.subject());
159173
let issuer = extract_subject_or_issuer(cert.issuer());
@@ -179,6 +193,7 @@ impl Certificate {
179193
fingerprint256,
180194
fingerprint512,
181195
subjectaltname,
196+
info_access,
182197
bits,
183198
exponent,
184199
modulus,
@@ -383,6 +398,9 @@ fn extract_subject_or_issuer(name: &X509Name) -> SubjectOrIssuer {
383398
oid if oid == &x509_parser::oid_registry::OID_X509_COMMON_NAME => {
384399
result.cn = Some(value_str);
385400
}
401+
oid if oid == &x509_parser::oid_registry::OID_PKCS9_EMAIL_ADDRESS => {
402+
result.email_address = Some(value_str);
403+
}
386404
_ => {}
387405
}
388406
}
@@ -435,9 +453,15 @@ fn x509name_to_string(
435453
let val_str =
436454
attribute_value_to_string(attr.attr_value(), attr.attr_type())?;
437455
// look ABBREV, and if not found, use shortname
438-
let abbrev = match oid2abbrev(attr.attr_type(), oid_registry) {
439-
Ok(s) => String::from(s),
440-
_ => format!("{:?}", attr.attr_type()),
456+
let abbrev = if *attr.attr_type()
457+
== x509_parser::oid_registry::OID_PKCS9_EMAIL_ADDRESS
458+
{
459+
String::from("emailAddress")
460+
} else {
461+
match oid2abbrev(attr.attr_type(), oid_registry) {
462+
Ok(s) => String::from(s),
463+
_ => format!("{:?}", attr.attr_type()),
464+
}
441465
};
442466
let rdn = format!("{}={}", abbrev, val_str);
443467
match acc2.len() {
@@ -456,14 +480,22 @@ fn x509name_to_string(
456480
#[string]
457481
pub fn op_node_x509_get_valid_from(#[cppgc] cert: &Certificate) -> String {
458482
let cert = cert.inner.get().deref();
459-
cert.validity().not_before.to_string()
483+
cert
484+
.validity()
485+
.not_before
486+
.to_string()
487+
.replace("+00:00", "GMT")
460488
}
461489

462490
#[op2]
463491
#[string]
464492
pub fn op_node_x509_get_valid_to(#[cppgc] cert: &Certificate) -> String {
465493
let cert = cert.inner.get().deref();
466-
cert.validity().not_after.to_string()
494+
cert
495+
.validity()
496+
.not_after
497+
.to_string()
498+
.replace("+00:00", "GMT")
467499
}
468500

469501
#[op2]
@@ -509,9 +541,25 @@ fn extract_key_info(spki: &x509_parser::x509::SubjectPublicKeyInfo) -> KeyInfo {
509541
let modulus_bytes = key.modulus;
510542
let exponent_bytes = key.exponent;
511543

512-
let bits = Some((modulus_bytes.len() * 8) as u32);
513-
let modulus = Some(data_encoding::HEXUPPER.encode(modulus_bytes));
514-
let exponent = Some(data_encoding::HEXUPPER.encode(exponent_bytes));
544+
// Strip leading zero byte used for ASN.1 positive integer encoding
545+
let modulus_trimmed = if modulus_bytes.first() == Some(&0) {
546+
&modulus_bytes[1..]
547+
} else {
548+
modulus_bytes
549+
};
550+
let bits = Some((modulus_trimmed.len() * 8) as u32);
551+
let modulus = Some(data_encoding::HEXUPPER.encode(modulus_trimmed));
552+
// Format exponent as "0x" + hex without leading zeros (e.g., "0x10001")
553+
let exp_hex = data_encoding::HEXLOWER.encode(exponent_bytes);
554+
let exp_trimmed = exp_hex.trim_start_matches('0');
555+
let exponent = Some(format!(
556+
"0x{}",
557+
if exp_trimmed.is_empty() {
558+
"0"
559+
} else {
560+
exp_trimmed
561+
}
562+
));
515563
let pubkey = Some(spki.raw.to_vec());
516564

517565
KeyInfo {
@@ -643,6 +691,60 @@ fn get_subject_alt_name(cert: &X509Certificate) -> Option<String> {
643691
}
644692
}
645693

694+
fn get_info_access_object(
695+
cert: &X509Certificate,
696+
) -> Option<HashMap<String, Vec<String>>> {
697+
let oid_aia = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 1, 1]).ok()?;
698+
let oid_ocsp = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 48, 1]).ok()?;
699+
let oid_ca_issuers = Oid::from(&[1, 3, 6, 1, 5, 5, 7, 48, 2]).ok()?;
700+
701+
let ext = cert.extensions().iter().find(|e| e.oid == oid_aia)?;
702+
703+
let data = ext.value;
704+
let (_, seq) =
705+
x509_parser::der_parser::asn1_rs::Sequence::from_der(data).ok()?;
706+
707+
let mut result: HashMap<String, Vec<String>> = HashMap::new();
708+
let mut remaining = seq.content.as_ref();
709+
710+
while !remaining.is_empty() {
711+
let (rest, access_desc) =
712+
x509_parser::der_parser::asn1_rs::Sequence::from_der(remaining).ok()?;
713+
remaining = rest;
714+
715+
let (general_name_data, method_oid) =
716+
Oid::from_der(access_desc.content.as_ref()).ok()?;
717+
718+
let method_name = if method_oid == oid_ocsp {
719+
"OCSP - URI"
720+
} else if method_oid == oid_ca_issuers {
721+
"CA Issuers - URI"
722+
} else {
723+
continue;
724+
};
725+
726+
if !general_name_data.is_empty() {
727+
let (_, any) =
728+
x509_parser::der_parser::asn1_rs::Any::from_der(general_name_data)
729+
.ok()?;
730+
if any.tag().0 == 6
731+
&& let Ok(uri) = std::str::from_utf8(any.data)
732+
{
733+
result
734+
.entry(method_name.to_string())
735+
.or_default()
736+
.push(uri.to_string());
737+
}
738+
}
739+
}
740+
741+
if result.is_empty() {
742+
None
743+
} else {
744+
Some(result)
745+
}
746+
}
747+
646748
#[op2]
647749
#[string]
648750
pub fn op_node_x509_to_string(#[cppgc] cert: &Certificate) -> String {
@@ -1087,6 +1189,44 @@ pub fn op_node_x509_verify(
10871189
}
10881190
}
10891191

1192+
/// Map well-known signature algorithm OIDs to their OpenSSL names.
1193+
fn sig_alg_oid_to_name(oid: &str) -> Option<&'static str> {
1194+
match oid {
1195+
"1.2.840.113549.1.1.4" => Some("md5WithRSAEncryption"),
1196+
"1.2.840.113549.1.1.5" => Some("sha1WithRSAEncryption"),
1197+
"1.2.840.113549.1.1.11" => Some("sha256WithRSAEncryption"),
1198+
"1.2.840.113549.1.1.12" => Some("sha384WithRSAEncryption"),
1199+
"1.2.840.113549.1.1.13" => Some("sha512WithRSAEncryption"),
1200+
"1.2.840.113549.1.1.10" => Some("rsassaPss"),
1201+
"1.2.840.10045.4.1" => Some("ecdsa-with-SHA1"),
1202+
"1.2.840.10045.4.3.2" => Some("ecdsa-with-SHA256"),
1203+
"1.2.840.10045.4.3.3" => Some("ecdsa-with-SHA384"),
1204+
"1.2.840.10045.4.3.4" => Some("ecdsa-with-SHA512"),
1205+
"1.3.101.112" => Some("ED25519"),
1206+
"1.3.101.113" => Some("ED448"),
1207+
_ => None,
1208+
}
1209+
}
1210+
1211+
#[op2]
1212+
#[string]
1213+
pub fn op_node_x509_get_signature_algorithm_name(
1214+
#[cppgc] cert: &Certificate,
1215+
) -> Option<String> {
1216+
let cert = cert.inner.get().deref();
1217+
let oid = cert.signature_algorithm.algorithm.to_id_string();
1218+
sig_alg_oid_to_name(&oid).map(|s| s.to_string())
1219+
}
1220+
1221+
#[op2]
1222+
#[string]
1223+
pub fn op_node_x509_get_signature_algorithm_oid(
1224+
#[cppgc] cert: &Certificate,
1225+
) -> String {
1226+
let cert = cert.inner.get().deref();
1227+
cert.signature_algorithm.algorithm.to_id_string()
1228+
}
1229+
10901230
#[op2]
10911231
#[string]
10921232
pub fn op_node_x509_get_info_access(

0 commit comments

Comments
 (0)