1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
//! First-order arithmetic masking for ML-KEM polynomials
//! (**countermeasure: DPA / DEMA / CPA on the K-PKE secret ``s``**).
//!
//! ## Principle
//!
//! Every secret polynomial is kept as two additive shares modulo `q`:
//! `s = s_0 + s_1 (mod q)`. Shares are drawn uniformly at random and
//! each operation consuming the secret (NTT, pointwise multiplication
//! with a public polynomial, additions / subtractions) is rewritten
//! to operate on the shares without ever materialising the unmasked
//! `s`. Fresh-randomness refreshes (`MaskedPoly::refresh`) break
//! cross-operation correlations that a high-order DPA could otherwise
//! exploit.
//!
//! Because the unmasked `s` never exists as a single value in memory,
//! the Hamming-weight / Hamming-distance hypothesis at the core of
//! CPA becomes non-identifiable: correlating the power trace with a
//! guess of any byte of `s` produces no peak since each share alone
//! is uniform.
//!
//! ## Available operations
//!
//! | Function | Description |
//! |----------|-------------|
//! | `MaskedPoly::mask` | Split a plaintext polynomial into two shares |
//! | `MaskedPoly::unmask` | Reconstruct the polynomial from shares |
//! | `MaskedPoly::refresh` | Re-randomize shares (prevents correlation buildup) |
//! | `masked_ntt` / `masked_ntt_inv` | NTT on each share independently |
//! | `masked_multiply_public` | Multiply masked poly by a public poly |
//! | `masked_add` / `masked_sub` | Add/subtract two masked polys |
//! | `masked_multiply_accumulate` | Fused multiply-add with a public poly |
//!
//! ## References
//!
//! * *Side-channel analysis of the ML-KEM pointwise multiplication*
//! (IACR ePrint 2025, `doc/papers/eprint2025_sca_mlkem_pointwise.pdf`)
//! — identifies the pointwise multiplication as the key DPA
//! target; masking the shares defeats the published attack.
//! * *ML-KEM and ML-DSA on OpenTitan: side-channel evaluation*
//! (cryptojedi, 2024) — independent evaluation of the trace-count
//! blow-up induced by masking.
//!
//! ## Where to look next
//!
//! * Countermeasure description and threat analysis:
//! `doc/sca/countermeasures/ml_kem.rst`, section
//! *DPA / DEMA / CPA — first-order masking of the KPKE secret*.
//! * Call sites: `keygen_internal_sca`,
//! `decaps_internal_sca`,
//! [`crate::ml_kem::kpke::decrypt_sca`].
//!
//! ## Scope and residual risk
//!
//! Masking is **first-order**. A higher-order DPA that combines two
//! independent time samples can still recover the secret with
//! substantially more traces. Tier-4 item `K-SCA3` (not yet
//! scheduled) would extend this to a 3-share scheme (CC EAL4+-grade).
use MlKemError;
use ;
use N;
use CryptoRng;
/// A polynomial split into two additive shares modulo q.
///
/// Maintains the invariant `real_value[i] = (share0[i] + share1[i]) mod q`
/// for all `i` in `0..256`. Neither share alone reveals any information
/// about the underlying polynomial.
///
/// Both shares have coefficients in `[0, q-1]`.
// ---- Masked NTT operations ----
// NTT is linear: NTT(s₀ + s₁) = NTT(s₀) + NTT(s₁)
// So we NTT each share independently.
/// Apply the forward NTT to each share of a [`MaskedPoly`] independently.
///
/// Exploits the linearity of the NTT: `NTT(s0 + s1) = NTT(s0) + NTT(s1)`.
/// Each share is transformed independently so the masking invariant is preserved.
/// Apply the inverse NTT to each share of a [`MaskedPoly`] independently.
///
/// The inverse of `masked_ntt`. Transforms both shares from the NTT domain
/// back to the standard domain while preserving the masking invariant.
/// Multiply a masked polynomial by a **public** polynomial in NTT domain.
///
/// Computes `(s0 + s1) * p = s0*p + s1*p` by multiplying each share
/// independently. This is secure because `p` is public data -- there is
/// no secret-times-secret interaction that would require second-order masking.
///
/// # Arguments
///
/// * `masked` - The masked secret polynomial (NTT domain).
/// * `public` - A public polynomial (NTT domain).
/// * `out` - Output masked polynomial for the product.
/// Add a **public** polynomial to a masked polynomial.
///
/// Adds the public value only to `share0`. Since the public polynomial
/// is not secret, it does not need to be split across shares.
///
/// # Arguments
///
/// * `m` - The masked polynomial to modify in place.
/// * `public` - A public polynomial to add.
/// Add two masked polynomials share-wise.
///
/// Computes `c.share0 = a.share0 + b.share0` and
/// `c.share1 = a.share1 + b.share1`, so
/// `unmask(c) = unmask(a) + unmask(b) mod q`.
/// Subtract two masked polynomials share-wise.
///
/// Computes `c.share0 = a.share0 - b.share0` and
/// `c.share1 = a.share1 - b.share1`, so
/// `unmask(c) = unmask(a) - unmask(b) mod q`.
/// Fused multiply-accumulate: `out += masked * public` (NTT domain).
///
/// Multiplies a masked polynomial by a public polynomial and adds the
/// result to the existing shares in `out`. Useful for computing matrix-vector
/// products share-wise without allocating temporaries for each column.
///
/// # Arguments
///
/// * `out` - Accumulator masked polynomial (modified in place).
/// * `masked` - The masked secret polynomial (NTT domain).
/// * `public` - A public polynomial (NTT domain).