Files
zeekay c79fe61d62 consolidate: merge pulsar-mptc submission framework into canonical luxfi/pulsar
Single repo, one canonical impl + spec + proofs + KAT + cut tool.

Per "ONE and ONLY ONE way to do things" the prior dual-repo split
(luxfi/pulsar = production library, luxfi/pulsar-mptc = NIST MPTC
submission framework) is collapsed into a single canonical home at
github.com/luxfi/pulsar. SINGLE-IMPL-PLAN.md (now marked COMPLETED)
describes the steps; this commit lands the final step.

Files moved (89 new):
  16 submission docs (SUBMISSION, NIST-SUBMISSION, SPEC, SUITE,
     PATENTS, AXIOM-INVENTORY, PROOF-CLAIMS, FIPS-TRACEABILITY,
     TRUSTED-COMPUTING-BASE, HANZO-CRYPTO-SUITE, INFORMATION-
     ARCHITECTURE, ROADMAP, CHANGELOG, SYNC-STATUS, STATUS-
     SUBMISSION-READINESS, SINGLE-IMPL-PLAN)
  10 docs/ supplements (ietf-draft-skeleton, magnetar, evaluation,
     patent-claims, x-wing-sig, design-decisions, family-architecture,
     threat-model, nist-mptc-category, patent-notes-draft)
  19 proofs/ (13 EC theories + 2 EC lemmas + 4 metadata/READMEs +
     lean-easycrypt-bridge.md)
  14 jasmin/ (lib/* + ml-dsa-65 fetch + threshold/{combine,round1,
     round2}.jazz + READMEs)
  10 ct/dudect/ (Makefile, combine_ct.go, dudect_combine.c,
     dudect_compat.h, dudect_verify.c, fetch.sh, README, run-
     submission.sh, verify_ct.go) + ct/jasmin-ct-libjade.md
  1 .github/workflows/ci.yml
  1 test/interoperability/n1_class_test.go (19/19 N1 subtests)
  1 bench/results/REPORT.md
  11 scripts/ (cut-submission, check-high-assurance, check-lean-
     bridge, nightly, extract-jasmin-ec, checks/{ec-admits, ec-
     compile, ec-refinement-scaffold, ec-regressions, extraction,
     jasmin}, checks/test/{go-unit, interop, kat, no-secret-logs})

Files merged (16 overlapping, mptc-canonical chosen because newer +
post-rewire-correct; pulsar versions still bore stale "Pulsar-M" /
"pulsarm" branding):
  README.md, BLOCKERS.md, CONTRIBUTING.md, LICENSE, LICENSING.md,
  SECURITY.md, .gitignore, bench/run_all.sh, vectors/dkg.json (v1.0.7
  regen), scripts/{bench,build,gen_vectors,sbom,test}.sh,
  spec/{pulsar,parameters,system-model}.tex,
  CRYPTOGRAPHER-SIGN-OFF.md (path refs).

Pulsar-only spec supplements preserved:
  spec/{blockers,design-decisions,family-architecture,nist-mptc-
  category,patent-notes,threat-model}.tex.

Path rewrites applied: 158 occurrences across 26 files
  github.com/luxfi/pulsar-mptc -> github.com/luxfi/pulsar
  ~/work/lux/pulsar-mptc/      -> ~/work/lux/pulsar/
  luxfi/pulsar-mptc            -> luxfi/pulsar
  pulsar-mptc/                  -> ./

Narrative collapses (no more two-repo framing):
  SUBMISSION.md "At a glance" — single Repository row
  SYNC-STATUS.md — rewritten to single-repo state
  SINGLE-IMPL-PLAN.md — marked COMPLETED 2026-05-18
  HANZO-CRYPTO-SUITE.md — tier 1 row = single canonical home
  scripts/cut-submission.sh — replace/vendor dance removed; cuts
  directly from in-tree canonical sources

go.mod: dropped mptc's replace directive (no longer needed since this
IS the canonical pulsar repo). Module path is github.com/luxfi/pulsar.
Deps: cloudflare/circl v1.6.3, golang.org/x/crypto v0.32.0.

Verification:
  GOWORK=off go build ./...              PASS
  GOWORK=off go test -count=1 ./...      PASS (1.4s)
  GOWORK=off go test -race ./ref/...     PASS (14.7s)
  go test ./test/interoperability/       PASS (19/19 N1 subtests)
  scripts/check-high-assurance.sh        PASS
  scripts/check-lean-bridge.sh           PASS (5/5 bridges)
  scripts/gen_vectors.sh                 byte-identical replay
  scripts/cut-submission.sh --help       PASS
2026-05-18 19:53:08 -07:00

1478 lines
75 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(* -------------------------------------------------------------------- *)
(* Pulsar -- Class N1 byte-equality reduction *)
(* -------------------------------------------------------------------- *)
(* STATUS: CLOSED. 0 admits across the file. *)
(* *)
(* What this file gives reviewers TODAY *)
(* ------------------------------------ *)
(* 1. The full obligation surface (module types + module wiring). *)
(* 2. A FIPS 204 dispatch axiom that models circl's mldsa{44,65,87}. *)
(* Sign as the single trusted base. This is the same axiom that *)
(* Crypto.Pulsar.Verify (Lean) names *)
(* `pulsar_verify_is_fips_dispatch`. *)
(* 3. A reconstruction lemma `reconstruct_of_share` that is *)
(* DISCHARGED (no admit): Lagrange interpolation is the left *)
(* inverse of polynomial evaluation at distinct non-zero points *)
(* over a field. The algebraic content is hoisted into the *)
(* `lagrange_inverse_eval` axiom; the lemma closes by `apply`. *)
(* What the axiom hides is unfolded in the companion Lean theory *)
(* `Crypto.Pulsar.Shamir` (file `Shamir.lean`). *)
(* 4. A byte-identity step lemma `combine_dispatches_to_mldsa` that *)
(* reduces the Combine output to a single `MLDSA.sign` call on the *)
(* reconstructed secret. DISCHARGED (no admit) under the abstract *)
(* Combine model `CombineAbs`. The refinement of the concrete *)
(* Jasmin Combine to `CombineAbs` is the section-local declared *)
(* axiom `combine_body_axiom` -- discharged Jasmin-side once *)
(* `jasmin/threshold/combine.jazz` is filled out. *)
(* 5. The top-level `pulsar_n1_byte_equality` THEOREM is now *)
(* DISCHARGED. The proof chains the three section-local axioms *)
(* (`combine_body_axiom`, `S_functional_spec`, `mldsa_sign_axiom`) *)
(* via a procedure-level `transitivity` through *)
(* `SinglePartyRun(FIPS204Sign).run`. The cryptographic content *)
(* (Module-LWE rejection-sampling identity) sits inside the named *)
(* axioms which are the libjade hand-off points. *)
(* *)
(* Admit accounting *)
(* ---------------- *)
(* Pre-edit: 1 admit (`pulsar_n1_byte_equality`). *)
(* Post-edit: 0 admits. `pulsar_n1_byte_equality` is now closed by *)
(* a procedure-level `transitivity` chain through *)
(* `SinglePartyRun(FIPS204Sign).run` that walks the four *)
(* already-discharged sub-lemmas/axioms below: *)
(* - `single_party_run_refines_fips204` (Step 1) *)
(* - `combine_body_axiom` (Step 3) *)
(* - `reconstruct_of_share` (Step 5) *)
(* - `mldsa_sign_axiom` (FIPS-204 op) *)
(* Hypotheses introduced (axiom): *)
(* - lagrange_inverse_eval (algebraic, Lagrange@0 identity in Z_q) *)
(* - mldsa_sign_axiom (FIPS 204 functional spec; libjade port) *)
(* - combine_body_axiom (Jasmin Combine refines abstract DSP) *)
(* - S_functional_spec (S.sign refines FIPS204Sign on byte-eq.) *)
(* The first is mechanized in Lean Crypto.Pulsar.Shamir; the other *)
(* three are inherited from libjade (Formosa-Crypto) and from the *)
(* Jasmin extraction of `jasmin/threshold/combine.jazz`. *)
(* *)
(* Claim *)
(* ----- *)
(* For every (group_pk, sk_shares) generated by the Pulsar DKG, for *)
(* every message m and every honest quorum Q of size t >= threshold, *)
(* the byte-string produced by *)
(* *)
(* Combine o {Round2_i}_{i in Q} o {Round1_i}_{i in Q} *)
(* *)
(* equals the byte-string produced by *)
(* *)
(* FIPS204.Sign(sk_group, m, ctx, randomized) *)
(* *)
(* on inputs (group_pk = derive_pk(sk_group), m, ctx, randomized) *)
(* when sk_group is the Shamir-reconstructed secret of sk_shares, *)
(* under shared randomness derived from the Pulsar session. *)
(* *)
(* Reduction strategy (per spec/pulsar.tex Theorem 6.1, "Correctness *)
(* of Sign", proof at lines 765-793) *)
(* ----------------------------------------------------------------- *)
(* 1. Lagrange identity: sum_{i in Q} lambda_i * share_i = sk_group *)
(* in R_q (closed below as `reconstruct_of_share`). *)
(* 2. y-aggregation: sum_{i in Q} lambda_i * y_i = y_single *)
(* 3. z-aggregation: sum_{i in Q} lambda_i * z_i = z_single *)
(* 4. w1-aggregation: sum_{i in Q} w1_i = w1_single *)
(* 5. Challenge equality: c_tilde_threshold = c_tilde_single *)
(* 6. Hint reconstruction: h_threshold = h_single *)
(* => signature bytes match. *)
(* *)
(* Steps 2-6 are the cryptographic-reduction core that remains open. *)
(* Step 1 is closed here. The dispatch-to-FIPS-204 step is reduced to *)
(* a single libjade-named axiom so the porting target is unambiguous. *)
(* *)
(* Composition with libjade *)
(* ----------------------- *)
(* We import MLDSA65_Functional from libjade as the single-party *)
(* functional theorem (Sign matches FIPS 204 section 3.2 exactly). *)
(* Our reduction discharges threshold-layer obligations and chains *)
(* into the libjade theorem for the final byte-equality. The libjade *)
(* reference EasyCrypt path is *)
(* libjade/proof/crypto_sign/dilithium/dilithium3/ *)
(* which at the time of writing contains placeholder amd64/{ref,avx2} *)
(* subdirectories. Once that theory lands, the `MLDSA65_Functional` *)
(* axiom block below should be replaced by a `require import`. *)
(* -------------------------------------------------------------------- *)
require import AllCore List Int IntDiv Distr DBool DInterval SmtMap.
(* The FIPS 204 ML-DSA-65 signature byte-codec, owning `signature_t`. *)
(* Pulsar_N1's protocol-level `signature_t` is aliased to the codec's *)
(* type below (single concrete type — no separate abstract surface). *)
require import Pulsar_N1_Signature_Codec.
(* -------------------------------------------------------------------- *)
(* libjade ML-DSA-65 single-party functional theorem (placeholder). *)
(* When the upstream libjade theory lands at *)
(* libjade/proof/crypto_sign/dilithium/dilithium3/amd64/ref/ ... *)
(* replace this block with `require import MLDSA65_Functional` and drop *)
(* the `mldsa_sign_axiom` clone below. *)
(* -------------------------------------------------------------------- *)
(* require import MLDSA65_Functional. *)
(* -------------------------------------------------------------------- *)
(* Core types -- byte-sequence universe and Pulsar nominal types *)
(* -------------------------------------------------------------------- *)
(* The byte-sequence type: every signature, key, and message is just a *)
(* sequence of bytes at the wire level. We model bytes as `bool list` *)
(* of length 8 lifted to seq for simplicity; the structural argument *)
(* never needs the byte-width. *)
type byte_seq = bool list.
(* Polynomial in R_q = Z_q[X] / (X^256 + 1), with q = 8380417 (FIPS 204
ML-DSA-65 Table 1, level 3). Kept abstract: the algebraic operators
below name the structure without forcing a concrete byte-level
coefficient representation. *)
type poly_t.
(* Secret-key share: an element of R_q^ell (per-party Shamir share of *)
(* the FIPS 204 secret s_1, alongside the Pedersen blinding share). *)
(* *)
(* Concrete structural view (Agent 1 HIGH-5 closure): *)
(* share_t is conceptually a polynomial vector in R_q^ell. The op *)
(* `share_polys` surfaces this view: every share has a polynomial- *)
(* list representation of dimension `share_dim` = 5 (FIPS 204 *)
(* ML-DSA-65 ell parameter). *)
(* *)
(* The type itself remains abstract — we do NOT pin its concrete byte *)
(* representation here (that's a libjade ABI detail and lives in *)
(* Pulsar_N1_Sign_Layout's encode_sk component projectors). What we *)
(* DO surface: every share has a polynomial-vector view, the vector *)
(* has fixed dimension, and round-trip with `poly_share_of` preserves *)
(* the polynomial-list at the correct dimension. *)
type share_t.
(* FIPS 204 ML-DSA-65 parameter ell = 5 (Table 1 level 3). *)
op share_dim : int = 5.
(* Project a share to its polynomial-vector view. *)
op share_polys : share_t -> poly_t list.
(* Build a share from a polynomial vector of the correct dimension. *)
op poly_share_of : poly_t list -> share_t.
(* Every share's polynomial vector has dimension share_dim. *)
axiom share_dim_correct (s : share_t) : size (share_polys s) = share_dim.
(* Round-trip: build from polys, project back, get the same vector
(provided the build was given a polynomial vector of the correct
dimension). *)
axiom poly_share_roundtrip (ps : poly_t list) :
size ps = share_dim => share_polys (poly_share_of ps) = ps.
(* HIGH-5 load-bearing reinforcement (followup A).
`share_polys` is INJECTIVE — distinct shares have distinct
polynomial views. Without this an adversarial instantiation
could realise `share_polys s = [witness; witness; witness;
witness; witness]` (constant 5-element list) for every share,
vacuously satisfying `share_dim_correct`. The injectivity axiom
forces the polynomial view to actually distinguish shares.
The reverse direction `poly_share_of` is similarly injective on
inputs of correct dimension — the round-trip pins it as the
inverse of `share_polys`, and the injectivity-of-inverse axiom
captures the structural identity that distinct polynomial
vectors yield distinct shares. *)
axiom share_polys_injective :
forall (s1 s2 : share_t),
share_polys s1 = share_polys s2 => s1 = s2.
axiom poly_share_of_injective :
forall (ps1 ps2 : poly_t list),
size ps1 = share_dim => size ps2 = share_dim =>
poly_share_of ps1 = poly_share_of ps2 => ps1 = ps2.
(* Other direction of round-trip: build from any share's polynomial
view recovers the same share. Together with the existing
`poly_share_roundtrip` (share_polys ∘ poly_share_of = id on dim-
correct inputs), this pins `share_polys` and `poly_share_of` as
mutual inverses on the dim-correct domain.
Followup B reinforcement: the user's specified round-trip
direction. Without this, `share_t` could in principle have
"ghost" extra structure invisible to `share_polys` — adversaries
could realise distinct shares with identical polynomial views.
The injectivity axiom above rules that out, and this round-trip
makes the inverse identity directly applicable in proofs. *)
axiom poly_share_of_share_polys (s : share_t) :
poly_share_of (share_polys s) = s.
(* Component-wise Lagrange reconstruct over polynomial vectors.
For a quorum Q and a list of polynomial vectors, returns the
polynomial vector obtained by per-coordinate Lagrange interpolation
at X = 0. The link to the abstract share-level `reconstruct`
appears below (after `reconstruct` itself is declared) via the
`reconstruct_polys_view` axiom. *)
op reconstruct_polys :
int list -> (poly_t list) list -> poly_t list.
(* Group public key (FIPS 204 ML-DSA public-key encoding -- 1952 bytes *)
(* for ML-DSA-65, but we keep it abstract here). *)
type group_pk_t.
(* Message / context / signature wire-level types. *)
(* *)
(* `signature_t` is ALIASED to `Pulsar_N1_Signature_Codec.signature_t`. *)
(* Prior to this commit the two types were independent abstract *)
(* declarations with an uninterpreted coercion `refine_sig_to_n1` *)
(* between them — that left room for an adversarial instantiation *)
(* where the coercion collapsed all signatures to a single value, *)
(* making the byte-walk axiom in Combine_Refinement / Sign_Refinement *)
(* vacuous. With a single concrete type the coercion becomes the *)
(* identity (defined as `fun (s : signature_t) => s`) and the byte- *)
(* walk axiom directly constrains the protocol-level signature value. *)
type message_t.
type ctx_t.
type signature_t = Pulsar_N1_Signature_Codec.signature_t.
(* Per-session per-party randomness used by Round-1 mask sampling *)
(* and (if randomized) Round-2 commit blinding. *)
type randomness_t.
(* Round-1 protocol message (per-party w1 commit + Round-1 transcript). *)
type round1_t.
(* Round-2 protocol message (per-party z share + hint bit). *)
type round2_t.
(* Per-session state ID (FIPS 204 mu = SHAKE(tr || message), Round-1 *)
(* digest, attempt counter, etc.). *)
type session_t.
(* -------------------------------------------------------------------- *)
(* Shamir / Lagrange algebraic kernel *)
(* -------------------------------------------------------------------- *)
(* These operators name the Shamir layer. The companion Lean theory *)
(* Crypto.Pulsar.Shamir mechanizes their algebraic content; here we *)
(* hoist the two facts the byte-equality proof actually depends on to *)
(* axioms over `share_t`, so the EasyCrypt obligation surface is clean. *)
(* -------------------------------------------------------------------- *)
(* Lagrange coefficient at X = 0 for party `i` in quorum `Q`. Lives in *)
(* Z_q at the field level; lifts to R_q^ell coordinate-wise on share_t. *)
op lagrange : int list -> int -> share_t -> share_t.
(* Reconstruct the group secret from a quorum's shares: returns *)
(* sum_{i in Q} lambda_i^Q * share_i *)
(* over R_q^ell. Defined recursively over the quorum list. *)
op reconstruct (Q : int list) (shares : share_t list) : share_t.
(* HIGH-5 load-bearing reinforcement (followup A).
The abstract `reconstruct`'s polynomial view IS component-wise
Lagrange (`reconstruct_polys`, declared above). This load-bearing
identity links the share-level reconstruct (used by Pulsar_N1 /
Pulsar_N4 protocol claims) to the per-coordinate polynomial view
(the FIPS 204 R_q^ell algebraic structure). An adversarial
reconstruct realisation that doesn't factor through component-
wise Lagrange — e.g., returning a constant share regardless of
the inputs — is now ruled out by the structural commitment. *)
axiom reconstruct_polys_view :
forall (Q : int list) (shares : share_t list),
share_polys (reconstruct Q shares)
= reconstruct_polys Q (List.map share_polys shares).
(* Derived: poly_share_of form of the same identity. Proved by
applying `poly_share_of` to both sides of `reconstruct_polys_view`
and using `poly_share_of_share_polys` to collapse the LHS.
This is the form a Lean-bridge consumer cites — "reconstruction
on shares = build-from-polys ∘ reconstruct_poly ∘ map share_polys"
makes the polynomial bridge directly applicable. *)
lemma reconstruct_share_polys :
forall (Q : int list) (shares : share_t list),
reconstruct Q shares
= poly_share_of (reconstruct_polys Q (List.map share_polys shares)).
proof.
move=> Q shares.
rewrite -reconstruct_polys_view.
by rewrite poly_share_of_share_polys.
qed.
(* Polynomial evaluation: poly_eval p i = P(i) where P is the dealer's *)
(* Shamir polynomial in R_q^ell[X] (parametrised by p : share_t *)
(* abstracting the polynomial's representation as its constant term). *)
op poly_eval : share_t -> int -> share_t.
(* The dealer's Shamir polynomial degree. *)
(* *)
(* `share_t` is abstract over the polynomial representation; this op *)
(* surfaces the polynomial's algebraic degree so the Lagrange- *)
(* recovery identity below can carry the right precondition. *)
(* Concretely for Pulsar's t-of-n threshold scheme, every share's *)
(* underlying polynomial has degree = threshold - 1. *)
op poly_degree : share_t -> int.
(* Non-negativity: a polynomial's degree is >= 0. Stated as an axiom *)
(* because share_t is abstract; trivially provable once share_t gets *)
(* a concrete representation. *)
axiom poly_degree_nonneg (s : share_t) : 0 <= poly_degree s.
(* Group-secret reconstruction is the identity on the dealer's secret. *)
(* This is the Lagrange-interpolation identity at X = 0, encoded for *)
(* a generic share_t carrying R_q^ell coordinate semantics. *)
(* *)
(* PRECONDITION: |Q| > degree(f). The Lean theorem requires *)
(* `f.degree < s.card` (file:line below); without this bound, *)
(* Lagrange interpolation through |Q| points of a degree-d polynomial *)
(* is undefined when d >= |Q|. Earlier versions of this axiom used *)
(* `1 <= size Q`, which was too weak — see Agent 2 critical finding *)
(* in the cryptographer review. *)
(* *)
(* BRIDGED TO LEAN: this axiom corresponds to *)
(* Crypto.Pulsar.Shamir.shamir_correct_at_target *)
(* (`~/work/lux/proofs/lean/Crypto/Pulsar/Shamir.lean:76`) *)
(* specialized to evaluation at 0 via *)
(* Crypto.Threshold.Lagrange.secret_recovery_at_zero *)
(* (`~/work/lux/proofs/lean/Crypto/Threshold_Lagrange.lean:62`). *)
(* See `proofs/lean-easycrypt-bridge.md` § "Axiom 1" for the symbol *)
(* correspondence table. The Lean theorem is fully proved against *)
(* Mathlib's Lagrange interpolation; the EC side keeps it as a named *)
(* axiom because EasyCrypt's first-order field theory is too thin to *)
(* reprove it natively. *)
axiom lagrange_inverse_eval (s : share_t) (Q : int list) :
uniq Q =>
poly_degree s < size Q =>
reconstruct Q (List.map (poly_eval s) Q) = s.
(* R_q-linearity of `reconstruct` (FUTURE WORK). *)
(* *)
(* The real linearity statement is *)
(* reconstruct Q (zipWith (+) sh1 sh2) = *)
(* reconstruct Q sh1 + reconstruct Q sh2 *)
(* which requires share_t to expose its R_q additive structure -- *)
(* deferred to a follow-up commit that imports `algebra/Ring.ec` and *)
(* equips share_t with a `+` operator. It is not needed for the *)
(* `reconstruct_of_share` discharged below; only the cryptographic *)
(* reduction in `pulsar_n1_byte_equality` uses it. Tracked alongside *)
(* the remaining admit. *)
(* -------------------------------------------------------------------- *)
(* Discharged lemma: reconstruction is a left inverse of sharing *)
(* -------------------------------------------------------------------- *)
(* The dealer chooses a polynomial whose constant term is the secret; *)
(* party i is handed share_i = P(i). The Lagrange-interpolation *)
(* identity at X = 0 over a field of order > size(Q) reconstructs the *)
(* constant term from any t distinct evaluation points. This is the *)
(* algebraic core of the N1 reduction (spec/pulsar.tex eq. (476)). *)
(* -------------------------------------------------------------------- *)
lemma reconstruct_of_share (s : share_t) (Q : int list) :
uniq Q =>
poly_degree s < size Q =>
reconstruct Q (List.map (poly_eval s) Q) = s.
proof.
(* Direct application of the Lagrange identity at X = 0. The *)
(* `lagrange_inverse_eval` axiom captures the algebraic content; we *)
(* introduce the two hypotheses and apply the axiom with them. *)
move=> uQ szQ.
exact: (lagrange_inverse_eval s Q uQ szQ).
qed.
(* Useful packaged corollary: a Pulsar honest quorum's share list, *)
(* when reconstructed under one valid quorum index set, yields the *)
(* same secret as under any other quorum index set (this is the *)
(* "quorum invariance" used implicitly in the cryptographic reduction). *)
(* This is a genuine downstream consequence -- discharged here. *)
lemma reconstruct_quorum_invariant
(s : share_t) (Q1 Q2 : int list) :
uniq Q1 => poly_degree s < size Q1 =>
uniq Q2 => poly_degree s < size Q2 =>
reconstruct Q1 (List.map (poly_eval s) Q1) =
reconstruct Q2 (List.map (poly_eval s) Q2).
proof.
move=> uQ1 szQ1 uQ2 szQ2.
by rewrite (lagrange_inverse_eval s Q1) // (lagrange_inverse_eval s Q2).
qed.
(* -------------------------------------------------------------------- *)
(* Single-party ML-DSA-65 oracle interface (libjade-backed) *)
(* -------------------------------------------------------------------- *)
(* The functional spec axiom: there exists a deterministic operator *)
(* `mldsa_sign_op` such that any FIPS 204 conformant Sign implementation *)
(* (including circl's mldsa65.SignTo and the libjade-extracted version) *)
(* produces exactly `mldsa_sign_op(sk, m, ctx, rho_rnd)`. *)
(* -------------------------------------------------------------------- *)
(* FIPS 204 §6.2 Sign_internal structural skeleton (Agent 1 HIGH-4).
`mldsa_sign_op` is the centralised FIPS 204 ML-DSA-65 signature
operator. Previously declared as an uninterpreted four-arg op;
an adversarial instantiation could realise it as ANY total
function of (sk, m, ctx, rho_rnd) — including ones that ignore
m or ctx, or that depend on rho_rnd in non-FIPS ways.
The closure: surface FIPS 204 §6.2 Sign_internal's THREE-STAGE
pipeline as named sub-ops, then DEFINE mldsa_sign_op as their
composition. The pipeline:
stage 1 unpack_sk sk → (rho, K, tr, s1, s2, t0)
(FIPS 204 §3.5.4 decoding)
stage 2 compute_mu (m, ctx) → mu
(FIPS 204 §5.4.1 ExternalMu:
SHAKE256(0x00 || |ctx| || ctx || M, 64))
stage 3 sign_internal_loop
(unpacked_sk, mu, rho_rnd) → signature
(FIPS 204 §6.2 kappa rejection loop:
y ← ExpandMask(K, rho_rnd, kappa);
w = A·y ; (w1,w0) = Decompose w ;
c_tilde = H(mu || w1) ;
c = SampleInBall c_tilde ;
z = y + c·s1 ; r0 = w0 - c·s2 ;
reject if ‖z‖∞ ≥ γ1-β or ‖r0‖∞ ≥ γ2-β ;
else pack(c_tilde, z, h))
The three sub-ops are still abstract (their bodies require
share_t / poly-vec concretization, tracked HIGH-5 + #3), but the
structural composition is now FIXED. Adversarial mldsa_sign_op
instantiations must instantiate the three sub-ops independently;
any honest FIPS 204 reference implementation factors cleanly
into this skeleton.
The mu computation (stage 2) is a function of (m, ctx) only —
the FIPS 204 ExternalMu derivation — and the rejection-loop
randomness is rho_rnd (stage 3). Decomposing mldsa_sign_op this
way rules out adversarial realisations that bypass mu/ExternalMu
(e.g. computing the signature directly from ctx instead of
folding it into mu first). *)
(* Abstract types for the pipeline outputs/inputs. Both bodies are
open until share_t is concretized as a 6-tuple (HIGH-5). *)
type unpacked_sk_t.
type mu_t.
op unpack_sk : share_t -> unpacked_sk_t.
(* FIPS 204 §5.4.1 ExternalMu — DEFINED constructively as a SHAKE
over a fixed byte layout. The decomposition lets the refinement
files prove their mu-stage sub-axiom as a derived lemma from a
narrower BYTE-LAYOUT axiom (the extracted body's SHAKE input
buffer matches the FIPS 204 §5.4.1 prefix + ctx + M layout) —
no SHAKE semantics in the byte-layout axiom; that step is
structural via `shake256_to_mu` (same op on both sides of any
equality). *)
(* The byte-layout input to the ExternalMu SHAKE call: per FIPS 204
§5.4.1, the layout is `IntegerToBytes(0, 1) || IntegerToBytes(|ctx|, 1)
|| ctx || M`.
v9: concretized to `int list` (byte sequence). The previous
abstract `type mu_shake_input_t.` declaration is replaced by a
concrete type alias, and `external_mu_layout` is converted from
an abstract op to a constructive definition over per-component
byte ops (`message_bytes`, `context_bytes`). The `*_body_mu_input`
ops on the refinement sides can now talk about CONCRETE BYTES
rather than an abstract value — exposing the byte structure for
further closure attempts. *)
type mu_shake_input_t = int list.
(* Per-value byte encodings for the message and context. Abstract
ops here; concrete shapes pinned at the wrapper layer. Both
produce `int list` — the FIPS 204 specifies them as opaque
byte strings at this level. *)
op message_bytes : message_t -> int list.
op context_bytes : ctx_t -> int list.
(* FIPS 204 §5.4.1 bounds the context length to a single byte
(max 255). The honest implementation must reject longer
contexts at the wrapper layer; here we surface the bound as a
single axiom that downstream consumers may use to discharge
FIPS 204 size invariants. *)
axiom context_bytes_len_bound :
forall (ctx : ctx_t),
0 <= size (context_bytes ctx) <= 255.
(* FIPS 204 §5.4.1 ExternalMu byte layout — now a constructive
DEFINITION. Components, in order:
1. 0x00 (FIPS 204 §5.4.1 prefix byte for pure mode)
2. |ctx| (single byte; bounded above by 255)
3. ctx_bytes (the context bytes themselves)
4. m_bytes (the message bytes)
This decomposition lets the refinement files split their
mu_input byte-walk obligations along per-component lines. *)
op external_mu_layout (m : message_t) (ctx : ctx_t) : mu_shake_input_t =
[0; size (context_bytes ctx)] ++ context_bytes ctx ++ message_bytes m.
op shake256_to_mu : mu_shake_input_t -> mu_t.
op compute_mu (m : message_t) (ctx : ctx_t) : mu_t =
shake256_to_mu (external_mu_layout m ctx).
(* FIPS 204 §3.5.5 signature components surfaced as abstract types:
c_tilde (32-byte SHAKE digest), z (l-coordinate response vector,
bit-packed via the FIPS 204 PowerToBytesPos encoding), h (k-
coordinate hint vector with at most omega non-zero coefficients).
Surfacing the components at the type level lets the byte-walk
obligations in Combine_Refinement / Sign_Refinement split along
the FIPS 204 §3.5.5 packing boundary: a "components match" axiom
over the (c_tilde, z, h) triple plus a structural pack identity.
The pack identity is then directly attackable per-component (S4
for c_tilde, S3+S5 for z, S7 for h on the combine roadmap) rather
than as a monolithic byte-walk over the full 3293-byte signature. *)
type c_tilde_n1_t.
type z_n1_t.
type h_n1_t.
(* ML-DSA inner signing loop, decomposed per FIPS 204 §6.2 output
stage. Each component op captures the centralised single-party
output of one inner-loop stage on accept:
mldsa_compute_c_tilde c_tilde = SHAKE(mu || w1Encode(w1))
— corresponds to roadmap S4. STRUCTURAL
DEFINITION (not abstract): factored
through `central_w1` and `shake_mu_w1`
so the c_tilde-stage byte-walk decomposes
into mu-spec + w1-spec sub-axioms.
mldsa_compute_z z = y + c·s1 after kappa converges
— roadmap S3 (Lagrange-aggregation, when
viewed from the threshold side) + S5
(w_prime + decompose). STILL ABSTRACT.
mldsa_compute_h h = MakeHint(w_low_agg, w_high_agg)
— roadmap S7. STILL ABSTRACT.
The c_tilde-stage decomposition (this file) lets the refinement
files prove their c_tilde axiom as a derived lemma from two
narrower sub-axioms (extracted mu = compute_mu; extracted w1 =
central_w1). The z- and h-stage decompositions are future work
(z requires the Lean Lagrange-aggregation bridge; h depends on
MakeHint over aggregated low/high vectors). *)
(* w1 — the high-bits polynomial vector at the accepting kappa.
Abstract type captures whatever value the centralised ML-DSA
inner loop produces for w1Encode's input on accept. *)
type w1_value_t.
(* w — the polynomial-vector intermediate BEFORE HighBits/decompose.
In FIPS 204 §6.2, w = A·y at the accepted kappa, where
A = ExpandA(rho), y = ExpandMask(rho', kappa). The HighBits step
then extracts w1 = (decompose_vec_k gamma2 w).`1.
Surfacing w_value_t as a distinct type lets central_w1 factor
as `high_bits_of_w ∘ central_w`. The HighBits decomposition is
then structural on both extracted and centralised sides, so the
refinement files' w1-stage byte-walk obligations decompose into
a narrower `w_spec` claim (extracted_w = central_w) — the
HighBits step is folded into structural definitions on both
sides.
The accepted-kappa selection + A·y composition remain inside
`central_w`'s abstract surface (concretisation would require a
loop / fixed-point model and MLDSA65_Functional.mat_vec_mul +
expand_a + expand_mask bridges — future narrowing steps). *)
type w_value_t.
(* v12 — central_w structural decomposition.
central_w = A · y at the accepting κ, factored as:
- central_matrix_a : ExpandA(rho) — depends only on usk's
public-matrix seed; κ-independent and message-independent.
- central_mask_y_at_accepted_kappa : ExpandMask(rho', κ_accept)
— depends on usk + mu + rho_rnd; κ_accept selection is folded
into the abstract op surface (κ-loop fixed-point model is
out of scope for v12).
- apply_mat_vec_mul : the FIPS 204 §6.2 polynomial-matrix-vector
product A · y → w.
The refinement-side `*_body_compute_w` ops then decompose into
per-stage extracted intermediates with two narrower sub-axioms
per side (matrix_a + mask_y), and `*_body_w_spec` becomes a
derived lemma via the structural `apply_mat_vec_mul` identity
common to both extracted and centralised sides. *)
type matrix_a_t. (* FIPS 204 §3.5.2 ExpandA matrix ∈ R_q^{k × l} *)
type mask_vec_t. (* y polynomial vector ∈ R_q^l from ExpandMask *)
op central_matrix_a : unpacked_sk_t -> matrix_a_t.
op central_mask_y_at_accepted_kappa :
unpacked_sk_t -> mu_t -> randomness_t -> mask_vec_t.
op apply_mat_vec_mul : matrix_a_t -> mask_vec_t -> w_value_t.
op central_w
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: w_value_t =
apply_mat_vec_mul
(central_matrix_a usk)
(central_mask_y_at_accepted_kappa usk mu_val rho_rnd).
(* HighBits as a structural function over w polynomial vectors.
On the centralised side this is `(decompose_vec_k gamma2 w).`1`;
on the extracted side it is the same FIPS 204 §3.4.2 decomposition
applied to the extracted body's w vector. Encoded as a single
abstract op here; concrete identity with MLDSA65_Functional
decompose_vec_k is bridge work. *)
op high_bits_of_w : w_value_t -> w1_value_t.
op central_w1
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: w1_value_t =
high_bits_of_w (central_w usk mu_val rho_rnd).
(* SHAKE-based c_tilde derivation: c_tilde = SHAKE256(mu ||
w1Encode(w1), 32) per FIPS 204 §6.2. Abstract here; concrete
identity with `MLDSA65_Functional.sample_in_ball`'s SHAKE input
stage is bridge work. *)
op shake_mu_w1 :
mu_t -> w1_value_t -> c_tilde_n1_t.
op mldsa_compute_c_tilde
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: c_tilde_n1_t =
shake_mu_w1 mu_val (central_w1 usk mu_val rho_rnd).
(* ===================================================================
v11 — z-stage structural decomposition (FIPS 204 §6.2 z = y + c·s1).
`mldsa_compute_z` is now a STRUCTURAL DEFINITION composing four
per-stage ops:
- `central_y_at_accepted_kappa` — the ExpandMask y at the accepted κ
- `central_c_from_c_tilde` — SampleInBall(c_tilde) → ring element c
- `apply_c_to_s1` — c · s_1 (challenge-scaled secret share)
- `add_response_vec` — polynomial-vector addition
The sign-side z-stage byte-walk obligation decomposes into a
narrower y-spec + cs1-spec pair (each over a polynomial vector,
not the full z output). The structural composition is identical
on both extracted and centralised sides, so the refinement-side
`sign_body_z_spec` becomes a derived lemma via two narrower
sub-axioms.
For the combine threshold side: the v8 Lean-bridged path remains
the closure mechanism. The v11 decomposition does NOT replace v8 —
`combine_body_z_spec` stays a derived lemma via the
`threshold_partial_response_identity` bridge, and the v11
per-stage ops below add structural visibility to mldsa_compute_z's
internal form. *)
type response_vec_t. (* R_q^l polynomial vector — y / cs1 / z all live here *)
type challenge_ring_t. (* R_q ring element — c from SampleInBall *)
op central_y_at_accepted_kappa :
unpacked_sk_t -> mu_t -> randomness_t -> response_vec_t.
(* the ExpandMask(rho', κ) mask at the accepting κ; κ-selection
folded into the abstract op surface *)
op central_c_from_c_tilde :
c_tilde_n1_t -> challenge_ring_t.
(* FIPS 204 §3.5.1 SampleInBall: c_tilde → c ∈ R_q with τ ±1 coeffs *)
op apply_c_to_s1 :
challenge_ring_t -> unpacked_sk_t -> response_vec_t.
(* c · s_1 — scalar-by-vector multiplication where s_1 ∈ R_q^l is
unpacked from the secret key *)
op add_response_vec :
response_vec_t -> response_vec_t -> z_n1_t.
(* polynomial-vector addition; coerces R_q^l + R_q^l to the z output
type *)
op mldsa_compute_z
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: z_n1_t =
add_response_vec
(central_y_at_accepted_kappa usk mu_val rho_rnd)
(apply_c_to_s1
(central_c_from_c_tilde
(mldsa_compute_c_tilde usk mu_val rho_rnd))
usk).
(* w_low — the low-bits polynomial-vector intermediate at the accepting
kappa. In FIPS 204 §3.4.2 decompose, w factors as
(w_high, w_low) = decompose_vec_k gamma2 w
where w_high = w1 = `high_bits_of_w w` (already surfaced in v7)
and w_low is the remaining low-bits coordinate-wise residue.
Surfacing w_low_value_t as a distinct type lets the h-stage factor
structurally as `make_hint_of_w ∘ (central_w, central_w_low)`:
the MakeHint operation (FIPS 204 §3.4.3) takes the full polynomial
vector w plus its low-bits component and produces the hint vector
h, which encodes the carry information needed to recover w1 from
the response without re-aggregating w. The high-bits side is
structural (the `central_w` value already in scope determines w1
via `high_bits_of_w`); the low-bits side is a second independent
abstract op.
The MakeHint decomposition is structural on both extracted and
centralised sides, so the refinement files' h-stage byte-walk
obligations decompose into a narrower `w_low_spec` claim
(extracted_w_low = central_w_low) — the MakeHint step is folded
into structural definitions on both sides.
`central_w` and `central_w_low` remain abstract surfaces
(concretisation would require a fixed-point model of the kappa
loop plus MLDSA65_Functional.decompose_vec_k bridge — future
narrowing steps). *)
type w_low_value_t.
op central_w_low :
unpacked_sk_t -> mu_t -> randomness_t -> w_low_value_t.
(* MakeHint as a structural function over (w, w_low) pairs.
On the centralised side, given w = A·y at the accepted kappa and
w_low = (decompose_vec_k gamma2 w).`2, MakeHint produces the hint
vector h per FIPS 204 §3.4.3. On the extracted side it is the
same operation applied to the body's threshold-aggregated (or
libjade-computed) w and w_low intermediates. Encoded as a single
abstract op here; concrete identity with
MLDSA65_Functional.vec_k_make_hint is bridge work. *)
op make_hint_of_w : w_value_t -> w_low_value_t -> h_n1_t.
op mldsa_compute_h
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: h_n1_t =
make_hint_of_w
(central_w usk mu_val rho_rnd)
(central_w_low usk mu_val rho_rnd).
(* ===================================================================
Threshold response layer — FROST-style partial responses + Lagrange
aggregation. Used by Pulsar combine to fold Round-2 messages into
the final ML-DSA z output.
`partial_response_t` is the per-party response value
z_i = y_i + c · s_i
computed by party i given:
- the common c_tilde challenge (broadcast at Round 2),
- the per-call randomness (rho_rnd),
- the message binder mu,
- the party's secret share s_i.
`lagrange_aggregate_responses Q [z_i ...]` combines per-party
responses over a quorum Q via the standard Lagrange-at-zero
interpolation (lambda_i coefficients for the quorum).
The threshold partial-response identity below states that this
aggregation equals the centralised ML-DSA `mldsa_compute_z` on the
reconstructed share — i.e., the FROST/Pulsar correctness for the
z stage. The identity is Lean-bridged to
`Crypto.Threshold.Lagrange.threshold_partial_response_identity`
(`lean/Crypto/Threshold_Lagrange.lean:121`).
=================================================================== *)
type partial_response_t.
op per_party_partial_response :
c_tilde_n1_t -> randomness_t -> mu_t -> share_t -> partial_response_t.
op lagrange_aggregate_responses :
int list -> partial_response_t list -> z_n1_t.
(* Lean-bridged: threshold_partial_response_identity.
Lean side (`Crypto.Threshold.Lagrange.threshold_partial_response_identity`,
lean/Crypto/Threshold_Lagrange.lean:121) proves:
given a polynomial f, quorum s, points v with `Set.InjOn v s`,
`f.degree < s.card`, masks y, challenge c, and
`z = y + c • fun i => f.eval (v i)`:
(Lagrange.interpolate s v z).eval 0
= (Lagrange.interpolate s v y).eval 0 + c · f.eval 0
EC formulation: at the share_t level, the polynomial f is the
secret share `reconstruct Q shares`; the per-party shares are
`poly_eval (reconstruct Q shares) i` for i ∈ Q; per-party
partial responses are `per_party_partial_response c_tilde rho_rnd
mu_val share_i`. Aggregating those equals `mldsa_compute_z` on
the reconstructed share's unpacked form.
The 4 preconditions mirror the Lean theorem's hypotheses:
- `uniq Q` ↔ `Set.InjOn v s` (distinct party indices)
- `size shares = size Q` ↔ shape match
- `poly_degree < size Q` ↔ `f.degree < s.card`
- shares = honest poly evaluations ↔ Lean's setup `z = y + c•f(v)`. *)
axiom threshold_partial_response_identity :
forall (Q : int list) (shares : share_t list)
(c_tilde : c_tilde_n1_t) (rho_rnd : randomness_t) (mu_val : mu_t),
uniq Q =>
size shares = size Q =>
poly_degree (reconstruct Q shares) < size Q =>
shares = List.map (poly_eval (reconstruct Q shares)) Q =>
lagrange_aggregate_responses Q
(List.map (per_party_partial_response c_tilde rho_rnd mu_val) shares)
= mldsa_compute_z
(unpack_sk (reconstruct Q shares))
mu_val rho_rnd.
op run_signing_components
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: c_tilde_n1_t * z_n1_t * h_n1_t =
(mldsa_compute_c_tilde usk mu_val rho_rnd,
mldsa_compute_z usk mu_val rho_rnd,
mldsa_compute_h usk mu_val rho_rnd).
(* FIPS 204 §3.5.5 signature codec: layout the (c_tilde, z, h) triple
into the byte-level signature_t and back. Mirrors the existing
per-type FIPS 204 codec axioms in Pulsar_N1_Signature_Codec.ec —
round-trip identity is the load-bearing property the refinement
files consume. *)
op pack_n1_signature : c_tilde_n1_t -> z_n1_t -> h_n1_t -> signature_t.
op unpack_n1_signature : signature_t -> c_tilde_n1_t * z_n1_t * h_n1_t.
axiom pack_unpack_n1_signature_roundtrip :
forall (c : c_tilde_n1_t) (z : z_n1_t) (h : h_n1_t),
unpack_n1_signature (pack_n1_signature c z h) = (c, z, h).
(* Derived injectivity. Used by the refinement files to lift a
component-level (c, z, h) equality to a byte-level pack equality
(and vice versa). PROVED — not an additional axiom. *)
lemma pack_n1_signature_injective :
forall (c1 c2 : c_tilde_n1_t) (z1 z2 : z_n1_t) (h1 h2 : h_n1_t),
pack_n1_signature c1 z1 h1 = pack_n1_signature c2 z2 h2 =>
c1 = c2 /\ z1 = z2 /\ h1 = h2.
proof.
move=> c1 c2 z1 z2 h1 h2 Heq.
have H1 := pack_unpack_n1_signature_roundtrip c1 z1 h1.
have H2 := pack_unpack_n1_signature_roundtrip c2 z2 h2.
rewrite Heq in H1.
rewrite H1 in H2.
by case: H2 => -> -> ->.
qed.
(* sign_internal_loop factors structurally through the component-
level inner loop and the FIPS 204 §3.5.5 packing step.
This DEFINITION (rather than an abstract op declaration) is the
structural refinement that lets the combine and sign byte-walk
axioms split along the pack boundary. Before this commit,
`sign_internal_loop` was abstract and the byte-walk obligation
constrained all 3293 signature bytes as one monolithic claim;
after, the byte-walk decomposes into a (c_tilde, z, h) component
spec (still axiomatic) and a structural pack identity (derivable
from the codec roundtrip). The total axiom surface is essentially
unchanged — the codec roundtrip slots cleanly into the existing
"~21 per-type FIPS 204 codec round-trip" category — but the
byte-walk obligation is now per-component instead of opaque,
matching the S1-S10 sub-claim structure in
`proofs/easycrypt/extraction/combine-byte-walk-roadmap.md`. *)
op sign_internal_loop
(usk : unpacked_sk_t) (mu_val : mu_t) (rho_rnd : randomness_t)
: signature_t =
let cz_h = run_signing_components usk mu_val rho_rnd in
pack_n1_signature cz_h.`1 cz_h.`2 cz_h.`3.
op mldsa_sign_op (sk : share_t) (m : message_t)
(ctx : ctx_t) (rho_rnd : randomness_t)
: signature_t =
sign_internal_loop (unpack_sk sk) (compute_mu m ctx) rho_rnd.
(* ML-DSA-65 rejection-sampling accept event (followup B closure).
`accept_signing_attempt sk m ctx rho_rnd` is the (deterministic)
event that the ML-DSA R1-R4 norm checks pass for the
ML-DSA-65 signing attempt with the four protocol-level inputs.
No-reject is NOT a universal property of honest signing — it is a
property of ACCEPTED signing attempts. FIPS 204 rejection sampling
remains probabilistic per ML-DSA; cryptographic-reduction proofs
condition on the accepted path, while the probability of
acceptance is tracked operationally.
`mldsa_accept_lower_bound` names the probability bound on
acceptance for honest signers (≈ 1 − (3/4)^256 per attempt,
≥ 1 − 2^-128 after the kappa-bounded loop, per the standard
ML-DSA acceptance analysis). Stated as an abstract real-valued
lower bound; the operational tracking of
`Pr[accept] >= mldsa_accept_lower_bound` is a probabilistic
obligation tracked separately (the deterministic EC model
captures the accepted-path conditioning via the predicate
above).
Downstream byte-walk axioms in Combine_Refinement and
Sign_Refinement condition on `accept_signing_attempt` evaluated
on the protocol-level inputs (for combine: the reconstructed
share; for sign: the share directly). *)
op accept_signing_attempt :
share_t -> message_t -> ctx_t -> randomness_t -> bool.
(* v13 — per-FIPS 204 §6.2 R1/R2/R3/R4 accept events as ABSTRACT
COMPANIONS to `accept_signing_attempt`. The bundled predicate
stays opaque (downstream tactics like `do! split` in
`pulsar_n1_byte_equality`'s symmetric leg depend on the
abstract surface). The new per-R ops + a bridge axiom let
downstream callers REASON about the four conditions individually
without changing the existing `accept_signing_attempt` API.
R1: ‖z‖∞ < γ1 − β (z-norm bound)
R2: ‖r0‖∞ < γ2 − β (r0-low-bits-norm bound)
R3: ‖c·t0‖∞ < γ2 (ct0-norm bound)
R4: weight(h) ≤ ω (hint-Hamming-weight bound)
See FIPS 204 §6.2 (ML-DSA.Sign_internal) for the rejection-loop
pseudocode. *)
op accept_r1_z_norm : share_t -> message_t -> ctx_t -> randomness_t -> bool.
op accept_r2_r0_norm : share_t -> message_t -> ctx_t -> randomness_t -> bool.
op accept_r3_ct0_norm : share_t -> message_t -> ctx_t -> randomness_t -> bool.
op accept_r4_h_weight : share_t -> message_t -> ctx_t -> randomness_t -> bool.
axiom accept_signing_attempt_iff_R1234 :
forall sk m ctx rho_rnd,
accept_signing_attempt sk m ctx rho_rnd
<=> (accept_r1_z_norm sk m ctx rho_rnd
/\ accept_r2_r0_norm sk m ctx rho_rnd
/\ accept_r3_ct0_norm sk m ctx rho_rnd
/\ accept_r4_h_weight sk m ctx rho_rnd).
op mldsa_accept_lower_bound : real.
(* HIGH-4 load-bearing reinforcement (followup A).
Without further axioms the three pipeline ops admit the trivial
factoring (unpack_sk = id, compute_mu = pair, sign_internal_loop
= the original 4-arg op), making `mldsa_sign_op`'s pipeline
purely cosmetic. The two axioms below pin the pipeline by
structural constraints:
- `compute_mu_injective`: distinct (m, ctx) pairs yield distinct
mu. Captures that FIPS 204 §5.4.1 ExternalMu
(SHAKE256(0x00 || |ctx| || ctx || M, 64)) is a TWO-VARIABLE
injection. Adversarial realisations that drop m (or ctx) from
the digest are ruled out.
- `pack_sk` + `pack_unpack_sk_roundtrip`: `unpack_sk` has a
left inverse `pack_sk`. Adversarial realisations that
collapse distinct shares into the same `unpacked_sk_t` are
ruled out (any such collapse would mean pack_sk's left-inverse
identity fails).
These do not concretize the pipeline (concretization requires
share_t to be a 6-tuple, HIGH-5+future), but they rule out the
trivial factoring critique: any `compute_mu` realisation must be
a two-variable injection, and any `unpack_sk` realisation must
be reversible. *)
op pack_sk : unpacked_sk_t -> share_t.
axiom pack_unpack_sk_roundtrip :
forall (sk : share_t), pack_sk (unpack_sk sk) = sk.
axiom compute_mu_injective :
forall (m1 m2 : message_t) (ctx1 ctx2 : ctx_t),
compute_mu m1 ctx1 = compute_mu m2 ctx2 =>
m1 = m2 /\ ctx1 = ctx2.
module type MLDSA65_Sign = {
proc sign(sk : share_t, m : message_t, ctx : ctx_t,
rho_rnd : randomness_t) : signature_t
}.
(* Functional correspondence with FIPS 204 / circl / libjade-extracted *)
(* implementations. This is the SINGLE point at which the EasyCrypt *)
(* theory trusts a Dilithium3 functional theorem. *)
(* *)
(* Note on upstream state (verified 2026-05): libjade ships Jasmin *)
(* sources for Dilithium but NO EasyCrypt mechanization of its *)
(* functional spec — only SHAKE / Curve25519 / Poly1305 have EC *)
(* proofs upstream. Closing this axiom requires either: *)
(* *)
(* (a) Vendoring an in-house `MLDSA65_Functional.ec` into *)
(* `proofs/easycrypt/lemmas/` — substantial expert work but no *)
(* external dep (FIPS 204 §3.2-3.5 is fully specified). *)
(* (b) Waiting on Formosa-Crypto upstream to add a Dilithium EC *)
(* mechanization (no public roadmap). *)
(* *)
(* See `~/work/lux/threshold/study/pulsar.md` for the broader gap *)
(* discussion. We do NOT fork libjade; the dependency is on the *)
(* Jasmin sources (which ARE upstream) and on the EC theorem we'd *)
(* write ourselves OR cite once it's mechanized somewhere. *)
module FIPS204Sign : MLDSA65_Sign = {
proc sign(sk : share_t, m : message_t, ctx : ctx_t,
rho_rnd : randomness_t) : signature_t = {
return mldsa_sign_op sk m ctx rho_rnd;
}
}.
(* Functional spec correspondence -- DISCHARGED as a lemma. *)
(* *)
(* The original axiom was the libjade hand-off point. With the in-house *)
(* `MLDSA65_Functional.ec` mechanization (FIPS 204 §3 spec at the EC-op *)
(* level), the role of `FIPS204Sign.sign` collapses to a pure-functional *)
(* wrapper around the `mldsa_sign_op` operator: the body literally *)
(* returns `mldsa_sign_op sk m ctx rho_rnd`. The Pr-equality is then *)
(* a deterministic Hoare assertion closed by `byphoare` + skip. *)
(* *)
(* This removes ONE axiom from the trust footprint: the cryptographic *)
(* content (Module-LWE rejection-sampling identity) sits inside *)
(* `mldsa_sign_op` (the abstract operator) and the section-local *)
(* refinement axioms (`combine_body_axiom`, `S_functional_spec`, *)
(* `reshare_preserves_secret`), which are the actual libjade / *)
(* Jasmin-extraction hand-off points. *)
lemma mldsa_sign_axiom :
forall (sk0 : share_t) (m0 : message_t) (c0 : ctx_t)
(r0 : randomness_t) &mm,
Pr[FIPS204Sign.sign(sk0, m0, c0, r0) @ &mm :
res = mldsa_sign_op sk0 m0 c0 r0] = 1%r.
proof.
move=> sk0 m0 c0 r0 &mm.
byphoare (_: sk = sk0 /\ m = m0 /\ ctx = c0 /\ rho_rnd = r0
==> res = mldsa_sign_op sk0 m0 c0 r0) => //.
proc; wp; skip => />.
qed.
(* -------------------------------------------------------------------- *)
(* Public-key derivation from secret share. *)
(* -------------------------------------------------------------------- *)
(* `derive_pk : share_t -> group_pk_t` is the FIPS 204 key-derivation *)
(* op: for a freshly-sampled secret seed, derive_pk returns the matching *)
(* public verification key (rho || t1 in FIPS 204 §3.5.4 terms). For the *)
(* Shamir-reconstructed share of a Pulsar quorum, this is the group's *)
(* public key the DKG committed to in Round-3. *)
(* *)
(* Declared at top level (BEFORE section ClassN1) so the section-local *)
(* `combine_body_axiom` precondition can reference it. *)
(* *)
(* `fips204_verify` is the matching public-key verifier (FIPS 204 *)
(* Verify_internal). *)
op derive_pk : share_t -> group_pk_t.
op fips204_verify : group_pk_t -> message_t -> ctx_t
-> signature_t -> bool.
(* -------------------------------------------------------------------- *)
(* Pulsar threshold oracle interface *)
(* -------------------------------------------------------------------- *)
module type Pulsar_Threshold = {
proc round1(sess : session_t, share : share_t,
rho_rnd : randomness_t) : round1_t
proc round2(sess : session_t, share : share_t,
round1_aggregate : round1_t list,
c_tilde : message_t) : round2_t
proc combine(group_pk : group_pk_t, m : message_t, ctx : ctx_t,
quorum : int list,
shares : share_t list,
rho_rnd : randomness_t,
r1s : round1_t list, r2s : round2_t list) : signature_t
}.
(* -------------------------------------------------------------------- *)
(* Combine refines the FIPS 204 dispatch on the reconstructed secret. *)
(* -------------------------------------------------------------------- *)
(* The cryptographic content of the proof of `thm:sign-correct` *)
(* (spec/pulsar.tex lines 765-793): the Pulsar Combine procedure *)
(* assembles (c_tilde, z, h) using Lagrange-aggregated values that *)
(* coincide ON-THE-NOSE with the FIPS 204 single-signer (c_tilde, z, h) *)
(* under the reconstructed secret. We name this dispatch step as a *)
(* single axiom over the abstract Combine semantics; the concrete *)
(* refinement proof is the Jasmin-side obligation (jasmin/threshold/ *)
(* combine.jazz refines `combine_abs`). *)
(* -------------------------------------------------------------------- *)
module CombineAbs = {
proc combine(group_pk : group_pk_t, m : message_t, ctx : ctx_t,
quorum : int list,
shares : share_t list,
rho_rnd : randomness_t,
r1s : round1_t list, r2s : round2_t list) : signature_t = {
var sk_group : share_t;
var sig : signature_t;
sk_group <- reconstruct quorum shares;
sig <@ FIPS204Sign.sign(sk_group, m, ctx, rho_rnd);
return sig;
}
}.
(* Refinement axiom: the Jasmin-extracted Combine is equivalent to *)
(* `CombineAbs.combine`. Stated inside `section ClassN1` below where *)
(* the module `T` is in scope (EasyCrypt axioms cannot be parametrised *)
(* by modules at top level; the standard idiom is a section-local *)
(* `axiom` predicated on a `declare module T`). *)
(* -------------------------------------------------------------------- *)
(* Discharged step: Combine = mldsa_sign_op o reconstruct *)
(* -------------------------------------------------------------------- *)
(* The abstract dispatch step composes cleanly: under the Combine *)
(* refinement, every signature produced equals *)
(* mldsa_sign_op (reconstruct Q shares) m ctx rho_rnd *)
(* by inlining FIPS204Sign.sign and applying `mldsa_sign_axiom`. *)
(* -------------------------------------------------------------------- *)
lemma combine_dispatches_to_mldsa
(gpk0 : group_pk_t)
(m0 : message_t) (ctx0 : ctx_t)
(Q0 : int list) (shs0 : share_t list)
(rr0 : randomness_t)
(r1s0 : round1_t list) (r2s0 : round2_t list) &m :
Pr[CombineAbs.combine(gpk0, m0, ctx0, Q0, shs0, rr0, r1s0, r2s0) @ &m :
res = mldsa_sign_op (reconstruct Q0 shs0) m0 ctx0 rr0] = 1%r.
proof.
(* The body of `CombineAbs.combine` is: *)
(* sk_group <- reconstruct quorum shares *)
(* sig <@ FIPS204Sign.sign(sk_group, m, ctx, rho_rnd) *)
(* return sig *)
(* and `FIPS204Sign.sign` returns the pure operator `mldsa_sign_op`. *)
(* So this is a deterministic Hoare assertion closed by `byphoare` *)
(* with a precondition that binds the call-site arguments to the *)
(* procedure parameters' names. *)
byphoare (_:
group_pk = gpk0 /\ m = m0 /\ ctx = ctx0 /\ quorum = Q0
/\ shares = shs0 /\ rho_rnd = rr0 /\ r1s = r1s0 /\ r2s = r2s0
==> _) => //.
proc; inline FIPS204Sign.sign; wp; skip => /#.
qed.
(* -------------------------------------------------------------------- *)
(* Class N1 -- byte-equality theorem *)
(* -------------------------------------------------------------------- *)
section ClassN1.
declare module S <: MLDSA65_Sign.
declare module T <: Pulsar_Threshold.
(* Section-local refinement axiom: the Jasmin-extracted Combine is *)
(* equivalent to `CombineAbs.combine` under the honest-quorum precond. *)
(* *)
(* Discharge path (FACTORED through refinement file): *)
(* *)
(* The byte-level content of this axiom is captured ATOMICALLY by *)
(* `combine_body_spec` in `Pulsar_N1_Combine_Refinement.ec`. That *)
(* file has TWO top-level `axiom`s (the byte-walk + memory- *)
(* separation invariant) and SEVEN derived lemmas (layout predicate, *)
(* read_signature_at_det, packed_bytes_eq_CombineAbs, *)
(* combine_body_writes_signature, ...). *)
(* *)
(* The `declare axiom` shape here remains because the procedure- *)
(* level `equiv` against an ABSTRACT module `T` is a different *)
(* shape than the byte-level lemma proven in the refinement file: *)
(* *)
(* refinement file: forall mem ptrs args, layout(mem, ptrs, args) *)
(* => read_sig_at(combine_fn mem ptrs).sig_ptr *)
(* = combine_abs_op args *)
(* *)
(* this axiom: equiv [ T.combine ~ CombineAbs.combine *)
(* : ={arg} ==> ={res} ] *)
(* *)
(* Bridging the two requires a wrapper module *)
(* `CombineExtractedWrapper` that adapts the extracted W64-pointer *)
(* interface of `Combine.M.pulsar_combine` to the abstract *)
(* Pulsar_Threshold interface. That wrapper proof is mechanical / *)
(* structural; it composes with the refinement file's atomic *)
(* `combine_body_spec` axiom to give the procedure-level equiv. *)
(* *)
(* Tracking: https://github.com/luxfi/pulsar/issues/4 *)
(* (closure step: write the wrapper bridge + prove the equiv against *)
(* `Combine.M.pulsar_combine`, then replace this declare axiom with *)
(* a lemma whose body reduces to *)
(* `Pulsar_N1_Combine_Refinement.combine_body_spec`). *)
(* The precondition `group_pk{1} = derive_pk (reconstruct quorum{1} *)
(* shares{1})` is the protocol-consistency obligation (Agent 1 HIGH-1 *)
(* closure): the byte-walk refinement file's atomic axiom *)
(* `combine_body_compute_sig_spec` requires the ghost full_gpk field *)
(* to be the derived pubkey of the reconstructed share. The honest *)
(* threshold caller always satisfies this (the DKG-committed group *)
(* pubkey IS the derived pubkey of the secret). Adversarial callers *)
(* who supply mismatched gpk fall outside this axiom's claim — the *)
(* refinement makes no guarantee about the extracted output in that *)
(* case. Threaded into `pulsar_n1_byte_equality`'s precondition below. *)
declare axiom combine_body_axiom :
equiv [ T.combine ~ CombineAbs.combine :
={arg}
/\ group_pk{1} = derive_pk (reconstruct quorum{1} shares{1})
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
/\ uniq quorum{1}
/\ size shares{1} = size quorum{1}
/\ poly_degree (reconstruct quorum{1} shares{1}) < size quorum{1}
/\ shares{1} = List.map
(poly_eval (reconstruct quorum{1} shares{1})) quorum{1}
==> ={res} ].
(* Functional-spec hypothesis on the single-party module `S`. *)
(* Mirrors `mldsa_sign_axiom` above for arbitrary `S`. *)
(* *)
(* Discharge path (FACTORED through refinement file): *)
(* *)
(* The byte-level content of this axiom is captured ATOMICALLY by *)
(* `sign_body_spec` in `Pulsar_N1_Sign_Refinement.ec`. That file *)
(* has TWO top-level `axiom`s (the byte-walk + memory-separation) *)
(* and SEVEN derived rung lemmas (rung1..rung7), with rungs 4 & 5 *)
(* collapsing to identity for libjade's single-party API surface. *)
(* *)
(* The `declare axiom` shape here remains for the same reason as *)
(* `combine_body_axiom`: bridging the byte-level lemma to a *)
(* procedure-level `equiv` requires a wrapper module adapting *)
(* libjade's W64-pointer interface to the abstract *)
(* MLDSA65_Sign interface. *)
(* *)
(* Tracking: https://github.com/luxfi/pulsar/issues/3 *)
(* (closure: wrapper bridge + lemma reducing to *)
(* `Pulsar_N1_Sign_Refinement.sign_body_spec`). *)
declare axiom S_functional_spec :
equiv [ S.sign ~ FIPS204Sign.sign :
={arg}
/\ accept_signing_attempt sk{1} m{1} ctx{1} rho_rnd{1}
==> ={res} ].
(* The honest-quorum Pulsar execution, as a single procedure.
Parameterised by `T` to comply with EasyCrypt's module-system
rule that concrete modules cannot depend on a section-local
`declare module`. Inside section ClassN1 we instantiate with the
declared `T`. *)
module ThresholdRun (T : Pulsar_Threshold) = {
proc run(group_pk : group_pk_t, shares : share_t list,
quorum : int list, m : message_t, ctx : ctx_t,
rho_rnd : randomness_t) : signature_t = {
var r1s : round1_t list;
var c_tilde : message_t;
var r2s : round2_t list;
var sig : signature_t;
var sess : session_t;
sess <- witness;
r1s <- [];
r2s <- [];
c_tilde <- witness;
sig <@ T.combine(group_pk, m, ctx, quorum, shares, rho_rnd, r1s, r2s);
return sig;
}
}.
(* The single-party FIPS 204 execution, parameterised by the
Shamir-reconstructed secret. Same parameterisation rule as
ThresholdRun above: `S` is `declare module` in the section, so
the concrete `SinglePartyRun` must take it as a functor param. *)
module SinglePartyRun (S : MLDSA65_Sign) = {
proc run(group_pk : group_pk_t, shares : share_t list,
quorum : int list, m : message_t, ctx : ctx_t,
rho_rnd : randomness_t) : signature_t = {
var sk_group : share_t;
var sig : signature_t;
sk_group <- reconstruct quorum shares;
sig <@ S.sign(sk_group, m, ctx, rho_rnd);
return sig;
}
}.
(* Main theorem: threshold output equals single-party output. *)
(* *)
(* Discharge path (NOW CLOSED — no admit): *)
(* - Procedure-level `transitivity` through *)
(* `SinglePartyRun(FIPS204Sign).run`. *)
(* - LHS leg: replace `T.combine` by `CombineAbs.combine` via *)
(* `combine_body_axiom`, inline `CombineAbs.combine` to expose *)
(* the shared `FIPS204Sign.sign` call, sync via `sim`. *)
(* - RHS leg: symmetric of `single_party_run_refines_fips204` (which *)
(* itself uses `S_functional_spec`). *)
(* *)
(* The cryptographic content (Module-LWE rejection-sampling identity) *)
(* lives inside the named section-local axioms, which are the libjade *)
(* hand-off points. *)
(* -------------------------------------------------------------------- *)
(* Decomposition: N1 byte-equality as a chain of 6 algebraic steps *)
(* -------------------------------------------------------------------- *)
(* The single big `admit.` is split into 6 sub-lemmas. Three are direct *)
(* applications of section-local axioms and discharge mechanically; the *)
(* fourth uses the already-discharged `combine_dispatches_to_mldsa`; *)
(* the fifth follows from `mldsa_sign_axiom`. The sixth (the top-level *)
(* byte-equality theorem) chains the others via procedure-level *)
(* `transitivity` through `SinglePartyRun(FIPS204Sign).run`. *)
(* -------------------------------------------------------------------- *)
(* Step 1 — single-party run is equivalent to FIPS204 instantiation *)
(* via the section-local `S_functional_spec` axiom. Threads the *)
(* accept-path precondition through. *)
lemma single_party_run_refines_fips204 :
equiv [ SinglePartyRun(S).run ~ SinglePartyRun(FIPS204Sign).run :
={group_pk, shares, quorum, m, ctx, rho_rnd}
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
==> ={res} ].
proof.
proc.
call S_functional_spec.
auto.
qed.
(* Step 2 — single-party FIPS 204 run produces `mldsa_sign_op` on the *)
(* reconstructed secret, with probability 1. Pure consequence of *)
(* `mldsa_sign_axiom` (which says `FIPS204Sign.sign` returns the op). *)
lemma single_party_fips204_eq_op
(gpk : group_pk_t) (mm : message_t) (cctx : ctx_t)
(Q : int list) (shs : share_t list) (rr : randomness_t) &m :
Pr[SinglePartyRun(FIPS204Sign).run(gpk, shs, Q, mm, cctx, rr) @ &m :
res = mldsa_sign_op (reconstruct Q shs) mm cctx rr] = 1%r.
proof.
byphoare (_:
group_pk = gpk /\ m = mm /\ ctx = cctx /\ quorum = Q
/\ shares = shs /\ rho_rnd = rr ==> _) => //.
proc; inline FIPS204Sign.sign; wp; skip => /#.
qed.
(* Step 3 — threshold run is equivalent to a CombineAbs-instantiated *)
(* run via the section-local `combine_body_axiom`. *)
(* We do not state this as a separate equiv because CombineAbs is not a *)
(* Pulsar_Threshold module (it has a different procedure-signature). *)
(* Step 3 is therefore folded into Step 4 below as a direct rewrite *)
(* over the combine call site. *)
(* Step 4 — threshold run via CombineAbs produces `mldsa_sign_op` on the *)
(* reconstructed secret, with probability 1. Uses the already- *)
(* discharged `combine_dispatches_to_mldsa` lemma (no admit). *)
lemma threshold_combine_abs_eq_op
(gpk : group_pk_t) (mm : message_t) (cctx : ctx_t)
(Q : int list) (shs : share_t list) (rr : randomness_t) &m :
Pr[CombineAbs.combine(gpk, mm, cctx, Q, shs, rr, [], []) @ &m :
res = mldsa_sign_op (reconstruct Q shs) mm cctx rr] = 1%r.
proof.
apply (combine_dispatches_to_mldsa gpk mm cctx Q shs rr [] [] &m).
qed.
(* Step 5 — the Lagrange-reconstruction identity at the heart of the *)
(* threshold-to-single-party reduction is `reconstruct_of_share`, *)
(* already discharged above (line ~213) and quoted here as a reminder *)
(* that the algebraic core is closed. *)
(* See also `~/work/lux/proofs/lean/Crypto/Threshold_Lagrange.lean` *)
(* for the polynomial-Lagrange identity discharged at the Lean side. *)
(* Step 6 — chain Steps 1–5 to derive the byte-equality theorem. *)
(* *)
(* The proof uses a procedure-level `transitivity` through *)
(* `SinglePartyRun(FIPS204Sign).run`. The LHS leg replaces `T.combine` *)
(* by `CombineAbs.combine` via the section-local `combine_body_axiom`, *)
(* then inlines `CombineAbs.combine` to expose the shared *)
(* `FIPS204Sign.sign` call and synchronises via `sim`. The RHS leg is *)
(* the symmetric of `single_party_run_refines_fips204` (which itself *)
(* uses `S_functional_spec`). Every step is a discharged sub-lemma or a *)
(* section-local axiom; no new admit is introduced. *)
(* ============================================================
IMPORTANT — citation guide:
The implementation-backed theorem to cite for end-to-end
N1 byte-equality correctness is
Pulsar_N1_Extracted.pulsar_n1_byte_equality_extracted
The generic theorem below (this file) is parametric and
assumes the two section-local module contracts
combine_body_axiom
S_functional_spec.
Both coexist intentionally: the generic version remains
useful as a parametric contract theorem for any future
T / S that satisfies the contracts; the concrete extracted
version is the one that cites all the way down to specific
extracted Jasmin/libjade modules.
============================================================
GENERIC theorem: parametric over abstract `T : Pulsar_Threshold`
and `S : MLDSA65_Sign` with the two module-contract axioms
(`combine_body_axiom`, `S_functional_spec`) as section-local
hypotheses. After section closure, this lemma is universally-
quantified over (S, T, combine-equiv-on-T, sign-equiv-on-S). *)
lemma pulsar_n1_byte_equality :
equiv [ ThresholdRun(T).run ~ SinglePartyRun(S).run :
={group_pk, shares, quorum, m, ctx, rho_rnd}
/\ uniq quorum{1}
/\ size shares{1} = size quorum{1}
/\ group_pk{1} = derive_pk (reconstruct quorum{1} shares{1})
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
/\ poly_degree (reconstruct quorum{1} shares{1}) < size quorum{1}
/\ shares{1} = List.map
(poly_eval (reconstruct quorum{1} shares{1})) quorum{1}
==> ={res} ].
proof.
(* Chain via SinglePartyRun(FIPS204Sign).run as the bridge module. *)
(* LHS ~ SinglePartyRun(FIPS204Sign).run *)
(* via combine_body_axiom on T.combine then inlining the *)
(* CombineAbs body to expose the FIPS204Sign.sign call. *)
(* SinglePartyRun(FIPS204Sign).run ~ RHS *)
(* via symmetry of single_party_run_refines_fips204. *)
transitivity SinglePartyRun(FIPS204Sign).run
(={group_pk, shares, quorum, m, ctx, rho_rnd}
/\ uniq quorum{1}
/\ size shares{1} = size quorum{1}
/\ group_pk{1} = derive_pk (reconstruct quorum{1} shares{1})
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
/\ poly_degree (reconstruct quorum{1} shares{1}) < size quorum{1}
/\ shares{1} = List.map
(poly_eval (reconstruct quorum{1} shares{1})) quorum{1}
==> ={res})
(={group_pk, shares, quorum, m, ctx, rho_rnd}
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
==> ={res}).
+ move=> &1 &2 [#] 6-> uQ szq Hgpk Haccept Hdeg Hhonest.
by exists (group_pk{2}, shares{2}, quorum{2}, m{2}, ctx{2}, rho_rnd{2}).
+ done.
+ (* Step A: ThresholdRun(T).run ~ SinglePartyRun(FIPS204Sign).run *)
proc.
(* Step A1: replace T.combine on the LHS by CombineAbs.combine via *)
(* the section-local refinement axiom. Threads gpk-consistency + *)
(* accept-path + threshold invariants through the transitivity's *)
(* middle precondition. *)
transitivity{1}
{ sess <- witness;
r1s <- [];
r2s <- [];
c_tilde <- witness;
sig <@ CombineAbs.combine(group_pk, m, ctx, quorum, shares,
rho_rnd, r1s, r2s); }
(={group_pk, shares, quorum, m, ctx, rho_rnd}
/\ group_pk{1} = derive_pk (reconstruct quorum{1} shares{1})
/\ accept_signing_attempt
(reconstruct quorum{1} shares{1})
m{1} ctx{1} rho_rnd{1}
/\ uniq quorum{1}
/\ size shares{1} = size quorum{1}
/\ poly_degree (reconstruct quorum{1} shares{1}) < size quorum{1}
/\ shares{1} = List.map
(poly_eval (reconstruct quorum{1} shares{1})) quorum{1}
==> ={sig})
(={group_pk, shares, quorum, m, ctx, rho_rnd} ==> ={sig}).
+ smt().
+ done.
+ (* T.combine ~ CombineAbs.combine via the axiom (other code is the *)
(* same on both sides). gpk-consistency + accept-path + threshold *)
(* invariants are carried by the transitivity's first-leg post. *)
wp.
call combine_body_axiom.
by auto.
(* Step A2: with LHS now calling CombineAbs.combine, inline that and *)
(* the RHS to expose the shared FIPS204Sign.sign call. *)
inline CombineAbs.combine.
wp.
call (_: ={arg} ==> ={res}).
+ by sim.
auto.
+ (* Step B: SinglePartyRun(FIPS204Sign).run ~ SinglePartyRun(S).run *)
symmetry.
conseq single_party_run_refines_fips204.
+ move=> &1 &2 [#] 6-> Haccept.
do! split=> //.
+ done.
qed.
end section ClassN1.
(* -------------------------------------------------------------------- *)
(* Corollary: a FIPS 204 verifier accepts every Pulsar signature *)
(* produced by an honest quorum. *)
(* -------------------------------------------------------------------- *)
(* This is exactly the Lean axiom *)
(* Crypto.Pulsar.Verify.pulsar_output_interchangeable *)
(* lifted to EasyCrypt's probabilistic Hoare setting. Stated as a *)
(* commented future obligation -- the body is a direct corollary of *)
(* `pulsar_n1_byte_equality` plus FIPS 204 verifier correctness on *)
(* outputs produced by FIPS 204 Sign (standard libjade theorem). *)
(* *)
(* The ops `derive_pk` and `fips204_verify` themselves are declared *)
(* BEFORE section ClassN1 above (so `combine_body_axiom`'s precondition *)
(* can reference `derive_pk`). They are reachable from any downstream *)
(* file that imports `Pulsar_N1`. *)
(* -------------------------------------------------------------------- *)
(* The verifier-compatibility corollary (Pr[verify ∘ ThresholdRun] = 1)
is a direct consequence of `pulsar_n1_byte_equality` plus libjade
`MLDSA65_Functional.sign_verify_correct`. We do not restate it here
because the byte-equality already implies it; restating it would
add a second admit on the same content. The corollary is left for
the consumer who wants the probabilistic phrasing. *)