diff options
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/Cargo.toml | 2 | ||||
-rw-r--r-- | ext/node/crypto/mod.rs | 1 | ||||
-rw-r--r-- | ext/node/crypto/x509.rs | 315 | ||||
-rw-r--r-- | ext/node/lib.rs | 12 | ||||
-rw-r--r-- | ext/node/polyfills/internal/crypto/x509.ts | 76 |
5 files changed, 366 insertions, 40 deletions
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index e74cf3805..d87a2f91d 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -16,6 +16,7 @@ path = "lib.rs" [dependencies] aes.workspace = true cbc.workspace = true +data-encoding = "2.3.3" deno_core.workspace = true digest = { version = "0.10.5", features = ["core-api", "std"] } ecb.workspace = true @@ -43,3 +44,4 @@ sha3 = "0.10.5" signature.workspace = true tokio.workspace = true typenum = "1.15.0" +x509-parser = "0.15.0" diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index adacdf6d6..55a7a5870 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -23,6 +23,7 @@ use rsa::RsaPublicKey; mod cipher; mod digest; mod primes; +pub mod x509; #[op] pub fn op_node_check_prime(num: serde_v8::BigInt, checks: usize) -> bool { diff --git a/ext/node/crypto/x509.rs b/ext/node/crypto/x509.rs new file mode 100644 index 000000000..c5f3ef206 --- /dev/null +++ b/ext/node/crypto/x509.rs @@ -0,0 +1,315 @@ +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::OpState; +use deno_core::Resource; + +use std::borrow::Cow; + +use x509_parser::der_parser::asn1_rs::Any; +use x509_parser::der_parser::asn1_rs::Tag; +use x509_parser::der_parser::oid::Oid; +use x509_parser::extensions; +use x509_parser::pem; +use x509_parser::prelude::*; + +use digest::Digest; + +struct Certificate { + _buf: Vec<u8>, + pem: Option<pem::Pem>, + cert: X509Certificate<'static>, +} + +impl Certificate { + fn fingerprint<D: Digest>(&self) -> Option<String> { + self.pem.as_ref().map(|pem| { + let mut hasher = D::new(); + hasher.update(&pem.contents); + 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(); + hex + }) + } +} + +impl std::ops::Deref for Certificate { + type Target = X509Certificate<'static>; + + fn deref(&self) -> &Self::Target { + &self.cert + } +} + +impl Resource for Certificate { + fn name(&self) -> Cow<str> { + "x509Certificate".into() + } +} + +#[op] +pub fn op_node_x509_parse( + state: &mut OpState, + buf: &[u8], +) -> Result<u32, AnyError> { + let pem = match pem::parse_x509_pem(buf) { + Ok((_, pem)) => Some(pem), + Err(_) => None, + }; + + let cert = pem + .as_ref() + .map(|pem| pem.parse_x509()) + .unwrap_or_else(|| X509Certificate::from_der(buf).map(|(_, cert)| cert))?; + + let cert = Certificate { + _buf: buf.to_vec(), + // SAFETY: Extending the lifetime of the certificate. Backing buffer is + // owned by the resource. + cert: unsafe { std::mem::transmute(cert) }, + pem, + }; + let rid = state.resource_table.add(cert); + Ok(rid) +} + +#[op] +pub fn op_node_x509_ca( + state: &mut OpState, + rid: u32, +) -> Result<bool, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.is_ca()) +} + +#[op] +pub fn op_node_x509_check_email( + state: &mut OpState, + rid: u32, + email: &str, +) -> Result<bool, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + + let subject = cert.subject(); + if subject + .iter_email() + .any(|e| e.as_str().unwrap_or("") == email) + { + return Ok(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 { + dbg!(name); + if let extensions::GeneralName::RFC822Name(n) = name { + if *n == email { + return Ok(true); + } + } + } + } + + Ok(false) +} + +#[op] +pub fn op_node_x509_fingerprint( + state: &mut OpState, + rid: u32, +) -> Result<Option<String>, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::<sha1::Sha1>()) +} + +#[op] +pub fn op_node_x509_fingerprint256( + state: &mut OpState, + rid: u32, +) -> Result<Option<String>, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::<sha2::Sha256>()) +} + +#[op] +pub fn op_node_x509_fingerprint512( + state: &mut OpState, + rid: u32, +) -> Result<Option<String>, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::<sha2::Sha512>()) +} + +#[op] +pub fn op_node_x509_get_issuer( + state: &mut OpState, + rid: u32, +) -> Result<String, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(x509name_to_string(cert.issuer(), oid_registry())?) +} + +#[op] +pub fn op_node_x509_get_subject( + state: &mut OpState, + rid: u32, +) -> Result<String, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(x509name_to_string(cert.subject(), oid_registry())?) +} + +// 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> { + name.iter_rdn().fold(Ok(String::new()), |acc, rdn| { + acc.and_then(|mut _vec| { + rdn + .iter() + .fold(Ok(String::new()), |acc2, attr| { + acc2.and_then(|mut _vec2| { + 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 rdn = format!("{}={}", abbrev, val_str); + match _vec2.len() { + 0 => Ok(rdn), + _ => Ok(_vec2 + " + " + &rdn), + } + }) + }) + .map(|v| match _vec.len() { + 0 => v, + _ => _vec + "\n" + &v, + }) + }) + }) +} + +#[op] +pub fn op_node_x509_get_valid_from( + state: &mut OpState, + rid: u32, +) -> Result<String, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.validity().not_before.to_string()) +} + +#[op] +pub fn op_node_x509_get_valid_to( + state: &mut OpState, + rid: u32, +) -> Result<String, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.validity().not_after.to_string()) +} + +#[op] +pub fn op_node_x509_get_serial_number( + state: &mut OpState, + rid: u32, +) -> Result<String, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + let mut s = cert.serial.to_str_radix(16); + s.make_ascii_uppercase(); + Ok(s) +} + +#[op] +pub fn op_node_x509_key_usage( + state: &mut OpState, + rid: u32, +) -> Result<u16, AnyError> { + let cert = state + .resource_table + .get::<Certificate>(rid) + .or_else(|_| Err(bad_resource_id()))?; + + 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, + }); + + Ok(key_usage.map(|k| k.flags).unwrap_or(0)) +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index bf947f5e8..3ef761cb7 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -194,6 +194,18 @@ deno_core::extension!(deno_node, crypto::op_node_generate_secret, crypto::op_node_generate_secret_async, crypto::op_node_sign, + crypto::x509::op_node_x509_parse, + crypto::x509::op_node_x509_ca, + crypto::x509::op_node_x509_check_email, + crypto::x509::op_node_x509_fingerprint, + crypto::x509::op_node_x509_fingerprint256, + crypto::x509::op_node_x509_fingerprint512, + crypto::x509::op_node_x509_get_issuer, + crypto::x509::op_node_x509_get_subject, + crypto::x509::op_node_x509_get_valid_from, + crypto::x509::op_node_x509_get_valid_to, + crypto::x509::op_node_x509_get_serial_number, + crypto::x509::op_node_x509_key_usage, winerror::op_node_sys_to_uv_error, v8::op_v8_cached_data_version_tag, v8::op_v8_get_heap_statistics, diff --git a/ext/node/polyfills/internal/crypto/x509.ts b/ext/node/polyfills/internal/crypto/x509.ts index e18d4fe68..7a8ee773b 100644 --- a/ext/node/polyfills/internal/crypto/x509.ts +++ b/ext/node/polyfills/internal/crypto/x509.ts @@ -5,9 +5,12 @@ import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts"; import { Buffer } from "ext:deno_node/buffer.ts"; import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; +import { validateString } from "ext:deno_node/internal/validators.mjs"; import { notImplemented } from "ext:deno_node/_utils.ts"; import { BinaryLike } from "ext:deno_node/internal/crypto/types.ts"; +const { ops } = globalThis.__bootstrap.core; + // deno-lint-ignore no-explicit-any export type PeerCertificate = any; @@ -35,6 +38,8 @@ export interface X509CheckOptions { } export class X509Certificate { + #handle: number; + constructor(buffer: BinaryLike) { if (typeof buffer === "string") { buffer = Buffer.from(buffer); @@ -48,20 +53,21 @@ export class X509Certificate { ); } - notImplemented("crypto.X509Certificate"); + this.#handle = ops.op_node_x509_parse(buffer); } get ca(): boolean { - notImplemented("crypto.X509Certificate.prototype.ca"); - - return false; + return ops.op_node_x509_ca(this.#handle); } checkEmail( - _email: string, + email: string, _options?: Pick<X509CheckOptions, "subject">, ): string | undefined { - notImplemented("crypto.X509Certificate.prototype.checkEmail"); + validateString(email, "email"); + if (ops.op_node_x509_check_email(this.#handle, email)) { + return email; + } } checkHost(_name: string, _options?: X509CheckOptions): string | undefined { @@ -81,21 +87,15 @@ export class X509Certificate { } get fingerprint(): string { - notImplemented("crypto.X509Certificate.prototype.fingerprint"); - - return ""; + return ops.op_node_x509_fingerprint(this.#handle); } get fingerprint256(): string { - notImplemented("crypto.X509Certificate.prototype.fingerprint256"); - - return ""; + return ops.op_node_x509_fingerprint256(this.#handle); } get fingerprint512(): string { - notImplemented("crypto.X509Certificate.prototype.fingerprint512"); - - return ""; + return ops.op_node_x509_fingerprint512(this.#handle); } get infoAccess(): string | undefined { @@ -105,21 +105,27 @@ export class X509Certificate { } get issuer(): string { - notImplemented("crypto.X509Certificate.prototype.issuer"); - - return ""; + return ops.op_node_x509_get_issuer(this.#handle); } get issuerCertificate(): X509Certificate | undefined { - notImplemented("crypto.X509Certificate.prototype.issuerCertificate"); - - return {} as X509Certificate; + return undefined; } - get keyUsage(): string[] { - notImplemented("crypto.X509Certificate.prototype.keyUsage"); - - return []; + get keyUsage(): string[] | undefined { + const flags = ops.op_node_x509_key_usage(this.#handle); + if (flags === 0) return undefined; + const result: string[] = []; + if (flags & 0x01) result.push("DigitalSignature"); + if (flags >> 1 & 0x01) result.push("NonRepudiation"); + if (flags >> 2 & 0x01) result.push("KeyEncipherment"); + if (flags >> 3 & 0x01) result.push("DataEncipherment"); + if (flags >> 4 & 0x01) result.push("KeyAgreement"); + if (flags >> 5 & 0x01) result.push("KeyCertSign"); + if (flags >> 6 & 0x01) result.push("CRLSign"); + if (flags >> 7 & 0x01) result.push("EncipherOnly"); + if (flags >> 8 & 0x01) result.push("DecipherOnly"); + return result; } get publicKey(): KeyObject { @@ -135,21 +141,15 @@ export class X509Certificate { } get serialNumber(): string { - notImplemented("crypto.X509Certificate.prototype.serialNumber"); - - return ""; + return ops.op_node_x509_get_serial_number(this.#handle); } get subject(): string { - notImplemented("crypto.X509Certificate.prototype.subject"); - - return ""; + return ops.op_node_x509_get_subject(this.#handle); } get subjectAltName(): string | undefined { - notImplemented("crypto.X509Certificate.prototype.subjectAltName"); - - return ""; + return undefined; } toJSON(): string { @@ -165,15 +165,11 @@ export class X509Certificate { } get validFrom(): string { - notImplemented("crypto.X509Certificate.prototype.validFrom"); - - return ""; + return ops.op_node_x509_get_valid_from(this.#handle); } get validTo(): string { - notImplemented("crypto.X509Certificate.prototype.validTo"); - - return ""; + return ops.op_node_x509_get_valid_to(this.#handle); } verify(_publicKey: KeyObject): boolean { |