From d15581fb19629c5560b09bf2893c8f1f90494c77 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 23 Jun 2025 02:34:44 -0700 Subject: [PATCH] fix(ext/node): implement Certificate API (#29828) --- Cargo.lock | 9 + Cargo.toml | 3 + ext/node/Cargo.toml | 1 + ext/node/lib.rs | 3 + ext/node/ops/crypto/mod.rs | 43 ++++ .../polyfills/internal/crypto/certificate.ts | 60 ++++-- ext/node/polyfills/internal/crypto/keys.ts | 7 + libs/crypto/Cargo.toml | 16 ++ libs/crypto/README.md | 3 + libs/crypto/ffi.rs | 97 +++++++++ libs/crypto/lib.rs | 9 + libs/crypto/spki.rs | 188 ++++++++++++++++++ tests/node_compat/config.jsonc | 4 + tests/node_compat/runner/TODO.md | 1 - .../test/fixtures/keys/rsa_public.pem | 9 + .../test/fixtures/keys/rsa_spkac.spkac | 1 + .../fixtures/keys/rsa_spkac_invalid.spkac | 1 + .../test/parallel/test-crypto-certificate.js | 128 ++++++++++++ 18 files changed, 570 insertions(+), 13 deletions(-) create mode 100644 libs/crypto/Cargo.toml create mode 100644 libs/crypto/README.md create mode 100644 libs/crypto/ffi.rs create mode 100644 libs/crypto/lib.rs create mode 100644 libs/crypto/spki.rs create mode 100644 tests/node_compat/test/fixtures/keys/rsa_public.pem create mode 100644 tests/node_compat/test/fixtures/keys/rsa_spkac.spkac create mode 100644 tests/node_compat/test/fixtures/keys/rsa_spkac_invalid.spkac create mode 100644 tests/node_compat/test/parallel/test-crypto-certificate.js diff --git a/Cargo.lock b/Cargo.lock index accaaa7534..a67891a64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1900,6 +1900,14 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "deno_crypto_provider" +version = "0.1.0" +dependencies = [ + "aws-lc-rs", + "aws-lc-sys", +] + [[package]] name = "deno_doc" version = "0.178.0" @@ -2345,6 +2353,7 @@ dependencies = [ "ctr", "data-encoding", "deno_core", + "deno_crypto_provider", "deno_error", "deno_fetch", "deno_fs", diff --git a/Cargo.toml b/Cargo.toml index df53061e55..4ef56c5af6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "ext/websocket", "ext/webstorage", "libs/config", + "libs/crypto", "libs/node_resolver", "libs/npm_cache", "libs/npm_installer", @@ -113,6 +114,7 @@ denort_helper = { version = "0.5.0", path = "./ext/rt_helper" } # workspace libraries deno_bench_util = { version = "0.201.0", path = "./bench_util" } deno_config = { version = "=0.57.0", features = ["workspace"], path = "./libs/config" } +deno_crypto_provider = { version = "0.1.0", path = "./libs/crypto" } deno_features = { version = "0.4.0", path = "./runtime/features" } deno_lib = { version = "0.25.0", path = "./cli/lib" } deno_npm_cache = { version = "0.26.0", path = "./libs/npm_cache" } @@ -134,6 +136,7 @@ async-once-cell = "0.5.4" async-stream = "0.3" async-trait = "0.1.73" aws-lc-rs = { version = "1.0.0" } +aws-lc-sys = { version = "0.26.0" } base32 = "=0.5.1" base64 = "0.22.1" base64-simd = "0.8" diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 885047a433..2fb876ae4e 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -30,6 +30,7 @@ const-oid.workspace = true ctr.workspace = true data-encoding.workspace = true deno_core.workspace = true +deno_crypto_provider.workspace = true deno_error.workspace = true deno_fetch.workspace = true deno_fs.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 7eb52b14fc..dc8a45ae92 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -314,6 +314,9 @@ deno_core::extension!(deno_node, ops::crypto::op_node_sign_ed25519, ops::crypto::op_node_verify, ops::crypto::op_node_verify_ed25519, + ops::crypto::op_node_verify_spkac, + ops::crypto::op_node_cert_export_public_key, + ops::crypto::op_node_cert_export_challenge, ops::crypto::keys::op_node_create_private_key, ops::crypto::keys::op_node_create_ed_raw, ops::crypto::keys::op_node_create_rsa_jwk, diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 5f97502114..d59d3b22b9 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -1140,3 +1140,46 @@ pub fn op_node_verify_ed25519( Ok(verified) } + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum SpkacError { + #[error("spkac is too large")] + #[property("code" = "ERR_OUT_OF_RANGE")] + #[class(range)] + BufferOutOfRange, +} + +#[op2(fast)] +pub fn op_node_verify_spkac( + #[buffer] spkac: &[u8], +) -> Result { + if spkac.len() > i32::MAX as usize { + return Err(SpkacError::BufferOutOfRange); + } + + Ok(deno_crypto_provider::spki::verify_spkac(spkac)) +} + +#[op2] +#[buffer] +pub fn op_node_cert_export_public_key( + #[buffer] spkac: &[u8], +) -> Result>, SpkacError> { + if spkac.len() > i32::MAX as usize { + return Err(SpkacError::BufferOutOfRange); + } + + Ok(deno_crypto_provider::spki::export_public_key(spkac)) +} + +#[op2] +#[buffer] +pub fn op_node_cert_export_challenge( + #[buffer] spkac: &[u8], +) -> Result>, SpkacError> { + if spkac.len() > i32::MAX as usize { + return Err(SpkacError::BufferOutOfRange); + } + + Ok(deno_crypto_provider::spki::export_challenge(spkac)) +} diff --git a/ext/node/polyfills/internal/crypto/certificate.ts b/ext/node/polyfills/internal/crypto/certificate.ts index cd64ba36d9..45c74f0c3e 100644 --- a/ext/node/polyfills/internal/crypto/certificate.ts +++ b/ext/node/polyfills/internal/crypto/certificate.ts @@ -1,23 +1,59 @@ // Copyright 2018-2025 the Deno authors. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. -import { notImplemented } from "ext:deno_node/_utils.ts"; +import { + op_node_cert_export_challenge, + op_node_cert_export_public_key, + op_node_verify_spkac, +} from "ext:core/ops"; import { Buffer } from "node:buffer"; -import { BinaryLike } from "ext:deno_node/internal/crypto/types.ts"; +import { getArrayBufferOrView } from "ext:deno_node/internal/crypto/keys.ts"; -export class Certificate { - static Certificate = Certificate; - static exportChallenge(_spkac: BinaryLike, _encoding?: string): Buffer { - notImplemented("crypto.Certificate.exportChallenge"); - } +// The functions contained in this file cover the SPKAC format +// (also referred to as Netscape SPKI). A general description of +// the format can be found at https://en.wikipedia.org/wiki/SPKAC - static exportPublicKey(_spkac: BinaryLike, _encoding?: string): Buffer { - notImplemented("crypto.Certificate.exportPublicKey"); - } +function verifySpkac(spkac, encoding) { + return op_node_verify_spkac( + getArrayBufferOrView(spkac, "spkac", encoding), + ); +} - static verifySpkac(_spkac: BinaryLike, _encoding?: string): boolean { - notImplemented("crypto.Certificate.verifySpkac"); +function exportPublicKey(spkac, encoding) { + const publicKey = op_node_cert_export_public_key( + getArrayBufferOrView(spkac, "spkac", encoding), + ); + return publicKey ? Buffer.from(publicKey) : ""; +} + +function exportChallenge(spkac, encoding) { + const challenge = op_node_cert_export_challenge( + getArrayBufferOrView(spkac, "spkac", encoding), + ); + return challenge ? Buffer.from(challenge) : ""; +} + +// The legacy implementation of this exposed the Certificate +// object and required that users create an instance before +// calling the member methods. This API pattern has been +// deprecated, however, as the method implementations do not +// rely on any object state. + +// For backwards compatibility reasons, this cannot be converted into a +// ES6 Class. +export function Certificate() { + // deno-lint-ignore prefer-primordials + if (!(this instanceof Certificate)) { + return new Certificate(); } } +Certificate.prototype.verifySpkac = verifySpkac; +Certificate.prototype.exportPublicKey = exportPublicKey; +Certificate.prototype.exportChallenge = exportChallenge; + +Certificate.exportChallenge = exportChallenge; +Certificate.exportPublicKey = exportPublicKey; +Certificate.verifySpkac = verifySpkac; + export default Certificate; diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index 39b4a36e02..3659bbcaef 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -96,6 +96,13 @@ export const getArrayBufferOrView = hideStackFrames( } return Buffer.from(buffer, encoding); } + if (buffer instanceof DataView) { + return new Uint8Array( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength, + ); + } if (!isArrayBufferView(buffer)) { throw new ERR_INVALID_ARG_TYPE( name, diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml new file mode 100644 index 0000000000..5eb2f82bce --- /dev/null +++ b/libs/crypto/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright 2018-2025 the Deno authors. MIT license. +[package] +name = "deno_crypto_provider" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Cryptography provider for Deno" + +[lib] +path = "lib.rs" + +[dependencies] +aws-lc-rs.workspace = true +aws-lc-sys.workspace = true diff --git a/libs/crypto/README.md b/libs/crypto/README.md new file mode 100644 index 0000000000..b324c83d78 --- /dev/null +++ b/libs/crypto/README.md @@ -0,0 +1,3 @@ +# `deno_crypto` + +Rust cryptography provider for Deno WebCrypto and `node:crypto`. diff --git a/libs/crypto/ffi.rs b/libs/crypto/ffi.rs new file mode 100644 index 0000000000..49666119ef --- /dev/null +++ b/libs/crypto/ffi.rs @@ -0,0 +1,97 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub struct PKey(pub *mut aws_lc_sys::EVP_PKEY); + +impl PKey { + pub fn from_ptr(ptr: *mut aws_lc_sys::EVP_PKEY) -> Option { + if ptr.is_null() { + None + } else { + Some(Self(ptr)) + } + } + + pub fn as_ptr(&self) -> *mut aws_lc_sys::EVP_PKEY { + self.0 + } +} + +impl Drop for PKey { + fn drop(&mut self) { + // SAFETY: We need to free the underlying EVP_PKEY when the PKey wrapper is dropped. + // The null check ensures we don't try to free a null pointer. + unsafe { + if self.0.is_null() { + return; + } + aws_lc_sys::EVP_PKEY_free(self.0); + } + } +} + +impl std::ops::Deref for PKey { + type Target = *mut aws_lc_sys::EVP_PKEY; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct Bio(pub *mut aws_lc_sys::BIO); + +impl Drop for Bio { + fn drop(&mut self) { + // SAFETY: We need to free the underlying BIO when the Bio wrapper is dropped. + // The null check ensures we don't try to free a null pointer. + unsafe { + if self.0.is_null() { + return; + } + aws_lc_sys::BIO_free(self.0); + } + } +} + +impl Bio { + pub fn new_memory() -> Result { + // SAFETY: Creating a new memory BIO requires FFI calls to the OpenSSL API. + // We check for null pointer returns to ensure safety. + unsafe { + let bio = aws_lc_sys::BIO_new(aws_lc_sys::BIO_s_mem()); + if bio.is_null() { + return Err("Failed to create memory BIO"); + } + Ok(Bio(bio)) + } + } + + pub fn get_contents(&self) -> Result, &'static str> { + // SAFETY: Retrieving content from a BIO requires FFI calls and raw pointer manipulation. + // We verify the pointer is not null and create a slice with the correct length. + // The data is copied into a Vec to ensure memory safety after this function returns. + unsafe { + let mut len = 0; + let mut content_ptr = std::ptr::null(); + aws_lc_sys::BIO_mem_contents(self.0, &mut content_ptr, &mut len); + + if content_ptr.is_null() || len == 0 { + return Err("No content in BIO"); + } + + let data = std::slice::from_raw_parts(content_ptr, len); + Ok(data.to_vec()) + } + } + + pub fn as_ptr(&self) -> *mut aws_lc_sys::BIO { + self.0 + } +} + +impl std::ops::Deref for Bio { + type Target = *mut aws_lc_sys::BIO; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/libs/crypto/lib.rs b/libs/crypto/lib.rs new file mode 100644 index 0000000000..a25e25b653 --- /dev/null +++ b/libs/crypto/lib.rs @@ -0,0 +1,9 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] +#![deny(clippy::unused_async)] +#![deny(clippy::unnecessary_wraps)] + +mod ffi; +pub mod spki; diff --git a/libs/crypto/spki.rs b/libs/crypto/spki.rs new file mode 100644 index 0000000000..915cf41b90 --- /dev/null +++ b/libs/crypto/spki.rs @@ -0,0 +1,188 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::ptr::NonNull; + +use crate::ffi::Bio; +use crate::ffi::PKey; + +#[derive(Debug)] +pub struct NetscapeSpki(*mut aws_lc_sys::NETSCAPE_SPKI); + +impl NetscapeSpki { + /// Decodes a base64-encoded SPKI certificate. + fn from_base64(data: &[u8]) -> Result { + // Trim trailing characters for compatibility with OpenSSL. + let end = data + .iter() + .rposition(|&b| !b" \n\r\t".contains(&b)) + .map_or(0, |i| i + 1); + + // SAFETY: Cast data pointer to convert base64 to NETSCAPE_SPKI + unsafe { + let spki = aws_lc_sys::NETSCAPE_SPKI_b64_decode( + data.as_ptr() as *const _, + end as isize, + ); + if spki.is_null() { + return Err("Failed to decode base64 SPKI data"); + } + Ok(NetscapeSpki(spki)) + } + } + + fn verify(&self, pkey: &PKey) -> bool { + // SAFETY: Use public key to verify SPKI certificate + unsafe { + let result = aws_lc_sys::NETSCAPE_SPKI_verify(self.0, pkey.as_ptr()); + result > 0 + } + } + + fn spkac(&self) -> Result<&aws_lc_sys::NETSCAPE_SPKAC, &'static str> { + // SAFETY: Access spkac field via raw pointer with null checks + unsafe { + if self.0.is_null() || (*self.0).spkac.is_null() { + return Err("Invalid SPKAC structure"); + } + Ok(&*(*self.0).spkac) + } + } + + fn get_public_key(&self) -> Result { + // SAFETY: Extract public key, null checked by PKey::from_ptr + unsafe { + let pkey = aws_lc_sys::NETSCAPE_SPKI_get_pubkey(self.0); + PKey::from_ptr(pkey).ok_or("Failed to extract public key") + } + } + + fn get_challenge(&self) -> Result, &'static str> { + // SAFETY: Extract challenge with null checks and BufferGuard for cleanup + unsafe { + let spkac = self.spkac()?; + let challenge = spkac.challenge; + if challenge.is_null() { + return Err("No challenge found in SPKI certificate"); + } + + let mut buf = std::ptr::null_mut(); + let buf_len = aws_lc_sys::ASN1_STRING_to_UTF8(&mut buf, challenge); + + if buf_len <= 0 || buf.is_null() { + return Err("Failed to extract challenge string"); + } + + let _guard = BufferGuard(NonNull::new(buf).unwrap()); + + let challenge_slice = + std::slice::from_raw_parts(buf as *const u8, buf_len as usize); + Ok(challenge_slice.to_vec()) + } + } + + pub fn as_ptr(&self) -> *mut aws_lc_sys::NETSCAPE_SPKI { + self.0 + } +} + +impl Drop for NetscapeSpki { + fn drop(&mut self) { + // SAFETY: Free NETSCAPE_SPKI with null check + unsafe { + if !self.0.is_null() { + aws_lc_sys::NETSCAPE_SPKI_free(self.0); + } + } + } +} + +// RAII guard for automatically freeing ASN1 string buffers +struct BufferGuard(NonNull); + +impl Drop for BufferGuard { + fn drop(&mut self) { + // SAFETY: Free ASN1_STRING buffer (NonNull guarantees non-null) + unsafe { + aws_lc_sys::OPENSSL_free(self.0.as_ptr() as *mut std::ffi::c_void); + } + } +} + +/// Validates the SPKAC data structure. +/// +/// Returns true if the signature in the SPKAC data is valid. +pub fn verify_spkac(data: &[u8]) -> bool { + let spki = match NetscapeSpki::from_base64(data) { + Ok(spki) => spki, + Err(_) => return false, + }; + + let pkey = match extract_public_key_from_spkac(&spki) { + Ok(pkey) => pkey, + Err(_) => return false, + }; + + spki.verify(&pkey) +} + +/// Extracts the public key from the SPKAC structure. +fn extract_public_key_from_spkac( + spki: &NetscapeSpki, +) -> Result { + // SAFETY: Extract public key with null checks and proper ownership + unsafe { + let spkac = spki.spkac()?; + let pubkey = spkac.pubkey; + if pubkey.is_null() { + return Err("No public key in SPKAC structure"); + } + + let pkey = aws_lc_sys::X509_PUBKEY_get(pubkey); + PKey::from_ptr(pkey).ok_or("Failed to extract public key from X509_PUBKEY") + } +} + +/// Exports the public key from the SPKAC data in PEM format. +pub fn export_public_key(data: &[u8]) -> Option> { + let spki = NetscapeSpki::from_base64(data).ok()?; + + let pkey = spki.get_public_key().ok()?; + + let bio = Bio::new_memory().ok()?; + // SAFETY: Write public key to BIO in PEM format, check result + unsafe { + let result = aws_lc_sys::PEM_write_bio_PUBKEY(bio.as_ptr(), pkey.as_ptr()); + if result <= 0 { + return None; + } + } + + bio.get_contents().ok() +} + +/// Exports the challenge string from the SPKAC data. +pub fn export_challenge(data: &[u8]) -> Option> { + let spki = NetscapeSpki::from_base64(data).ok()?; + + spki.get_challenge().ok() +} + +#[cfg(test)] +mod tests { + use crate::spki::verify_spkac; + + #[test] + fn test_md_spkac() { + // md4 and md5 based signatures are not supported. + // https://github.com/aws/aws-lc/commit/7e28b9ee89d85fbc80b69bc0eeb0070de81ac563 + let spkac_data = br#"MIICUzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC33FiIiiexwLe/P8DZx5HsqFlmUO7/lvJ7necJVNwqdZ3ax5jpQB0p6uxfqeOvzcN3k5V7UFb/Am+nkSNZMAZhsWzCU2Z4Pjh50QYz3f0Hour7/yIGStOLyYY3hgLK2K8TbhgjQPhdkw9+QtKlpvbL8fLgONAoGrVOFnRQGcr70iFffsm79mgZhKVMgYiHPJqJgGHvCtkGg9zMgS7p63+Q3ZWedtFS2RhMX3uCBy/mH6EOlRCNBbRmA4xxNzyf5GQaki3T+Iz9tOMjdPP+CwV2LqEdylmBuik8vrfTb3qIHLKKBAI8lXN26wWtA3kN4L7NP+cbKlCRlqctvhmylLH1AgMBAAEWE3RoaXMtaXMtYS1jaGFsbGVuZ2UwDQYJKoZIhvcNAQEEBQADggEBAIozmeW1kfDfAVwRQKileZGLRGCD7AjdHLYEe16xTBPve8Af1bDOyuWsAm4qQLYA4FAFROiKeGqxCtIErEvm87/09tCfF1My/1Uj+INjAk39DK9J9alLlTsrwSgd1lb3YlXY7TyitCmh7iXLo4pVhA2chNA3njiMq3CUpSvGbpzrESL2dv97lv590gUD988wkTDVyYsf0T8+X0Kww3AgPWGji+2f2i5/jTfD/s1lK1nqi7ZxFm0pGZoy1MJ51SCEy7Y82ajroI+5786nC02mo9ak7samca4YDZOoxN4d3tax4B/HDF5dqJSm1/31xYLDTfujCM5FkSjRc4m6hnriEkc="#; + + assert!(!verify_spkac(spkac_data)); + } + + #[test] + fn test_spkac_verify() { + let spkac = b"MIICUzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXzfKgGnkkOF7+VwMzGpiWy5nna/VGJOfPBsCVg5WooJHN9nAFyqLxoV0WyhwvIdHhIgcTX2L4BHRa+4B0zb4stRHK02ZknJvionK4kBfa+k7Q4DzasW3ulLCTXPLVBKzW9QSzE4Wult17BX6uSUy3Bpr/Nuk6B4Ja3JnFpdSYmJbWP55kRONFBZYPCXr7T8k6hzEHcevFE/PUi6IU+LKiwyGH5KXAUzRbMtqbZLn/rEAmEBxmv/z/+shAwiRE8s9RqBi+pVdwqWdw6ibNkbM7G3j4CMyfAk7EOpGf5loRIrVWB4XrVYWb2EQ6sd9LfiQ9GwqlFYw006MUo6nxoEtNAgMBAAEWE3RoaXMtaXMtYS1jaGFsbGVuZ2UwDQYJKoZIhvcNAQELBQADggEBAHUw1UoZjG7TCb/JhFo5p8XIFeizGEwYoqttBoVTQ+MeCfnNoLLeAyId0atb2jPnYsI25Z/PHHV1N9t0L/NelY3rZC/Z00Wx8IGeslnGXXbqwnp36Umb0r2VmxTr8z1QaToGyOQXp4Xor9qbQFoANIivyVUYsuqJ1FnDJCC/jBPo4IWiQbTst331v2fiVdV+/XUh9AIjcm4085b65HjFwLxDeWhbgAZ+UfhqBbTVA1K8uUqS8e3gbeaNstZvnclxZ3PlHSk8v1RdIG4e5ThTOwPH5u/7KKeafn9SwgY/Q8KqaVfHHCv1IeVlijamjnyFhWc35kGlBUNgLOnWAOE3GsM="; + assert!(verify_spkac(spkac)); + } +} diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index cad2505d66..fd68198d26 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -7,6 +7,9 @@ "echo.js", "elipses.txt", "empty.txt", + "keys/rsa_public.pem", + "keys/rsa_spkac_invalid.spkac", + "keys/rsa_spkac.spkac", "x.txt" ], "internet": [ @@ -380,6 +383,7 @@ "test-console-sync-write-error.js", "test-console-table.js", "test-console-tty-colors.js", + "test-crypto-certificate.js", "test-crypto-dh-constructor.js", "test-crypto-dh-errors.js", "test-crypto-dh-odd-key.js", diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md index 7359d49030..5f0ef9498c 100644 --- a/tests/node_compat/runner/TODO.md +++ b/tests/node_compat/runner/TODO.md @@ -488,7 +488,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co - [parallel/test-crypto-async-sign-verify.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-async-sign-verify.js) - [parallel/test-crypto-authenticated-stream.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-authenticated-stream.js) - [parallel/test-crypto-authenticated.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-authenticated.js) -- [parallel/test-crypto-certificate.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-certificate.js) - [parallel/test-crypto-cipheriv-decipheriv.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-cipheriv-decipheriv.js) - [parallel/test-crypto-classes.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-classes.js) - [parallel/test-crypto-des3-wrap.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-crypto-des3-wrap.js) diff --git a/tests/node_compat/test/fixtures/keys/rsa_public.pem b/tests/node_compat/test/fixtures/keys/rsa_public.pem new file mode 100644 index 0000000000..03f8436781 --- /dev/null +++ b/tests/node_compat/test/fixtures/keys/rsa_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl83yoBp5JDhe/lcDMxqY +lsuZ52v1RiTnzwbAlYOVqKCRzfZwBcqi8aFdFsocLyHR4SIHE19i+AR0WvuAdM2+ +LLURytNmZJyb4qJyuJAX2vpO0OA82rFt7pSwk1zy1QSs1vUEsxOFrpbdewV+rklM +twaa/zbpOgeCWtyZxaXUmJiW1j+eZETjRQWWDwl6+0/JOocxB3HrxRPz1IuiFPiy +osMhh+SlwFM0WzLam2S5/6xAJhAcZr/8//rIQMIkRPLPUagYvqVXcKlncOomzZGz +Oxt4+AjMnwJOxDqRn+ZaESK1VgeF61WFm9hEOrHfS34kPRsKpRWMNNOjFKOp8aBL +TQIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/node_compat/test/fixtures/keys/rsa_spkac.spkac b/tests/node_compat/test/fixtures/keys/rsa_spkac.spkac new file mode 100644 index 0000000000..066b496a88 --- /dev/null +++ b/tests/node_compat/test/fixtures/keys/rsa_spkac.spkac @@ -0,0 +1 @@ +MIICUzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXzfKgGnkkOF7+VwMzGpiWy5nna/VGJOfPBsCVg5WooJHN9nAFyqLxoV0WyhwvIdHhIgcTX2L4BHRa+4B0zb4stRHK02ZknJvionK4kBfa+k7Q4DzasW3ulLCTXPLVBKzW9QSzE4Wult17BX6uSUy3Bpr/Nuk6B4Ja3JnFpdSYmJbWP55kRONFBZYPCXr7T8k6hzEHcevFE/PUi6IU+LKiwyGH5KXAUzRbMtqbZLn/rEAmEBxmv/z/+shAwiRE8s9RqBi+pVdwqWdw6ibNkbM7G3j4CMyfAk7EOpGf5loRIrVWB4XrVYWb2EQ6sd9LfiQ9GwqlFYw006MUo6nxoEtNAgMBAAEWE3RoaXMtaXMtYS1jaGFsbGVuZ2UwDQYJKoZIhvcNAQELBQADggEBAHUw1UoZjG7TCb/JhFo5p8XIFeizGEwYoqttBoVTQ+MeCfnNoLLeAyId0atb2jPnYsI25Z/PHHV1N9t0L/NelY3rZC/Z00Wx8IGeslnGXXbqwnp36Umb0r2VmxTr8z1QaToGyOQXp4Xor9qbQFoANIivyVUYsuqJ1FnDJCC/jBPo4IWiQbTst331v2fiVdV+/XUh9AIjcm4085b65HjFwLxDeWhbgAZ+UfhqBbTVA1K8uUqS8e3gbeaNstZvnclxZ3PlHSk8v1RdIG4e5ThTOwPH5u/7KKeafn9SwgY/Q8KqaVfHHCv1IeVlijamjnyFhWc35kGlBUNgLOnWAOE3GsM= diff --git a/tests/node_compat/test/fixtures/keys/rsa_spkac_invalid.spkac b/tests/node_compat/test/fixtures/keys/rsa_spkac_invalid.spkac new file mode 100644 index 0000000000..0460a32b81 --- /dev/null +++ b/tests/node_compat/test/fixtures/keys/rsa_spkac_invalid.spkac @@ -0,0 +1 @@ +UzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC33FiIiiexwLe/P8DZx5HsqFlmUO7/lvJ7necJVNwqdZ3ax5jpQB0p6uxfqeOvzcN3k5V7UFb/Am+nkSNZMAZhsWzCU2Z4Pjh50QYz3f0Hour7/yIGStOLyYY3hgLK2K8TbhgjQPhdkw9+QtKlpvbL8fLgONAoGrVOFnRQGcr70iFffsm79mgZhKVMgYiHPJqJgGHvCtkGg9zMgS7p63+Q3ZWedtFS2RhMX3uCBy/mH6EOlRCNBbRmA4xxNzyf5GQaki3T+Iz9tOMjdPP+CwV2LqEdylmBuik8vrfTb3qIHLKKBAI8lXN26wWtA3kN4L7NP+cbKlCRlqctvhmylLH1AgMBAAEWE3RoaXMtaXMtYS1jaGFsbGVuZ2UwDQYJKoZIhvcNAQEEBQADggEBAIozmeW1kfDfAVwRQKileZGLRGCD7AjdHLYEe16xTBPve8Af1bDOyuWsAm4qQLYA4FAFROiKeGqxCtIErEvm87/09tCfF1My/1Uj+INjAk39DK9J9alLlTsrwSgd1lb3YlXY7TyitCmh7iXLo4pVhA2chNA3njiMq3CUpSvGbpzrESL2dv97lv590gUD988wkTDVyYsf0T8+X0Kww3AgPWGji+2f2i5/jTfD/s1lK1nqi7ZxFm0pGZoy1MJ51SCEy7Y82ajroI+5786nC02mo9ak7samca4YDZOoxN4d3tax4B/HDF5dqJSm1/31xYLDTfujCM5FkSjRc4m6hnriEkc= diff --git a/tests/node_compat/test/parallel/test-crypto-certificate.js b/tests/node_compat/test/parallel/test-crypto-certificate.js new file mode 100644 index 0000000000..20efdf077a --- /dev/null +++ b/tests/node_compat/test/parallel/test-crypto-certificate.js @@ -0,0 +1,128 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 23.9.0 +// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually. + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { Certificate } = crypto; +const fixtures = require('../common/fixtures'); + +// Test Certificates +const spkacValid = fixtures.readKey('rsa_spkac.spkac'); +const spkacChallenge = 'this-is-a-challenge'; +const spkacFail = fixtures.readKey('rsa_spkac_invalid.spkac'); +const spkacPublicPem = fixtures.readKey('rsa_public.pem'); + +function copyArrayBuffer(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +function checkMethods(certificate) { + + assert.strictEqual(certificate.verifySpkac(spkacValid), true); + assert.strictEqual(certificate.verifySpkac(spkacFail), false); + + assert.strictEqual( + stripLineEndings(certificate.exportPublicKey(spkacValid).toString('utf8')), + stripLineEndings(spkacPublicPem.toString('utf8')) + ); + assert.strictEqual(certificate.exportPublicKey(spkacFail), ''); + + assert.strictEqual( + certificate.exportChallenge(spkacValid).toString('utf8'), + spkacChallenge + ); + assert.strictEqual(certificate.exportChallenge(spkacFail), ''); + + const ab = copyArrayBuffer(spkacValid); + assert.strictEqual(certificate.verifySpkac(ab), true); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); +} + +{ + // Test maximum size of input buffer + let buf; + let skip = false; + try { + buf = Buffer.alloc(2 ** 31); + } catch { + // The allocation may fail on some systems. That is expected due + // to architecture and memory constraints. If it does, go ahead + // and skip this test. + skip = true; + } + if (!skip) { + assert.throws( + () => Certificate.verifySpkac(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportChallenge(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws( + () => Certificate.exportPublicKey(buf), { + code: 'ERR_OUT_OF_RANGE' + }); + } +} + +{ + // Test instance methods + checkMethods(new Certificate()); +} + +{ + // Test static methods + checkMethods(Certificate); +} + +function stripLineEndings(obj) { + return obj.replace(/\n/g, ''); +} + +// Direct call Certificate() should return instance +assert(Certificate() instanceof Certificate); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + assert.throws( + () => Certificate.verifySpkac(val), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +}); + +[1, {}, [], Infinity, true, undefined, null].forEach((val) => { + const errObj = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => Certificate.exportPublicKey(val), errObj); + assert.throws(() => Certificate.exportChallenge(val), errObj); +});