Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion ext/node/polyfills/internal/crypto/x509.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
};
2 changes: 2 additions & 0 deletions ext/node_crypto/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,],
Expand Down
165 changes: 153 additions & 12 deletions ext/node_crypto/x509.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,6 +42,8 @@ struct SubjectOrIssuer {
ou: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cn: Option<String>,
#[serde(rename = "emailAddress", skip_serializing_if = "Option::is_none")]
email_address: Option<String>,
}

#[derive(serde::Serialize)]
Expand All @@ -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<HashMap<String, Vec<String>>>,
// RSA key fields
#[serde(skip_serializing_if = "Option::is_none")]
bits: Option<u32>,
Expand Down Expand Up @@ -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();
Expand All @@ -154,6 +167,7 @@ impl Certificate {
let fingerprint512 = self.fingerprint::<sha2::Sha512>().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());
Expand All @@ -179,6 +193,7 @@ impl Certificate {
fingerprint256,
fingerprint512,
subjectaltname,
info_access,
bits,
exponent,
modulus,
Expand Down Expand Up @@ -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);
}
_ => {}
}
}
Expand Down Expand Up @@ -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() {
Expand All @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -643,6 +691,61 @@ fn get_subject_alt_name(cert: &X509Certificate) -> Option<String> {
}
}

fn get_info_access_object(
cert: &X509Certificate,
) -> Option<HashMap<String, Vec<String>>> {
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<String, Vec<String>> = 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 {
Expand Down Expand Up @@ -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<String> {
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(
Expand Down Expand Up @@ -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));
Expand Down
14 changes: 10 additions & 4 deletions tests/unit_node/crypto/crypto_key_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading