refactor(noir): single entrypoint for attestation verification - #887
Closed
kilianglas wants to merge 1 commit into
Closed
refactor(noir): single entrypoint for attestation verification#887kilianglas wants to merge 1 commit into
kilianglas wants to merge 1 commit into
Conversation
Splits the WIP-106 verification into two crate-private primitives and one
public composite:
verify_takt(takt, trust_anchor_key_x, trust_anchor_key_y, now) pub(crate)
verify_aat(aat, assertion_key_x, assertion_key_y, now) pub(crate)
verify_attestation(aat, takt, trust_anchor_key_x, _y, now) pub
Since un-nesting, the shared assertion_key is the *sole* binding between the
two tokens, so it matters more than before that neither can be verified alone.
The composite feeds verify_aat the key read from the TAKT it just verified, so
the binding holds by construction with no equality assert, and the primitives
are crate-private so a consumer cannot skip either half. Verified from a
throwaway external package: calling verify_aat directly is a compile error
("verify_aat is private and not visible from the current module").
Two structural fixes fall out of this:
- The AAT no longer carries the TAKT as a field, and the TAKT no longer
carries the trust_anchor_key. Neither was part of the token it sat in: the
TAKT travels in the AAT's unprotected header, and the trust anchor key is
identified only by the kid protected-header parameter, which WIP-106 leaves
outside the signature. It is a verifier-supplied input, like now, and the
signature now says so.
- Struct fields are pub. They were module-private, so no external package
could construct either token -- the library was not consumable at all.
Adds a negative test for an untrusted trust_anchor_key, moves the
key-not-attested test to the composite where the binding now lives, and
documents the public-input obligations the library cannot enforce.
14 Noir tests (was 11), 16 Rust tests unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Folding this into #884 instead of stacking another PR — commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the attestation library consumable, and makes it impossible to consume unsafely. Stacked on #884 — review that one first.
The problem
Two things were wrong with the API, both surfaced while sketching how the DeepFace circuits would call it.
The library could not be used at all. Every field of
TrustAnchorKeyTokenandAuthenticatorAssertionTokenwas module-private, so no external Noir package could construct either token. I confirmed this with a throwaway consumer package:error: exp is private and not visible from the current module, and the same for all eight fields.verify_aatwas reachable but uncallable.The binding was carried by struct nesting. Since #884 un-nested the TAKT, the shared
assertion_keyis the sole thing tying the two tokens together — the spec says so explicitly ("This equality is the sole binding between the two tokens"). That made it more important, not less, that neither token can be verified in isolation. But the AAT held the TAKT as a field, which mirrors the old nested wire format rather than the current one.The shape
The composite feeds
verify_aattheassertion_keyread from the TAKT it just verified. The binding therefore holds by construction — no equality assert, no second witness for the same key, nothing a refactor can drop. And because the primitives are crate-private, a consumer cannot verify one half and skip the other. From the same external package:That is the bypass — verify an AAT under a prover-chosen key, never check a TAKT — failing at compile time.
Each primitive is also independently testable now. Previously every AAT test had to drag in a full valid TAKT fixture.
Two structural fixes that fall out
The AAT no longer carries the TAKT, and the TAKT no longer carries the
trust_anchor_key. Neither was part of the token it sat in. The TAKT travels in the AAT's unprotected header. And the trust anchor key is not a TAKT claim — the token identifies its signer only by thekidprotected-header parameter, which WIP-106 leaves outside the signature and the Security section says to treat as an untrusted hint. It is a verifier-supplied input, exactly likenow;takt_digestnever hashed it. The signature now reflects that.Struct fields are
pub, so tokens can be constructed. This is the fix for "not consumable," and it is orthogonal to the guarantee above: the prover supplies the token witnesses either way — what matters is that it cannot skip a verification step.Tests
attestation::test_untrusted_trust_anchor_key_rejected— new. A well-formed TAKT signed by anyone other than the RP's allowlisted key must fail. Checked against the BabyJubJub prime-order subgroup generator so the failure is the signature check, not a point-validity assert (an off-curve key tripsPublic key must be on curvefirst, which would have made the test pass for the wrong reason).test_aat_key_not_attestedmoved to the composite, where the binding now lives.aat::test_aat_wrong_assertion_key— the primitive-level counterpart.What the library still cannot enforce
Documented in
crates/proof/noir/README.mdwith a workedmainexample, because no API shape can enforce it: the calling circuit must exposetrust_anchor_keyandnowas public inputs (a privatenowlets the prover pick a time at which any token is fresh; a private anchor key lets it sign its own TAKT), must bindaat.nonce/aat.cdhto the surrounding proof, and must expose whichever ofsec_flagsandauthenticator_metaits RP needs. A CI check over each consumer's compiled ABI would catch the public-input half; worth adding alongside the first consumer.Test plan
cd crates/proof/noir/authenticator-attestation && nargo test— 14 passing (was 11)cargo test -p world-id-proof authenticator_attestation— 16 passing, unchanged (the Rust encoder is untouched)nargo fmt --checkcleanverify_attestation, withtrust_anchor_key_x/_yandnowreported aspublicin the emitted ABIverify_aatorverify_taktdirectly