diff --git a/Cargo.lock b/Cargo.lock index 9376a65..f2859e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ name = "dhkem" version = "0.1.0" dependencies = [ "ctutils", + "curve25519-dalek", "elliptic-curve", "getrandom", "hex-literal", @@ -372,6 +373,7 @@ dependencies = [ "p256", "p384", "p521", + "rand", "rand_core", "sha2", "x25519-dalek", @@ -1030,9 +1032,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom", diff --git a/dhkem/Cargo.toml b/dhkem/Cargo.toml index a6e7da4..22344da 100644 --- a/dhkem/Cargo.toml +++ b/dhkem/Cargo.toml @@ -26,12 +26,14 @@ p256 = { version = "0.14", optional = true, default-features = false, features = p384 = { version = "0.14", optional = true, default-features = false, features = ["arithmetic"] } p521 = { version = "0.14", optional = true, default-features = false, features = ["arithmetic"] } x25519 = { version = "3", package = "x25519-dalek", optional = true, default-features = false } +curve25519-dalek = { version = "5", optional = true, default-features = false } zeroize = { version = "1.9", optional = true, default-features = false } [dev-dependencies] getrandom = { version = "0.4", features = ["sys_rng"] } hex-literal = "1" hkdf = "0.13" +rand = "0.10.3" sha2 = "0.11" [features] @@ -43,7 +45,7 @@ k256 = ["dep:k256", "ecdh"] p256 = ["dep:p256", "ecdh"] p384 = ["dep:p384", "ecdh"] p521 = ["dep:p521", "ecdh"] -x25519 = ["dep:x25519", "x25519/static_secrets"] +x25519 = ["dep:x25519", "dep:curve25519-dalek", "x25519/static_secrets"] zeroize = ["dep:zeroize"] [lints] diff --git a/dhkem/src/x25519_kem.rs b/dhkem/src/x25519_kem.rs index db4df39..7f3b614 100644 --- a/dhkem/src/x25519_kem.rs +++ b/dhkem/src/x25519_kem.rs @@ -2,6 +2,7 @@ use crate::{DecapsulationKey, EncapsulationKey, Error, HpkeKemId}; use ctutils::CtEq; +use curve25519_dalek::scalar::clamp_integer; use kem::{ Decapsulator, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeyInit, KeySizeUser, TryDecapsulate, TryKeyInit, @@ -74,7 +75,7 @@ impl KeyInit for X25519DecapsulationKey { /// [RFC9810 ยง7.1.2]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.2 impl KeyExport for X25519DecapsulationKey { fn to_bytes(&self) -> Key { - self.dk.to_bytes().into() + clamp_integer(self.dk.to_bytes()).into() } } @@ -179,3 +180,20 @@ impl Encapsulate for X25519EncapsulationKey { (pk.to_bytes().into(), ss.to_bytes().into()) } } + +// Regression test: DHKEM(X25519) serialized private keys MUST be clamped +// Issue: https://github.com/RustCrypto/KEMs/issues/401 +#[test] +fn regression_unclamped_privkey() { + let mut rng = rand::rng(); + + // Clamping affects 5 bits. The likelihood that they're all set correctly by chance is 2^-5. + // The liklihood that this happens 30 times in a row is 2^(-150). + for _ in 0..30 { + let dk = X25519DecapsulationKey::generate_from_rng(&mut rng); + let dk_bytes = dk.to_bytes().into(); + // You can test it's clamped by just clamping again and seeing if anything changed. This is + // because clamping is idempotent. + assert_eq!(dk_bytes, clamp_integer(dk_bytes)); + } +}