Collision resistance of the random oracle #
A 101-crypto example exercising the framework. We define the standard
collision-resistance game (cr_experiment) parameterised by a query budget
q, an init step, and a query step. Two flavours are then obtained by
plugging in lazy_* or random_oracle_* (= eager) primitives.
The high-level claims are:
cr_transfer— the collision probability is identical under lazy and eager random oracle (proved viaoracle_loop_wp_lazy_eq_random_oracle).cr_lazy_bound— the lazy collision probability is bounded by the birthday bound(q+2)(q+1) / (2 · |output|)(the +2 accounts for the final two collision-check queries).cr_eager_bound— the same bound for eager, bycr_transfer.
All results below are fully proved (no sorry): the transfer via the
ProgramDenotation.transfer framework, and the birthday bound by a unary
wp-expectation induction.
Disjointness: the claim variables don't alias the RO state.
Output equality needed for the collision check. We get it for free from classical logic (the rest of the file is noncomputable anyway).
Equations
CR experiment parameterised over an adversary #
cr_adv is a parameter (via the variable declaration below), not an
axiom. Every cr_experiment-, cr_lazy_bound-, cr_transfer-style
definition and theorem in this section takes an arbitrary CR adversary
cr_adv : ProgramDenotation state Unit together with h_cr_adv : cr_adv.inFootprint ....
The adversary may set oracle_input (queried each round), may set
claim_x and claim_x' (its candidate collision), and may not touch
random_oracle_state directly.
One round of the CR loop body: the adversary computes, then we run one
query on whatever cr_adv placed in oracle_input. Thin alias for the
generic oracle_step in RO.lean.
Equations
- cr_loop_body cr_adv oracle = oracle_step cr_adv oracle
Instances For
Run the adversary-and-query loop for q rounds. Thin alias for the
generic oracle_loop_n in RO.lean.
Equations
- cr_loop cr_adv q oracle = oracle_loop_n cr_adv q oracle
Instances For
The CR experiment parameterised by query budget q, init, and oracle.
Run the adversary for q rounds, then read its claim (x, x'), query the
oracle at both, and report whether (x ≠ x') ∧ (y = y').
Equations
- One or more equations did not get rendered due to their size.
Instances For
Phase 2 — Transfer (lazy = eager) #
The output of cr_experiment is RO-invariant (it only depends on state
variables disjoint from random_oracle_state), so the dist of the result
bit agrees under lazy and eager RO.
Building blocks for cr_transfer via the ProgramDenotation.transfer framework. #
Transfer theorem: The marginal distribution of the result bit is identical under lazy and eager random oracle.
Phase 3 — Birthday bound on the lazy CR experiment #
The proof is decomposed into helper lemmas and a top-level
composition. The composition cr_lazy_bound itself is
proved — no sorry — modulo the two helpers.
cr_true_implies_collision_wp(bookkeeping): if the experiment's result bit istrue, then the final state has a collision. This is a structural / support-analysis fact:cr_experimentwriteslazy_query xandlazy_query x'into the RO table before returningdecide (x ≠ x' ∧ y = y'), so atruereturn impliesRO[x] = RO[x'] = some ywithx ≠ x'.cr_collision_birthday_bound(the real probability work): the expected collision indicator is bounded by(q+2)(q+1) / (2·N). This is the actual birthday argument — a union bound over pairs of distinct queried inputs, each pair colliding with probability ≤ 1/N because lazy_query yields uniform fresh samples.
A state has a "collision": two distinct inputs both have cached RO values, and those cached values are equal.
Equations
Instances For
Equations
0/1-valued collision indicator on state.
Equations
- collision_indicator σ = if has_collision σ then 1 else 0
Instances For
Helper: lazy_query postcondition strengthening #
lazy_query support strengthening: if an invariant I : output → state → Prop
holds at every state reachable from lazy_query x σ — namely, on the cached
output (when the entry is cached) and on every fresh-sample post-state (when
not cached) — then strengthening the postcondition with if I then F else 0
leaves the wp value unchanged.
lazy_query writes its output: integrating any F against
lazy_query x σ is the same as integrating F restricted to states
where RO[x] = some (returned y).
lazy_query preserves disjoint state: querying doesn't change
the value of any variable disjoint from random_oracle_state.
lazy_query preserves other RO entries: querying x doesn't
change RO[x'] for x' ≠ x.
Bookkeeping helper: at every state in the support of
cr_experiment cr_adv q lazy_init lazy_query, if the result bit is true
then the state has a collision. Stated at the wp level so it
composes with the birthday bound below.
Proof plan (~80-100 lines; helpers all in place):
Goal after
simp only [cr_experiment, wp_bind, wp_get, wp_pure]: wp-tower with bothlazy_querycalls visible. Call the inner state parametersσ₂(aftercr_loop),σ₅(after first lazy_query),σ₆(after second).Apply
lazy_query_wp_writes_outputto the first lazy_query (returningyfromx_v = claim_x.get σ₂). This strengthens the postcondition oflazy_query x_vwithRO[x_v] = some yatσ₅.Apply
lazy_query_wp_writes_outputto the second lazy_query (y' ← lazy_query x'_v). Postcondition gainsRO[x'_v] = some y'atσ₆.Apply
lazy_query_wp_preserves_other_ROto the second lazy_query withx = x'_v, x' = x_v(underx_v ≠ x'_v):RO[x_v]preserved fromσ₅toσ₆, so still= some yatσ₆.Apply
lazy_query_wp_preserves_disjointto both lazy_query calls forclaim_xandclaim_x'. These give usclaim_x.get σ₆ = x_vandclaim_x'.get σ₆ = x'_v.At the leaf, with all invariants strengthened into the post: result =
decide (x_v ≠ x'_v ∧ y = y') = true⟹claim_x.get σ₆ = x_v ≠ x'_v = claim_x'.get σ₆, andRO[x_v] = some y = some y' = RO[x'_v]atσ₆. Sohas_collision σ₆with witnesses(x_v, x'_v, y).The strengthened post is pointwise ≤
collision_indicator σ₆; applyMeasureTheory.lintegral_monopropagated outward through each wp_bind layer.
Birthday-bound decomposition #
The proof is decomposed into:
RO_size— counts the cached entries of the random oracle.lazy_query_collision_step(Layer A) — each query bumps the collision probability by at mostRO_size σ / N. Core probability content.lazy_query_RO_size_step(Layer B, proved) — each query's expectedRO_sizegrows by at most 1. Combinatorial.cr_loop_birthday_step(Layer C) — combining A and B overcr_loop k: afterkqueries the collision bump is at most the triangular sumk * (2 * RO_size σ + k - 1) / (2N).cr_collision_birthday_bound(Layer D, proved modulo A & C) — combinescr_loop qwith the two final queries.
Layer A and its sublemmas #
lazy_query_collision_step (Layer A): each lazy_query bumps the collision
probability by at most RO_size σ / N. The fresh-sample case is a union bound:
each uniform sample collides with at most RO_size σ existing entries (each
with probability 1/N). Decomposed into 5 helpers below.
The Finset of output values that a fresh sample at x could collide with:
values that already appear in the RO at some other input.
Equations
- inducing_set x σ = Finset.image (fun (x' : input) => (random_oracle_state.get σ x').getD default) ({x' ∈ Finset.univ.erase x | (random_oracle_state.get σ x').isSome = true})
Instances For
Membership characterization.
The inducing set has at most RO_size σ elements.
Layer B: the expected RO_size after one lazy_query is at most
one more than before. (Tight: in the cached branch, equal; in the fresh
branch, exactly +1.) Reduced to the generic lazy_query_wp_step with
pointwise bad-event 1.
RO_size factors through the RO content: equal RO maps give equal sizes.
cr_adv doesn't touch the RO, so its expected RO_size is preserved.
Setting a variable disjoint from random_oracle_state doesn't change RO_size.
One iteration of cr_loop_body bumps RO_size by at most 1 in expectation.
Reduced to the generic oracle_step_wp_indicator_bump_const.
Layer B-iterated: expected RO_size after cr_loop k grows by at most
k. Needed by Layer D to bound the size at intermediate points. Reduced
to the generic oracle_loop_n_wp_linear_bound with c = 1.
Collision-side helpers (mirror of the RO_size helpers above) #
collision_indicator factors through the RO content.
cr_adv doesn't touch the RO, so the collision indicator is preserved
in expectation.
Setting a variable disjoint from random_oracle_state doesn't change
collision_indicator.
One iteration of cr_loop_body bumps the collision indicator by at most
RO_size σ / N (in expectation). Reduced to the generic
oracle_step_wp_indicator_bump.
Layer C: the cumulative collision bound after cr_loop k queries.
Triangular sum of Layer A across the loop.
Proof plan (induction on k):
- Base k=0: cr_loop 0 = pure, no bump.
- Step k+1: cr_loop_body bumps collision by ≤ RO_size σ / N (helper above). Then by IH at post-body state σ' (with RO_size σ' ≤ RO_size σ + 1 in expectation), the rest adds at most k(2*(RO_size σ + 1) + k - 1)/(2N) = k(2RO_size σ + k + 1)/(2N). Combined: RO_size σ/N + k(2RO_size σ + k + 1)/(2N) = (k+1)(2*RO_size σ + k)/(2N), matching the bound at level k+1.
After lazy_init, no collision exists.
Layer D — cr_collision_birthday_bound: composition of A, B, C.
After lazy_init, RO_size = 0. After cr_loop q, by Layer C the bound is
q(q-1)/(2N). The two final queries add at most (q + (q+1))/N (via Layer
A applied twice). Total: (q+2)(q+1)/(2N).
Birthday bound for the lazy CR experiment. Proved by composing the bookkeeping lemma with the probability bound.
Transfer of cr_transfer from the SubProb marginal level to the
wp level, for postconditions that depend only on the result bit
bσ.1. This bridges Phase 2 and Phase 3 for cr_eager_bound. Thin
wrapper over the generic ProgramDenotation.wp_eq_of_marginal_eq.
Birthday bound for the eager (true random oracle) game,
obtained by transferring cr_lazy_bound via cr_transfer.
Generic lazy-oracle collision bound. For any adversary A that only
touches the random-oracle state, after lazy_init and q query rounds
(each round runs A, then answers one lazy_query), the probability
that the oracle map contains a collision is at most q(q−1)/2N.
This is the birthday framework loop_n_birthday_bound instantiated at the
RO collision/size potentials, with lazy_init zeroing both. Unlike
cr_collision_birthday_bound (which carries two extra challenge queries,
giving (q+2)(q+1)/2N), this is the clean q(q−1)/2N for the bare
q-round loop — the Pr[bad] input to the PRP/PRF switching lemma.