nx_poly1305.nx source
↩ module page · 334 lines · 14338 B
1// poly1305.nx -- Bernstein's Poly1305 MAC (RFC 8439 variant).
2//
3// license_tier: INDEPENDENT_REDERIVE
4// genealogy_id: international-research-sources/ietf/rfc_8439
5//
6// One-time authenticator over prime p = 2^130 - 5. Takes a 256-bit
7// one-time key (128-bit r clamped + 128-bit s), a message of any
8// length, and produces a 16-byte tag that is unforgeable under a
9// unique-per-message key assumption.
10//
11// Why Poly1305:
12// - Proven secure in the one-time-key model (Bernstein 2005).
13// - Pairs with ChaCha20 to form the RFC 8439 AEAD construction
14// (ChaCha20-Poly1305), the primary TLS 1.3 cipher suite that
15// avoids AES cache-side-channel concerns.
16// - Naturally constant-time on this design: only modular
17// multiplication and addition, no branches on key/message
18// bits. (Some implementations use table lookups for
19// performance -- we avoid those for side-channel reasons.)
20// - Under Grover's algorithm a 128-bit MAC has 64-bit effective
21// security against quantum second-preimage -- that's marginal,
22// but second-preimage on a MAC isn't a practical attack; the
23// one-time-key property is what matters.
24//
25// Arithmetic representation:
26// r and acc live as 5 x 26-bit limbs (total ~130 bits). Each
27// limb fits in i64 with headroom; partial products during
28// multiplication sum to at most ~5 * 2^52 = 2^54.3, still in
29// i64 range. After multiplication we carry-propagate and
30// reduce modulo p = 2^130 - 5 via the standard Bernstein trick
31// (multiply high bits by 5, add back to low).
32//
33// Invariants:
34// P1 No branch depends on secret data (key or message bytes).
35// P2 The message is processed exactly once per block; padding
36// of the final short block is explicit (append 0x01, zero-
37// extend), no early exit.
38// P3 The r-clamp (RFC 8439 §2.5) is applied once at key setup;
39// never reapplied mid-stream so no key material leaks.
40// P4 Caller guarantees a *fresh* 256-bit key per message.
41// Poly1305 is not secure if the same r+s pair authenticates
42// two distinct messages. In ChaCha20-Poly1305 this is
43// enforced by deriving (r, s) from ChaCha20(key, nonce, 0).
44//
45// References:
46// - RFC 8439 section 2.5 (Poly1305 algorithm)
47// - RFC 8439 Appendix A.3 (test vectors)
48// - Bernstein 2005 "The Poly1305-AES MAC"
49//
50// nx_safety_envelope:
51// intended_use: "Poly1305 authenticator -- one-time MAC for
52// ChaCha20-Poly1305 AEAD (RFC 8439) + general
53// per-key-per-message authenticator"
54// sil_target: SIL3 (MAC primitive; forgery = arbitrary
55// message accepted)
56// asil_target: QM
57// dal_target: DAL B
58// evidence: [RFC_8439_canonical_basis,
59// Bernstein_2005_paper, no_FP,
60// constant_time_clamping_design]
61// hazard_register: [bug-tape-key-reuse-CATASTROPHIC,
62// bug-tape-tag-truncation-via-bad-length]
63// residual_risk: "Each (r, s) Poly1305 key is ONE-TIME by
64// construction (Wegman-Carter). Caller
65// MUST derive per-message via ChaCha20
66// keystream or equivalent; reuse breaks
67// unforgeability."
68// verdict: NOT_YET_EVALUATED
69
70import "nx_syscalls.nx"
71
72// Load a little-endian u32 from buf[off..off+4].
73func p_load_u32_le(buf: *u8, off: i64) -> i64 {
74 let b0: i64 = buf[off + 0]
75 let b1: i64 = buf[off + 1]
76 let b2: i64 = buf[off + 2]
77 let b3: i64 = buf[off + 3]
78 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
79}
80
81// Store a little-endian u32 to buf[off..off+4].
82func p_store_u32_le(buf: *u8, off: i64, v: i64) -> i64 {
83 buf[off + 0] = v & 0xFF
84 buf[off + 1] = (v >> 8) & 0xFF
85 buf[off + 2] = (v >> 16) & 0xFF
86 buf[off + 3] = (v >> 24) & 0xFF
87 return 0
88}
89
90// Clamp r per RFC 8439 §2.5.1: certain bits must be zero to keep
91// the multiply results bounded and the security proof valid.
92// r[3], r[7], r[11], r[15] top 4 bits cleared (mask 0x0f)
93// r[4], r[8], r[12] bottom 2 bits cleared (mask 0xfc)
94// We operate on the 16 raw bytes in-place.
95func poly1305_clamp(r: *u8) -> i64 {
96 r[3] = r[3] & 0x0F
97 r[7] = r[7] & 0x0F
98 r[11] = r[11] & 0x0F
99 r[15] = r[15] & 0x0F
100 r[4] = r[4] & 0xFC
101 r[8] = r[8] & 0xFC
102 r[12] = r[12] & 0xFC
103 return 0
104}
105
106// Full MAC computation.
107// key: 32 bytes = r(16) || s(16)
108// msg: message bytes to authenticate
109// n: message length in bytes
110// tag: 16-byte output buffer
111//
112// Uses a 5 x 26-bit limb representation for both r and acc. The
113// message is fed in 16-byte blocks (final partial block padded with
114// 0x01 then zero-extended); each block's 130-bit value is added to
115// acc (with the high "1" bit per full block), then acc *= r mod p.
116// After the last block, acc += s mod 2^128 and serializes.
117func poly1305_mac(key: *u8, msg: *u8, n: i64, tag: *u8) -> i64 {
118 // --- key setup ---
119 // r-clamp operates on a local copy; never mutate caller's key.
120 let rbuf: *u8 = sys_mmap(16)
121 var ki: i64 = 0
122 while ki < 16 { rbuf[ki] = key[ki]; ki = ki + 1 }
123 poly1305_clamp(rbuf)
124
125 // Split clamped r into 5 x 26-bit limbs.
126 let r_lo: i64 = p_load_u32_le(rbuf, 0) // bits 0..31
127 let r_m1: i64 = p_load_u32_le(rbuf, 4) // bits 32..63
128 let r_m2: i64 = p_load_u32_le(rbuf, 8) // bits 64..95
129 let r_hi: i64 = p_load_u32_le(rbuf, 12) // bits 96..127
130
131 let r0: i64 = r_lo & 0x3FFFFFF
132 let r1: i64 = ((r_lo >> 26) | (r_m1 << 6)) & 0x3FFFFFF
133 let r2: i64 = ((r_m1 >> 20) | (r_m2 << 12)) & 0x3FFFFFF
134 let r3: i64 = ((r_m2 >> 14) | (r_hi << 18)) & 0x3FFFFFF
135 let r4: i64 = (r_hi >> 8) & 0x3FFFFFF
136
137 // Precompute 5 * r1..r4 for the reduction step. (Each scaled
138 // value still fits in 32 bits since r_i <= 2^26 and 5 < 2^3.)
139 let s1: i64 = r1 * 5
140 let s2: i64 = r2 * 5
141 let s3: i64 = r3 * 5
142 let s4: i64 = r4 * 5
143
144 // Accumulator starts at 0.
145 var h0: i64 = 0
146 var h1: i64 = 0
147 var h2: i64 = 0
148 var h3: i64 = 0
149 var h4: i64 = 0
150
151 // --- block loop ---
152 let block: *u8 = sys_mmap(16)
153 var pos: i64 = 0
154 while pos < n {
155 // Build a 17-byte block: up to 16 message bytes, padded 0x01.
156 var take: i64 = 16
157 if n - pos < 16 { take = n - pos }
158 var bi: i64 = 0
159 while bi < take { block[bi] = msg[pos + bi]; bi = bi + 1 }
160 // Zero any remaining bytes in the 16-byte buffer.
161 while bi < 16 { block[bi] = 0; bi = bi + 1 }
162
163 // Load block as 5 x 26-bit limbs; the high "1" bit goes on
164 // the high limb, or becomes 2^(8*take) for the final partial.
165 let b_lo: i64 = p_load_u32_le(block, 0)
166 let b_m1: i64 = p_load_u32_le(block, 4)
167 let b_m2: i64 = p_load_u32_le(block, 8)
168 let b_hi: i64 = p_load_u32_le(block, 12)
169
170 var c0: i64 = b_lo & 0x3FFFFFF
171 var c1: i64 = ((b_lo >> 26) | (b_m1 << 6)) & 0x3FFFFFF
172 var c2: i64 = ((b_m1 >> 20) | (b_m2 << 12)) & 0x3FFFFFF
173 var c3: i64 = ((b_m2 >> 14) | (b_hi << 18)) & 0x3FFFFFF
174 var c4: i64 = (b_hi >> 8) & 0x3FFFFFF
175
176 // Add the "1" bit. For a full 16-byte block the bit is at
177 // position 128; for a short final block it's at position
178 // 8*take (so the tag is still unambiguous about length).
179 if take == 16 {
180 c4 = c4 | (1 << 24)
181 } else {
182 // Recompute c0..c4 from a zero-padded buffer where byte
183 // `take` holds 0x01 and the rest are zero -- simpler
184 // than patching individual limbs.
185 bi = 0
186 while bi < 16 { block[bi] = 0; bi = bi + 1 }
187 bi = 0
188 while bi < take { block[bi] = msg[pos + bi]; bi = bi + 1 }
189 block[take] = 0x01
190 let q_lo: i64 = p_load_u32_le(block, 0)
191 let q_m1: i64 = p_load_u32_le(block, 4)
192 let q_m2: i64 = p_load_u32_le(block, 8)
193 let q_hi: i64 = p_load_u32_le(block, 12)
194 c0 = q_lo & 0x3FFFFFF
195 c1 = ((q_lo >> 26) | (q_m1 << 6)) & 0x3FFFFFF
196 c2 = ((q_m1 >> 20) | (q_m2 << 12)) & 0x3FFFFFF
197 c3 = ((q_m2 >> 14) | (q_hi << 18)) & 0x3FFFFFF
198 c4 = (q_hi >> 8) & 0x3FFFFFF
199 }
200
201 // h += c
202 h0 = h0 + c0
203 h1 = h1 + c1
204 h2 = h2 + c2
205 h3 = h3 + c3
206 h4 = h4 + c4
207
208 // h = (h * r) mod p. Schoolbook multiplication of 5x5 limbs
209 // with the "high limb times 5" trick for reduction.
210 let d0: i64 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
211 let d1: i64 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
212 let d2: i64 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
213 let d3: i64 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
214 let d4: i64 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
215
216 // Carry-propagate back to 26-bit limbs.
217 var c_h0: i64 = d0 & 0x3FFFFFF
218 let k1: i64 = d0 >> 26
219 var c_h1: i64 = (d1 + k1) & 0x3FFFFFF
220 let k2: i64 = (d1 + k1) >> 26
221 var c_h2: i64 = (d2 + k2) & 0x3FFFFFF
222 let k3: i64 = (d2 + k2) >> 26
223 var c_h3: i64 = (d3 + k3) & 0x3FFFFFF
224 let k4: i64 = (d3 + k3) >> 26
225 var c_h4: i64 = (d4 + k4) & 0x3FFFFFF
226 let k5: i64 = (d4 + k4) >> 26
227 // Fold overflow back into h0 via the 2^130 = 5 mod p trick.
228 c_h0 = c_h0 + k5 * 5
229 c_h1 = c_h1 + (c_h0 >> 26)
230 c_h0 = c_h0 & 0x3FFFFFF
231
232 h0 = c_h0
233 h1 = c_h1
234 h2 = c_h2
235 h3 = c_h3
236 h4 = c_h4
237
238 pos = pos + take
239 }
240
241 // --- finalise ---
242 // Collapse 5 limbs back to a 128-bit value, then add s.
243 // First a final carry pass to get each limb into [0, 2^26).
244 h1 = h1 + (h0 >> 26); h0 = h0 & 0x3FFFFFF
245 h2 = h2 + (h1 >> 26); h1 = h1 & 0x3FFFFFF
246 h3 = h3 + (h2 >> 26); h2 = h2 & 0x3FFFFFF
247 h4 = h4 + (h3 >> 26); h3 = h3 & 0x3FFFFFF
248 h0 = h0 + (h4 >> 26) * 5
249 h4 = h4 & 0x3FFFFFF
250 h1 = h1 + (h0 >> 26)
251 h0 = h0 & 0x3FFFFFF
252
253 // Conditional subtract p = 2^130 - 5. If h >= p, set h := h - p.
254 // Implemented constant-time by computing h + 5 (which would
255 // overflow 2^130 iff h >= p) and selecting by the carry out.
256 var g0: i64 = h0 + 5
257 var g1: i64 = h1 + (g0 >> 26); g0 = g0 & 0x3FFFFFF
258 var g2: i64 = h2 + (g1 >> 26); g1 = g1 & 0x3FFFFFF
259 var g3: i64 = h3 + (g2 >> 26); g2 = g2 & 0x3FFFFFF
260 var g4: i64 = h4 + (g3 >> 26) - (1 << 26)
261 g3 = g3 & 0x3FFFFFF
262
263 // Shift-semantics-independent sign-bit extract. Both nxc2
264 // backends (RV64 + x86_64) emit ARITHMETIC right-shift for `>>`,
265 // so `(g4 >> 63)` gives -1 when g4<0, not 1; the single-line
266 // `^ -1` trick the original NaCl reference uses assumes a
267 // LOGICAL shift via uint32, which we don't have. Masking bit 0
268 // then negating produces -1 for g4<0 regardless of shift kind.
269 // (F17 cross-backend codegen divergence trap, except both
270 // backends are wrong here. Cardinal: bench/audit_rv64_honesty_sweep.)
271 let mask: i64 = 0 - ((g4 >> 63) & 1) // -1 if g4<0 (keep h), else 0
272 let nmask: i64 = mask ^ -1 // 0 if g4<0, else -1
273 h0 = (h0 & mask) | (g0 & nmask)
274 h1 = (h1 & mask) | (g1 & nmask)
275 h2 = (h2 & mask) | (g2 & nmask)
276 h3 = (h3 & mask) | (g3 & nmask)
277 h4 = (h4 & mask) | (g4 & nmask)
278
279 // Serialize h as 4 x u32 LE, adding s (bytes 16..31 of key) mod 2^128.
280 // Pack the 5x26-bit selected limbs into 4x32-bit u32 words.
281 // The `& 0xFFFFFFFF` mask MUST be applied here, not just on the
282 // sum: `(h1 << 26)` leaks bits 32..51 into the i64, which then
283 // contaminate the carry when we do `>> 32` on (a + s). Bug
284 // class F-poly-1: convert-to-u32 must mask BEFORE addition, not
285 // just when extracting the low 32 of the sum. (OpenSSL's
286 // poly1305_finish does this masking explicitly; the NaCl 32-bit
287 // reference uses unsigned types so masking is implicit.)
288 let a0: i64 = ( h0 | (h1 << 26)) & 0xFFFFFFFF
289 let a1: i64 = ((h1 >> 6) | (h2 << 20)) & 0xFFFFFFFF
290 let a2: i64 = ((h2 >> 12) | (h3 << 14)) & 0xFFFFFFFF
291 let a3: i64 = ((h3 >> 18) | (h4 << 8)) & 0xFFFFFFFF
292
293 let s0: i64 = p_load_u32_le(key, 16)
294 let s_s1: i64 = p_load_u32_le(key, 20)
295 let s_s2: i64 = p_load_u32_le(key, 24)
296 let s_s3: i64 = p_load_u32_le(key, 28)
297
298 var t0: i64 = (a0 + s0) & 0xFFFFFFFF
299 let ca0: i64 = (a0 + s0) >> 32
300 var t1: i64 = (a1 + s_s1 + ca0) & 0xFFFFFFFF
301 let ca1: i64 = (a1 + s_s1 + ca0) >> 32
302 var t2: i64 = (a2 + s_s2 + ca1) & 0xFFFFFFFF
303 let ca2: i64 = (a2 + s_s2 + ca1) >> 32
304 var t3: i64 = (a3 + s_s3 + ca2) & 0xFFFFFFFF
305
306 p_store_u32_le(tag, 0, t0)
307 p_store_u32_le(tag, 4, t1)
308 p_store_u32_le(tag, 8, t2)
309 p_store_u32_le(tag, 12, t3)
310 return 0
311}
312
313// Constant-time tag comparison. Returns 1 if a[0..16] == b[0..16],
314// 0 otherwise. TLS Finished verification MUST use this rather than
315// a short-circuiting memcmp; the timing leak otherwise lets a remote
316// attacker bisect the tag byte-by-byte.
317func poly1305_tag_equal(a: *u8, b: *u8) -> i64 {
318 var diff: i64 = 0
319 var i: i64 = 0
320 while i < 16 {
321 diff = diff | ((a[i] & 0xff) ^ (b[i] & 0xff))
322 i = i + 1
323 }
324 let neg: i64 = 0 - diff
325 let neq: i64 = (neg >> 63) & 1
326 return 1 - neq
327}
328
329// Real KAT execution lives in runtime/nx_poly1305_test.nx, which
330// imports this module and runs RFC 8439 §2.5.2 (34-byte
331// "Cryptographic Forum Research Group" vector) against the
332// expected tag a8061dc1305136c6c22b8baf0c0127a9. Removed the
333// stub `main` here so importing this module from a test file
334// doesn't double-define main.