diff --git a/libcrux-kem/CHANGELOG.md b/libcrux-kem/CHANGELOG.md index 27680b9862..d6d026f897 100644 --- a/libcrux-kem/CHANGELOG.md +++ b/libcrux-kem/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- [#1560](https://github.com/celabshq/libcrux/pull/1560): Reject malformed hybrid key encodings without panicking + ## [0.0.9] (2026-07-15) ### Changed diff --git a/libcrux-kem/src/kem.rs b/libcrux-kem/src/kem.rs index 062f54f2fa..31daee0718 100644 --- a/libcrux-kem/src/kem.rs +++ b/libcrux-kem/src/kem.rs @@ -156,13 +156,13 @@ pub struct X25519MlKem768Draft00PrivateKey { impl X25519MlKem768Draft00PrivateKey { pub fn decode(bytes: &[u8]) -> Result { + let key: &[u8; MlKem768PrivateKey::len() + 32] = + bytes.try_into().map_err(|_| Error::InvalidPrivateKey)?; + let (mlkem, x25519) = key.split_at(MlKem768PrivateKey::len()); + Ok(Self { - mlkem: bytes[..2400] - .try_into() - .map_err(|_| Error::InvalidPrivateKey)?, - x25519: bytes[2400..] - .try_into() - .map_err(|_| Error::InvalidPrivateKey)?, + mlkem: mlkem.try_into().map_err(|_| Error::InvalidPrivateKey)?, + x25519: x25519.try_into().map_err(|_| Error::InvalidPrivateKey)?, }) } @@ -210,18 +210,20 @@ pub struct X25519MlKem768Draft00PublicKey { impl X25519MlKem768Draft00PublicKey { pub fn decode(bytes: &[u8]) -> Result { + let key: &[u8; MlKem768PublicKey::len() + 32] = + bytes.try_into().map_err(|_| Error::InvalidPublicKey)?; + let (mlkem, x25519) = key.split_at(MlKem768PublicKey::len()); + Ok(Self { mlkem: { - let key = MlKem768PublicKey::try_from(&bytes[..1184]) - .map_err(|_| Error::InvalidPublicKey)?; + let key = + MlKem768PublicKey::try_from(mlkem).map_err(|_| Error::InvalidPublicKey)?; if !mlkem768::validate_public_key(&key) { return Err(Error::InvalidPublicKey); } key }, - x25519: bytes[1184..] - .try_into() - .map_err(|_| Error::InvalidPublicKey)?, + x25519: x25519.try_into().map_err(|_| Error::InvalidPublicKey)?, }) } @@ -241,18 +243,19 @@ pub struct XWingKemDraft06PublicKey { impl XWingKemDraft06PublicKey { pub fn decode(bytes: &[u8]) -> Result { + let key: &[u8; MlKem768PublicKey::len() + 32] = + bytes.try_into().map_err(|_| Error::InvalidPublicKey)?; + let (pk_m, pk_x) = key.split_at(MlKem768PublicKey::len()); + Ok(Self { pk_m: { - let key = MlKem768PublicKey::try_from(&bytes[0..1184]) - .map_err(|_| Error::InvalidPublicKey)?; + let key = MlKem768PublicKey::try_from(pk_m).map_err(|_| Error::InvalidPublicKey)?; if !mlkem768::validate_public_key(&key) { return Err(Error::InvalidPublicKey); } key }, - pk_x: bytes[1184..] - .try_into() - .map_err(|_| Error::InvalidPublicKey)?, + pk_x: pk_x.try_into().map_err(|_| Error::InvalidPublicKey)?, }) } diff --git a/libcrux-kem/tests/decode.rs b/libcrux-kem/tests/decode.rs new file mode 100644 index 0000000000..731182e86c --- /dev/null +++ b/libcrux-kem/tests/decode.rs @@ -0,0 +1,69 @@ +use libcrux_kem::{ + key_gen, + Algorithm::{X25519MlKem768Draft00, XWingKemDraft06}, + Error, MlKem768PrivateKey, MlKem768PublicKey, PrivateKey, PublicKey, +}; + +const X25519_KEY_LEN: usize = 32; + +#[test] +fn hybrid_private_key_decode_rejects_invalid_lengths() { + const MLKEM_KEY_LEN: usize = MlKem768PrivateKey::len(); + const KEY_LEN: usize = MLKEM_KEY_LEN + X25519_KEY_LEN; + + for len in [ + 0, + 1, + MLKEM_KEY_LEN - 1, + MLKEM_KEY_LEN, + KEY_LEN - 1, + KEY_LEN + 1, + ] { + let encoded = vec![0; len]; + assert!( + matches!( + PrivateKey::decode(X25519MlKem768Draft00, &encoded), + Err(Error::InvalidPrivateKey) + ), + "accepted an invalid {len}-byte private key" + ); + } +} + +#[test] +fn hybrid_public_key_decode_rejects_invalid_lengths() { + const MLKEM_KEY_LEN: usize = MlKem768PublicKey::len(); + const KEY_LEN: usize = MLKEM_KEY_LEN + X25519_KEY_LEN; + + for algorithm in [X25519MlKem768Draft00, XWingKemDraft06] { + for len in [ + 0, + 1, + MLKEM_KEY_LEN - 1, + MLKEM_KEY_LEN, + KEY_LEN - 1, + KEY_LEN + 1, + ] { + let encoded = vec![0; len]; + assert!( + matches!( + PublicKey::decode(algorithm, &encoded), + Err(Error::InvalidPublicKey) + ), + "{algorithm:?} accepted an invalid {len}-byte public key" + ); + } + } +} + +#[test] +fn hybrid_keys_round_trip_decoding() { + let mut rng = rand::rng(); + + for algorithm in [X25519MlKem768Draft00, XWingKemDraft06] { + let (private_key, public_key) = key_gen(algorithm, &mut rng).unwrap(); + + assert!(PrivateKey::decode(algorithm, &private_key.encode()).is_ok()); + assert!(PublicKey::decode(algorithm, &public_key.encode()).is_ok()); + } +}