diff --git a/Cargo.lock b/Cargo.lock index b7c91a870..d713a4a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3320,6 +3320,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ + "ctutils", "subtle", "typenum", "zeroize", @@ -3886,6 +3887,16 @@ dependencies = [ "cpufeatures 0.3.0", ] +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "kqueue" version = "1.2.0" @@ -4369,6 +4380,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "const-oid 0.10.2", + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3 0.11.0", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.15" @@ -5825,6 +5862,7 @@ dependencies = [ "libc", "lru", "md-5 0.11.0", + "ml-kem", "mongodb", "nanoid", "once_cell", diff --git a/crates/perry-runtime/src/buffer/header.rs b/crates/perry-runtime/src/buffer/header.rs index a31d86223..fe5591efb 100644 --- a/crates/perry-runtime/src/buffer/header.rs +++ b/crates/perry-runtime/src/buffer/header.rs @@ -104,7 +104,8 @@ thread_local! { /// 15 ECDSA P-384, 16 ECDH P-384, 17 ECDSA P-521, /// 18 ECDH P-521, 19 Argon2d, 20 Argon2i, 21 Argon2id, /// 22 ChaCha20-Poly1305, 23 KMAC128, 24 KMAC256, 25 AES-OCB, - /// 26 X448, 27 Ed448 + /// 26 X448, 27 Ed448, 30 ML-KEM-512, 31 ML-KEM-768, + /// 32 ML-KEM-1024 /// hash: 1 SHA-1, 2 SHA-256, 3 SHA-384, 4 SHA-512 /// kind: 1 secret, 2 private, 3 public /// extractable: WebCrypto CryptoKey.extractable @@ -368,6 +369,10 @@ fn default_crypto_key_usages(algo: u8, kind: u8) -> u32 { const DERIVE_BITS: u32 = 1 << 5; const WRAP_KEY: u32 = 1 << 6; const UNWRAP_KEY: u32 = 1 << 7; + const ENCAPSULATE_BITS: u32 = 1 << 8; + const DECAPSULATE_BITS: u32 = 1 << 9; + const ENCAPSULATE_KEY: u32 = 1 << 10; + const DECAPSULATE_KEY: u32 = 1 << 11; match (algo, kind) { (1, 1) => SIGN | VERIFY, @@ -380,6 +385,8 @@ fn default_crypto_key_usages(algo: u8, kind: u8) -> u32 { (9 | 11 | 16 | 18 | 26, 2) => DERIVE_KEY | DERIVE_BITS, (13, 2) => DECRYPT | UNWRAP_KEY, (13, 3) => ENCRYPT | WRAP_KEY, + (30 | 31 | 32, 2) => DECAPSULATE_BITS | DECAPSULATE_KEY, + (30 | 31 | 32, 3) => ENCAPSULATE_BITS | ENCAPSULATE_KEY, _ => 0, } } diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 43567d51a..e3a68c048 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -22,6 +22,10 @@ const CRYPTO_USAGE_DERIVE_KEY: u32 = 1 << 4; const CRYPTO_USAGE_DERIVE_BITS: u32 = 1 << 5; const CRYPTO_USAGE_WRAP_KEY: u32 = 1 << 6; const CRYPTO_USAGE_UNWRAP_KEY: u32 = 1 << 7; +const CRYPTO_USAGE_ENCAPSULATE_BITS: u32 = 1 << 8; +const CRYPTO_USAGE_DECAPSULATE_BITS: u32 = 1 << 9; +const CRYPTO_USAGE_ENCAPSULATE_KEY: u32 = 1 << 10; +const CRYPTO_USAGE_DECAPSULATE_KEY: u32 = 1 << 11; unsafe fn crypto_key_property_value(addr: usize, key_bytes: &[u8]) -> Option { let (algo, hash, kind, extractable, usages) = crate::buffer::crypto_key_meta(addr)?; @@ -93,6 +97,9 @@ fn crypto_key_algorithm_name(algo: u8) -> &'static str { 25 => "AES-OCB", 26 => "X448", 27 => "Ed448", + 30 => "ML-KEM-512", + 31 => "ML-KEM-768", + 32 => "ML-KEM-1024", _ => "", } } @@ -133,6 +140,10 @@ unsafe fn crypto_key_usages_value(usages: u32) -> JSValue { (CRYPTO_USAGE_DERIVE_BITS, "deriveBits"), (CRYPTO_USAGE_WRAP_KEY, "wrapKey"), (CRYPTO_USAGE_UNWRAP_KEY, "unwrapKey"), + (CRYPTO_USAGE_ENCAPSULATE_BITS, "encapsulateBits"), + (CRYPTO_USAGE_DECAPSULATE_BITS, "decapsulateBits"), + (CRYPTO_USAGE_ENCAPSULATE_KEY, "encapsulateKey"), + (CRYPTO_USAGE_DECAPSULATE_KEY, "decapsulateKey"), ]; let count = entries.iter().filter(|(bit, _)| usages & *bit != 0).count(); let mut arr = crate::array::js_array_alloc(count as u32); diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 243ec003f..bba89bd4b 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -219,7 +219,7 @@ bundled-mongodb = ["dep:mongodb", "dep:bson", "dep:futures-util", "async-runtime # bindings so the well-known flip (#466 Phase 4 step 2) can route them # to perry-ext-bcrypt / perry-ext-argon2 without taking the rest of # the crypto surface offline. -crypto = ["dep:sha2", "dep:sha1", "dep:sha3", "dep:sha3_010", "dep:sha3-utils", "dep:rsa-sha1", "dep:md-5", "dep:hex", "dep:hmac", "dep:aes", "dep:cbc", "dep:ecb", "dep:ctr", "dep:scrypt", "dep:pbkdf2", "dep:base64", "dep:x25519-dalek", "dep:x448", "dep:ed25519-dalek", "dep:ed448-goldilocks", "dep:aes-gcm", "dep:chacha20poly1305", "dep:ghash", "dep:aes-kw", "dep:hkdf", "dep:p256", "dep:p384", "dep:p521", "dep:x509-cert", "async-runtime", "ids", "bundled-bcrypt", "bundled-argon2", "bundled-jsonwebtoken", "bundled-ethers"] +crypto = ["dep:sha2", "dep:sha1", "dep:sha3", "dep:sha3_010", "dep:sha3-utils", "dep:rsa-sha1", "dep:md-5", "dep:hex", "dep:hmac", "dep:aes", "dep:cbc", "dep:ecb", "dep:ctr", "dep:scrypt", "dep:pbkdf2", "dep:base64", "dep:x25519-dalek", "dep:x448", "dep:ed25519-dalek", "dep:ed448-goldilocks", "dep:aes-gcm", "dep:chacha20poly1305", "dep:ghash", "dep:aes-kw", "dep:hkdf", "dep:p256", "dep:p384", "dep:p521", "dep:x509-cert", "dep:ml-kem", "async-runtime", "ids", "bundled-bcrypt", "bundled-argon2", "bundled-jsonwebtoken", "bundled-ethers"] bundled-bcrypt = ["dep:bcrypt", "async-runtime"] bundled-argon2 = ["dep:argon2", "async-runtime"] bundled-jsonwebtoken = ["dep:jsonwebtoken", "dep:p256", "dep:rsa", "dep:spki"] @@ -394,6 +394,7 @@ ghash = { version = "0.5", optional = true } # `crypto` umbrella so it tracks the rest of the symmetric surface. aes-kw = { version = "0.3.0", optional = true } hkdf = { version = "0.13", optional = true } +ml-kem = { version = "0.3.2", features = ["pkcs8"], optional = true } # Compression flate2 = { version = "1.0", optional = true } diff --git a/crates/perry-stdlib/src/webcrypto/jwk.rs b/crates/perry-stdlib/src/webcrypto/jwk.rs index aeb065b5a..1f9e7f3e7 100644 --- a/crates/perry-stdlib/src/webcrypto/jwk.rs +++ b/crates/perry-stdlib/src/webcrypto/jwk.rs @@ -169,6 +169,24 @@ pub unsafe extern "C" fn js_webcrypto_import_key( KeyAlgo::X25519 }; (key_algo, HashAlgo::Sha256, kind) + } else if let Some(key_algo) = ml_kem_key_algo_from_name(&algo_upper) { + if format_lower == "spki" { + (key_algo, HashAlgo::Sha256, KeyKind::Public) + } else if format_lower == "pkcs8" { + (key_algo, HashAlgo::Sha256, KeyKind::Private) + } else if format_lower == "jwk" { + let kind = if object_field_string(key_bits.to_bits(), b"priv").is_some() { + KeyKind::Private + } else { + KeyKind::Public + }; + (key_algo, HashAlgo::Sha256, kind) + } else { + return reject_with_dom_exception( + "NotSupportedError", + "Unsupported algorithm for the given key format", + ); + } } else if (algo_upper == "RSA-OAEP" || algo_upper == "RSASSA-PKCS1-V1_5" || algo_upper == "RSA-PSS") @@ -224,6 +242,12 @@ pub unsafe extern "C" fn js_webcrypto_import_key( Ok(u) => u, Err((name, message)) => return reject_with_dom_exception(name, message), }; + if format_lower == "jwk" + && is_ml_kem_key_algo(key_algo) + && !jwk_key_ops_match(key_bits.to_bits(), key_algo, kind, usages) + { + return reject_with_dom_exception("DataError", "Key operations and usage mismatch"); + } let key_bytes = if format_lower == "jwk" { jwk_import_key_bytes(key_bits.to_bits(), key_algo, kind).unwrap_or_else(|| Vec::new()) @@ -330,6 +354,16 @@ pub unsafe extern "C" fn js_webcrypto_import_key( return reject_with_dom_exception("OperationError", "The operation failed"); } } + if is_ml_kem_key_algo(key_algo) { + let ok = if kind == KeyKind::Public { + ml_kem_public_bytes_from_der(key_algo, &key_bytes).is_some() + } else { + ml_kem_private_seed_and_public_from_der(key_algo, &key_bytes).is_some() + }; + if !ok { + return reject_with_dom_exception("DataError", "Invalid keyData"); + } + } let buf = alloc_uint8array_from_slice(&key_bytes); if buf.is_null() { return reject_with_dom_exception("OperationError", "The operation failed"); @@ -381,6 +415,33 @@ pub unsafe extern "C" fn js_webcrypto_export_key(format_bits: f64, key_bits: f64 "Unable to export AES-OCB secret key using raw format", ); } + if is_ml_kem_key_algo(mat.algo) { + if format_lower != "raw" + && format_lower != "spki" + && format_lower != "pkcs8" + && format_lower != "jwk" + { + return reject_with_dom_exception("OperationError", "The operation failed"); + } + let invalid_format = format_lower == "raw" + || (format_lower == "spki" && mat.kind != KeyKind::Public) + || (format_lower == "pkcs8" && mat.kind != KeyKind::Private); + if invalid_format { + return reject_with_dom_exception( + "NotSupportedError", + "Unsupported key format for ML-KEM key", + ); + } + let key_bytes = bytes_from_jsvalue(key_bits.to_bits()); + if format_lower == "jwk" { + let obj = match ml_kem_jwk_export_object(&key_bytes, mat) { + Some(o) => o, + None => return reject_with_dom_exception("OperationError", "The operation failed"), + }; + return resolve_with_bits(JSValue::pointer(obj as *const u8).bits()); + } + return resolve_with_bytes(&key_bytes); + } if format_lower == "raw" && mat.kind == KeyKind::Private { return reject_with_dom_exception("OperationError", "The operation failed"); } @@ -617,6 +678,86 @@ pub(super) unsafe fn jwk_okp_bytes( } } +unsafe fn set_object_value_field( + obj: *mut perry_runtime::ObjectHeader, + name: &[u8], + value: JSValue, +) { + let key = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name(obj, key, f64::from_bits(value.bits())); +} + +unsafe fn set_object_bool_field(obj: *mut perry_runtime::ObjectHeader, name: &[u8], value: bool) { + set_object_value_field(obj, name, JSValue::bool(value)); +} + +unsafe fn key_ops_array(usages: u32) -> JSValue { + let entries = [ + (USAGE_ENCAPSULATE_KEY, "encapsulateKey"), + (USAGE_ENCAPSULATE_BITS, "encapsulateBits"), + (USAGE_DECAPSULATE_KEY, "decapsulateKey"), + (USAGE_DECAPSULATE_BITS, "decapsulateBits"), + ]; + let mut arr = perry_runtime::js_array_alloc(0); + for (bit, name) in entries { + if usages & bit == 0 { + continue; + } + let s = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32); + arr = perry_runtime::js_array_push(arr, JSValue::string_ptr(s)); + } + JSValue::array_ptr(arr) +} + +pub(super) unsafe fn jwk_key_ops_match( + obj_bits: u64, + key_algo: KeyAlgo, + kind: KeyKind, + requested_usages: u32, +) -> bool { + let Some(bits) = object_field_bits(obj_bits, b"key_ops") else { + return false; + }; + let Some(key_ops) = key_usages_from_jsvalue(bits) else { + return false; + }; + key_ops & !supported_usages(key_algo, kind) == 0 && requested_usages & !key_ops == 0 +} + +pub(super) unsafe fn jwk_ml_kem_bytes( + obj_bits: u64, + key_algo: KeyAlgo, + kind: KeyKind, +) -> Option> { + let kty = object_field_string(obj_bits, b"kty")?; + if kty != "AKP" { + return None; + } + let alg = object_field_string(obj_bits, b"alg")?; + if alg != ml_kem_algorithm_name(key_algo)? { + return None; + } + let public_value = object_field_string(obj_bits, b"pub")?; + let public_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(public_value.as_bytes()) + .ok()?; + if kind == KeyKind::Public { + return ml_kem_public_der_from_bytes(key_algo, &public_bytes); + } + + let private_value = object_field_string(obj_bits, b"priv")?; + let seed_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(private_value.as_bytes()) + .ok()?; + let (private_der, public_der) = ml_kem_der_pair_from_seed(key_algo, &seed_bytes)?; + let derived_public = ml_kem_public_bytes_from_der(key_algo, &public_der)?; + if derived_public == public_bytes { + Some(private_der) + } else { + None + } +} + pub(super) unsafe fn jwk_import_key_bytes( obj_bits: u64, key_algo: KeyAlgo, @@ -632,6 +773,9 @@ pub(super) unsafe fn jwk_import_key_bytes( ) { return jwk_okp_bytes(obj_bits, key_algo, kind); } + if is_ml_kem_key_algo(key_algo) { + return jwk_ml_kem_bytes(obj_bits, key_algo, kind); + } if matches!( key_algo, KeyAlgo::Hmac @@ -816,6 +960,41 @@ pub(super) unsafe fn okp_jwk_export_object( Some(obj) } +pub(super) unsafe fn ml_kem_jwk_export_object( + key_bytes: &[u8], + mat: CryptoKeyMaterial, +) -> Option<*mut perry_runtime::ObjectHeader> { + let alg = ml_kem_algorithm_name(mat.algo)?; + let (public_bytes, private_seed) = if mat.kind == KeyKind::Private { + let (seed, public) = ml_kem_private_seed_and_public_from_der(mat.algo, key_bytes)?; + (public, Some(seed)) + } else { + (ml_kem_public_bytes_from_der(mat.algo, key_bytes)?, None) + }; + + let obj = js_object_alloc(0, if private_seed.is_some() { 6 } else { 5 }); + if obj.is_null() { + return None; + } + set_object_value_field(obj, b"key_ops", key_ops_array(mat.usages)); + set_object_bool_field(obj, b"ext", mat.extractable); + set_object_string_field(obj, b"kty", "AKP"); + set_object_string_field(obj, b"alg", alg); + set_object_string_field( + obj, + b"pub", + &base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&public_bytes), + ); + if let Some(seed) = private_seed { + set_object_string_field( + obj, + b"priv", + &base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&seed), + ); + } + Some(obj) +} + pub(super) unsafe fn ec_jwk_export_object( key_bytes: &[u8], mat: CryptoKeyMaterial, diff --git a/crates/perry-stdlib/src/webcrypto/keys.rs b/crates/perry-stdlib/src/webcrypto/keys.rs index b688a492e..0b0249e20 100644 --- a/crates/perry-stdlib/src/webcrypto/keys.rs +++ b/crates/perry-stdlib/src/webcrypto/keys.rs @@ -281,6 +281,80 @@ pub unsafe extern "C" fn js_webcrypto_generate_key( ); return resolve_with_bits(JSValue::pointer(obj as *const u8).bits()); } + if let Some(key_algo) = ml_kem_key_algo_from_name(&algo_upper) { + let bad_message = match key_algo { + KeyAlgo::MlKem512 => "Unsupported key usage for a ML-KEM-512 key", + KeyAlgo::MlKem768 => "Unsupported key usage for a ML-KEM-768 key", + KeyAlgo::MlKem1024 => "Unsupported key usage for a ML-KEM-1024 key", + _ => unreachable!(), + }; + let (private_usages, public_usages) = match validate_key_pair_usages( + key_algo, + usages_bits.to_bits(), + "Usages cannot be empty when creating a key.", + bad_message, + ) { + Ok(u) => u, + Err((name, message)) => return reject_with_dom_exception(name, message), + }; + if private_usages == 0 { + return reject_with_dom_exception( + "SyntaxError", + "Usages cannot be empty when creating a key.", + ); + } + + let mut seed = [0u8; 64]; + rand::rngs::OsRng.fill_bytes(&mut seed); + let (private_der, public_der) = match ml_kem_der_pair_from_seed(key_algo, &seed) { + Some(pair) => pair, + None => return reject_with_dom_exception("OperationError", "The operation failed"), + }; + + let private_buf = alloc_uint8array_from_slice(&private_der); + let public_buf = alloc_uint8array_from_slice(&public_der); + if private_buf.is_null() || public_buf.is_null() { + return reject_with_dom_exception("OperationError", "The operation failed"); + } + register_crypto_key( + private_buf as usize, + CryptoKeyMaterial::new( + key_algo, + HashAlgo::Sha256, + KeyKind::Private, + extractable, + private_usages, + ), + ); + register_crypto_key( + public_buf as usize, + CryptoKeyMaterial::new( + key_algo, + HashAlgo::Sha256, + KeyKind::Public, + true, + public_usages, + ), + ); + + let obj = js_object_alloc(0, 2); + if obj.is_null() { + return reject_with_dom_exception("OperationError", "The operation failed"); + } + let public_key_name = perry_runtime::js_string_from_bytes(b"publicKey".as_ptr(), 9); + let private_key_name = perry_runtime::js_string_from_bytes(b"privateKey".as_ptr(), 10); + js_object_set_field_by_name( + obj, + public_key_name, + f64::from_bits(JSValue::pointer(public_buf as *const u8).bits()), + ); + js_object_set_field_by_name( + obj, + private_key_name, + f64::from_bits(JSValue::pointer(private_buf as *const u8).bits()), + ); + return resolve_with_bits(JSValue::pointer(obj as *const u8).bits()); + } if algo_upper == "X448" { let (private_usages, public_usages) = match validate_key_pair_usages( KeyAlgo::X448, diff --git a/crates/perry-stdlib/src/webcrypto/supports.rs b/crates/perry-stdlib/src/webcrypto/supports.rs index 8d5f63ab3..9c0f1515a 100644 --- a/crates/perry-stdlib/src/webcrypto/supports.rs +++ b/crates/perry-stdlib/src/webcrypto/supports.rs @@ -26,7 +26,8 @@ unsafe fn algorithm_curve(bits: u64) -> Option { unsafe fn supports_generate_key(algorithm_bits: u64, algorithm: &str) -> bool { let object_form = algorithm_is_object(algorithm_bits); match algorithm { - "ED25519" | "ED448" | "X25519" | "X448" | "KMAC128" | "KMAC256" => true, + "ED25519" | "ED448" | "X25519" | "X448" | "KMAC128" | "KMAC256" | "ML-KEM-512" + | "ML-KEM-768" | "ML-KEM-1024" => true, "CHACHA20-POLY1305" => true, "HMAC" | "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW" => object_form, "ECDSA" | "ECDH" => { @@ -44,7 +45,8 @@ unsafe fn supports_import_key(algorithm_bits: u64, algorithm: &str) -> bool { match algorithm { "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW" | "AES-OCB" | "CHACHA20-POLY1305" | "PBKDF2" | "HKDF" | "ARGON2D" | "ARGON2I" | "ARGON2ID" | "ED25519" | "ED448" - | "X25519" | "X448" | "KMAC128" | "KMAC256" => true, + | "X25519" | "X448" | "KMAC128" | "KMAC256" | "ML-KEM-512" | "ML-KEM-768" + | "ML-KEM-1024" => true, "HMAC" => algorithm_is_object(algorithm_bits), "ECDSA" | "ECDH" => algorithm_curve(algorithm_bits) .as_deref() @@ -73,6 +75,9 @@ fn supports_export_key(algorithm: &str) -> bool { | "X448" | "KMAC128" | "KMAC256" + | "ML-KEM-512" + | "ML-KEM-768" + | "ML-KEM-1024" | "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" diff --git a/crates/perry-stdlib/src/webcrypto/util.rs b/crates/perry-stdlib/src/webcrypto/util.rs index 8437eaf37..c7221901b 100644 --- a/crates/perry-stdlib/src/webcrypto/util.rs +++ b/crates/perry-stdlib/src/webcrypto/util.rs @@ -50,6 +50,12 @@ pub(super) use rsa::{BigUint as RsaBigUint, Oaep, RsaPrivateKey, RsaPublicKey}; pub(super) use sha1::Sha1; pub(super) use sha2::{Digest as Sha2Digest, Sha256, Sha384, Sha512}; +pub(super) use ml_kem::kem::KeyExport as MlKemKeyExport; +pub(super) use ml_kem::pkcs8::{ + DecodePrivateKey as MlKemDecodePrivateKey, DecodePublicKey as MlKemDecodePublicKey, + EncodePrivateKey as MlKemEncodePrivateKey, EncodePublicKey as MlKemEncodePublicKey, +}; + pub(super) use perry_runtime::{ buffer::{buffer_alloc, buffer_data_mut, is_registered_buffer, BufferHeader}, js_object_alloc, js_object_set_field_by_name, js_promise_resolved, JSValue, Promise, @@ -128,6 +134,9 @@ pub(super) enum KeyAlgo { RsaPss, Kmac128, Kmac256, + MlKem512, + MlKem768, + MlKem1024, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -232,6 +241,10 @@ pub(super) const USAGE_DERIVE_KEY: u32 = 1 << 4; pub(super) const USAGE_DERIVE_BITS: u32 = 1 << 5; pub(super) const USAGE_WRAP_KEY: u32 = 1 << 6; pub(super) const USAGE_UNWRAP_KEY: u32 = 1 << 7; +pub(super) const USAGE_ENCAPSULATE_BITS: u32 = 1 << 8; +pub(super) const USAGE_DECAPSULATE_BITS: u32 = 1 << 9; +pub(super) const USAGE_ENCAPSULATE_KEY: u32 = 1 << 10; +pub(super) const USAGE_DECAPSULATE_KEY: u32 = 1 << 11; #[derive(Copy, Clone, Debug)] pub(super) struct CryptoKeyMaterial { @@ -309,6 +322,9 @@ fn runtime_algo_id(algo: KeyAlgo) -> u8 { KeyAlgo::AesOcb => 25, KeyAlgo::X448 => 26, KeyAlgo::Ed448 => 27, + KeyAlgo::MlKem512 => 30, + KeyAlgo::MlKem768 => 31, + KeyAlgo::MlKem1024 => 32, } } @@ -366,6 +382,9 @@ pub(super) fn lookup_crypto_key(buf_addr: usize) -> Option { 25 => KeyAlgo::AesOcb, 26 => KeyAlgo::X448, 27 => KeyAlgo::Ed448, + 30 => KeyAlgo::MlKem512, + 31 => KeyAlgo::MlKem768, + 32 => KeyAlgo::MlKem1024, _ => return None, }; let hash = match hash { @@ -557,6 +576,10 @@ pub(super) fn usage_bit(name: &str) -> Option { "deriveBits" => Some(USAGE_DERIVE_BITS), "wrapKey" => Some(USAGE_WRAP_KEY), "unwrapKey" => Some(USAGE_UNWRAP_KEY), + "encapsulateBits" => Some(USAGE_ENCAPSULATE_BITS), + "decapsulateBits" => Some(USAGE_DECAPSULATE_BITS), + "encapsulateKey" => Some(USAGE_ENCAPSULATE_KEY), + "decapsulateKey" => Some(USAGE_DECAPSULATE_KEY), _ => None, } } @@ -629,10 +652,163 @@ pub(super) fn supported_usages(algo: KeyAlgo, kind: KeyKind) -> u32 { ) => 0, (KeyAlgo::RsaOaep, KeyKind::Public) => USAGE_ENCRYPT | USAGE_WRAP_KEY, (KeyAlgo::RsaOaep, KeyKind::Private) => USAGE_DECRYPT | USAGE_UNWRAP_KEY, + (KeyAlgo::MlKem512 | KeyAlgo::MlKem768 | KeyAlgo::MlKem1024, KeyKind::Public) => { + USAGE_ENCAPSULATE_BITS | USAGE_ENCAPSULATE_KEY + } + (KeyAlgo::MlKem512 | KeyAlgo::MlKem768 | KeyAlgo::MlKem1024, KeyKind::Private) => { + USAGE_DECAPSULATE_BITS | USAGE_DECAPSULATE_KEY + } _ => 0, } } +pub(super) fn ml_kem_key_algo_from_name(name: &str) -> Option { + match name { + "ML-KEM-512" => Some(KeyAlgo::MlKem512), + "ML-KEM-768" => Some(KeyAlgo::MlKem768), + "ML-KEM-1024" => Some(KeyAlgo::MlKem1024), + _ => None, + } +} + +pub(super) fn is_ml_kem_key_algo(algo: KeyAlgo) -> bool { + matches!( + algo, + KeyAlgo::MlKem512 | KeyAlgo::MlKem768 | KeyAlgo::MlKem1024 + ) +} + +pub(super) fn ml_kem_algorithm_name(algo: KeyAlgo) -> Option<&'static str> { + match algo { + KeyAlgo::MlKem512 => Some("ML-KEM-512"), + KeyAlgo::MlKem768 => Some("ML-KEM-768"), + KeyAlgo::MlKem1024 => Some("ML-KEM-1024"), + _ => None, + } +} + +pub(super) fn ml_kem_der_pair_from_seed( + algo: KeyAlgo, + seed_bytes: &[u8], +) -> Option<(Vec, Vec)> { + let seed = ml_kem::Seed::try_from(seed_bytes).ok()?; + match algo { + KeyAlgo::MlKem512 => { + let private = ml_kem::DecapsulationKey512::from_seed(seed); + let private_der = private.to_pkcs8_der().ok()?.as_bytes().to_vec(); + let public_der = private + .encapsulation_key() + .to_public_key_der() + .ok()? + .as_bytes() + .to_vec(); + Some((private_der, public_der)) + } + KeyAlgo::MlKem768 => { + let private = ml_kem::DecapsulationKey768::from_seed(seed); + let private_der = private.to_pkcs8_der().ok()?.as_bytes().to_vec(); + let public_der = private + .encapsulation_key() + .to_public_key_der() + .ok()? + .as_bytes() + .to_vec(); + Some((private_der, public_der)) + } + KeyAlgo::MlKem1024 => { + let private = ml_kem::DecapsulationKey1024::from_seed(seed); + let private_der = private.to_pkcs8_der().ok()?.as_bytes().to_vec(); + let public_der = private + .encapsulation_key() + .to_public_key_der() + .ok()? + .as_bytes() + .to_vec(); + Some((private_der, public_der)) + } + _ => None, + } +} + +pub(super) fn ml_kem_public_bytes_from_der(algo: KeyAlgo, der: &[u8]) -> Option> { + match algo { + KeyAlgo::MlKem512 => Some( + ml_kem::EncapsulationKey512::from_public_key_der(der) + .ok()? + .to_bytes() + .as_slice() + .to_vec(), + ), + KeyAlgo::MlKem768 => Some( + ml_kem::EncapsulationKey768::from_public_key_der(der) + .ok()? + .to_bytes() + .as_slice() + .to_vec(), + ), + KeyAlgo::MlKem1024 => Some( + ml_kem::EncapsulationKey1024::from_public_key_der(der) + .ok()? + .to_bytes() + .as_slice() + .to_vec(), + ), + _ => None, + } +} + +pub(super) fn ml_kem_private_seed_and_public_from_der( + algo: KeyAlgo, + der: &[u8], +) -> Option<(Vec, Vec)> { + match algo { + KeyAlgo::MlKem512 => { + let private = ml_kem::DecapsulationKey512::from_pkcs8_der(der).ok()?; + Some(( + private.to_seed()?.as_slice().to_vec(), + private.encapsulation_key().to_bytes().as_slice().to_vec(), + )) + } + KeyAlgo::MlKem768 => { + let private = ml_kem::DecapsulationKey768::from_pkcs8_der(der).ok()?; + Some(( + private.to_seed()?.as_slice().to_vec(), + private.encapsulation_key().to_bytes().as_slice().to_vec(), + )) + } + KeyAlgo::MlKem1024 => { + let private = ml_kem::DecapsulationKey1024::from_pkcs8_der(der).ok()?; + Some(( + private.to_seed()?.as_slice().to_vec(), + private.encapsulation_key().to_bytes().as_slice().to_vec(), + )) + } + _ => None, + } +} + +pub(super) fn ml_kem_public_der_from_bytes(algo: KeyAlgo, public_bytes: &[u8]) -> Option> { + match algo { + KeyAlgo::MlKem512 => { + let public = ml_kem::Key::::try_from(public_bytes).ok()?; + let key = ml_kem::EncapsulationKey512::new(&public).ok()?; + Some(key.to_public_key_der().ok()?.as_bytes().to_vec()) + } + KeyAlgo::MlKem768 => { + let public = ml_kem::Key::::try_from(public_bytes).ok()?; + let key = ml_kem::EncapsulationKey768::new(&public).ok()?; + Some(key.to_public_key_der().ok()?.as_bytes().to_vec()) + } + KeyAlgo::MlKem1024 => { + let public = + ml_kem::Key::::try_from(public_bytes).ok()?; + let key = ml_kem::EncapsulationKey1024::new(&public).ok()?; + Some(key.to_public_key_der().ok()?.as_bytes().to_vec()) + } + _ => None, + } +} + pub(super) unsafe fn validate_key_usages( algo: KeyAlgo, kind: KeyKind, diff --git a/test-parity/node-suite/crypto/webcrypto/mlkem-key-material.ts b/test-parity/node-suite/crypto/webcrypto/mlkem-key-material.ts new file mode 100644 index 000000000..8536e0efd --- /dev/null +++ b/test-parity/node-suite/crypto/webcrypto/mlkem-key-material.ts @@ -0,0 +1,104 @@ +import { webcrypto } from "node:crypto"; + +(process as any).emitWarning = () => undefined; + +const subtle = webcrypto.subtle; +const variants = ["ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"] as const; + +const b64Bytes = (value?: string) => value ? Buffer.from(value, "base64url").byteLength : -1; +const tamper = (value: string) => { + const bytes = Buffer.from(value, "base64url"); + bytes[0] ^= 1; + return bytes.toString("base64url"); +}; + +const rejectName = async (label: string, fn: () => Promise) => { + try { + await fn(); + console.log(`${label}: ok`); + } catch (error: any) { + console.log(`${label}:`, error.name); + } +}; + +for (const algorithm of variants) { + console.log(`algorithm: ${algorithm}`); + console.log("supports generate:", SubtleCrypto.supports("generateKey", algorithm)); + console.log("supports import:", SubtleCrypto.supports("importKey", algorithm)); + console.log("supports export:", SubtleCrypto.supports("exportKey", algorithm)); + + const pair = await subtle.generateKey({ name: algorithm }, true, ["decapsulateBits"]); + console.log("generated alg:", pair.publicKey.algorithm.name, pair.privateKey.algorithm.name); + console.log("generated type:", pair.publicKey.type, pair.privateKey.type); + console.log("generated usages:", JSON.stringify(pair.publicKey.usages), JSON.stringify(pair.privateKey.usages)); + console.log("generated extractable:", pair.publicKey.extractable, pair.privateKey.extractable); + + const spki = await subtle.exportKey("spki", pair.publicKey); + const pkcs8 = await subtle.exportKey("pkcs8", pair.privateKey); + const publicJwk = await subtle.exportKey("jwk", pair.publicKey) as JsonWebKey & { pub?: string; priv?: string }; + const privateJwk = await subtle.exportKey("jwk", pair.privateKey) as JsonWebKey & { pub?: string; priv?: string }; + console.log("der lens:", (spki as ArrayBuffer).byteLength, (pkcs8 as ArrayBuffer).byteLength); + console.log( + "jwk public:", + publicJwk.kty, + publicJwk.alg, + b64Bytes(publicJwk.pub), + !!publicJwk.priv, + JSON.stringify(publicJwk.key_ops), + publicJwk.ext, + ); + console.log( + "jwk private:", + privateJwk.kty, + privateJwk.alg, + b64Bytes(privateJwk.pub), + b64Bytes(privateJwk.priv), + JSON.stringify(privateJwk.key_ops), + privateJwk.ext, + ); + + const importedSpki = await subtle.importKey("spki", spki, algorithm, true, ["encapsulateBits"]); + const importedPkcs8 = await subtle.importKey("pkcs8", pkcs8, algorithm, true, ["decapsulateBits"]); + console.log("imported spki:", importedSpki.algorithm.name, importedSpki.type, JSON.stringify(importedSpki.usages)); + console.log("imported pkcs8:", importedPkcs8.algorithm.name, importedPkcs8.type, JSON.stringify(importedPkcs8.usages)); + + const importedJwkPublic = await subtle.importKey( + "jwk", + { ...publicJwk, key_ops: ["encapsulateBits"] }, + algorithm, + true, + ["encapsulateBits"], + ); + const importedJwkPrivate = await subtle.importKey("jwk", privateJwk, algorithm, true, ["decapsulateBits"]); + const roundtripPrivateJwk = await subtle.exportKey("jwk", importedJwkPrivate) as JsonWebKey & { + pub?: string; + priv?: string; + }; + console.log("imported jwk public:", importedJwkPublic.type, JSON.stringify(importedJwkPublic.usages)); + console.log("imported jwk private:", importedJwkPrivate.type, JSON.stringify(importedJwkPrivate.usages)); + console.log( + "jwk private roundtrip:", + roundtripPrivateJwk.pub === privateJwk.pub, + roundtripPrivateJwk.priv === privateJwk.priv, + ); + + await rejectName("export raw public", () => subtle.exportKey("raw", pair.publicKey)); + await rejectName("export raw private", () => subtle.exportKey("raw", pair.privateKey)); + await rejectName("generate encap only", () => subtle.generateKey(algorithm, true, ["encapsulateBits"])); + await rejectName("generate bad usage", () => subtle.generateKey(algorithm, true, ["deriveBits"] as any)); + await rejectName("import public decap usage", () => + subtle.importKey("spki", spki, algorithm, true, ["decapsulateBits"] as any), + ); + await rejectName("import jwk keyops mismatch", () => + subtle.importKey("jwk", publicJwk, algorithm, true, ["encapsulateBits"]), + ); + await rejectName("import jwk tamper pub", () => + subtle.importKey( + "jwk", + { ...privateJwk, pub: tamper(privateJwk.pub!) }, + algorithm, + true, + ["decapsulateBits"], + ), + ); +} diff --git a/test-parity/node-suite/crypto/webcrypto/subtle-supports.ts b/test-parity/node-suite/crypto/webcrypto/subtle-supports.ts index 703264189..0ea53cc63 100644 --- a/test-parity/node-suite/crypto/webcrypto/subtle-supports.ts +++ b/test-parity/node-suite/crypto/webcrypto/subtle-supports.ts @@ -39,16 +39,21 @@ for (const [label, op, algorithm] of [ ["verify ed448", "verify", "Ed448"], ["generate x448", "generateKey", "X448"], ["generate ed448", "generateKey", "Ed448"], + ["generate mlkem512", "generateKey", "ML-KEM-512"], + ["generate mlkem768", "generateKey", "ML-KEM-768"], + ["generate mlkem1024", "generateKey", "ML-KEM-1024"], ["generate aes object", "generateKey", { name: "AES-GCM", length: 128 }], ["generate ecdh p256", "generateKey", { name: "ECDH", namedCurve: "P-256" }], ["import aes", "importKey", "AES-GCM"], ["import aes ocb", "importKey", "AES-OCB"], ["import x448", "importKey", "X448"], ["import ed448", "importKey", "Ed448"], + ["import mlkem768", "importKey", "ML-KEM-768"], ["export rsa", "exportKey", "RSA-OAEP"], ["export aes ocb", "exportKey", "AES-OCB"], ["export x448", "exportKey", "X448"], ["export ed448", "exportKey", "Ed448"], + ["export mlkem768", "exportKey", "ML-KEM-768"], ["digest sha3 false", "digest", "SHA-3-256"], ["encrypt aes ocb false", "encrypt", "AES-OCB"],