From a0dd0cbcb99ae0e78aeb8493cf7d43b01e0faf55 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Fri, 7 Apr 2023 22:54:16 +0530 Subject: [PATCH] fix(ext/node): add X509Certificate (#18625) Towards #18455 --- Cargo.lock | 128 +++++++ cli/tests/node_compat/config.json | 1 + .../test/fixtures/keys/ca1-cert.pem | 22 ++ .../test/parallel/test-crypto-x509.js | 109 ++++++ ext/node/Cargo.toml | 2 + ext/node/crypto/mod.rs | 1 + ext/node/crypto/x509.rs | 315 ++++++++++++++++++ ext/node/lib.rs | 12 + ext/node/polyfills/internal/crypto/x509.ts | 76 ++--- 9 files changed, 626 insertions(+), 40 deletions(-) create mode 100644 cli/tests/node_compat/test/fixtures/keys/ca1-cert.pem create mode 100644 cli/tests/node_compat/test/parallel/test-crypto-x509.js create mode 100644 ext/node/crypto/x509.rs diff --git a/Cargo.lock b/Cargo.lock index 0498b9d2d8..2f8b546285 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,45 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "ast_node" version = "0.8.6" @@ -1108,6 +1147,7 @@ version = "0.34.0" dependencies = [ "aes", "cbc", + "data-encoding", "deno_core", "digest 0.10.6", "ecb", @@ -1135,6 +1175,7 @@ dependencies = [ "signature", "tokio", "typenum", + "x509-parser", ] [[package]] @@ -1339,6 +1380,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1388,6 +1443,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "dissimilar" version = "1.0.4" @@ -2722,6 +2788,12 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.5.4" @@ -2812,6 +2884,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "5.0.0" @@ -2910,6 +2992,15 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -3601,6 +3692,15 @@ dependencies = [ "semver 1.0.14", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.36.9" @@ -4745,8 +4845,10 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -4755,6 +4857,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -5670,6 +5781,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab0c2f54ae1d92f4fcb99c0b7ccf0b1e3451cbd395e5f115ccbdbcb18d4f634" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "xattr" version = "0.2.3" diff --git a/cli/tests/node_compat/config.json b/cli/tests/node_compat/config.json index fc9a8d1318..5ee86a03c2 100644 --- a/cli/tests/node_compat/config.json +++ b/cli/tests/node_compat/config.json @@ -232,6 +232,7 @@ "test-crypto-hmac.js", "test-crypto-prime.js", "test-crypto-secret-keygen.js", + "test-crypto-x509.js", "test-dgram-close-during-bind.js", "test-dgram-close-signal.js", "test-diagnostics-channel-has-subscribers.js", diff --git a/cli/tests/node_compat/test/fixtures/keys/ca1-cert.pem b/cli/tests/node_compat/test/fixtures/keys/ca1-cert.pem new file mode 100644 index 0000000000..4ba203b585 --- /dev/null +++ b/cli/tests/node_compat/test/fixtures/keys/ca1-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIUSrFsjf1qfQ0t/KvfnEsOksatAikwDQYJKoZIhvcNAQEL +BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G +A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe +BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMCAXDTIyMDkwMzIxNDAzN1oY +DzIyOTYwNjE3MjE0MDM3WjB6MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExCzAJ +BgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMxDDAK +BgNVBAMMA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNvf4OGGep+ak+4DNjbuNgy0S/ +AZPxahEFp4gpbcvsi9YLOPZ31qpilQeQf7d27scIZ02Qx1YBAzljxELB8H/ZxuYS +cQK0s+DNP22xhmgwMWznO7TezkHP5ujN2UkbfbUpfUxGFgncXeZf9wR7yFWppeHi +RWNBOgsvY7sTrS12kXjWGjqntF7xcEDHc7h+KyF6ZjVJZJCnP6pJEQ+rUjd51eCZ +Xt4WjowLnQiCS1VKzXiP83a++Ma1BKKkUitTR112/Uwd5eGoiByhmLzb/BhxnHJN +07GXjhlMItZRm/jfbZsx1mwnNOO3tx4r08l+DaqkinIadvazs+1ugCaKQn8xAgMB +AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFqG0RXURDam +56x5accdg9sY5zEGP5VQhkK3ZDc2NyNNa25rwvrjCpO+e0OSwKAmm4aX6iIf2woY +wF2f9swWYzxn9CG4fDlUA8itwlnHxupeL4fGMTYb72vf31plUXyBySRsTwHwBloc +F7KvAZpYYKN9EMH1S/267By6H2I33BT/Ethv//n8dSfmuCurR1kYRaiOC4PVeyFk +B3sj8TtolrN0y/nToWUhmKiaVFnDx3odQ00yhmxR3t21iB7yDkko6D8Vf2dVC4j/ +YYBVprXGlTP/hiYRLDoP20xKOYznx5cvHPJ9p+lVcOZUJsJj/Iy750+2n5UiBmXt +lz88C25ucKA= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/cli/tests/node_compat/test/parallel/test-crypto-x509.js b/cli/tests/node_compat/test/parallel/test-crypto-x509.js new file mode 100644 index 0000000000..eeee2f7d7d --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-crypto-x509.js @@ -0,0 +1,109 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { + X509Certificate, +} = require('crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { readFileSync } = require('fs'); + +const cert = readFileSync(fixtures.path('keys', 'agent1-cert.pem')); +const ca = readFileSync(fixtures.path('keys', 'ca1-cert.pem')); + +[1, {}, false, null].forEach((i) => { + assert.throws(() => new X509Certificate(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +const subjectCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=agent1 +Email=ry@tinyclouds.org`; + +const issuerCheck = `C=US +ST=CA +L=SF +O=Joyent +OU=Node.js +CN=ca1 +Email=ry@tinyclouds.org`; + +let infoAccessCheck = `OCSP - URI:http://ocsp.nodejs.org/ +CA Issuers - URI:http://ca.nodejs.org/ca.cert`; +if (!common.hasOpenSSL3) + infoAccessCheck += '\n'; + +const der = Buffer.from( + '308203e8308202d0a0030201020214147d36c1c2f74206de9fab5f2226d78adb00a42630' + + '0d06092a864886f70d01010b0500307a310b3009060355040613025553310b3009060355' + + '04080c024341310b300906035504070c025346310f300d060355040a0c064a6f79656e74' + + '3110300e060355040b0c074e6f64652e6a73310c300a06035504030c036361313120301e' + + '06092a864886f70d010901161172794074696e79636c6f7564732e6f72673020170d3232' + + '303930333231343033375a180f32323936303631373231343033375a307d310b30090603' + + '55040613025553310b300906035504080c024341310b300906035504070c025346310f30' + + '0d060355040a0c064a6f79656e743110300e060355040b0c074e6f64652e6a73310f300d' + + '06035504030c066167656e74313120301e06092a864886f70d010901161172794074696e' + + '79636c6f7564732e6f726730820122300d06092a864886f70d01010105000382010f0030' + + '82010a0282010100d456320afb20d3827093dc2c4284ed04dfbabd56e1ddae529e28b790' + + 'cd4256db273349f3735ffd337c7a6363ecca5a27b7f73dc7089a96c6d886db0c62388f1c' + + 'dd6a963afcd599d5800e587a11f908960f84ed50ba25a28303ecda6e684fbe7baedc9ce8' + + '801327b1697af25097cee3f175e400984c0db6a8eb87be03b4cf94774ba56fffc8c63c68' + + 'd6adeb60abbe69a7b14ab6a6b9e7baa89b5adab8eb07897c07f6d4fa3d660dff574107d2' + + '8e8f63467a788624c574197693e959cea1362ffae1bba10c8c0d88840abfef103631b2e8' + + 'f5c39b5548a7ea57e8a39f89291813f45a76c448033a2b7ed8403f4baa147cf35e2d2554' + + 'aa65ce49695797095bf4dc6b0203010001a361305f305d06082b06010505070101045130' + + '4f302306082b060105050730018617687474703a2f2f6f6373702e6e6f64656a732e6f72' + + '672f302806082b06010505073002861c687474703a2f2f63612e6e6f64656a732e6f7267' + + '2f63612e63657274300d06092a864886f70d01010b05000382010100c3349810632ccb7d' + + 'a585de3ed51e34ed154f0f7215608cf2701c00eda444dc2427072c8aca4da6472c1d9e68' + + 'f177f99a90a8b5dbf3884586d61cb1c14ea7016c8d38b70d1b46b42947db30edc1e9961e' + + 'd46c0f0e35da427bfbe52900771817e733b371adf19e12137235141a34347db0dfc05579' + + '8b1f269f3bdf5e30ce35d1339d56bb3c570de9096215433047f87ca42447b44e7e6b5d0e' + + '48f7894ab186f85b6b1a74561b520952fea888617f32f582afce1111581cd63efcc68986' + + '00d248bb684dedb9c3d6710c38de9e9bc21f9c3394b729d5f707d64ea890603e5989f8fa' + + '59c19ad1a00732e7adc851b89487cc00799dde068aa64b3b8fd976e8bc113ef2', + 'hex'); + +{ + const x509 = new X509Certificate(cert); + + assert(!x509.ca); + assert.strictEqual(x509.subject, subjectCheck); + assert.strictEqual(x509.subjectAltName, undefined); + assert.strictEqual(x509.issuer, issuerCheck); + assert.strictEqual(x509.validFrom, 'Sep 3 21:40:37 2022 +00:00'); + assert.strictEqual(x509.validTo, 'Jun 17 21:40:37 2296 +00:00'); + assert.strictEqual( + x509.fingerprint, + '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53'); + assert.strictEqual( + x509.fingerprint256, + '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:' + + '22:7C:B6:77:D3:34:E7:53:4B:05' + ); + assert.strictEqual( + x509.fingerprint512, + '0B:6F:D0:4D:6B:22:53:99:66:62:51:2D:2C:96:F2:58:3F:95:1C:CC:4C:44:' + + '9D:B5:59:AA:AD:A8:F6:2A:24:8A:BB:06:A5:26:42:52:30:A3:37:61:30:A9:' + + '5A:42:63:E0:21:2F:D6:70:63:07:96:6F:27:A7:78:12:08:02:7A:8B' + ); + assert.strictEqual(x509.keyUsage, undefined); + assert.strictEqual(x509.serialNumber, '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'); + + assert.strictEqual(x509.checkEmail('ry@tinyclouds.org'), 'ry@tinyclouds.org'); + assert.strictEqual(x509.checkEmail('sally@example.com'), undefined); +} \ No newline at end of file diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index e74cf38053..d87a2f91d8 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 adacdf6d68..55a7a5870e 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 0000000000..c5f3ef2066 --- /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, + pem: Option, + cert: X509Certificate<'static>, +} + +impl Certificate { + fn fingerprint(&self) -> Option { + 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 { + "x509Certificate".into() + } +} + +#[op] +pub fn op_node_x509_parse( + state: &mut OpState, + buf: &[u8], +) -> Result { + 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 { + let cert = state + .resource_table + .get::(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 { + let cert = state + .resource_table + .get::(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, AnyError> { + let cert = state + .resource_table + .get::(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::()) +} + +#[op] +pub fn op_node_x509_fingerprint256( + state: &mut OpState, + rid: u32, +) -> Result, AnyError> { + let cert = state + .resource_table + .get::(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::()) +} + +#[op] +pub fn op_node_x509_fingerprint512( + state: &mut OpState, + rid: u32, +) -> Result, AnyError> { + let cert = state + .resource_table + .get::(rid) + .or_else(|_| Err(bad_resource_id()))?; + Ok(cert.fingerprint::()) +} + +#[op] +pub fn op_node_x509_get_issuer( + state: &mut OpState, + rid: u32, +) -> Result { + let cert = state + .resource_table + .get::(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 { + let cert = state + .resource_table + .get::(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 { + // 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 { + 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 { + let cert = state + .resource_table + .get::(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 { + let cert = state + .resource_table + .get::(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 { + let cert = state + .resource_table + .get::(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 { + let cert = state + .resource_table + .get::(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 bf947f5e8d..3ef761cb7c 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 e18d4fe682..7a8ee773b6 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, ): 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 {