You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 34b280c
Browse filesBrowse the repository at this point in the historyBrowse files
feat(zk): authenticate the commitment method from the issuer signature (#7082)
* feat(zk): authenticate the commitment method from the issuer signature
The verifier no longer has to assume which commitment method a committed
graph was made under, and it does not read one from the manifest either --
it RECOVERS it from the issuer signature.
`sparq-zk-compose::method_auth` builds one candidate signed message per
`CommitmentMethod::recognised()` method from values the verifier itself
established (the commitment, the issuer-attested salt, the issuer-bound
status reference, and the attested holder digest when the attestation is
holder-bound) plus the method-UNBOUND message, and `classify` reports
which one the presented signature verifies over. Exactly one bound
candidate verifying names the method; more than one is refused rather
than resolved. The prover only SELECTS among messages the verifier
computed, so it contributes no message of its own.
`MethodPolicy` is the relying party's decision over the answer, carried
on the external trust anchor (`KeySet::with_method_policy`, the
`with_hidden_issuer_depth` precedent, so no public verifier signature
changes). The default accepts a method-unbound attestation, which is what
makes this back-compatible: an existing manifest's `ZKSIG_C3`/`ZKSIG_C4`
attestation authenticates as `Unbound`, the recomputed message is
byte-identical to the one the verifier computed before, and a bad
signature still reports `InvalidIssuerSignature`.
New in `sparq-zk`: the `ZKSIG_M4` message (the one shape binding both a
holder key and a method) with its two signers, `fragment_typed_filters`
for a datatype-general view of a query's FILTER obligations, and
`CommitmentMethod::recognised()`.
`sparq-zk-compose::value_pin` (behind the OFF-by-default `dual-leaf`
feature) states what a value-lane sub-proof's member, lane constant,
scale, op, bound and verdict would have to be for an authenticated
`DualLeafV2` method and a typed query FILTER. **The value-lane verifier
gates stay CLOSED:** the unconditional stage-0 refusal is unchanged and
still first, the stage-2 operand match and `filter_edge_true` are
untouched, and a manifest every one of whose pins holds is still refused
(a test asserts exactly that). Computing a pin is not enforcing one.
Hardening only. Not externally audited (sq-qhy4); no soundness or privacy
property is claimed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: fix extended-fragment test build, test hidden-only method policy, register ZKSIG_M4
- The three extended-fragment prefilter tests read Prefiltered.required.
- A hidden-only commitment is tested against the method policy: an attested V1
message is refused by default and admitted with the opt-in; an unbound
message is refused under require_method_bound.
- ZKSIG_M4 moves to module scope and joins the pinned domain-tag table.
- The re-spelled lane-constant test keeps a decodable 0x prefix so only the
canonical-spelling check can refuse it.
- Research note separates the enforced method check from the computed-only
pins; CHANGELOG states the back-compat claim precisely.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: separate the presentation message from the registry one, hold the hidden-only method to the policy
Three hardening changes to verifier-side commitment-method authentication.
1. The relying party's `MethodPolicy` now applies to the method the hidden-issuer
stage actually binds. A hidden-only commitment's declared `message` must EQUAL
one of the verifier-built candidate messages: one matching none of them is
refused in the pre-filter with `HiddenIssuerMessageMismatch` (the same refusal
the bb stage's own byte-compare raises), and every hidden-only commitment the
attestation gate accepts now carries an explicit entry in the method map, so
the absent-key default is not what decides its bound message. Where no
candidate set can be built at all, the policy is applied to `Unbound`.
2. Domain separation between a REGISTRY record and a PRESENTATION attestation.
`sig::presentation_message_with_status_and_scheme` adds the `ZKSIG_M5` tag
(same preimage shape as the registry's `ZKSIG_M3`, own tag), signed by
`SecretKey::sign_presentation_with_status_and_scheme`; the bearer candidate set
on the clear and hidden paths uses it, `ZKSIG_M4` still covers the holder-bound
axis and `ZKSIG_C3`/`ZKSIG_C4` remain the method-unbound candidates. Registry
signing (`ZKSIG_M2`/`ZKSIG_M3`) is unchanged, and a registry signature
presented as a clear attestation reports `InvalidIssuerSignature`.
`CommitmentAttestation::from_registry_entry` is removed: a registry entry is
not a presentation attestation.
3. Candidate trial-verification is memoized per (commitment, attestation index)
within one attestation-gate call, and per commitment on the hidden-only path,
so a commitment many scans reference is verified once. Every other check in
the loop still runs on every occurrence; `classify` keeps no early exit.
Docs follow the code: who chooses what on each path, that a hidden-only
commitment's recorded method is the one its declared message selects and the bb
stage then holds it to, and an accurate back-compatibility statement (every
manifest that verified before still verifies under the default policy; the newly
accepted ones are those signed over a method-bound presentation message). Not
externally audited (sq-qhy4).
Tests: `ZKSIG_M5` wire-definition KAT and a registry-signature-is-not-a-
presentation pair in `sig.rs`; the same split at the candidate-set level and
through the pre-filter; the unmatched hidden-only message refused under both
policies, structurally and live end to end over a real hidden-issuer proof.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: refuse an undecodable hidden-only message once a candidate set exists
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: check every hidden-issuer entry's declared message, not just the first
`authenticate_hidden_only_method` read only the FIRST hidden-issuer entry
declared over a hidden-only commitment, so the verdict depended on the order
the prover listed its entries in: [honest, unmatched] passed the pre-filter
while [unmatched, honest] was refused. Every entry over that commitment is now
checked — each must decode and equal a verifier-built candidate, and all of
them must select the SAME candidate, else HiddenIssuerMessageMismatch. The bb
stage already byte-compares every entry against a recomputed message, so this
removes the order dependence without widening what is accepted.
Tests (sparq-zk-compose, tests/method_auth_binding.rs):
- every_hidden_entry_declared_message_is_checked_in_either_order — the two
orders above, plus two entries that each match a candidate but different
ones, in both orders.
- each_hidden_only_commitment_gets_its_own_policy_decision — two distinct
hidden-only commitments under require_method_bound, the first declaring the
M5 message and the second the unbound one: MethodNotAuthenticated.
- a_hidden_only_commitment_without_a_status_reference_is_refused_when_a_method_is_required
— no candidate set, so the policy decides on Unbound, through the pre-filter.
- live_additive_clear_m5_attestation_plus_a_hidden_proof_over_the_m5_message
(#[ignore], nargo/bb) — a clear M5 attestation alongside a hidden proof over
that same message still verifies end to end under the strict policy.
- sparq-zk sig.rs: M5 and M4 hex known answers alongside C2/C3/M2/M3.
Docs: qualify the back-compat statement in CHANGELOG.md and
skills/zk-query-proofs/SKILL.md (the declared message was previously not read;
a hidden-only manifest whose declared message is absent, non-canonically
spelled or unmatched is now refused, and a refusal may be reported earlier, in
the pre-filter); correct the M3/M4 family note in sparq-zk-compose/src/lib.rs
to M5/M4; place M4 among the method-bound shapes and note in the M5 rationale
that a registry entry signed before the method binding carries a C3 signature
indistinguishable from a C3 presentation attestation (accepted as Unbound by
default, excluded by require_method_bound); state the additive-mode behaviour
on HiddenIssuerAttestation::message; correct Prefiltered::methods on a
hidden-only commitment with no resolvable salt.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: scope the hidden-message docs to what is checked, tighten two tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: check every hidden-issuer entry's declared message against the message its proof is checked against
The hidden-issuer stage now refuses, before decoding any proof, an entry
whose declared message differs from the recomputed one, including over a
commitment that also has a clear attestation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
* zk: keep the depth test's doc on the depth test; state the declared-message qualification precisely
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CB7XiW3NDsggNCuDcuJJo
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+62Lines changed: 62 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
10
10
### Added
11
11
12
+
-**sparq-zk-compose:** verifier-side **authenticated commitment method**. The verifier no longer has
13
+
to assume which commitment method a committed graph was made under, and it does not read one from
14
+
the manifest either — it RECOVERS it from the issuer signature. `method_auth` builds one candidate
15
+
signed message per `CommitmentMethod::recognised()` method from values the verifier already
16
+
established (the commitment, the issuer-attested salt, the issuer-bound status reference, and the
17
+
attested holder digest when the attestation is holder-bound) plus the method-UNBOUND message, and
18
+
`classify` reports which one the presented signature verifies over: `AuthenticatedMethod::Bound(m)`
19
+
or `AuthenticatedMethod::Unbound`. `MethodPolicy` is the relying party's decision over the answer,
20
+
carried on the external trust anchor as `KeySet::with_method_policy` (the
21
+
`with_hidden_issuer_depth` precedent, so no public verifier signature changes):
22
+
`MethodPolicy::current()` is the default, `require_method_bound()` refuses an attestation that binds
23
+
no method (`CheckError::MethodNotAuthenticated`), and `admit_legacy_dual_leaf_v1()` is the
24
+
written-down opt-in for an attested superseded method (refused otherwise with
25
+
`CheckError::MethodNotAdmitted`); more than one method's message verifying is
26
+
`CheckError::AmbiguousMethod`. For a HIDDEN-ONLY commitment the method is selected by the hidden
27
+
entry's declared `message`, which must EQUAL one of the candidates — a value matching none of them
28
+
is refused with `CheckError::HiddenIssuerMessageMismatch`, and the policy applies to the method the
29
+
hidden-issuer proof is then required to bind. Every entry over a hidden-only commitment is checked,
30
+
and they must all select the same candidate, so the message check does not depend on the order
31
+
the entries are listed in. New PRESENTATION message shapes and signers in `sparq-zk`, each under its own
32
+
domain tag: `sig::presentation_message_with_status_and_scheme` (`ZKSIG_M5`, for a bearer
33
+
attestation) and `sig::commitment_message_with_holder_and_scheme`
34
+
(`ZKSIG_M4`, the one shape binding both a holder key and a method), signed with
35
+
`SecretKey::{sign_presentation_with_status_and_scheme, sign_commitment_with_holder_and_scheme}`. A
36
+
REGISTRY entry's own `ZKSIG_M3` signature is deliberately NOT a presentation attestation — recording
37
+
a commitment in an issuer's registry is a separate statement from authorising its presentation, so
38
+
presenting one as a clear attestation reports `CheckError::InvalidIssuerSignature`. Back-compatible
39
+
with ONE qualification, about the declared `message` of hidden-issuer entries: a hidden-issuer entry's declared `message` was previously not read at all,
40
+
and over a hidden-only commitment it is now required to equal a verifier-built candidate (for every
41
+
entry over that commitment), so a hidden-only manifest whose declared message is empty,
42
+
not decodable as a field element, or unmatched is now refused with `CheckError::HiddenIssuerMessageMismatch`;
43
+
a refusal of a malformed hidden-only manifest may also be reported earlier, in the pre-filter, than
44
+
it was before. The hidden-issuer stage likewise requires EVERY entry's declared `message`, including
45
+
one over a commitment that also has a clear attestation, to equal the message its proof is checked
46
+
against, and that check may now be reported ahead of an entry's depth or proof refusal. Otherwise
47
+
every manifest that verified before still verifies under the default
48
+
policy — an attestation over the method-unbound `ZKSIG_C3`/`ZKSIG_C4` message authenticates as
49
+
`Unbound`, which the default policy accepts, and a bad signature still reports
50
+
`CheckError::InvalidIssuerSignature`. The newly accepted manifests are only those carrying an
51
+
issuer signature over a method-bound PRESENTATION message (`ZKSIG_M5`/`ZKSIG_M4`). Not externally
52
+
audited (sq-qhy4); no soundness or
53
+
privacy property is claimed.
54
+
-**sparq-zk:**`verify::fragment_typed_filters` re-derives a query's FILTER obligations keeping each
55
+
constant as the typed `oxrdf::Literal` the query spells (`verify::TypedQueryFilter { variable, op,
56
+
constant }`), for a relying party that needs the constant's datatype and lexical form rather than a
57
+
`u64`. It shares one private walker with `fragment_filters`, whose behaviour and error strings are
58
+
unchanged (a regression test asserts the messages verbatim), and widens the datatype axis only: the
59
+
admitted comparison shape is still `?var op <literal>` after the `const op ?var` flip, with
60
+
everything else fail-closed. Also `commit::CommitmentMethod::recognised()`, the enumeration
61
+
counterpart of `from_scheme_iri`.
62
+
-**sparq-zk-compose:**`value_pin` holds the relying party's **pinned value-lane inputs** for the
63
+
dual-leaf lanes (OFF-by-default `dual-leaf` feature): given an authenticated `DualLeafV2` method and
64
+
a typed query FILTER, `expected_value_lane` / `check_value_lane_pin` state what a value-lane
65
+
sub-proof's member, lane constant (for the decimal lane, the scale it names), op, bound and verdict
66
+
would have to be, each equality failing closed with its own `PinError`; `scan_method` requires every
67
+
commitment a scan draws from to have authenticated the same method, and `check_lane_routed` refuses
68
+
a lane the whole-graph commitment builder does not route. **The value-lane verifier gates stay
69
+
CLOSED:** the unconditional stage-0 `CheckError::ValueLaneSubProofNotAccepted` refusal is unchanged
70
+
and still first, the stage-2 binding-edge operand match and `filter_edge_true` are untouched, and a
71
+
manifest every one of whose pins holds is still refused (a test asserts exactly that). Computing a
72
+
pin is not enforcing one; the gate-opening change is separate. Research-grade, not externally
73
+
audited (sq-qhy4); the value lanes carry the #769-accepted INV-VL downgrade (CR-G8).
12
74
-**sparq-zk:**`dual_leaf_v2` is a second dual-leaf commitment method (`CommitmentMethod::DualLeafV2`,
13
75
scheme IRI `https://sparq.dev/ns/zk#poseidon2-dualleaf-v2`) behind the OFF-by-default `dual-leaf`
14
76
feature. It keeps V1's leaf shapes and re-derives the constants: lane constants in BLAKE3's
0 commit comments