Skip to content

Commit 33e1d73

Browse files
committed
clean up
1 parent 03854a4 commit 33e1d73

File tree

8 files changed

+137
-129
lines changed

8 files changed

+137
-129
lines changed

.github/workflows/pull_request.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ jobs:
3535
if: ${{ matrix.os == 'windows-latest' }}
3636
uses: ilammy/setup-nasm@72793074d3c8cdda771dba85f6deafe00623038b # 1.5.2
3737

38-
- name: Run tests
38+
- name: Run tests (all features)
3939
run: cargo test --workspace --verbose --all-features --no-fail-fast ${{ runner.os == 'macOS' && '-- --test-threads=1' || '' }}
4040

41+
- name: Run tests (no features)
42+
run: cargo test --workspace --verbose --no-fail-fast ${{ runner.os == 'macOS' && '-- --test-threads=1' || '' }}
43+
4144
miri:
4245
name: Miri
4346
runs-on: "ubuntu-latest"

.github/workflows/push.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ jobs:
2323
- name: Show CPU info
2424
run: lscpu
2525

26-
- name: Test
26+
- name: Run tests (all features)
2727
run: cargo test --workspace --verbose --all-features --no-fail-fast
28+
29+
- name: Run tests (no features)
30+
run: cargo test --workspace --verbose --no-fail-fast

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ bincode = "1.3.3"
2020
bitvec = "1.0.1"
2121
blake3 = "1.5.5"
2222
bytemuck = { version = "1.25.0", features = ["must_cast"] }
23+
cfg-if = "1"
2324
cpufeatures = "0.3.0"
2425
criterion = { version = "0.8", features = ["async_tokio", "html_reports"] }
2526
cryprot-codes = { version = "0.2.2", path = "cryprot-codes" }

cryprot-ot/Cargo.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ authors.workspace = true
1010
repository.workspace = true
1111

1212
[features]
13-
# ML-KEM-based base OT. Pickm only one variant.
13+
# ML-KEM-based base OT. Pick only one variant.
1414
ml-kem-base-ot-512 = ["_ml-kem-base-ot"]
1515
ml-kem-base-ot-768 = ["_ml-kem-base-ot"]
1616
ml-kem-base-ot-1024 = ["_ml-kem-base-ot"]
1717
# Internal feature — do not enable directly.
18-
_ml-kem-base-ot = []
18+
_ml-kem-base-ot = ["dep:ml-kem", "dep:module-lattice", "dep:hybrid-array", "dep:sha3"]
1919

2020
[lints]
2121
workspace = true
@@ -27,17 +27,18 @@ bench = false
2727
[dependencies]
2828
bitvec = { workspace = true, features = ["serde"] }
2929
bytemuck.workspace = true
30+
cfg-if.workspace = true
3031
cryprot-codes.workspace = true
3132
cryprot-core = { workspace = true, features = ["tokio-rayon"] }
3233
cryprot-net.workspace = true
3334
cryprot-pprf.workspace = true
3435
curve25519-dalek = { workspace = true, features = ["rand_core", "serde"] }
3536
futures.workspace = true
36-
hybrid-array.workspace = true
37-
ml-kem.workspace = true
38-
module-lattice.workspace = true
37+
hybrid-array = { workspace = true, optional = true }
38+
ml-kem = { workspace = true, optional = true }
39+
module-lattice = { workspace = true, optional = true }
3940
rand.workspace = true
40-
sha3.workspace = true
41+
sha3 = { workspace = true, optional = true }
4142
serde_bytes.workspace = true
4243
serde = { workspace = true, features = ["derive"] }
4344
subtle.workspace = true

