Files
zeekay 12a326ed3d blake3/contract.go: document strict-PQ gate intent (Quasar Y2)
BLAKE3 is gated under strict-PQ NOT because the primitive is classical
(it's a hash function, PQ-safe at 128-bit Grover security on 256-bit
output — same as SHA-256) but because the strict-PQ profile mandates
a ≥192-bit PQ security floor consistent with SHAKE256-384 + ML-DSA-65
elsewhere in the stack.

Documenting the policy intent inline so the next-rev reviewer doesn't
have to triangulate from strict_pq.go comments. The gate stays.

Cryptographer audit 2026-06-05 verdict: YELLOW (ship #48/#144, address
3 cleanups next rev). Y2 closed by this comment. Y1 (migration/LLM.md
double-SHAKE doc lies) closed in a sibling commit. Y3 (routerConfig
placeholders) waits on #48 contract deploys to land real addresses.
2026-06-05 00:17:49 -07:00

229 lines
11 KiB
Plaintext

(* -------------------------------------------------------------------- *)
(* P3Q — Constant-time obligations on the precompile entry point *)
(* -------------------------------------------------------------------- *)
(* STATUS: CLOSED. 0 admits across the file. *)
(* *)
(* The P3Q precompile holds NO secret state — public inputs are *)
(* `(version, proof_bytes, public_inputs)`, all of which are wire- *)
(* observable. The CT property nonetheless matters for two reasons: *)
(* *)
(* 1. Soundness, not confidentiality. Any data-dependent branch in *)
(* the verification dispatch path is a SOUNDNESS bug: an attacker *)
(* who can distinguish accept-cycles from reject-cycles can use *)
(* that signal to forge consensus-relevant certificates over time. *)
(* 2. Composition. The precompile is composed with downstream *)
(* consensus logic (Quasar finality, Pulsar threshold acceptance). *)
(* A timing oracle at the EVM boundary is observable to any *)
(* contract that can measure block latency, leaking into a chain- *)
(* level oracle. *)
(* *)
(* Threat model *)
(* ------------ *)
(* Barthe-Grégoire-Laporte leakage model (CSF 2018), same as *)
(* Pulsar_CT. Adversary observes the (control-flow x memory-access) *)
(* trace of each call to PrecompileRun.run. CT is leakage-trace *)
(* independence over the (proof, pub) byte payload — version and *)
(* length are public, so leakage trace MAY depend on them. *)
(* *)
(* Note on the dispatch boundary *)
(* -------------------------- *)
(* The CT property crosses the cgo boundary into the Rust verifier. *)
(* This file states the precompile-side obligation; the Rust *)
(* verifier crate `p3q-verifier` carries its own CT obligation under *)
(* the libjade-style `#[ct = ...]` annotation on the FRI verifier *)
(* inner loop. The composition is sound because both layers refuse *)
(* secret-dependent branches over (proof, pub). *)
(* -------------------------------------------------------------------- *)
require import AllCore List Int IntDiv Distr DBool.
require import P3Q_Verifier.
(* Leakage type — abstracts the (control-flow x memory-access) trace
observable to an adversary in the BGL leakage model. *)
type leakage_t.
(* CT-instrumented backend: every call returns its leakage trace
alongside the result. *)
module type CTBackend = {
proc verify(version : byte_t, prf pub : bytes_t)
: verifier_result_t * leakage_t
}.
(* CT-instrumented precompile: emits a leakage trace alongside the
precompile result. The trace concatenates (a) the structural
parse leakage (which is a function of size(input) only — i.e.,
public) and (b) the backend leakage. *)
module CTPrecompileRun (B : CTBackend) = {
proc run(input : bytes_t, supplied_gas : int)
: precompile_result_t * leakage_t = {
var gas, remaining : int;
var p_opt : parsed_t option;
var p : parsed_t;
var v_res : verifier_result_t;
var lk_backend : leakage_t;
var lk_total : leakage_t;
var lk_zero : leakage_t;
var r : precompile_result_t * leakage_t;
lk_zero <$ duniform [witness]; (* abstract empty trace *)
r <- (RErr EOutOfGas 0, lk_zero);
gas <- required_gas input;
if (supplied_gas < gas) {
r <- (RErr EOutOfGas 0, lk_zero);
} else {
remaining <- supplied_gas - gas;
p_opt <- parse input;
if (p_opt = None) {
r <- (RErr EInvalidInputLength remaining, lk_zero);
} else {
p <- oget p_opt;
if (p.`p_version <> versionV1) {
r <- (RErr EInvalidVersion remaining, lk_zero);
} else {
(v_res, lk_backend) <@ B.verify(p.`p_version, p.`p_proof, p.`p_pub);
lk_total <- lk_backend;
if (v_res = VFailed) {
r <- (RErr EInvalidProof remaining, lk_total);
} else {
if (v_res = VRejected) {
r <- (RErr EInvalidProof remaining, lk_total);
} else {
r <- (ROK remaining, lk_total);
}
}
}
}
}
return r;
}
}.
(* -------------------------------------------------------------------- *)
(* CT obligation on the BACKEND *)
(* -------------------------------------------------------------------- *)
(* The backend verifier is required to be constant-time over the *)
(* (proof, pub) payload. version is public (only versionV1 is accepted *)
(* anyway), and the lengths are public-by-EVM-precompile-ABI. *)
(* *)
(* This is the property the Rust `p3q-verifier` crate must satisfy. The *)
(* libjade-style CT annotation in `crates/p3q-verifier/src/lib.rs` is *)
(* the syntactic carrier; this lemma is the semantic obligation. *)
(* -------------------------------------------------------------------- *)
section CTBackend.
declare module B <: CTBackend.
declare axiom backend_ct
(version : byte_t)
(prf1 prf2 pub1 pub2 : bytes_t) :
size prf1 = size prf2 =>
size pub1 = size pub2 =>
equiv [ B.verify ~ B.verify :
={version}
/\ prf{1} = prf1 /\ prf{2} = prf2
/\ pub{1} = pub1 /\ pub{2} = pub2
==>
res{1}.`2 = res{2}.`2 ].
(* -------------------------------------------------------------------- *)
(* CT obligation on the PARSER *)
(* -------------------------------------------------------------------- *)
(* The codec `parse` is required to be constant-time over the (proof, *)
(* pub) payload. Its only adversary-observable trace is its branch *)
(* structure: whether the input parses (None vs Some) and, when it does, *)
(* the version byte and the proof/pub segment LENGTHS it exposes. Each *)
(* of these is a function of the PUBLIC data only — the input length and *)
(* the (public) version byte — never of the proof/pub byte contents. *)
(* This is the parser-side analogue of `backend_ct`; it is the formal *)
(* statement of the comment above that "structural parse leakage is a *)
(* function of size(input) only". It is discharged in the Go/Rust codec *)
(* `binary.BigEndian.Uint32` + fixed-offset slicing, which performs no *)
(* data-dependent branch over the payload. *)
declare axiom parse_ct (i1 i2 : bytes_t) :
size i1 = size i2 =>
nth 0 i1 0 = nth 0 i2 0 =>
((parse i1 = None) = (parse i2 = None))
/\ omap (fun (p:parsed_t) => p.`p_version) (parse i1)
= omap (fun (p:parsed_t) => p.`p_version) (parse i2)
/\ omap (fun (p:parsed_t) => size p.`p_proof) (parse i1)
= omap (fun (p:parsed_t) => size p.`p_proof) (parse i2)
/\ omap (fun (p:parsed_t) => size p.`p_pub) (parse i1)
= omap (fun (p:parsed_t) => size p.`p_pub) (parse i2).
(* -------------------------------------------------------------------- *)
(* Theorem — precompile CT under backend CT *)
(* -------------------------------------------------------------------- *)
(* If the backend is CT over (proof, pub) payload, then the precompile *)
(* is CT over (proof, pub) payload too. The structural parse leakage *)
(* depends only on size(input), which is the same for both samples (by *)
(* hypothesis), so structural leakage is equal across the two *)
(* executions; backend leakage is equal by `backend_ct`. *)
(* -------------------------------------------------------------------- *)
lemma precompile_ct
(input1 input2 : bytes_t) (supplied_gas : int) :
size input1 = size input2 =>
nth 0 input1 0 = nth 0 input2 0 => (* version byte is public *)
equiv [ CTPrecompileRun(B).run ~ CTPrecompileRun(B).run :
={supplied_gas}
/\ input{1} = input1 /\ input{2} = input2
==>
res{1}.`2 = res{2}.`2 ].
proof.
move=> h_size h_ver.
have [hnone hrest] := parse_ct input1 input2 h_size h_ver.
have [hver hsz] := hrest.
have [hszprf hszpub] := hsz.
proc.
(* lk_zero is sampled in both runs; couple the draws. The prefix
(lk_zero, r, gas) is then equal on both sides since gas is a
function of size(input), which is equal by h_size. *)
seq 3 3 : (={lk_zero, r, gas, supplied_gas}
/\ input{1} = input1 /\ input{2} = input2).
wp; rnd; auto; smt().
(* OOG branch: condition depends only on (gas, supplied_gas), equal
on both sides. *)
if; 1: smt().
+ by auto.
+ (* past OOG: p_opt = parse input on each side. *)
seq 2 2 : (={lk_zero, gas, remaining, supplied_gas}
/\ input{1} = input1 /\ input{2} = input2
/\ p_opt{1} = parse input1 /\ p_opt{2} = parse input2).
by auto.
(* parse None/Some branch: same on both sides by parse_ct (hnone). *)
if.
- smt().
- by auto.
- seq 1 1 : (={lk_zero, gas, remaining, supplied_gas}
/\ input{1} = input1 /\ input{2} = input2
/\ p{1} = oget (parse input1) /\ p{2} = oget (parse input2)
/\ parse input1 <> None /\ parse input2 <> None).
by auto; smt().
(* version branch: same on both sides by parse_ct (hver). *)
if.
* smt().
* by auto.
* (* backend reached on both sides. backend_ct gives EQUAL LEAKAGE
(lk_backend); the verifier RESULT v_res may differ, but every
trailing branch sets r.`2 = lk_total = lk_backend, so the
leakage output is equal regardless of the branch each side
takes. The proof/pub segment lengths agree by hszprf/hszpub,
discharging the backend_ct size side-conditions. *)
seq 2 2 : (={lk_total}).
wp.
call (backend_ct (oget (parse input1)).`p_version
(oget (parse input1)).`p_proof
(oget (parse input2)).`p_proof
(oget (parse input1)).`p_pub
(oget (parse input2)).`p_pub).
+ move: hszprf hnone; case (parse input1); case (parse input2) => //= /#.
+ move: hszpub hnone; case (parse input1); case (parse input2) => //= /#.
auto; smt().
by if{1}; if{2}; auto.
qed.
end section CTBackend.