diff --git a/changelog.d/6836-node26-crypto-parity.md b/changelog.d/6836-node26-crypto-parity.md new file mode 100644 index 0000000000..a0bbc56258 --- /dev/null +++ b/changelog.d/6836-node26-crypto-parity.md @@ -0,0 +1 @@ +fix(crypto): #6783 match Node 26 X509 `checkIP` errors and KMAC edge behavior, including non-byte-aligned lengths and empty JWK keys. diff --git a/crates/perry-runtime/src/buffer/header.rs b/crates/perry-runtime/src/buffer/header.rs index b93938bb58..306d7ae27d 100644 --- a/crates/perry-runtime/src/buffer/header.rs +++ b/crates/perry-runtime/src/buffer/header.rs @@ -80,7 +80,7 @@ fn notify_crypto_key_death(addr: usize) { hook(addr); } -pub type CryptoKeyMeta = (u8, u8, u8, bool, u32); +pub type CryptoKeyMeta = (u8, u8, u8, bool, u32, u32); thread_local! { static BUFFER_REGISTRY: RefCell> = RefCell::new(new_ptr_hash_set()); @@ -290,6 +290,7 @@ pub fn mark_as_crypto_key(addr: usize, algo: u8, hash: u8, kind: u8) { kind, true, default_crypto_key_usages(algo, kind), + 0, ); } @@ -300,10 +301,11 @@ pub fn mark_as_crypto_key_with_flags( kind: u8, extractable: bool, usages: u32, + bit_length: u32, ) { CRYPTO_KEY_META_REGISTRY.with(|r| { r.borrow_mut() - .insert(addr, (algo, hash, kind, extractable, usages)); + .insert(addr, (algo, hash, kind, extractable, usages, bit_length)); }); } @@ -315,10 +317,11 @@ pub extern "C" fn js_buffer_mark_as_crypto_key_external( kind: u8, extractable: u8, usages: u32, + bit_length: u32, ) { register_buffer(addr as *const BufferHeader); mark_as_uint8array(addr); - mark_as_crypto_key_with_flags(addr, algo, hash, kind, extractable != 0, usages); + mark_as_crypto_key_with_flags(addr, algo, hash, kind, extractable != 0, usages, bit_length); // Latch BEFORE the insert — see js_buffer_register_external. EXTERNAL_BUFFERS_NONEMPTY.store(true, std::sync::atomic::Ordering::Release); if let Ok(mut r) = external_buffers().lock() { @@ -328,7 +331,10 @@ pub extern "C" fn js_buffer_mark_as_crypto_key_external( r.insert(addr); } if let Ok(mut r) = external_crypto_keys().lock() { - r.insert(addr, (algo, hash, kind, extractable != 0, usages)); + r.insert( + addr, + (algo, hash, kind, extractable != 0, usages, bit_length), + ); } } diff --git a/crates/perry-runtime/src/object/field_get_set/crypto_key.rs b/crates/perry-runtime/src/object/field_get_set/crypto_key.rs index 455a368986..064a22f877 100644 --- a/crates/perry-runtime/src/object/field_get_set/crypto_key.rs +++ b/crates/perry-runtime/src/object/field_get_set/crypto_key.rs @@ -23,9 +23,9 @@ const CRYPTO_USAGE_ENCAPSULATE_KEY: u32 = 1 << 10; const CRYPTO_USAGE_DECAPSULATE_KEY: u32 = 1 << 11; pub(crate) unsafe fn crypto_key_property_value(addr: usize, key_bytes: &[u8]) -> Option { - let (algo, hash, kind, extractable, usages) = crate::buffer::crypto_key_meta(addr)?; + let (algo, hash, kind, extractable, usages, bit_length) = crate::buffer::crypto_key_meta(addr)?; match key_bytes { - b"algorithm" => Some(crypto_key_algorithm_value(addr, algo, hash)), + b"algorithm" => Some(crypto_key_algorithm_value(addr, algo, hash, bit_length)), b"extractable" => Some(JSValue::bool(extractable)), b"type" => Some(string_value(match kind { 2 => "private", @@ -41,7 +41,7 @@ pub(crate) unsafe fn crypto_key_property_value(addr: usize, key_bytes: &[u8]) -> } } -unsafe fn crypto_key_algorithm_value(addr: usize, algo: u8, hash: u8) -> JSValue { +unsafe fn crypto_key_algorithm_value(addr: usize, algo: u8, hash: u8, bit_length: u32) -> JSValue { let obj = js_object_alloc(0, 3); if obj.is_null() { return JSValue::undefined(); @@ -56,7 +56,9 @@ unsafe fn crypto_key_algorithm_value(addr: usize, algo: u8, hash: u8) -> JSValue } if crypto_key_algorithm_has_length(algo) { let key = addr as *const crate::buffer::BufferHeader; - let bits = if key.is_null() { + let bits = if bit_length != 0 { + bit_length as f64 + } else if key.is_null() { 0.0 } else { crate::buffer::js_buffer_length(key) as f64 * 8.0 diff --git a/crates/perry-runtime/src/object/native_module_crypto_key_object.rs b/crates/perry-runtime/src/object/native_module_crypto_key_object.rs index 7164ea5997..a93c977c94 100644 --- a/crates/perry-runtime/src/object/native_module_crypto_key_object.rs +++ b/crates/perry-runtime/src/object/native_module_crypto_key_object.rs @@ -50,7 +50,8 @@ fn conversion_failed(detail: &str) -> ! { /// registry — never produced a KeyObject at all. pub(super) unsafe fn key_object_from(value: f64) -> f64 { let addr = value_addr(value); - let Some((_algo, _hash, kind, _extractable, _usages)) = crate::buffer::crypto_key_meta(addr) + let Some((_algo, _hash, kind, _extractable, _usages, _bit_length)) = + crate::buffer::crypto_key_meta(addr) else { invalid_key(value); }; diff --git a/crates/perry-stdlib/src/crypto/x509.rs b/crates/perry-stdlib/src/crypto/x509.rs index bc560db5cb..a42dc136ed 100644 --- a/crates/perry-stdlib/src/crypto/x509.rs +++ b/crates/perry-stdlib/src/crypto/x509.rs @@ -408,7 +408,7 @@ fn x509_check_ip_value(cert: &x509_cert::Certificate, ip: &str) -> Option().unwrap_or_else(|_| { perry_runtime::fs::validate::throw_type_error_with_code( - "Invalid IP", + "Invalid name", "ERR_INVALID_ARG_VALUE", ) }); diff --git a/crates/perry-stdlib/src/webcrypto/hmac.rs b/crates/perry-stdlib/src/webcrypto/hmac.rs index f5ba7eb9b3..8beb8eeebe 100644 --- a/crates/perry-stdlib/src/webcrypto/hmac.rs +++ b/crates/perry-stdlib/src/webcrypto/hmac.rs @@ -480,9 +480,6 @@ unsafe fn kmac_output_length(algo_bits: u64) -> Result bytes, + None if matches!(key_algo, KeyAlgo::Kmac128 | KeyAlgo::Kmac256) => { + return reject_with_dom_exception("DataError", "Invalid keyData"); + } + None => Vec::new(), + } } else { bytes_from_jsvalue(key_bits.to_bits()) }; if key_algo == KeyAlgo::ChaCha20Poly1305 && key_bytes.len() != 32 { return reject_with_dom_exception("DataError", "Invalid key length"); } - if key_bytes.is_empty() && matches!(key_algo, KeyAlgo::Kmac128 | KeyAlgo::Kmac256) { - return reject_with_dom_exception("DataError", "Zero-length key is not supported"); - } if key_bytes.is_empty() && !matches!( key_algo, @@ -268,6 +271,8 @@ pub unsafe extern "C" fn js_webcrypto_import_key( | KeyAlgo::Argon2d | KeyAlgo::Argon2i | KeyAlgo::Argon2id + | KeyAlgo::Kmac128 + | KeyAlgo::Kmac256 ) { return reject_with_dom_exception("DataError", "Key data is empty or could not be read"); @@ -801,9 +806,12 @@ pub(super) unsafe fn jwk_import_key_bytes( } } let k = object_field_string(obj_bits, b"k")?; - let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(k.as_bytes()) - .ok()?; + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(k.as_bytes()); + let bytes = if matches!(key_algo, KeyAlgo::Kmac128 | KeyAlgo::Kmac256) { + decoded.unwrap_or_default() + } else { + decoded.ok()? + }; if key_algo == KeyAlgo::AesOcb { if let Some(alg) = object_field_string(obj_bits, b"alg") { if let Some(expected) = aes_ocb_jwk_alg(bytes.len()) { diff --git a/crates/perry-stdlib/src/webcrypto/keys.rs b/crates/perry-stdlib/src/webcrypto/keys.rs index 0b0249e207..e686d9e639 100644 --- a/crates/perry-stdlib/src/webcrypto/keys.rs +++ b/crates/perry-stdlib/src/webcrypto/keys.rs @@ -657,20 +657,19 @@ pub unsafe extern "C" fn js_webcrypto_generate_key( "KmacKeyGenParams.length cannot be 0", ); } - if bit_len % 8 != 0 { - return reject_with_dom_exception( - "NotSupportedError", - "Unsupported KmacKeyGenParams.length", - ); - } - let mut key_bytes = vec![0u8; (bit_len / 8) as usize]; + let mut key_bytes = vec![0u8; bit_len.div_ceil(8) as usize]; use rand::RngCore; rand::rngs::OsRng.fill_bytes(&mut key_bytes); + if bit_len % 8 != 0 { + if let Some(last) = key_bytes.last_mut() { + *last &= 0xFF << (8 - bit_len % 8); + } + } let buf = alloc_uint8array_from_slice(&key_bytes); if buf.is_null() { return reject_with_dom_exception("OperationError", "The operation failed"); } - register_crypto_key( + register_crypto_key_with_bit_length( buf as usize, CryptoKeyMaterial::new( key_algo, @@ -679,6 +678,7 @@ pub unsafe extern "C" fn js_webcrypto_generate_key( extractable, usages, ), + bit_len, ); return resolve_with_bits(JSValue::pointer(buf as *const u8).bits()); } diff --git a/crates/perry-stdlib/src/webcrypto/util.rs b/crates/perry-stdlib/src/webcrypto/util.rs index 829a96ac7b..5976903cb1 100644 --- a/crates/perry-stdlib/src/webcrypto/util.rs +++ b/crates/perry-stdlib/src/webcrypto/util.rs @@ -72,6 +72,7 @@ extern "C" { kind: u8, extractable: u8, usages: u32, + bit_length: u32, ); } @@ -280,6 +281,14 @@ pub(super) static CRYPTO_KEY_REGISTRY: Lazy Option { .get(&buf_addr) .copied() .or_else(|| { - let (algo, hash, kind, extractable, usages) = + let (algo, hash, kind, extractable, usages, _bit_length) = perry_runtime::buffer::crypto_key_meta(buf_addr)?; let algo = match algo { 1 => KeyAlgo::Hmac, @@ -973,10 +983,7 @@ pub(super) fn compute_kmac( data: &[u8], output_bits: u32, ) -> Option> { - if output_bits % 8 != 0 { - return None; - } - let mut out = vec![0u8; (output_bits / 8) as usize]; + let mut out = vec![0u8; output_bits.div_ceil(8) as usize]; match algo { KeyAlgo::Kmac128 => { use sha3_010::digest::{core_api::CoreProxy, ExtendableOutput, Update, XofReader}; diff --git a/test-parity/node-suite/crypto/webcrypto/kmac.ts b/test-parity/node-suite/crypto/webcrypto/kmac.ts index 0ad467ee5a..c659bc9055 100644 --- a/test-parity/node-suite/crypto/webcrypto/kmac.ts +++ b/test-parity/node-suite/crypto/webcrypto/kmac.ts @@ -98,7 +98,12 @@ async function main() { await logReject("kmac128 bad usage", crypto.subtle.generateKey("KMAC128", true, ["encrypt" as any])); await logReject("kmac128 bad key length", crypto.subtle.generateKey({ name: "KMAC128", length: 7 }, true, ["sign"])); await logReject("kmac128 zero key", crypto.subtle.importKey("jwk", { kty: "oct", alg: "K128", k: "", ext: true, key_ops: ["sign"] }, "KMAC128", true, ["sign"])); - await logReject("kmac128 bad output", crypto.subtle.sign({ name: "KMAC128", outputLength: 7 }, key128, data)); + await logReject("kmac128 missing jwk key", crypto.subtle.importKey("jwk", { kty: "oct", alg: "K128", ext: true, key_ops: ["sign"] }, "KMAC128", true, ["sign"])); + const shortKey = await crypto.subtle.generateKey({ name: "KMAC128", length: 7 }, true, ["sign"]); + const shortJwk: any = await crypto.subtle.exportKey("jwk", shortKey); + console.log("kmac128 short key:", (shortKey.algorithm as any).length, Buffer.from(shortJwk.k, "base64url")[0] & 1); + const shortSig = await crypto.subtle.sign({ name: "KMAC128", outputLength: 7 }, key128, data); + console.log("kmac128 short output:", Buffer.from(shortSig).toString("hex")); } await main(); diff --git a/test-parity/node-suite/crypto/x509/signature-legacy-metadata.ts b/test-parity/node-suite/crypto/x509/signature-legacy-metadata.ts index aa198c36e2..0dbf408f4b 100644 --- a/test-parity/node-suite/crypto/x509/signature-legacy-metadata.ts +++ b/test-parity/node-suite/crypto/x509/signature-legacy-metadata.ts @@ -33,7 +33,7 @@ console.log("typeof toLegacyObject:", typeof toLegacy); if (typeof toLegacy !== "function") { console.log("legacy unavailable"); } else { - const legacy = toLegacy(); + const legacy = cert["toLegacyObject"](); console.log("legacy subject CN:", legacy["subject"]["CN"]); console.log("legacy issuer CN:", legacy["issuer"]["CN"]); console.log("legacy ca:", legacy["ca"]);