spec/crypto/poly1305.bend source
spec/crypto/poly1305.bend on the hub · documented module
import Baseimport ../lib/common.bend as C# Specification of the Poly1305 one-time authenticator (RFC 8439 section# 2.5), transcribed from the RFC's pseudocode over natural numbers. Nothing# is shared with the implementation (src/crypto/poly1305/): the accumulator# is one Nat reduced mod p = 2^130 - 5 after every block, as in HACL*'s# Spec.Poly1305. Numbers are little-endian byte strings; a byte is the low 8# bits of a U32. Multiplication by 256 is written C.shift(8n, x) (eight# doublings, spec/lib/common.bend).## The proof checker expands a closed Nat in unary, so 2^130 is never formed# (as spec/math/w64.bend never forms 2^64): "x mod p" is modp(x) below, which# uses only 2^130 = p + 5 through C.low / C.high (k halvings). That modp(x)# is Nat.mod(x, 2^130 - 5) is proved for every x in# proofs/crypto/poly1305/modp.bend (law modp_mod, stated with 2^130 written# C.shift(130n, one) for a variable one = 1). p() below is the constant,# for the reader; no proof unfolds it.## Bend's native runtime keeps a Nat below 2^48, so this specification is a# proof object; tools/check_poly1305.py runs a line-by-line mirror of it.# p = 2^130 - 5def p() -> Nat: Nat.sub(C.pow2(130n), 5n)# x = lo + 2^130 hi is congruent to lo + 5 hi mod p (2^130 = p + 5); the fold# strictly decreases x while hi > 0, so x folds bring x below 2^130.def fold(+x: Nat) -> Nat: Nat.add(C.low(130n, x), Nat.mul(C.high(130n, x), 5n))def folds(n: Nat, x: Nat) -> Nat: match n: case 0n: x case 1n+k: folds(k, fold(x))def pick(b: Bool, x: Nat, y: Nat) -> Nat: match b: case True{}: x case False{}: y# y < 2^130: y itself when y < p, else y - p = (y + 5) - 2^130.def canon(+y: Nat) -> Nat: match y: case 0n: 0n case 1n+z: pick(C.fits(130n, Nat.add(1n+z, 5n)), 1n+z, C.low(130n, Nat.add(1n+z, 5n)))# x mod pdef modp(+x: Nat) -> Nat: canon(folds(x, x))# le_bytes_to_numdef le_num(bytes: List<&2, U32>) -> Nat: match bytes: case Nil{}: 0n case b <> rest: Nat.add(U32.to_nat(U32.and(b, 255)), C.shift(8n, le_num(rest)))# num_to_n_le_bytes: the low n bytes of x.def le_bytes(n: Nat, +x: Nat) -> List<&2, U32>: match n: case 0n: Nil{} case 1n+k: U32.from_nat(C.low(8n, x)) <> le_bytes(k, C.high(8n, x))def prefix(n: Nat, xs: List<&2, U32>) -> List<&2, U32>: match n xs: case 0n _: Nil{} case 1n+k Nil{}: Nil{} case 1n+k x <> rest: x <> prefix(k, rest)def suffix(n: Nat, xs: List<&2, U32>) -> List<&2, U32>: match n xs: case 0n _: xs case 1n+k Nil{}: Nil{} case 1n+k x <> rest: suffix(k, rest)# clamp(r): r &= 0x0ffffffc0ffffffc0ffffffc0fffffff, i.e. r[3], r[7], r[11]# and r[15] keep their low four bits, r[4], r[8] and r[12] lose their low two.def clamp_mask(i: Nat) -> U32: match i: case 3n: 15 case 7n: 15 case 11n: 15 case 15n: 15 case 4n: 252 case 8n: 252 case 12n: 252 case _: 255def clamp_from(r: List<&2, U32>, +i: Nat) -> List<&2, U32>: match r: case Nil{}: Nil{} case b <> rest: U32.and(b, clamp_mask(i)) <> clamp_from(rest, 1n+i)def clamp(r: List<&2, U32>) -> List<&2, U32>: clamp_from(r, 0n)# The message cut into 16-byte blocks, the last one possibly shorter; fuel# bounds the number of blocks (the length of the message is enough).def blocks(fuel: Nat, +msg: List<&2, U32>) -> List<&2, List<&2, U32>>: match fuel msg: case 0n _: Nil{} case 1n+k Nil{}: Nil{} case 1n+k b <> rest: prefix(16n, b <> rest) <> blocks(k, suffix(16n, b <> rest))# for each block: n = le_bytes_to_num(block | [0x01]); a += n; a = (r * a) % pdef absorb(bs: List<&2, List<&2, U32>>, +r: Nat, a: Nat) -> Nat: match bs: case Nil{}: a case b <> rest: +n = le_num(List.append(&2, U32, b, [1])) absorb(rest, r, modp(Nat.mul(r, Nat.add(a, n))))# poly1305_mac(msg, key): r = clamp(key[0..16]), s = key[16..32];# a = 0, absorb every block, a += s; the tag is num_to_16_le_bytes(a).def poly1305_mac(+key: List<&2, U32>, +msg: List<&2, U32>) -> List<&2, U32>: +r = le_num(clamp(prefix(16n, key))) +s = le_num(prefix(16n, suffix(16n, key))) le_bytes(16n, Nat.add(absorb(blocks(List.length(&2, U32, msg), msg), r, 0n), s))# The same function, split on the message first so that a proof about an# unknown message does not unfold it.def mac(+key: List<&2, U32>, +msg: List<&2, U32>) -> List<&2, U32>: match msg: case Nil{}: poly1305_mac(key, []) case b <> rest: poly1305_mac(key, b <> rest)# ---------------------------------------------------------------- the polynomial# RFC 8439 2.5 describes the accumulator as the evaluation, at r and# modulo p, of the polynomial whose coefficients are the blocks (each with# its 0x01 byte): with q blocks n_1 .. n_q,# poly(r) = n_1 r^q + n_2 r^(q-1) + ... + n_q r# (as Spec.Poly1305 in HACL* and the field statements of Mathlib's ZMod p).# Clause absorb_poly (proofs/crypto/poly1305/poly.bend) proves# absorb(blocks, r, 0) == poly(blocks, r) mod (2^130 - 5).def coeff(b: List<&2, U32>) -> Nat: le_num(List.append(&2, U32, b, [1]))def poly(+bs: List<&2, List<&2, U32>>, +r: Nat) -> Nat: match bs: case Nil{}: 0n case b <> rest: Nat.add(Nat.mul(coeff(b), Nat.pow(r, 1n+List.length(&2, List<&2, U32>, rest))), poly(rest, r))