Files
pulsar/proofs/easycrypt/Pulsar_N1_Signature_Codec.ec
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

196 lines
9.1 KiB
Plaintext

(* -------------------------------------------------------------------- *)
(* Pulsar — FIPS 204 signature byte-codec *)
(* -------------------------------------------------------------------- *)
(* Decomplected from Pulsar_N1_Combine_Layout. *)
(* *)
(* This file holds the FIPS 204 ML-DSA-65 signature-bytes view shared *)
(* by combine and sign. Three concerns live here, and only three: *)
(* *)
(* 1. The abstract `signature_t` type (FIPS 204 §3.5.5 c_tilde || z *)
(* || h, 3293 bytes for ML-DSA-65). *)
(* 2. Encode / decode + length, as named per-type axioms (the *)
(* FIPS 204 packing is fully specified at the bit level in *)
(* `lemmas/MLDSA65_Functional.ec`; we surface only the round-trip *)
(* property here). *)
(* 3. read_sig_at / write_sig_at — read/write a packed signature at *)
(* a memory pointer — plus the two proved frame lemmas *)
(* (read_after_write_sig + write_sig_separation). These compose *)
(* Pulsar_N1_Memory's bulk primitives with the codec. *)
(* *)
(* What this file is NOT: *)
(* *)
(* - It does NOT contain the bit-level FIPS 204 §3.5.5 packing *)
(* spec — that's `MLDSA65_Functional.pack_signature` / *)
(* `unpack_signature` (when those land). Here we only need the *)
(* round-trip + length identities to discharge layout proofs. *)
(* *)
(* - It does NOT contain combine- or sign-specific encoders for *)
(* other wire types (c_tilde, t0, r2_msg, sk packing, message *)
(* bytes). Those live in the per-procedure layout files. *)
(* *)
(* Decomplect benefit: Pulsar_N1_Sign_Layout no longer imports *)
(* Pulsar_N1_Combine_Layout. The two layouts now share Memory + this *)
(* codec only. *)
(* -------------------------------------------------------------------- *)
require import AllCore List Int IntDiv.
require import Pulsar_N1_Memory.
(* ===================================================================
FIPS 204 ML-DSA-65 signature length (bytes).
3293 = 32 (c_tilde) + 5 * 640 (z packing) + 55 (h.weight cap) + 6
(per FIPS 204 §3.5.5 sig packing for parameter set 3).
=================================================================== *)
op sig_len : int = 3293.
(* ===================================================================
The signature type + codec ops.
v11 concretization: `signature_t` is now a CONCRETE 1-field record
wrapping `int list`, with `encode_signature` / `decode_signature`
defined STRUCTURALLY as record field projection / construction.
This collapses the previous (encode_decode, decode_encode_wf)
roundtrip axioms to TRIVIAL provable lemmas (record reconstruction
is structurally the identity).
The well-formedness predicate `wf_signature_bytes` is also
CONCRETIZED to `size = sig_len`: FIPS 204 §3.5.5 specifies
additional structural invariants on the byte string (h-weight
bound, z bit-packing range), but the length identity is the
load-bearing structural property the layout proofs consume. The
richer FIPS 204 wf invariant remains in
`lemmas/MLDSA65_Functional.ec` for future work — surfacing it
here only as `size = sig_len` keeps the layout-side proofs
honest about what they verify.
What stays AXIOMATIC: `encode_signature_wf` — every value of type
`signature_t` produced by the protocol (via `pack_n1_signature`,
`mldsa_sign_op`, etc.) MUST have length `sig_len`. With the
record wrapper this is a structural invariant on producers
(not a constraint on the type itself, which doesn't enforce it).
`encode_signature_len` is now DERIVED from this single axiom.
=================================================================== *)
(* v11: concrete 1-field record wrapping `int list`. The single
field surfaces the bytes; encode/decode are structural. *)
type signature_t = { sig_bytes : int list }.
op encode_signature (x : signature_t) : int list = x.`sig_bytes.
op decode_signature (bs : int list) : signature_t = {| sig_bytes = bs |}.
(* Well-formedness predicate on signature bytes. v11: concretized to
the FIPS 204 §3.5.5 length identity. The richer FIPS 204 wf
invariant (h-weight bound, z bit-packing) is future work — kept
in MLDSA65_Functional. *)
op wf_signature_bytes (bs : int list) : bool = size bs = sig_len.
(* The single load-bearing producer-side invariant: every
`signature_t` value has byte-length `sig_len`. With the concrete
record wrapper, this CANNOT be derived structurally because the
record carries an arbitrary int list; it constrains the
protocol-level producers (`pack_n1_signature`, `mldsa_sign_op`,
the wrapper-bridge sig outputs) to produce sig_len bytes. *)
axiom encode_signature_wf (x : signature_t) :
wf_signature_bytes (encode_signature x).
(* PROVED — record reconstruction is structurally identity.
v11 closure: was an axiom, now a lemma. The roundtrip
`decode_signature (encode_signature x) = x` reduces to
`{| sig_bytes = x.\`sig_bytes |} = x` which is the standard
record-eta identity. *)
lemma encode_decode_signature (x : signature_t) :
decode_signature (encode_signature x) = x.
proof. by rewrite /encode_signature /decode_signature; case: x. qed.
(* PROVED — analogous record-eta on the other direction.
v11 closure: was an axiom, now a lemma. Trivially provable from
the structural definitions of encode/decode; the wf hypothesis
is no longer needed (in v10 the wf gate was there to rule out
adversarial decoder realisations — with the record-concrete
decoder there are no such realisations). *)
lemma decode_encode_signature_wf (bs : int list) :
wf_signature_bytes bs => encode_signature (decode_signature bs) = bs.
proof. by move=> _; rewrite /encode_signature /decode_signature. qed.
(* PROVED — length identity follows directly from
`encode_signature_wf` + the concrete definition of
`wf_signature_bytes`.
v11 closure: was an axiom, now a lemma. *)
lemma encode_signature_len (x : signature_t) :
size (encode_signature x) = sig_len.
proof.
have Hwf := encode_signature_wf x.
by rewrite /wf_signature_bytes in Hwf.
qed.
(* ===================================================================
Memory-level signature read / write.
These compose the memory primitives (load_bytes / store_bytes from
Pulsar_N1_Memory) with the codec above. They're shared by both
layout files via the thin wrappers
`Pulsar_N1_Combine_Layout.read_signature_at` /
`Pulsar_N1_Sign_Layout.read_sig_sign` (kept for backward
compatibility with the existing Refinement-file consumers).
=================================================================== *)
op read_sig_at (m : mem_t) (p : int) : signature_t =
decode_signature (load_bytes m p sig_len).
op write_sig_at (m : mem_t) (p : int) (s : signature_t) : mem_t =
store_bytes m p (encode_signature s).
(* ===================================================================
Frame lemmas — PROVED, no axioms beyond the per-type codec ones.
=================================================================== *)
(* Round-trip: writing a signature at p and reading back from p
returns the original. *)
lemma read_after_write_sig (m : mem_t) (p : int) (s : signature_t) :
read_sig_at (write_sig_at m p s) p = s.
proof.
rewrite /read_sig_at /write_sig_at.
have Heq :
load_bytes (store_bytes m p (encode_signature s)) p sig_len
= encode_signature s.
- have <-: size (encode_signature s) = sig_len
by exact encode_signature_len.
by apply store_bytes_load_bytes.
by rewrite Heq encode_decode_signature.
qed.
(* Separation: a write to [p, p + sig_len) doesn't affect reads at
addresses outside that range. *)
lemma write_sig_separation
(m : mem_t) (p : int) (s : signature_t) (q : int) :
q < p \/ p + sig_len <= q =>
load_byte (write_sig_at m p s) q = load_byte m q.
proof.
move=> Hdisj.
rewrite /write_sig_at.
apply store_bytes_disjoint.
by have ->: size (encode_signature s) = sig_len
by exact encode_signature_len.
qed.
(* ===================================================================
ACCOUNTING
axioms (2 — per-type FIPS 204 §3.5.5 round-trip + length):
encode_decode_signature
encode_signature_len
ops (definitions):
sig_len, signature_t,
encode_signature, decode_signature,
read_sig_at, write_sig_at.
PROVED lemmas (0 admits):
read_after_write_sig
write_sig_separation
=================================================================== *)