Skip to content

Commit d310c99

Browse files
Partial fix for celabshq/libcrux#1275
Co-authored-by: Rolfe Schmidt <rolfe@signal.org>
1 parent b647f49 commit d310c99

7 files changed

Lines changed: 201 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 14 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spqr"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
edition = "2021"
55
license = "AGPL-3.0-only"
66
rust-version = "1.83.0"
@@ -13,7 +13,7 @@ displaydoc = "0.2"
1313
hax-lib = "0.3.5"
1414
hkdf = "0.12"
1515
libcrux-hmac = "0.0.4"
16-
libcrux-ml-kem = { version = "0.0.4", default-features = false, features = ["incremental", "mlkem768"] }
16+
libcrux-ml-kem = { version = "0.0.5", default-features = false, features = ["incremental", "mlkem768"] }
1717
log = "0.4.21"
1818
num_enum = "0.7.3"
1919
prost = "0.14.1"
@@ -29,6 +29,7 @@ spqr = { path = ".", features = ["test-utils"] }
2929

3030
env_logger = "0.11"
3131
galois_field_2pm = "0.1.0"
32+
hex = "0.4"
3233
hmac = "0.12.1"
3334
matches = "0.1.10"
3435
rand_08 = { package = "rand", version = "0.8" }

Cross.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ pre-build = [
55
"unzip protoc-29.3-linux-x86_64.zip -d protoc",
66
"mv protoc/bin/protoc /usr/bin",
77
]
8+
9+
[build.env]
10+
passthrough = [
11+
"RUST_LOG",
12+
]
13+
volumes = ["/tmp:/tmp"]

src/incremental_mlkem768.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,87 @@ pub fn encaps1<R: Rng + CryptoRng>(
6969
#[hax_lib::requires(es.len() == 2080 && ek.len() == 1152)]
7070
#[hax_lib::ensures(|result| result.len() == 128)]
7171
pub fn encaps2(ek: &EncapsulationKey, es: &EncapsulationState) -> Ciphertext2 {
72+
let maybe_fix = potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275(es);
73+
let es = maybe_fix.as_ref().unwrap_or(es);
7274
let ct2 = incremental::encapsulate2(
7375
es.as_slice().try_into().expect("size should be correct"),
7476
ek.as_slice().try_into().expect("size should be correct"),
7577
);
7678
ct2.value.to_vec()
7779
}
7880

81+
/// Due to https://github.com/cryspen/libcrux/issues/1275, state may
82+
/// contain incorrect endian-ness. We need to fix this locally before
83+
/// using it. Luckily, this is doable by checking that the values in
84+
/// error2 are in the range [-2, 2].
85+
#[hax_lib::requires(es.len() == 2080)]
86+
#[hax_lib::ensures(|result| if let Some(es) = result {
87+
es.len() == 2080
88+
} else {
89+
true
90+
})]
91+
#[hax_lib::opaque]
92+
fn potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275(
93+
es: &EncapsulationState,
94+
) -> Option<EncapsulationState> {
95+
assert_eq!(es.len(), 2080);
96+
// Look at each value within the error2 portion of EncapsState.
97+
//
98+
// The last 32 bytes are a raw random vector, thus they don't have endianness
99+
// and we shouldn't flip them. All other bytes are encoded i16s.
100+
// This is the libcrux-specific encoding of the following struct, where all
101+
// PolynomialRingElements are encoded as described.
102+
//
103+
// pub struct EncapsState<const K: usize, Vector: Operations> {
104+
// pub(super) r_as_ntt: [PolynomialRingElement<Vector>; K],
105+
// pub(super) error2: PolynomialRingElement<Vector>,
106+
// pub(super) randomness: [u8; 32],
107+
// }
108+
//
109+
// To determine whether it's valid or not, we look at the encoding of `error2`.
110+
// All error2 values should be in the range [-2, 2].
111+
// This is 𝜂2 from https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.203.pdf
112+
// page 39 table 2, generated as 𝑒2 on page 30 algorithm 14 line 17.
113+
const NEG1_I16: i16 = 0xFFFFu16 as i16;
114+
const NEG2_I16_GOOD: i16 = 0xFFFEu16 as i16;
115+
const NEG2_I16_BAD: i16 = 0xFEFFu16 as i16;
116+
117+
for c in es[1536..2080 - 32].chunks(2) {
118+
match i16::from_le_bytes(c.try_into().expect("chunk should be size 2")) {
119+
0x0000i16 | NEG1_I16 => {} // These have the same encoding for i16 in little/big-endian
120+
0x0001i16 | 0x0002i16 | NEG2_I16_GOOD => {
121+
return None;
122+
}
123+
0x0100i16 | 0x0200i16 | NEG2_I16_BAD => {
124+
// If they aren't in the expected range, we probably have a state with bad endian-ness. So flip it.
125+
#[cfg(not(hax))]
126+
log::info!("spqr fixing bad encapsulate1 stored state endianness");
127+
return Some(flip_endianness_of_encapsulation_state(es));
128+
}
129+
_ => {
130+
// We're in a weird state, so just use what we had initially.
131+
#[cfg(not(hax))]
132+
log::warn!("spqr unable to fix encapsulate1 stored state endianness");
133+
return None;
134+
}
135+
}
136+
}
137+
None
138+
}
139+
140+
#[hax_lib::requires(es.len()%2 == 0 && es.len() == 2080)]
141+
#[hax_lib::ensures(|result| result.len() == es.len())]
142+
#[hax_lib::opaque]
143+
fn flip_endianness_of_encapsulation_state(es: &EncapsulationState) -> EncapsulationState {
144+
assert!(es.len() % 2 == 0);
145+
assert!(es.len() > 32);
146+
let mut fixed_es = es.clone();
147+
for i in (0..fixed_es.len() - 32).step_by(2) {
148+
(fixed_es[i], fixed_es[i + 1]) = (fixed_es[i + 1], fixed_es[i])
149+
}
150+
fixed_es
151+
}
152+
79153
/// Decapsulate ciphertext to get shared secret.
80154
#[hax_lib::requires(ct1.len() == 960 && ct2.len() == 128 && dk.len() == 2400)]
81155
#[hax_lib::ensures(|result| result.len() == 32)]

