poly1305.nx source
↩ module page · 288 lines · 11636 B
1// poly1305.nx -- Bernstein's Poly1305 MAC (RFC 8439 variant).
2//
3// One-time authenticator over prime p = 2^130 - 5. Takes a 256-bit
4// one-time key (128-bit r clamped + 128-bit s), a message of any
5// length, and produces a 16-byte tag that is unforgeable under a
6// unique-per-message key assumption.
7//
8// Why Poly1305:
9// - Proven secure in the one-time-key model (Bernstein 2005).
10// - Pairs with ChaCha20 to form the RFC 8439 AEAD construction
11// (ChaCha20-Poly1305), the primary TLS 1.3 cipher suite that
12// avoids AES cache-side-channel concerns.
13// - Naturally constant-time on this design: only modular
14// multiplication and addition, no branches on key/message
15// bits. (Some implementations use table lookups for
16// performance -- we avoid those for side-channel reasons.)
17// - Under Grover's algorithm a 128-bit MAC has 64-bit effective
18// security against quantum second-preimage -- that's marginal,
19// but second-preimage on a MAC isn't a practical attack; the
20// one-time-key property is what matters.
21//
22// Arithmetic representation:
23// r and acc live as 5 x 26-bit limbs (total ~130 bits). Each
24// limb fits in i64 with headroom; partial products during
25// multiplication sum to at most ~5 * 2^52 = 2^54.3, still in
26// i64 range. After multiplication we carry-propagate and
27// reduce modulo p = 2^130 - 5 via the standard Bernstein trick
28// (multiply high bits by 5, add back to low).
29//
30// Invariants:
31// P1 No branch depends on secret data (key or message bytes).
32// P2 The message is processed exactly once per block; padding
33// of the final short block is explicit (append 0x01, zero-
34// extend), no early exit.
35// P3 The r-clamp (RFC 8439 §2.5) is applied once at key setup;
36// never reapplied mid-stream so no key material leaks.
37// P4 Caller guarantees a *fresh* 256-bit key per message.
38// Poly1305 is not secure if the same r+s pair authenticates
39// two distinct messages. In ChaCha20-Poly1305 this is
40// enforced by deriving (r, s) from ChaCha20(key, nonce, 0).
41//
42// References:
43// - RFC 8439 section 2.5 (Poly1305 algorithm)
44// - RFC 8439 Appendix A.3 (test vectors)
45// - Bernstein 2005 "The Poly1305-AES MAC"
46
47import "syscalls.nx"
48
49// Load a little-endian u32 from buf[off..off+4].
50func p_load_u32_le(buf: *u8, off: i64) -> i64 {
51 let b0: i64 = buf[off + 0]
52 let b1: i64 = buf[off + 1]
53 let b2: i64 = buf[off + 2]
54 let b3: i64 = buf[off + 3]
55 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
56}
57
58// Store a little-endian u32 to buf[off..off+4].
59func p_store_u32_le(buf: *u8, off: i64, v: i64) -> i64 {
60 buf[off + 0] = v & 0xFF
61 buf[off + 1] = (v >> 8) & 0xFF
62 buf[off + 2] = (v >> 16) & 0xFF
63 buf[off + 3] = (v >> 24) & 0xFF
64 return 0
65}
66
67// Clamp r per RFC 8439 §2.5.1: certain bits must be zero to keep
68// the multiply results bounded and the security proof valid.
69// r[3], r[7], r[11], r[15] top 4 bits cleared (mask 0x0f)
70// r[4], r[8], r[12] bottom 2 bits cleared (mask 0xfc)
71// We operate on the 16 raw bytes in-place.
72func poly1305_clamp(r: *u8) -> i64 {
73 r[3] = r[3] & 0x0F
74 r[7] = r[7] & 0x0F
75 r[11] = r[11] & 0x0F
76 r[15] = r[15] & 0x0F
77 r[4] = r[4] & 0xFC
78 r[8] = r[8] & 0xFC
79 r[12] = r[12] & 0xFC
80 return 0
81}
82
83// Full MAC computation.
84// key: 32 bytes = r(16) || s(16)
85// msg: message bytes to authenticate
86// n: message length in bytes
87// tag: 16-byte output buffer
88//
89// Uses a 5 x 26-bit limb representation for both r and acc. The
90// message is fed in 16-byte blocks (final partial block padded with
91// 0x01 then zero-extended); each block's 130-bit value is added to
92// acc (with the high "1" bit per full block), then acc *= r mod p.
93// After the last block, acc += s mod 2^128 and serializes.
94func poly1305_mac(key: *u8, msg: *u8, n: i64, tag: *u8) -> i64 {
95 // --- key setup ---
96 // r-clamp operates on a local copy; never mutate caller's key.
97 let rbuf: *u8 = sys_mmap(16)
98 var ki: i64 = 0
99 while ki < 16 { rbuf[ki] = key[ki]; ki = ki + 1 }
100 poly1305_clamp(rbuf)
101
102 // Split clamped r into 5 x 26-bit limbs.
103 let r_lo: i64 = p_load_u32_le(rbuf, 0) // bits 0..31
104 let r_m1: i64 = p_load_u32_le(rbuf, 4) // bits 32..63
105 let r_m2: i64 = p_load_u32_le(rbuf, 8) // bits 64..95
106 let r_hi: i64 = p_load_u32_le(rbuf, 12) // bits 96..127
107
108 let r0: i64 = r_lo & 0x3FFFFFF
109 let r1: i64 = ((r_lo >> 26) | (r_m1 << 6)) & 0x3FFFFFF
110 let r2: i64 = ((r_m1 >> 20) | (r_m2 << 12)) & 0x3FFFFFF
111 let r3: i64 = ((r_m2 >> 14) | (r_hi << 18)) & 0x3FFFFFF
112 let r4: i64 = (r_hi >> 8) & 0x3FFFFFF
113
114 // Precompute 5 * r1..r4 for the reduction step. (Each scaled
115 // value still fits in 32 bits since r_i <= 2^26 and 5 < 2^3.)
116 let s1: i64 = r1 * 5
117 let s2: i64 = r2 * 5
118 let s3: i64 = r3 * 5
119 let s4: i64 = r4 * 5
120
121 // Accumulator starts at 0.
122 var h0: i64 = 0
123 var h1: i64 = 0
124 var h2: i64 = 0
125 var h3: i64 = 0
126 var h4: i64 = 0
127
128 // --- block loop ---
129 let block: *u8 = sys_mmap(16)
130 var pos: i64 = 0
131 while pos < n {
132 // Build a 17-byte block: up to 16 message bytes, padded 0x01.
133 var take: i64 = 16
134 if n - pos < 16 { take = n - pos }
135 var bi: i64 = 0
136 while bi < take { block[bi] = msg[pos + bi]; bi = bi + 1 }
137 // Zero any remaining bytes in the 16-byte buffer.
138 while bi < 16 { block[bi] = 0; bi = bi + 1 }
139
140 // Load block as 5 x 26-bit limbs; the high "1" bit goes on
141 // the high limb, or becomes 2^(8*take) for the final partial.
142 let b_lo: i64 = p_load_u32_le(block, 0)
143 let b_m1: i64 = p_load_u32_le(block, 4)
144 let b_m2: i64 = p_load_u32_le(block, 8)
145 let b_hi: i64 = p_load_u32_le(block, 12)
146
147 var c0: i64 = b_lo & 0x3FFFFFF
148 var c1: i64 = ((b_lo >> 26) | (b_m1 << 6)) & 0x3FFFFFF
149 var c2: i64 = ((b_m1 >> 20) | (b_m2 << 12)) & 0x3FFFFFF
150 var c3: i64 = ((b_m2 >> 14) | (b_hi << 18)) & 0x3FFFFFF
151 var c4: i64 = (b_hi >> 8) & 0x3FFFFFF
152
153 // Add the "1" bit. For a full 16-byte block the bit is at
154 // position 128; for a short final block it's at position
155 // 8*take (so the tag is still unambiguous about length).
156 if take == 16 {
157 c4 = c4 | (1 << 24)
158 } else {
159 // Recompute c0..c4 from a zero-padded buffer where byte
160 // `take` holds 0x01 and the rest are zero -- simpler
161 // than patching individual limbs.
162 bi = 0
163 while bi < 16 { block[bi] = 0; bi = bi + 1 }
164 bi = 0
165 while bi < take { block[bi] = msg[pos + bi]; bi = bi + 1 }
166 block[take] = 0x01
167 let q_lo: i64 = p_load_u32_le(block, 0)
168 let q_m1: i64 = p_load_u32_le(block, 4)
169 let q_m2: i64 = p_load_u32_le(block, 8)
170 let q_hi: i64 = p_load_u32_le(block, 12)
171 c0 = q_lo & 0x3FFFFFF
172 c1 = ((q_lo >> 26) | (q_m1 << 6)) & 0x3FFFFFF
173 c2 = ((q_m1 >> 20) | (q_m2 << 12)) & 0x3FFFFFF
174 c3 = ((q_m2 >> 14) | (q_hi << 18)) & 0x3FFFFFF
175 c4 = (q_hi >> 8) & 0x3FFFFFF
176 }
177
178 // h += c
179 h0 = h0 + c0
180 h1 = h1 + c1
181 h2 = h2 + c2
182 h3 = h3 + c3
183 h4 = h4 + c4
184
185 // h = (h * r) mod p. Schoolbook multiplication of 5x5 limbs
186 // with the "high limb times 5" trick for reduction.
187 let d0: i64 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
188 let d1: i64 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
189 let d2: i64 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
190 let d3: i64 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
191 let d4: i64 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
192
193 // Carry-propagate back to 26-bit limbs.
194 var c_h0: i64 = d0 & 0x3FFFFFF
195 let k1: i64 = d0 >> 26
196 var c_h1: i64 = (d1 + k1) & 0x3FFFFFF
197 let k2: i64 = (d1 + k1) >> 26
198 var c_h2: i64 = (d2 + k2) & 0x3FFFFFF
199 let k3: i64 = (d2 + k2) >> 26
200 var c_h3: i64 = (d3 + k3) & 0x3FFFFFF
201 let k4: i64 = (d3 + k3) >> 26
202 var c_h4: i64 = (d4 + k4) & 0x3FFFFFF
203 let k5: i64 = (d4 + k4) >> 26
204 // Fold overflow back into h0 via the 2^130 = 5 mod p trick.
205 c_h0 = c_h0 + k5 * 5
206 c_h1 = c_h1 + (c_h0 >> 26)
207 c_h0 = c_h0 & 0x3FFFFFF
208
209 h0 = c_h0
210 h1 = c_h1
211 h2 = c_h2
212 h3 = c_h3
213 h4 = c_h4
214
215 pos = pos + take
216 }
217
218 // --- finalise ---
219 // Collapse 5 limbs back to a 128-bit value, then add s.
220 // First a final carry pass to get each limb into [0, 2^26).
221 h1 = h1 + (h0 >> 26); h0 = h0 & 0x3FFFFFF
222 h2 = h2 + (h1 >> 26); h1 = h1 & 0x3FFFFFF
223 h3 = h3 + (h2 >> 26); h2 = h2 & 0x3FFFFFF
224 h4 = h4 + (h3 >> 26); h3 = h3 & 0x3FFFFFF
225 h0 = h0 + (h4 >> 26) * 5
226 h4 = h4 & 0x3FFFFFF
227 h1 = h1 + (h0 >> 26)
228 h0 = h0 & 0x3FFFFFF
229
230 // Conditional subtract p = 2^130 - 5. If h >= p, set h := h - p.
231 // Implemented constant-time by computing h + 5 (which would
232 // overflow 2^130 iff h >= p) and selecting by the carry out.
233 var g0: i64 = h0 + 5
234 var g1: i64 = h1 + (g0 >> 26); g0 = g0 & 0x3FFFFFF
235 var g2: i64 = h2 + (g1 >> 26); g1 = g1 & 0x3FFFFFF
236 var g3: i64 = h3 + (g2 >> 26); g2 = g2 & 0x3FFFFFF
237 var g4: i64 = h4 + (g3 >> 26) - (1 << 26)
238 g3 = g3 & 0x3FFFFFF
239
240 // mask = 0 if g4 < 0 (i.e. h < p), else all-ones.
241 let mask: i64 = (g4 >> 63) ^ -1 // -1 when g4 < 0, else 0
242 let nmask: i64 = mask ^ -1
243 h0 = (h0 & mask) | (g0 & nmask)
244 h1 = (h1 & mask) | (g1 & nmask)
245 h2 = (h2 & mask) | (g2 & nmask)
246 h3 = (h3 & mask) | (g3 & nmask)
247 h4 = (h4 & mask) | (g4 & nmask)
248
249 // Serialize h as 4 x u32 LE, adding s (bytes 16..31 of key) mod 2^128.
250 let a0: i64 = h0 | (h1 << 26)
251 let a1: i64 = (h1 >> 6) | (h2 << 20)
252 let a2: i64 = (h2 >> 12) | (h3 << 14)
253 let a3: i64 = (h3 >> 18) | (h4 << 8)
254
255 let s0: i64 = p_load_u32_le(key, 16)
256 let s_s1: i64 = p_load_u32_le(key, 20)
257 let s_s2: i64 = p_load_u32_le(key, 24)
258 let s_s3: i64 = p_load_u32_le(key, 28)
259
260 var t0: i64 = (a0 + s0) & 0xFFFFFFFF
261 let ca0: i64 = (a0 + s0) >> 32
262 var t1: i64 = (a1 + s_s1 + ca0) & 0xFFFFFFFF
263 let ca1: i64 = (a1 + s_s1 + ca0) >> 32
264 var t2: i64 = (a2 + s_s2 + ca1) & 0xFFFFFFFF
265 let ca2: i64 = (a2 + s_s2 + ca1) >> 32
266 var t3: i64 = (a3 + s_s3 + ca2) & 0xFFFFFFFF
267
268 p_store_u32_le(tag, 0, t0)
269 p_store_u32_le(tag, 4, t1)
270 p_store_u32_le(tag, 8, t2)
271 p_store_u32_le(tag, 12, t3)
272 return 0
273}
274
275// Compile-only smoke. Real KAT validation (RFC 8439 Appendix A.3
276// test vector 2: key+msg -> a8 06 1d c1 30 51 36 c6 c2 2b 8b af 0c
277// 01 27 a9) requires execution harness pending.
278func main() -> i64 {
279 let key: *u8 = sys_mmap(32)
280 let msg: *u8 = sys_mmap(16)
281 let tag: *u8 = sys_mmap(16)
282 var i: i64 = 0
283 while i < 32 { key[i] = 0; i = i + 1 }
284 i = 0
285 while i < 16 { msg[i] = 0; i = i + 1 }
286 poly1305_mac(key, msg, 16, tag)
287 return tag[0] as i64
288}