From d8021f20e1223278452bb510446bcfc92efca9bf Mon Sep 17 00:00:00 2001 From: fffzlfk Date: Thu, 8 Oct 2026 11:19:57 +0800 Subject: [PATCH] feat(ext/node): implement X509Certificate methods for Apple verifier Expose DER and PEM encodings, authority information access, and certificate signature verification through the Node crypto polyfill. Co-Authored-By: Codex --- ext/node/lib.rs | 4 + .../ops/crypto/testdata/x509-test-cert.pem | 20 +++ ext/node/ops/crypto/x509.rs | 144 ++++++++++++++++++ ext/node/polyfills/internal/crypto/x509.ts | 26 ++-- 4 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 ext/node/ops/crypto/testdata/x509-test-cert.pem diff --git a/ext/node/lib.rs b/ext/node/lib.rs index bf593ad43..32b652f2e 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -297,12 +297,16 @@ deno_core::extension!(deno_node, ops::crypto::x509::op_node_x509_fingerprint256, ops::crypto::x509::op_node_x509_fingerprint512, ops::crypto::x509::op_node_x509_get_issuer, + ops::crypto::x509::op_node_x509_info_access, ops::crypto::x509::op_node_x509_get_subject, ops::crypto::x509::op_node_x509_get_valid_from, ops::crypto::x509::op_node_x509_get_valid_to, ops::crypto::x509::op_node_x509_get_serial_number, ops::crypto::x509::op_node_x509_key_usage, ops::crypto::x509::op_node_x509_public_key, + ops::crypto::x509::op_node_x509_raw, + ops::crypto::x509::op_node_x509_to_string, + ops::crypto::x509::op_node_x509_verify, ops::fs::op_node_fs_exists_sync

, ops::fs::op_node_fs_exists

, ops::fs::op_node_cp_sync

, diff --git a/ext/node/ops/crypto/testdata/x509-test-cert.pem b/ext/node/ops/crypto/testdata/x509-test-cert.pem new file mode 100644 index 000000000..9113d866b --- /dev/null +++ b/ext/node/ops/crypto/testdata/x509-test-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgIUCNhWDISbT5DJKeTWc5nlP5O37s8wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJeDUwOS10ZXN0MB4XDTI2MTAwODAyNDcxM1oXDTM2MTAw +NTAyNDcxM1owFDESMBAGA1UEAwwJeDUwOS10ZXN0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAjeKeEYUUEPxpkHW2Y+dioOCosSwdYkxbnC+mMSFVFuSy +IpsnBY4oLpm63Xa9BG1hAYz8M5quozUatSBzWW298i3+a8O6CXMli2G9UgMOr0jr +eb2mYvR2MQs4FSuaLPt9A3QG9+mbxCWXUxdQ14XqPz/kqaoUiCQgD0zklxsZOT74 +Mdbvsx46cqdU7II6yOd2dmhIXK+BR6o3ntNIYaH4QEr2b7/bhAudozRo9fCygeFK +T3m2L9D5fmE8vEV2pda5Whnx2VcGfx5k3O2BBmWX6YMicN8YvhqnSX4MWEfAQN/w +a7vBSXnbV5wSvt9gLcO/3WMWCPX7oJCmqyUi+GlFhQIDAQABo4GKMIGHMB0GA1Ud +DgQWBBSJssJQcXpDcfr3s6m9QeKccMtmtjAfBgNVHSMEGDAWgBSJssJQcXpDcfr3 +s6m9QeKccMtmtjAPBgNVHRMBAf8EBTADAQH/MDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuZXhhbXBsZS50ZXN0MA0GCSqGSIb3DQEBCwUA +A4IBAQBXm1Mny5ceQdKfScLNfikw5wIfnerqGsAWOks4mo5Kpdb+AKNDo+piwo4s +h9HQKTzssWH6PGWkcz7gGU0x6g67NZcKNTBytLX9K2kDNTUsyhbrHcQB4tFv7OUC +uMYI31+A0TRBngveCEUtfNCi76B7P0JL7+Cdqsqrsx/wb9NVBCt/L8kHGcvnq+yI +syjsdzOEOBs7fp4+IS/1kDPa4zX+E67e2lwOed1nKLC67K8x5pKJVBsDpBJlHASt +dlgnNxbw7RPpzbt+9xFndj4Maco/imc0DbNL633eCPnoiVTwIcet/C2iP6Zg6kmo +QEoIm21twcBieiwKlWj9d4WNu/Aa +-----END CERTIFICATE----- diff --git a/ext/node/ops/crypto/x509.rs b/ext/node/ops/crypto/x509.rs index ab8e52f70..c91c276da 100644 --- a/ext/node/ops/crypto/x509.rs +++ b/ext/node/ops/crypto/x509.rs @@ -10,6 +10,7 @@ use x509_parser::extensions; use x509_parser::pem; use x509_parser::prelude::*; +use super::sign::KeyObjectHandlePrehashedSignAndVerifyError; use super::KeyObjectHandle; use std::ops::Deref; @@ -35,6 +36,13 @@ pub(crate) struct Certificate { impl deno_core::GarbageCollected for Certificate {} impl Certificate { + fn der_bytes(&self) -> &[u8] { + match self.inner.backing_cart().as_ref() { + CertificateSources::Der(der) => der, + CertificateSources::Pem(pem) => &pem.contents, + } + } + fn fingerprint(&self) -> Option { if let CertificateSources::Pem(pem) = self.inner.backing_cart().as_ref() { let mut hasher = D::new(); @@ -66,6 +74,10 @@ impl<'a> Deref for CertificateView<'a> { pub fn op_node_x509_parse( #[buffer] buf: &[u8], ) -> Result { + parse_certificate(buf) +} + +fn parse_certificate(buf: &[u8]) -> Result { let source = match pem::parse_x509_pem(buf) { Ok((_, pem)) => CertificateSources::Pem(pem), Err(_) => CertificateSources::Der(buf.to_vec().into_boxed_slice()), @@ -181,6 +193,107 @@ pub fn op_node_x509_public_key( KeyObjectHandle::new_x509_public_key(public_key) } +#[op2] +#[buffer] +pub fn op_node_x509_raw(#[cppgc] cert: &Certificate) -> Box<[u8]> { + cert.der_bytes().to_vec().into_boxed_slice() +} + +#[op2] +#[string] +pub fn op_node_x509_to_string(#[cppgc] cert: &Certificate) -> String { + certificate_to_pem(cert) +} + +fn certificate_to_pem(cert: &Certificate) -> String { + let encoded = data_encoding::BASE64.encode(cert.der_bytes()); + let mut pem = String::from("-----BEGIN CERTIFICATE-----\n"); + for line in encoded.as_bytes().chunks(64) { + pem.push_str(std::str::from_utf8(line).expect("base64 is ASCII")); + pem.push('\n'); + } + pem.push_str("-----END CERTIFICATE-----\n"); + pem +} + +#[op2] +#[string] +pub fn op_node_x509_info_access(#[cppgc] cert: &Certificate) -> Option { + certificate_info_access(cert) +} + +fn certificate_info_access(cert: &Certificate) -> Option { + let cert = cert.inner.get().deref(); + let aia = cert.extensions().iter().find_map(|ext| { + if let extensions::ParsedExtension::AuthorityInfoAccess(aia) = + ext.parsed_extension() + { + Some(aia) + } else { + None + } + })?; + + let descriptions: Vec = aia + .iter() + .map(|entry| { + let method = match entry.access_method.to_id_string().as_str() { + "1.3.6.1.5.5.7.48.1" => "OCSP".to_string(), + "1.3.6.1.5.5.7.48.2" => "CA Issuers".to_string(), + _ => entry.access_method.to_id_string(), + }; + match &entry.access_location { + extensions::GeneralName::URI(uri) => format!("{method} - URI:{uri}"), + location => format!("{method} - {location}"), + } + }) + .collect(); + Some(descriptions.join("\n")) +} + +#[derive(Debug, thiserror::Error)] +pub enum X509VerifyError { + #[error("unsupported X.509 signature algorithm: {0}")] + UnsupportedSignatureAlgorithm(String), + #[error(transparent)] + Key(#[from] KeyObjectHandlePrehashedSignAndVerifyError), +} + +#[op2(fast)] +pub fn op_node_x509_verify( + #[cppgc] cert: &Certificate, + #[cppgc] public_key: &KeyObjectHandle, +) -> Result { + verify_certificate(cert, public_key) +} + +fn verify_certificate( + cert: &Certificate, + public_key: &KeyObjectHandle, +) -> Result { + let cert = cert.inner.get().deref(); + if cert.signature_algorithm != cert.tbs_certificate.signature { + return Ok(false); + } + let tbs = cert.tbs_certificate.as_ref(); + let signature = cert.signature_value.data.as_ref(); + let oid = cert.signature_algorithm.algorithm.to_id_string(); + let (digest_name, digest): (&str, Vec) = match oid.as_str() { + "1.2.840.113549.1.1.5" => ("sha1", sha1::Sha1::digest(tbs).to_vec()), + "1.2.840.113549.1.1.11" | "1.2.840.10045.4.3.2" => { + ("sha256", sha2::Sha256::digest(tbs).to_vec()) + } + "1.2.840.113549.1.1.12" | "1.2.840.10045.4.3.3" => { + ("sha384", sha2::Sha384::digest(tbs).to_vec()) + } + "1.2.840.113549.1.1.13" | "1.2.840.10045.4.3.4" => { + ("sha512", sha2::Sha512::digest(tbs).to_vec()) + } + _ => return Err(X509VerifyError::UnsupportedSignatureAlgorithm(oid)), + }; + Ok(public_key.verify_prehashed(digest_name, &digest, signature, None, 0)?) +} + // Attempt to convert attribute to string. If type is not a string, return value is the hex // encoding of the attribute value fn attribute_value_to_string( @@ -278,3 +391,34 @@ pub fn op_node_x509_key_usage(#[cppgc] cert: &Certificate) -> u16 { key_usage.map(|k| k.flags).unwrap_or(0) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn x509_certificate_encoding_access_and_signature() { + let pem = include_bytes!("testdata/x509-test-cert.pem"); + let cert = parse_certificate(pem).unwrap(); + let der = cert.der_bytes(); + + assert_eq!(certificate_to_pem(&cert).as_bytes(), pem); + assert_eq!( + certificate_info_access(&cert).as_deref(), + Some("OCSP - URI:http://ocsp.example.test") + ); + assert_eq!(parse_certificate(der).unwrap().der_bytes(), der); + + let public_key = KeyObjectHandle::new_x509_public_key( + &cert.inner.get().deref().tbs_certificate.subject_pki, + ) + .unwrap(); + assert!(verify_certificate(&cert, &public_key).unwrap()); + + let mut corrupted = der.to_vec(); + let last = corrupted.last_mut().unwrap(); + *last ^= 1; + let tampered = parse_certificate(&corrupted).unwrap(); + assert!(!verify_certificate(&tampered, &public_key).unwrap()); + } +} diff --git a/ext/node/polyfills/internal/crypto/x509.ts b/ext/node/polyfills/internal/crypto/x509.ts index 699e45a51..623cde84f 100644 --- a/ext/node/polyfills/internal/crypto/x509.ts +++ b/ext/node/polyfills/internal/crypto/x509.ts @@ -15,12 +15,18 @@ import { op_node_x509_get_subject, op_node_x509_get_valid_from, op_node_x509_get_valid_to, + op_node_x509_info_access, op_node_x509_key_usage, op_node_x509_parse, op_node_x509_public_key, + op_node_x509_raw, + op_node_x509_to_string, + op_node_x509_verify, } from "ext:core/ops"; import { + getKeyObjectHandle, + kConsumePublic, KeyObject, PublicKeyObject, } from "ext:deno_node/internal/crypto/keys.ts"; @@ -119,9 +125,7 @@ export class X509Certificate { } get infoAccess(): string | undefined { - notImplemented("crypto.X509Certificate.prototype.infoAccess"); - - return ""; + return op_node_x509_info_access(this.#handle); } get issuer(): string { @@ -154,9 +158,7 @@ export class X509Certificate { } get raw(): Buffer { - notImplemented("crypto.X509Certificate.prototype.raw"); - - return {} as Buffer; + return Buffer.from(op_node_x509_raw(this.#handle)); } get serialNumber(): string { @@ -180,7 +182,7 @@ export class X509Certificate { } toString(): string { - notImplemented("crypto.X509Certificate.prototype.toString"); + return op_node_x509_to_string(this.#handle); } get validFrom(): string { @@ -191,8 +193,14 @@ export class X509Certificate { return op_node_x509_get_valid_to(this.#handle); } - verify(_publicKey: KeyObject): boolean { - notImplemented("crypto.X509Certificate.prototype.verify"); + verify(publicKey: KeyObject): boolean { + if (!(publicKey instanceof KeyObject)) { + throw new ERR_INVALID_ARG_TYPE("publicKey", "KeyObject", publicKey); + } + return op_node_x509_verify( + this.#handle, + getKeyObjectHandle(publicKey, kConsumePublic), + ); } }