Skip to content

Commit eb16b16

Browse files
committed
xwing: Perform FIPS 203 key-checks on ML-KEM-768 for encapsulation key part of X-Wing
1 parent 20baa42 commit eb16b16

6 files changed

Lines changed: 42 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- [Breaking change] `orion::hazardous::ecc::x25519::PublicKey` no longer stores the u-coordinate in masked form, but original byte slice. The `PartialEq` still respects (applies masking) the u-coordinate condition. Masking is applied before Montgomery ladder.
2121
- [Breaking change] `orion::hazardous::ecc::x25519::SecretKey` no longer stores the clamped scalar, but the original byte slice. This changes the inherited `PartialEq`, which now operates on the original bytes, not the clamped. Clamping is applied before Montgomery ladder.
2222
- [Breaking change] `orion::hazardous::ecc::x25519::SharedSecret` now respects (applies masking) the u-coordinate condition for `PartialEq`.
23+
- [Breaking change] `orion::hazardous::kem::xwing::EncapsulationKey` now fails on `TryFrom<&[u8]>` if the ML-KEM-768 public-part does not pass the FIPS-203 keys checks.
2324

2425
- MSRV bumped to `1.87`
2526
- Add constants for BLAKE2b: `BLAKE2B_MIN_OUTSIZE, BLAKE2B_MAX_OUTSIZE, BLAKE2B_MIN_KEYSIZE, BLAKE2B_MAX_KEYSIZE` making the conditions more discernable.

