/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node_crypto/x509.rs
1 387 строк
40 KB
Bartek Iwańczuk
fix(ext/node): use signatureAlgorithm digest for X509 ECDSA verify (#36326)
27 июл 2026, 15:53
Не верифицирован
27 июл 2026, 15:53
aa0f88f
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::HashMap; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; use std::ops::Deref; use base64::Engine; use deno_core::ToJsBuffer; use deno_core::op2; use digest::Digest; use x509_parser::der_parser::asn1_rs::Any; use x509_parser::der_parser::asn1_rs::Tag; use x509_parser::der_parser::oid::Oid; pub use x509_parser::error::X509Error; use x509_parser::extensions; use x509_parser::pem; use x509_parser::prelude::*; use yoke::Yoke; use yoke::Yokeable; use crate::keys::KeyObjectHandle; enum CertificateSources { Der(Box<[u8]>), Pem(pem::Pem), } /// A certificate field value that may have one or multiple values. /// Serializes as a plain string when there is exactly one value, /// or as an array of strings when there are multiple (matching Node.js behavior). #[derive(Default, Debug, PartialEq, Eq)] struct StringOrArray(Vec<String>); impl StringOrArray { fn is_empty(&self) -> bool { self.0.is_empty() } fn push(&mut self, value: String) { self.0.push(value); } } impl serde::Serialize for StringOrArray { fn serialize<S: serde::Serializer>( &self, serializer: S, ) -> Result<S::Ok, S::Error> { if self.0.len() == 1 { serializer.serialize_str(&self.0[0]) } else { self.0.serialize(serializer) } } } #[derive(serde::Serialize, Default)] #[serde(rename_all = "UPPERCASE")] struct SubjectOrIssuer { #[serde(skip_serializing_if = "StringOrArray::is_empty")] c: StringOrArray, #[serde(skip_serializing_if = "StringOrArray::is_empty")] st: StringOrArray, #[serde(skip_serializing_if = "StringOrArray::is_empty")] l: StringOrArray, #[serde(skip_serializing_if = "StringOrArray::is_empty")] o: StringOrArray, #[serde(skip_serializing_if = "StringOrArray::is_empty")] ou: StringOrArray, #[serde(skip_serializing_if = "StringOrArray::is_empty")] cn: StringOrArray, #[serde( rename = "emailAddress", skip_serializing_if = "StringOrArray::is_empty" )] email_address: StringOrArray, } #[derive(serde::Serialize)] pub struct CertificateObject { ca: bool, raw: ToJsBuffer, subject: SubjectOrIssuer, issuer: SubjectOrIssuer, valid_from: String, valid_to: String, #[serde(rename = "serialNumber")] serial_number: String, fingerprint: String, 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>, #[serde(skip_serializing_if = "Option::is_none")] exponent: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] modulus: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pubkey: Option<ToJsBuffer>, // EC key fields #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "asn1Curve")] asn1_curve: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "nistCurve")] nist_curve: Option<String>, } #[derive(Yokeable)] struct CertificateView<'a> { cert: X509Certificate<'a>, } pub struct Certificate { inner: Yoke<CertificateView<'static>, Box<CertificateSources>>, } // SAFETY: we're sure this can be GCed unsafe impl deno_core::GarbageCollected for Certificate { fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {} fn get_name(&self) -> &'static std::ffi::CStr { c"Certificate" } } impl Certificate { pub fn from_der(der: &[u8]) -> Result<Certificate, X509Error> { let source = CertificateSources::Der(der.to_vec().into_boxed_slice()); let inner = Yoke::<CertificateView<'static>, Box<CertificateSources>>::try_attach_to_cart( Box::new(source), |source| { let cert = match source { CertificateSources::Der(buf) => { X509Certificate::from_der(buf).map(|(_, cert)| cert)? } _ => unreachable!(), }; Ok::<_, X509Error>(CertificateView { cert }) }, )?; Ok(Certificate { inner }) } fn fingerprint<D: Digest>(&self) -> Option<String> { let data = match self.inner.backing_cart().as_ref() { CertificateSources::Pem(pem) => &pem.contents, CertificateSources::Der(der) => der.as_ref(), }; let mut hasher = D::new(); hasher.update(data); let bytes = hasher.finalize(); // OpenSSL returns colon separated upper case hex values. let mut hex = String::with_capacity(bytes.len() * 2); for byte in bytes { hex.push_str(&format!("{:02X}:", byte)); } hex.pop(); Some(hex) } pub fn to_object( &self, _detailed: bool, ) -> Result<CertificateObject, X509Error> { let cert = self.inner.get().deref(); let raw = match self.inner.backing_cart().as_ref() { CertificateSources::Pem(pem) => pem.contents.clone(), CertificateSources::Der(der) => der.to_vec(), }; 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(); let fingerprint = self.fingerprint::<sha1::Sha1>().unwrap_or_default(); let fingerprint256 = self.fingerprint::<sha2::Sha256>().unwrap_or_default(); 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()); let KeyInfo { bits, exponent, modulus, pubkey, asn1_curve, nist_curve, } = extract_key_info(&cert.tbs_certificate.subject_pki); Ok(CertificateObject { ca: cert.is_ca(), raw: raw.into(), subject, issuer, valid_from, valid_to, serial_number, fingerprint, fingerprint256, fingerprint512, subjectaltname, info_access, bits, exponent, modulus, pubkey: pubkey.map(|p| p.into()), asn1_curve, nist_curve, }) } } impl<'a> Deref for CertificateView<'a> { type Target = X509Certificate<'a>; fn deref(&self) -> &Self::Target { &self.cert } } deno_error::js_error_wrapper!(X509Error, JsX509Error, "Error"); #[op2] #[cppgc] pub fn op_node_x509_parse( #[buffer] buf: &[u8], ) -> Result<Certificate, JsX509Error> { let source = match pem::parse_x509_pem(buf) { Ok((_, pem)) => CertificateSources::Pem(pem), Err(_) => CertificateSources::Der(buf.to_vec().into_boxed_slice()), }; let inner = Yoke::<CertificateView<'static>, Box<CertificateSources>>::try_attach_to_cart( Box::new(source), |source| { let cert = match source { CertificateSources::Pem(pem) => pem.parse_x509()?, CertificateSources::Der(buf) => { X509Certificate::from_der(buf).map(|(_, cert)| cert)? } }; Ok::<_, X509Error>(CertificateView { cert }) }, )?; Ok(Certificate { inner }) } #[op2(fast)] pub fn op_node_x509_ca(#[cppgc] cert: &Certificate) -> bool { let cert = cert.inner.get().deref(); cert.is_ca() } #[op2(fast)] pub fn op_node_x509_check_email( #[cppgc] cert: &Certificate, #[string] email: &str, ) -> bool { let cert = cert.inner.get().deref(); let subject = cert.subject(); if subject .iter_email() .any(|e| e.as_str().unwrap_or("") == email) { return true; } let subject_alt = cert .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::SubjectAlternativeName(s) => Some(s), _ => None, }); if let Some(subject_alt) = subject_alt { for name in &subject_alt.general_names { if let extensions::GeneralName::RFC822Name(n) = name && *n == email { return true; } } } false } #[op2(fast)] pub fn op_node_x509_check_host( #[cppgc] cert: &Certificate, #[string] host: &str, ) -> bool { let cert = cert.inner.get().deref(); let subject = cert.subject(); if subject .iter_common_name() .any(|e| e.as_str().unwrap_or("") == host) { return true; } let subject_alt = cert .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::SubjectAlternativeName(s) => Some(s), _ => None, }); if let Some(subject_alt) = subject_alt { for name in &subject_alt.general_names { if let extensions::GeneralName::DNSName(n) = name && *n == host { return true; } } } false } #[op2] #[string] pub fn op_node_x509_fingerprint(#[cppgc] cert: &Certificate) -> Option<String> { cert.fingerprint::<sha1::Sha1>() } #[op2] #[string] pub fn op_node_x509_fingerprint256( #[cppgc] cert: &Certificate, ) -> Option<String> { cert.fingerprint::<sha2::Sha256>() } #[op2] #[string] pub fn op_node_x509_fingerprint512( #[cppgc] cert: &Certificate, ) -> Option<String> { cert.fingerprint::<sha2::Sha512>() } #[op2] #[string] pub fn op_node_x509_get_issuer( #[cppgc] cert: &Certificate, ) -> Result<String, JsX509Error> { let cert = cert.inner.get().deref(); x509name_to_string(cert.issuer(), oid_registry()).map_err(Into::into) } #[op2] #[string] pub fn op_node_x509_get_subject( #[cppgc] cert: &Certificate, ) -> Result<String, JsX509Error> { let cert = cert.inner.get().deref(); x509name_to_string(cert.subject(), oid_registry()).map_err(Into::into) } #[op2] #[cppgc] pub fn op_node_x509_public_key( #[cppgc] cert: &Certificate, ) -> Result<KeyObjectHandle, super::keys::X509PublicKeyError> { let cert = cert.inner.get().deref(); let public_key = &cert.tbs_certificate.subject_pki; KeyObjectHandle::new_x509_public_key(public_key) } fn extract_subject_or_issuer(name: &X509Name) -> SubjectOrIssuer { let mut result = SubjectOrIssuer::default(); for rdn in name.iter_rdn() { for attr in rdn.iter() { if let Ok(value_str) = attribute_value_to_string(attr.attr_value(), attr.attr_type()) { match attr.attr_type() { oid if oid == &x509_parser::oid_registry::OID_X509_COUNTRY_NAME => { result.c.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_X509_STATE_OR_PROVINCE_NAME => { result.st.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_X509_LOCALITY_NAME => { result.l.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_X509_ORGANIZATION_NAME => { result.o.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_X509_ORGANIZATIONAL_UNIT => { result.ou.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_X509_COMMON_NAME => { result.cn.push(value_str); } oid if oid == &x509_parser::oid_registry::OID_PKCS9_EMAIL_ADDRESS => { result.email_address.push(value_str); } _ => {} } } } } result } // 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( attr: &Any, _attr_type: &Oid, ) -> Result<String, X509Error> { // TODO: replace this with helper function, when it is added to asn1-rs match attr.tag() { Tag::NumericString | Tag::BmpString | Tag::VisibleString | Tag::PrintableString | Tag::GeneralString | Tag::ObjectDescriptor | Tag::GraphicString | Tag::T61String | Tag::VideotexString | Tag::Utf8String | Tag::Ia5String => { let s = core::str::from_utf8(attr.data) .map_err(|_| X509Error::InvalidAttributes)?; Ok(s.to_owned()) } _ => { // type is not a string, get slice and convert it to base64 Ok(data_encoding::HEXUPPER.encode(attr.as_bytes())) } } } fn x509name_to_string( name: &X509Name, oid_registry: &oid_registry::OidRegistry, ) -> Result<String, x509_parser::error::X509Error> { // Lifted from https://github.com/rusticata/x509-parser/blob/4d618c2ed6b1fc102df16797545895f7c67ee0fe/src/x509.rs#L543-L566 // since it's a private function (Copyright 2017 Pierre Chifflier) name.iter_rdn().try_fold(String::new(), |acc, rdn| { rdn .iter() .try_fold(String::new(), |acc2, attr| { let val_str = attribute_value_to_string(attr.attr_value(), attr.attr_type())?; // look ABBREV, and if not found, use shortname 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() { 0 => Ok(rdn), _ => Ok(acc2 + " + " + rdn.as_str()), } }) .map(|v| match acc.len() { 0 => v, _ => acc + "\n" + v.as_str(), }) }) } #[op2] #[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() .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() .replace("+00:00", "GMT") } #[op2] #[string] pub fn op_node_x509_get_serial_number(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); let mut s = cert.serial.to_str_radix(16); s.make_ascii_uppercase(); s } #[op2(fast)] pub fn op_node_x509_key_usage(#[cppgc] cert: &Certificate) -> u16 { let cert = cert.inner.get().deref(); let key_usage = cert .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::KeyUsage(k) => Some(k), _ => None, }); key_usage.map(|k| k.flags).unwrap_or(0) } #[derive(Default)] struct KeyInfo { bits: Option<u32>, exponent: Option<String>, modulus: Option<String>, pubkey: Option<Vec<u8>>, asn1_curve: Option<String>, nist_curve: Option<String>, } fn extract_key_info(spki: &x509_parser::x509::SubjectPublicKeyInfo) -> KeyInfo { use x509_parser::der_parser::asn1_rs::oid; use x509_parser::public_key::PublicKey; match spki.parsed() { Ok(PublicKey::RSA(key)) => { let modulus_bytes = key.modulus; let exponent_bytes = key.exponent; // 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 { bits, exponent, modulus, pubkey, asn1_curve: None, nist_curve: None, } } Ok(PublicKey::EC(point)) => { let pubkey = Some(point.data().to_vec()); let mut asn1_curve = None; let mut nist_curve = None; let mut bits = None; if let Some(params) = &spki.algorithm.parameters && let Ok(curve_oid) = params.as_oid() { const ID_SECP224R1: &[u8] = &oid!(raw 1.3.132.0.33); const ID_SECP256R1: &[u8] = &oid!(raw 1.2.840.10045.3.1.7); const ID_SECP384R1: &[u8] = &oid!(raw 1.3.132.0.34); const ID_SECP521R1: &[u8] = &oid!(raw 1.3.132.0.35); // Use OpenSSL short names for asn1Curve and NIST names for // nistCurve to match Node.js behavior. match curve_oid.as_bytes() { ID_SECP224R1 => { asn1_curve = Some("secp224r1".to_string()); nist_curve = Some("P-224".to_string()); bits = Some(224); } ID_SECP256R1 => { asn1_curve = Some("prime256v1".to_string()); nist_curve = Some("P-256".to_string()); bits = Some(256); } ID_SECP384R1 => { asn1_curve = Some("secp384r1".to_string()); nist_curve = Some("P-384".to_string()); bits = Some(384); } ID_SECP521R1 => { asn1_curve = Some("secp521r1".to_string()); nist_curve = Some("P-521".to_string()); bits = Some(521); } _ => { asn1_curve = Some(curve_oid.to_string()); } } } KeyInfo { bits, exponent: None, modulus: None, pubkey, asn1_curve, nist_curve, } } _ => KeyInfo::default(), } } fn format_ip_address(ip_bytes: &[u8]) -> Option<String> { match ip_bytes.len() { 4 => { let addr = Ipv4Addr::new(ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]); Some(addr.to_string()) } 16 => { let mut segments = [0u16; 8]; for i in 0..8 { segments[i] = u16::from_be_bytes([ip_bytes[i * 2], ip_bytes[i * 2 + 1]]); } let addr = Ipv6Addr::from(segments); Some(addr.to_string()) } _ => None, } } fn get_subject_alt_name(cert: &X509Certificate) -> Option<String> { let subject_alt = cert .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::SubjectAlternativeName(s) => Some(s), _ => None, })?; let mut alt_names = Vec::new(); for name in &subject_alt.general_names { match name { extensions::GeneralName::DNSName(dns) => { alt_names.push(format!("DNS:{}", dns)); } extensions::GeneralName::RFC822Name(email) => { alt_names.push(format!("email:{}", email)); } extensions::GeneralName::IPAddress(ip) => { if let Some(formatted) = format_ip_address(ip) { alt_names.push(format!("IP Address:{}", formatted)); } else { alt_names .push(format!("IP Address:{}", data_encoding::HEXUPPER.encode(ip))); } } extensions::GeneralName::URI(uri) => { alt_names.push(format!("URI:{}", uri)); } extensions::GeneralName::DirectoryName(dn) => { if let Ok(s) = x509name_to_string(dn, oid_registry()) { alt_names.push(format!("DirName:{}", s)); } } _ => {} } } if alt_names.is_empty() { None } else { Some(alt_names.join(", ")) } } 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 { let der_bytes = match cert.inner.backing_cart().as_ref() { CertificateSources::Pem(pem) => &pem.contents, CertificateSources::Der(der) => der.as_ref(), }; let b64 = base64::engine::general_purpose::STANDARD.encode(der_bytes); let mut pem_str = String::from("-----BEGIN CERTIFICATE-----\n"); for chunk in b64.as_bytes().chunks(64) { pem_str.push_str(std::str::from_utf8(chunk).unwrap()); pem_str.push('\n'); } pem_str.push_str("-----END CERTIFICATE-----\n"); pem_str } #[op2] #[buffer] pub fn op_node_x509_get_raw(#[cppgc] cert: &Certificate) -> Box<[u8]> { match cert.inner.backing_cart().as_ref() { CertificateSources::Pem(pem) => pem.contents.clone().into_boxed_slice(), CertificateSources::Der(der) => der.clone(), } } #[op2] #[string] pub fn op_node_x509_get_subject_alt_name( #[cppgc] cert: &Certificate, ) -> Option<String> { let cert = cert.inner.get().deref(); get_subject_alt_name(cert) } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum X509CheckIpError { #[class(type)] #[property("code" = "ERR_INVALID_ARG_VALUE")] #[error("Invalid IP")] InvalidIp, } #[op2] #[string] pub fn op_node_x509_check_ip( #[cppgc] cert: &Certificate, #[string] ip: &str, ) -> Result<Option<String>, X509CheckIpError> { let target_ip: IpAddr = ip.parse().map_err(|_| X509CheckIpError::InvalidIp)?; let cert = cert.inner.get().deref(); let subject_alt = cert .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::SubjectAlternativeName(s) => Some(s), _ => None, }); let subject_alt = match subject_alt { Some(s) => s, None => return Ok(None), }; for name in &subject_alt.general_names { if let extensions::GeneralName::IPAddress(ip_bytes) = name { let san_ip = match ip_bytes.len() { 4 => IpAddr::V4(Ipv4Addr::new( ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3], )), 16 => { let mut segments = [0u16; 8]; for i in 0..8 { segments[i] = u16::from_be_bytes([ip_bytes[i * 2], ip_bytes[i * 2 + 1]]); } IpAddr::V6(Ipv6Addr::from(segments)) } _ => continue, }; if san_ip == target_ip { return Ok(Some(ip.to_string())); } } } Ok(None) } #[op2(fast)] pub fn op_node_x509_check_issued( #[cppgc] cert: &Certificate, #[cppgc] other: &Certificate, ) -> bool { let cert = cert.inner.get().deref(); let other = other.inner.get().deref(); // 1. Check if other's subject matches cert's issuer (name comparison) if cert.issuer().as_raw() != other.subject().as_raw() { return false; } // 2. If cert has an Authority Key Identifier extension with a key_identifier, // it must match the issuer's Subject Key Identifier. let cert_aki = cert .extensions() .iter() .find(|e| { e.oid == x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER }) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::AuthorityKeyIdentifier(aki) => Some(aki), _ => None, }); if let Some(aki) = cert_aki && let Some(aki_key_id) = &aki.key_identifier { let other_ski = other .extensions() .iter() .find(|e| { e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_KEY_IDENTIFIER }) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::SubjectKeyIdentifier(ski) => Some(ski), _ => None, }); match other_ski { Some(ski) => { if aki_key_id.0 != ski.0 { return false; } } None => return false, } } // 3. If issuer has KeyUsage extension, keyCertSign bit must be set. let other_key_usage = other .extensions() .iter() .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE) .and_then(|e| match e.parsed_extension() { extensions::ParsedExtension::KeyUsage(k) => Some(k), _ => None, }); if let Some(key_usage) = other_key_usage && !key_usage.key_cert_sign() { return false; } true } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum X509CheckPrivateKeyError { #[class(generic)] #[error(transparent)] X509(#[from] X509Error), #[class(generic)] #[error("Failed to export public key")] ExportFailed, } #[op2(fast)] pub fn op_node_x509_check_private_key( #[cppgc] cert: &Certificate, #[cppgc] key: &KeyObjectHandle, ) -> Result<bool, X509CheckPrivateKeyError> { let private_key = match key.as_private_key() { Some(k) => k, None => return Ok(false), }; let derived_public_key = private_key.to_public_key(); let derived_spki_der = derived_public_key .export_der("spki") .map_err(|_| X509CheckPrivateKeyError::ExportFailed)?; let cert = cert.inner.get().deref(); let cert_spki_raw = cert.tbs_certificate.subject_pki.raw; // Both `subject_pki.raw` and `export_der("spki")` produce the full // SubjectPublicKeyInfo DER SEQUENCE (tag + length + contents). DER // encoding is canonical, so a byte comparison is sufficient. Ok(cert_spki_raw == derived_spki_der.as_ref()) } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum X509VerifyError { #[class(generic)] #[error(transparent)] X509(#[from] X509Error), #[class(generic)] #[error("Failed to export public key")] ExportFailed, #[class(generic)] #[error("Failed to parse public key")] ParseFailed, #[class(generic)] #[error("Unsupported EC curve for X509 verification")] UnsupportedEcCurve, } /// Verify an RSA-PSS signature. Parses the hash algorithm from the /// RSASSA-PSS-params in the certificate's signature algorithm. fn verify_rsa_pss( rsa_key: &rsa::RsaPublicKey, sig_alg: &x509_parser::x509::AlgorithmIdentifier, tbs_raw: &[u8], sig_value: &[u8], ) -> Result<bool, X509VerifyError> { use rsa::signature::Verifier; // Parse the hash algorithm OID from the RSA-PSS parameters. // RSASSA-PSS-params ::= SEQUENCE { // hashAlgorithm [0] AlgorithmIdentifier DEFAULT sha1, // ... // } // Default hash algorithm is SHA-1 if parameters are absent. let hash_oid = sig_alg.parameters.as_ref().and_then(|params| { let (_, seq) = x509_parser::der_parser::asn1_rs::Sequence::from_der(params.as_bytes()) .ok()?; let mut remaining = seq.content.as_ref(); while !remaining.is_empty() { let (rest, any) = x509_parser::der_parser::asn1_rs::Any::from_der(remaining).ok()?; remaining = rest; // [0] EXPLICIT tag for hashAlgorithm if any.tag().0 == 0 { // The content is an AlgorithmIdentifier SEQUENCE containing the OID let (_, inner_seq) = x509_parser::der_parser::asn1_rs::Sequence::from_der(any.data) .ok()?; let (_, oid) = Oid::from_der(inner_seq.content.as_ref()).ok()?; return Some(oid.to_id_string()); } } None }); let hash_alg = hash_oid.as_deref().unwrap_or("1.3.14.3.2.26"); // SHA-1 default let sig = rsa::pss::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; let result = match hash_alg { // id-sha1 "1.3.14.3.2.26" => { let verifier = rsa::pss::VerifyingKey::<sha1::Sha1>::new(rsa_key.clone()); verifier.verify(tbs_raw, &sig).is_ok() } // id-sha256 "2.16.840.1.101.3.4.2.1" => { let verifier = rsa::pss::VerifyingKey::<sha2::Sha256>::new(rsa_key.clone()); verifier.verify(tbs_raw, &sig).is_ok() } // id-sha384 "2.16.840.1.101.3.4.2.2" => { let verifier = rsa::pss::VerifyingKey::<sha2::Sha384>::new(rsa_key.clone()); verifier.verify(tbs_raw, &sig).is_ok() } // id-sha512 "2.16.840.1.101.3.4.2.3" => { let verifier = rsa::pss::VerifyingKey::<sha2::Sha512>::new(rsa_key.clone()); verifier.verify(tbs_raw, &sig).is_ok() } _ => false, }; Ok(result) } #[op2(fast)] pub fn op_node_x509_verify( #[cppgc] cert: &Certificate, #[cppgc] key: &KeyObjectHandle, ) -> Result<bool, X509VerifyError> { use crate::keys::AsymmetricPublicKey; let public_key = match key.as_public_key() { Some(k) => k, None => return Ok(false), }; let cert_inner = cert.inner.get().deref(); // Get the raw TBS (to-be-signed) certificate bytes and signature. // `as_ref()` returns the raw DER bytes of the TBSCertificate including // the SEQUENCE header (tag + length), which is what the signature covers. // See: https://github.com/rusticata/x509-parser/blob/b7dcc9397b596cf9fa3df65115c3f405f1748b2a/src/certificate.rs#L770-L773 let tbs_raw = cert_inner.tbs_certificate.as_ref(); let sig_value = cert_inner.signature_value.as_ref(); let sig_alg_oid = cert_inner.signature_algorithm.algorithm.to_id_string(); // Verify based on key type and signature algorithm match &*public_key { AsymmetricPublicKey::Rsa(rsa_key) => { use rsa::signature::Verifier; let result = match sig_alg_oid.as_str() { // sha1WithRSAEncryption "1.2.840.113549.1.1.5" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha1::Sha1>::new(rsa_key.clone()); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha256WithRSAEncryption "1.2.840.113549.1.1.11" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha256>::new(rsa_key.clone()); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha384WithRSAEncryption "1.2.840.113549.1.1.12" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha384>::new(rsa_key.clone()); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha512WithRSAEncryption "1.2.840.113549.1.1.13" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha512>::new(rsa_key.clone()); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // id-RSASSA-PSS "1.2.840.113549.1.1.10" => verify_rsa_pss( rsa_key, &cert_inner.signature_algorithm, tbs_raw, sig_value, )?, _ => false, }; Ok(result) } AsymmetricPublicKey::RsaPss(rsa_pss_key) => { use rsa::signature::Verifier; let result = match sig_alg_oid.as_str() { // sha1WithRSAEncryption "1.2.840.113549.1.1.5" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha1::Sha1>::new( rsa_pss_key.key.clone(), ); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha256WithRSAEncryption "1.2.840.113549.1.1.11" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha256>::new( rsa_pss_key.key.clone(), ); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha384WithRSAEncryption "1.2.840.113549.1.1.12" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha384>::new( rsa_pss_key.key.clone(), ); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // sha512WithRSAEncryption "1.2.840.113549.1.1.13" => { let verifier = rsa::pkcs1v15::VerifyingKey::<sha2::Sha512>::new( rsa_pss_key.key.clone(), ); let sig = rsa::pkcs1v15::Signature::try_from(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; verifier.verify(tbs_raw, &sig).is_ok() } // id-RSASSA-PSS "1.2.840.113549.1.1.10" => verify_rsa_pss( &rsa_pss_key.key, &cert_inner.signature_algorithm, tbs_raw, sig_value, )?, _ => false, }; Ok(result) } AsymmetricPublicKey::Ec(ec_key) => { use crate::keys::EcPublicKey; // The ECDSA digest is determined by the certificate's `signatureAlgorithm` // (e.g. ecdsa-with-SHA256), not by the issuer key's curve. Using the // curve's default hash (P-256 -> SHA-256, P-384 -> SHA-384) breaks // cross-curve chains where, for example, a P-384 issuer signs with // SHA-256 (as Apple App Attest certificates do). See #36309. let prehash = match sig_alg_oid.as_str() { // ecdsa-with-SHA1 "1.2.840.10045.4.1" => sha1::Sha1::digest(tbs_raw).to_vec(), // ecdsa-with-SHA256 "1.2.840.10045.4.3.2" => sha2::Sha256::digest(tbs_raw).to_vec(), // ecdsa-with-SHA384 "1.2.840.10045.4.3.3" => sha2::Sha384::digest(tbs_raw).to_vec(), // ecdsa-with-SHA512 "1.2.840.10045.4.3.4" => sha2::Sha512::digest(tbs_raw).to_vec(), _ => return Ok(false), }; match ec_key { EcPublicKey::P256(key) => { use p256::ecdsa::signature::hazmat::PrehashVerifier; let verifying_key = p256::ecdsa::VerifyingKey::from(key); let sig = p256::ecdsa::Signature::from_der(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; Ok(verifying_key.verify_prehash(&prehash, &sig).is_ok()) } EcPublicKey::P384(key) => { use p384::ecdsa::signature::hazmat::PrehashVerifier; let verifying_key = p384::ecdsa::VerifyingKey::from(key); let sig = p384::ecdsa::Signature::from_der(sig_value) .map_err(|_| X509VerifyError::ParseFailed)?; Ok(verifying_key.verify_prehash(&prehash, &sig).is_ok()) } _ => Err(X509VerifyError::UnsupportedEcCurve), } } AsymmetricPublicKey::Ed25519(key) => { let verified = aws_lc_rs::signature::UnparsedPublicKey::new( &aws_lc_rs::signature::ED25519, key.as_bytes().as_slice(), ) .verify(tbs_raw, sig_value) .is_ok(); Ok(verified) } _ => Ok(false), } } /// 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( #[cppgc] cert: &Certificate, ) -> Option<String> { let cert = cert.inner.get().deref(); // OID for Authority Information Access 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)?; // Parse the AIA extension value manually // AIA is a SEQUENCE of AccessDescription // Each AccessDescription is SEQUENCE { accessMethod OID, accessLocation GeneralName } let data = ext.value; let (_, seq) = x509_parser::der_parser::asn1_rs::Sequence::from_der(data).ok()?; let mut entries = Vec::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; }; // GeneralName is context-tagged. Tag [6] = uniformResourceIdentifier (IA5String) 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) { entries.push(format!("{}:{}", method_name, uri)); } } } if entries.is_empty() { None } else { Some(entries.join("\n")) } } #[op2] #[serde] pub fn op_node_x509_to_legacy_object( #[cppgc] cert: &Certificate, ) -> Result<CertificateObject, JsX509Error> { cert.to_object(true).map_err(Into::into) } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_subject_or_issuer() { let cert_pem = b"-----BEGIN CERTIFICATE----- MIICljCCAX4CCQCKmSl7UdG4tjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDDAJD TjAeFw0yNTAxMjIxNzQyNDFaFw0yNjAxMjIxNzQyNDFaMA0xCzAJBgNVBAMMAkNO MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0K/qV+9PQH3Kg2g6tK6X VxY7F8/2YKi8cKnX0YT5g9QnKjS1v8R9kKvR+LLx0Y1+pT8zFZr7BjU1cKxz8fmY 7P+vKH1R3O5p2qKvOxY4GlO6U3cQ1HtQ9TjIGiXn7T6v9BkKH6k8zL4m5W6Kp4s4 tR9J9n4rGY3j6TxC9h3W3d/dW9H6nF9r3oF9F5KvG0p8H0R7WXoO6h4J5m8J5k6b 5K3E7j9O9J1V9R8h4I5k8h0x7P2s1J8F1c5Z5T8l8e0K8N9J0x8Z8r9m0O0k6F0r 9B3G4e2j8d6F8r0t8I2W4K2v4g1g8N6f4j8c9w2r6m8O3J5I5h5E5i7n8d9v3QAU lQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAWOKj9Z7ZuY8fz8N3bYh8G4kFh2J7R B6QFzT4M6gF3jl6oJ5E3K0k5z7n9L9T5c4p8x5X8f2w8T2r4N8b4y2B8W6z4N5S8 y8M7R4H0t4R8y6S9c8o8r8g8Y8b8J8t6N8p4M3O4K8f8Z7w8P8T8G8N8q8b8H6H8 r6C3V5F4Z9y8o8i9E4j5V8O5Q7Y8Z4W8n7R8B8l8H8L4P4F8r8c8A4v3O4g8L8S6 8r8t3C6h8Y6k8b3F8w8z8H8g8k8m8B3R6K8C6P4R8f8M6g8Z2N8B8x8Z8F3A2N8R 8r8H8x2F8J2h8c8Y8x8H8g8n4l8x4E8r8p8j8S8m6F3k8L8S8z6A8F8k8B9U8L3R -----END CERTIFICATE-----"; let pem = pem::parse_x509_pem(cert_pem).unwrap().1; let cert = Certificate::from_der(&pem.contents).unwrap(); let cert_inner = cert.inner.get().deref(); let result = extract_subject_or_issuer(cert_inner.subject()); assert_eq!(result.cn, StringOrArray(vec!["CN".to_string()])); assert!(result.c.is_empty()); assert!(result.st.is_empty()); assert!(result.l.is_empty()); assert!(result.o.is_empty()); assert!(result.ou.is_empty()); } }