src/issue1275_a_state.in

5.49 KB
Binary file not shown.

src/issue1275_b_state.in

6.01 KB
Binary file not shown.

src/lib.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,4 +1066,108 @@ mod lib_test {
10661066
));
10671067
Ok(())
10681068
}
1069+
1070+
/// A test that runs a set number of steps, always sending one message A->B, then
1071+
/// one message B->A, with logging turned on, so we can watch how things work
1072+
/// in the most predictable case.
1073+
#[test]
1074+
fn lockstep_run_with_logging() -> Result<(), Error> {
1075+
let _ = env_logger::builder().is_test(true).try_init();
1076+
let mut rng = OsRng.unwrap_err();
1077+
1078+
let version = Version::V1;
1079+
1080+
let mut alex_pq_state = initial_state(Params {
1081+
version,
1082+
min_version: version,
1083+
direction: Direction::A2B,
1084+
auth_key: &[41u8; 32],
1085+
chain_params: ChainParams::default(),
1086+
})?;
1087+
let mut blake_pq_state = initial_state(Params {
1088+
version,
1089+
min_version: version,
1090+
direction: Direction::B2A,
1091+
auth_key: &[41u8; 32],
1092+
chain_params: ChainParams::default(),
1093+
})?;
1094+
1095+
for i in 0..30 {
1096+
log::info!("step {}", i);
1097+
// Now let's send some messages
1098+
let Send {
1099+
state,
1100+
msg,
1101+
key: alex_key,
1102+
} = send(&alex_pq_state, &mut rng)?;
1103+
alex_pq_state = state;
1104+
let Recv {
1105+
state,
1106+
key: blake_key,
1107+
} = recv(&blake_pq_state, &msg)?;
1108+
blake_pq_state = state;
1109+
assert_eq!(alex_key, blake_key);
1110+
let Send {
1111+
state,
1112+
msg,
1113+
key: blake_key,
1114+
} = send(&blake_pq_state, &mut rng)?;
1115+
blake_pq_state = state;
1116+
let Recv {
1117+
state,
1118+
key: alex_key,
1119+
} = recv(&alex_pq_state, &msg)?;
1120+
alex_pq_state = state;
1121+
assert_eq!(alex_key, blake_key);
1122+
}
1123+
log::info!("alex_state: {}", hex::encode(alex_pq_state));
1124+
log::info!("blake_state: {}", hex::encode(blake_pq_state));
1125+
Ok(())
1126+
}
1127+
1128+
#[test]
1129+
fn regression_test_libcrux_issue_1275_from_generated_states() -> Result<(), Error> {
1130+
let _ = env_logger::builder().is_test(true).try_init();
1131+
let mut rng = OsRng.unwrap_err();
1132+
1133+
// These states are generated using the old "portable" logic for libcrux-ml-kem
1134+
// EncapsState serialization prior to libcrux:pr/1276, 30 steps into a lockstep
1135+
// protocol. The send_ct side has already generated the encapsulation state
1136+
// and stored it locally, but hasn't yet called encapsulate2 on it. This tests
1137+
// to make sure that the incremental_mlkem code path correctly notices and handles
1138+
// this eventuality.
1139+
let mut alex_pq_state = include_bytes!("issue1275_a_state.in").to_vec();
1140+
let mut blake_pq_state = include_bytes!("issue1275_b_state.in").to_vec();
1141+
1142+
// After 20 additional steps, we should be in epoch 2 successfully. If
1143+
// we're unable to handle the bad state, one of these steps will fail.
1144+
for i in 30..50 {
1145+
log::info!("step {}", i);
1146+
let Send {
1147+
state,
1148+
msg,
1149+
key: alex_key,
1150+
} = send(&alex_pq_state, &mut rng)?;
1151+
alex_pq_state = state;
1152+
let Recv {
1153+
state,
1154+
key: blake_key,
1155+
} = recv(&blake_pq_state, &msg)?;
1156+
blake_pq_state = state;
1157+
assert_eq!(alex_key, blake_key);
1158+
let Send {
1159+
state,
1160+
msg,
1161+
key: blake_key,
1162+
} = send(&blake_pq_state, &mut rng)?;
1163+
blake_pq_state = state;
1164+
let Recv {
1165+
state,
1166+
key: alex_key,
1167+
} = recv(&alex_pq_state, &msg)?;
1168+
alex_pq_state = state;
1169+
assert_eq!(alex_key, blake_key);
1170+
}
1171+
Ok(())
1172+
}
10691173
}

0 commit comments

Comments
 (0)