cryprot-ot/docs/mlkem-ot-protocol.md

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,14 @@ ML-KEM implementation: [ML-KEM](https://github.com/RustCrypto/KEMs/blob/5a7f3ab7
1818
### Vectors and Keys
1919

2020
- `k`: determines the module dimension (k=2 for ML-KEM-512, k=3 for ML-KEM-768, k=4 for ML-KEM-1024)
21-
- `NttVector<k>`: a vector of `k` `NttPolynomial`s in `T_q^k`
21+
- `NttVector`: a vector of `k` `NttPolynomial`s, i.e `T_q^k`
2222

23-
An ML-KEM encapsulation key (public key) consists of two parts:
24-
25-
```
26-
ek = (t_hat, rho)
27-
```
28-
29-
where:
30-
- `t_hat` is an `NttVector<k>`: the public key vector in NTT domain (`t_hat = A_hat * s + e` in NTT form)
23+
An ML-KEM encapsulation key is represented as `EncapsulationKey(t_hat, rho)` where:
24+
- `t_hat` is in `T_q^k`, i.e. an `NttVector` — the public key vector in NTT domain (`t_hat = A_hat * s + e` in NTT form)
3125
- `rho` is a 32-byte seed used to derive the public matrix `A_hat`
3226

27+
We write `ek.t_hat` and `ek.rho` to refer to the two components.
28+
3329
Note that the ML-KEM encapsulation key is the same as the K-PKE encryption key (FIPS 203, Section 5).
3430

3531
The serialized form is:
@@ -40,80 +36,76 @@ ek_bytes = ByteEncode_12(t_hat) || rho
4036

4137
where `ByteEncode_12` encodes each of the `256*k` coefficients using 12 bits (FIPS 203, Algorithm 5 ByteEncode_d).
4238

43-
We write `ek.t_hat` and `ek.rho` to refer to the two components of an encapsulation key.
44-
4539
A decapsulation key `dk` contains the secret vector `s` and some additional data (FIPS 203, Algorithm 16 KeyGen_internal).
4640

4741
### Operations
4842

49-
- `+` and `-` on `NttVector<k>`: component-wise addition and subtraction in `T_q^k`
50-
- `SampleNTT(B)`: Algorithm 7 from FIPS 203. Reads from a byte stream `B` and produces a pseudorandom element in `T_q`, i.e. an `NttPolynomial`
43+
- `+` and `-` on `NttVector`: component-wise addition and subtraction in `T_q^k`
44+
- `EncapsulationKey +/- NttVector -> EncapsulationKey`: operates on the `t_hat` component only, `rho` is preserved from the `EncapsulationKey`
5145

5246
### Helper Functions
5347

54-
**`SampleNTTVector(seed, rho) -> (t_hat, rho)`**
48+
**`sample_ntt_poly(xof) -> NttPolynomial`**
49+
50+
Algorithm 7 from FIPS 203. Reads bytes from a XOF and produces a pseudorandom element in `T_q`.
51+
52+
**`sample_ntt_vector(seed) -> NttVector`**
5553

56-
Our helper (not from FIPS 203 or MR19). Produces a pseudorandom `NttVector<k>` from a 32-byte
57-
`seed` by calling `SampleNTT` (FIPS 203, Algorithm 7) `k` times, once per polynomial. Each
58-
`SampleNTT` call produces a pseudorandom element of `T_q`. The resulting `NttVector<k>`
54+
Our helper (not from FIPS 203 or MR19). Produces a pseudorandom `NttVector` from a 32-byte
55+
`seed` by calling `sample_ntt_poly` (FIPS 203, Algorithm 7) `k` times, once per polynomial. Each
56+
`sample_ntt_poly` call produces a pseudorandom element of `T_q`. The resulting `NttVector`
5957
is indistinguishable from a real `t_hat`, since a real `t_hat = A_hat * s + e` is computationally
6058
indistinguishable from a pseudorandom vector in `T_q^k`.
6159

6260
```
6361
for j in 0..k:
64-
t_hat[j] = SampleNTT(seed || j || 0) // 34 bytes: 32-byte seed + 2 index bytes
62+
x = xof(seed || 0 || j) // 34 bytes: 32-byte seed + 2 index bytes
63+
t_hat[j] = sample_ntt_poly(x)
6564
```
6665

67-
Each call uses different index bytes `(j, 0)` in FIPS 203 Algorithm 7 for domain separation.
66+
Each call uses different index bytes `(0, j)` for different XOF stream.
6867
In libOTe, this corresponds to `randomPK`, where it instead generates `A_hat` and takes the first row from it.
6968

70-
Output: `(t_hat, rho)`. The `rho` is passed through unchanged.
69+
**`hash_ek(ek) -> NttVector`**
7170

72-
**`HashEK(ek) -> (h, ek.rho)`**
71+
Corresponds to libOTe's `pkHash`. Hashes `ek.t_hat` to a 32-byte seed and then samples a new `NttVector` from it.
7372

74-
HashEK corresponds to libOTe's `pkHash`. Maps an encapsulation key to another
75-
encapsulation key. Takes an element of `T_q^k`, hashes it to a 32-byte seed, and uses that seed to sample a new element of `T_q^k`.
76-
77-
Given an encapsulation key `ek = (t_hat, rho)`:
73+
Given an `EncapsulationKey` `ek`:
7874

7975
```
80-
seed = SHA3-256(ByteEncode_12(ek.t_hat)) // hash only the t_hat bytes, not rho
81-
h = SampleNTTVector(seed, ek.rho) // sample a new NttVector<k> from the seed
76+
seed = sha3_256(ByteEncode_12(ek.t_hat)) // hash only the t_hat bytes, not rho
77+
h = sample_ntt_vector(seed) // sample a new NttVector from the seed
8278
```
8379

84-
Output: `(h, ek.rho)` where `h` is an `NttVector<k>` in `T_q^k`.
80+
Output: `h`.
8581

86-
**`RandomEK(rng, rho) -> (t_hat, rho)`**
82+
**`random_ek(rng, rho) -> EncapsulationKey`**
8783

88-
Generate a random encapsulation key. A 32-byte `seed` is sampled from `rng`:
84+
Generate a random encapsulation key. A 32-byte random `seed` is sampled from a cryptographically secure random number generator `rng`:
8985

90-
Output: `SampleNTTVector(seed, rho)`
86+
Output: `EncapsulationKey(sample_ntt_vector(seed), rho)`
9187

92-
This is identical to `HashEK` except the seed is random rather than derived from a hash.
88+
Sampling is identical to the one on `hash_ek`, except that the seed is random rather than derived from a hash.
9389

9490
## Protocol
9591

96-
**Convention:** All arithmetic (`+`, `-`) in the protocol steps operates on the `t_hat`
97-
component only. The `rho` component is always the same across all keys in a single OT
98-
(taken from the real key generated in step 1) and is carried along unchanged.
99-
10092
### Receiver (choice bit `b`)
10193

10294
1. **Generate real keypair:**
10395
```
10496
(dk, ek) = ML-KEM.KeyGen()
10597
```
106-
where `ek = (t_hat, rho)`.
98+
where `ek` is an `EncapsulationKey`.
10799

108100
2. **Sample random key for position `1-b`:**
109101
```
110-
r_{1-b} = RandomEK(rng, ek.rho)
102+
r_{1-b} = random_ek(rng, ek.rho)
111103
```
112104
where `rng` is a cryptographically secure random number generator.
113105

114-
3. **Compute the correlated key for position `b`:**
106+
3. **Compute the real key for position `b`:**
115107
```
116-
r_b = ek - HashEK(r_{1-b})
108+
r_b = ek - hash_ek(r_{1-b})
117109
```
118110

119111
4. **Send to sender:**
@@ -128,7 +120,7 @@ component only. The `rho` component is always the same across all keys in a sing
128120

129121
6. **For each `j in {0, 1}`, reconstruct the encapsulation key:**
130122
```
131-
ek_j = r_j + HashEK(r_{1-j})
123+
ek_j = r_j + hash_ek(r_{1-j})
132124
```
133125

134126
7. **Encapsulate to both reconstructed keys:**
@@ -172,33 +164,32 @@ component only. The `rho` component is always the same across all keys in a sing
172164
173165
**Correctness:**
174166
175-
For the chosen side `b`, the sender reconstructs in step 6:
167+
For the chosen side `b`, the sender reconstructs in step 6 by expanding `r_b`:
176168
```
177-
ek_b = r_b + HashEK(r_{1-b})
178-
= (ek - HashEK(r_{1-b})) + HashEK(r_{1-b})
169+
ek_b = r_b + hash_ek(r_{1-b})
170+
= (ek - hash_ek(r_{1-b})) + hash_ek(r_{1-b})
179171
= ek
180172
```
181-
So `ek_b = ek`, the real public key. In step 10, the receiver calls `ML-KEM.Decaps(dk, ct_b)` and
173+
So `ek_b = ek`, the real encapsulation key. In step 10, the receiver calls `ML-KEM.Decaps(dk, ct_b)` and
182174
recovers the same shared secret `ss_b` that the sender computed via `ML-KEM.Encaps(ek_b)` in step 7.
183175
184176
**Security:**
185177
186178
For the other side `1-b`, the sender reconstructs in step 6:
187179
```
188-
ek_{1-b} = r_{1-b} + HashEK(r_b)
180+
ek_{1-b} = r_{1-b} + hash_ek(r_b)
189181
```
190182
191183
Expanding `r_b` (from step 3):
192184
```
193-
ek_{1-b} = r_{1-b} + HashEK(ek - HashEK(r_{1-b}))
185+
ek_{1-b} = r_{1-b} + hash_ek(ek - hash_ek(r_{1-b}))
194186
```
195187
196-
This does NOT simplify — `HashEK` is a hash function, so `HashEK(ek - HashEK(r_{1-b}))` does not
197-
cancel with `HashEK(r_{1-b})`. The result `ek_{1-b}` is an unrelated key for which the
198-
receiver does not have a decapsulation key `dk`, so they cannot decapsulate `ct_{1-b}`.
188+
This does not simplify — `hash_ek` is a hash function, so the nested `hash_ek(ek - hash_ek(r_{1-b}))`
189+
cannot be reduced. The result `ek_{1-b}` is an unrelated key for which the receiver does not
190+
have a decapsulation key `dk`, so they cannot decapsulate `ct_{1-b}`.
199191
200-
The choice bit `b` is hidden because `r_b = ek - HashEK(r_{1-b})`. Since `ek` is
201-
indistinguishable from uniform under the MLWE assumption, and `HashEK(r_{1-b})` is determined by the
202-
already-public `r_{1-b}`, subtracting it from a uniform value still yields a uniform
203-
value. So both `r_0` and `r_1` appear uniform to the sender — neither reveals which
204-
is the real key.
192+
The choice bit `b` is hidden from the sender. The sender reconstructs both `ek_0` and `ek_1` in
193+
step 6, but under the MLWE assumption, a real encapsulation key is indistinguishable from a random
194+
one. Since both reconstructed keys appear as valid encapsulation keys, the sender cannot determine
195+
which one has a corresponding decapsulation key `dk`.

cryprot-ot/src/lib.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
//!
44
//! - base OT: "Simplest OT" [[CO15](https://eprint.iacr.org/2015/267)]
55
//! (classical security)
6-
//! - post-quantum base OT: ML-KEM-768 based OT [[MR19](https://eprint.iacr.org/2019/706)]
7-
//! (post-quantum security)
6+
//! - post-quantum base OT: ML-KEM based OT [[MR19](https://eprint.iacr.org/2019/706)]
7+
//! (post-quantum security, enable one of the `ml-kem-base-ot-{512,768,1024}`
8+
//! features)
89
//! - semi-honest OT extension: optimized [[IKNP03](https://www.iacr.org/archive/crypto2003/27290145/27290145.pdf)]
910
//! protocol
1011
//! - malicious OT extension: optimized [[KOS15](https://eprint.iacr.org/2015/546.pdf)]
@@ -18,8 +19,8 @@
1819
//!
1920
//! ## ML-KEM Base OT
2021
//!
21-
//! Enable one of the `ml-kem-base-ot-{512,768,1024}` features to use ML-KEM-based OT for the base OT
22-
//! protocol, providing post-quantum security:
22+
//! Enable one of the `ml-kem-base-ot-{512,768,1024}` features to use
23+
//! ML-KEM-based OT for the base OT protocol, providing post-quantum security:
2324
//!
2425
//! This replaces the classical "Simplest OT" with an ML-KEM-based construction
2526
//! following FIPS 203 at <https://csrc.nist.gov/pubs/fips/203/final>, similar to libOTe's `ENABLE_MR_KYBER` option.
@@ -77,7 +78,8 @@ pub mod simplest_ot;
7778

7879
/// Base OT implementation used by extension protocols.
7980
///
80-
/// When the `ml-kem-base-ot` feature is enabled, use [`mlkem_ot::MlKemOt`]
81+
/// When one of the `ml-kem-base-ot-{512,768,1024}` features is enabled, uses
82+
/// [`mlkem_ot::MlKemOt`].
8183
#[cfg(feature = "_ml-kem-base-ot")]
8284
pub type BaseOt = mlkem_ot::MlKemOt;
8385

0 commit comments

Comments
 (0)