src/hazardous/kem/ml_kem/mlkem1024.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ mod tests {
405405

406406
#[test]
407407
#[cfg(feature = "serde")]
408-
fn test_encapuslation_key_serialization() {
408+
fn test_encapsulation_key_serialization() {
409409
use crate::test_framework::newtypes::public::PublicNewtype;
410410
PublicNewtype::test_serialization::<EK_SIZE, MlKem1024EncapKey>();
411411
}

src/hazardous/kem/ml_kem/mlkem512.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ mod tests {
405405

406406
#[test]
407407
#[cfg(feature = "serde")]
408-
fn test_encapuslation_key_serialization() {
408+
fn test_encapsulation_key_serialization() {
409409
use crate::test_framework::newtypes::public::PublicNewtype;
410410
PublicNewtype::test_serialization::<EK_SIZE, MlKem512EncapKey>();
411411
}

src/hazardous/kem/ml_kem/mlkem768.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ mod tests {
405405

406406
#[test]
407407
#[cfg(feature = "serde")]
408-
fn test_encapuslation_key_serialization() {
408+
fn test_encapsulation_key_serialization() {
409409
use crate::test_framework::newtypes::public::PublicNewtype;
410410
PublicNewtype::test_serialization::<EK_SIZE, MlKem768EncapKey>();
411411
}

src/hazardous/kem/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
pub mod x25519_hkdf_sha256;
2525

2626
/// ML-KEM as specified in [FIPS-203](https://doi.org/10.6028/NIST.FIPS.203).
27-
mod ml_kem;
27+
pub(crate) mod ml_kem;
2828

2929
pub use ml_kem::mlkem512;
3030
pub use ml_kem::mlkem768;

src/hazardous/kem/xwing.rs

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ use crate::generics::{ByteArrayData, Public, Secret, TypeSpec, sealed::Data};
7777
use crate::hazardous::ecc::x25519;
7878
use crate::hazardous::hash::sha3::sha3_256;
7979
use crate::hazardous::hash::sha3::shake256::Shake256;
80-
use crate::hazardous::kem::ml_kem::mlkem768;
80+
use crate::hazardous::kem::ml_kem::{self, mlkem768};
8181

8282
/// KEM-label used by X-Wing.
8383
const LABEL: &[u8; 6] = b"\\.//^\\";
@@ -95,10 +95,6 @@ pub const CIPHERTEXT_SIZE: usize = 1120;
9595
pub const SHARED_SECRET_SIZE: usize = 32;
9696

9797
/// X-Wing encapsulation key.
98-
///
99-
/// **SECURITY**: This type simply holds bytes and performs no checks whatsoever. If an invalid
100-
/// ML-KEM-768 is part of the bytes parsed from this type, the check will first surface
101-
/// when encapsulation is performed.
10298
pub type EncapsulationKey = Public<XWingEncapKey>;
10399

104100
/// X-Wing ciphertext.
@@ -114,20 +110,23 @@ pub type SharedSecret = Secret<XWingSharedSecret>;
114110
/// X-Wing encapsulation key implementation. See [`EncapsulationKey`] type for convenience.
115111
///
116112
///
117-
/// **SECURITY**: This type simply holds bytes and performs no checks whatsoever. If an invalid
118-
/// ML-KEM-768 is part of the bytes parsed from this type, the check will first surface
119-
/// when encapsulation is performed.
113+
/// **SECURITY**: This type performs ML-KEM-768 key-checks and no checks for the X25519 part.
120114
pub struct XWingEncapKey {}
121115
impl Sealed for XWingEncapKey {}
122116

123117
impl TypeSpec for XWingEncapKey {
124118
const NAME: &'static str = stringify!(EncapsulationKey);
125119
type TypeData = ByteArrayData<EK_SIZE>;
126-
}
127120

128-
impl From<[u8; EK_SIZE]> for Public<XWingEncapKey> {
129-
fn from(value: [u8; EK_SIZE]) -> Self {
130-
Self::from_data(<XWingEncapKey as TypeSpec>::TypeData::from(value))
121+
// Perform FIPS-203 Encapsulation Key checks, with without allocating
122+
// an actual EncapKey, to save work. It will be properly expanded later anyway.
123+
fn parse_bytes(bytes: &[u8]) -> Result<Self::TypeData, UnknownCryptoError> {
124+
use crate::hazardous::kem::ml_kem::internal::PkeParameters;
125+
126+
let ek: [u8; EK_SIZE] = bytes.try_into().map_err(|_| UnknownCryptoError)?;
127+
ml_kem::internal::MlKem768Internal::encapsulation_key_check(&ek[..mlkem768::EK_SIZE])?;
128+
129+
Ok(Self::TypeData::from(ek))
131130
}
132131
}
133132

@@ -537,18 +536,34 @@ mod tests {
537536
)
538537
}
539538

540-
#[test]
541-
fn test_encapuslation_key() {
542-
use crate::test_framework::newtypes::public::PublicNewtype;
543-
PublicNewtype::test_no_generate::<EK_SIZE, EK_SIZE, XWingEncapKey>();
539+
// NOTE(brycx): PublicNewtype generic tests aren't run for Encapsulation keys
540+
// because their parsing logic depends on valid ML-KEM768 keys
541+
// which isn't compatible with test framework.
544542

545-
// Test of From<[u8; N]>
546-
assert_ne!(
547-
EncapsulationKey::from([0u8; EK_SIZE]),
548-
EncapsulationKey::from([1u8; EK_SIZE])
549-
);
543+
#[test]
544+
#[cfg(test)]
545+
fn test_encapsulation_key() {
546+
use crate::hazardous::kem::mlkem768;
547+
548+
let seed = mlkem768::Seed::from([21u8; mlkem768::SEED_SIZE]);
549+
let kp = mlkem768::KeyPair::new(seed).unwrap();
550+
551+
let mut xwing_public_bytes = [0u8; EK_SIZE];
552+
crate::util::secure_rand_bytes(&mut xwing_public_bytes).unwrap();
553+
554+
// Length mismatch
555+
assert!(EncapsulationKey::try_from(&xwing_public_bytes[..EK_SIZE - 1]).is_err());
556+
// With invalid/random ML-KEM-768 public part, X-Wing fails.
557+
assert!(EncapsulationKey::try_from(&xwing_public_bytes).is_err());
558+
// With valid ML-KEM-768 and completely random X25519, which is not parsed, X-Wing succeeds.
559+
xwing_public_bytes[..mlkem768::EK_SIZE].copy_from_slice(kp.public().as_ref());
560+
assert!(EncapsulationKey::try_from(&xwing_public_bytes).is_ok());
561+
}
550562

551-
#[cfg(feature = "serde")]
563+
#[test]
564+
#[cfg(feature = "serde")]
565+
fn test_encapsulation_key_serialization() {
566+
use crate::test_framework::newtypes::public::PublicNewtype;
552567
PublicNewtype::test_serialization::<EK_SIZE, XWingEncapKey>();
553568
}
554569

0 commit comments

Comments
 (0)