Manav.id
Compliance · 4 min read

A verifier small enough that someone can read all of it

A verifier small enough that someone can read all of it

If the whole argument is that a third party can check the evidence themselves, the tool they use to check it should be something they can read in an afternoon.

Why should a verifier have no dependencies?

Because its dependency tree is part of what a relying party must trust. If the argument is that a third party can check the evidence themselves, the tool they use has to be something they can read in an afternoon — and a verifier pulling hundreds of transitive packages is not.

Key takeaways
  • A verifier's dependency tree is part of its trust base, and a large one cannot be reviewed by the parties who need to trust it.
  • The work is small: canonicalise, hash, verify a signature, check a few fields. Platform primitives cover the cryptography.
  • Constraining the format to one algorithm and one canonicalisation is what keeps the verifier small.

Why dependency count is a security property here

Large dependency treeHundreds of packagesSupply chain is the trust baseUncertain to build in ten yearsImpractical to reimplementMinimal verifierA few hundred linesPlatform crypto onlyFew moving partsReimplementable from the specvs

For most software, dependencies are an engineering trade-off. For a verifier they are part of what a relying party must trust.

QuestionLarge dependency treeMinimal verifier
Can an auditor review it?No — hundreds of packagesYes — a few hundred lines
What is the supply chain exposure?Every transitive packageThe platform's own crypto
Will it build in ten years?UncertainLikely — few moving parts
Can a sceptical party reimplement it?ImpracticalYes, from the specification

The last row is the strongest argument. A format whose verifier can be reimplemented independently is one where a dispute can be settled by two parties writing their own checks and comparing.

What the verifier does

The whole task, enumerated. It is shorter than people expect.

  1. Parse the receipt as JSON.
  2. Canonicalise the statement per RFC 8785 and hash it.
  3. Compare the hash to the challenge in the authenticator data.
  4. Verify the authenticator's signature over the authenticator data and client data hash.
  5. Check the origin and relying party identifier against expected values.
  6. Check the user verification flag.
  7. Verify the issuer countersignature over the whole receipt.
  8. Check the key id against the published key set and the signing window.
  9. Check expiry and revocation.
  10. Return a structured result with a specific reason for any failure.

Canonicalisation is the largest piece — number formatting and string escaping have edge cases. Everything else is short.

Using platform primitives

// Browser and Node: built in, no dependency
const ok = await crypto.subtle.verify(
  "Ed25519", publicKey, signature, message
);

// Python: standard library plus one well-audited primitive
// Go:     crypto/ed25519 in the standard library
// Rust:   a single audited crate

// The cryptography is not where the dependencies come from.
// Convenience layers are.

This is the practical insight. Verifier bloat comes from JSON schema validators, date libraries, HTTP clients and logging frameworks — not from cryptography, which every platform now provides.

What constraining the format buys

Each option in a format is code in every verifier, forever.

These are restrictions that feel limiting when designing and are the reason the verifier is readable.

Error reporting matters more than it seems

A verifier that returns false is useless in an audit. The person running it needs to know which check failed and what that implies.

{
  "valid": false,
  "reason": "challenge_mismatch",
  "detail": "statement digest 9c1f...a83e does not match
             challenge 2b7d...4056 in authenticator data",
  "implication": "the signed statement differs from the one
                  presented"
}

An expired receipt, a revoked key and a statement mismatch mean entirely different things. Collapsing them into one boolean discards exactly the information an investigation needs.

What to publish alongside it

  1. The source, under a licence permitting inspection and reimplementation.
  2. Test vectors, including negative cases — tampered statements, wrong keys, expired receipts.
  3. A written specification sufficient to reimplement without reading the code.
  4. A statement of what verification establishes and, explicitly, what it does not.

The fourth is the one that tends to be omitted and is the most important. A verifier confirms a signature and a binding; it does not confirm that the signer understood what they signed, and saying so plainly is what makes the rest credible.

The whole task, enumerated

Ten steps, and where the work actually is
StepEffort
Parse the receiptTrivial
Canonicalise and hash the statementThe largest piece
Compare to the challengeTrivial
Verify the authenticator signaturePlatform primitive
Check origin and RP IDTrivial
Check user verificationTrivial
Verify the issuer countersignaturePlatform primitive
Check key id and signing windowTrivial
Check expiry and revocationTrivial
Return a structured resultMatters more than it looks

Verifier bloat comes from JSON schema validators, date libraries, HTTP clients and logging frameworks — not from cryptography, which every modern platform provides natively.

Objections and honest limits

“A boolean result is simpler.” And useless in an audit. An expired receipt, a revoked key and a statement mismatch mean entirely different things, and collapsing them discards exactly what an investigation needs.

“Constraining the format limits future needs.” Every option is code in every verifier, forever. One algorithm and one canonicalisation is what keeps it readable, and readability is the whole point.

What to publish with a verifier

  1. The source, under a licence permitting reimplementation. Inspection is not enough.
  2. Test vectors including negative cases. Tampered statements, wrong keys, expired receipts.
  3. A specification sufficient to reimplement without the code. So a sceptic can write their own.
  4. A statement of what verification does not establish. The item most often omitted, and the most important.

Terms used here

Transitive dependency
A package your dependency depends on — part of your trust base whether or not you chose it.
Test vector
A known input and expected output used to validate an independent implementation.
Negative test
A case that must fail, which is what distinguishes a correct verifier from a permissive one.

Frequently asked questions

Why do dependencies matter for a verifier specifically? They are part of what a relying party must trust. If an auditor cannot review the tool, they are trusting a supply chain instead of checking evidence.

Where does verifier bloat actually come from? Schema validators, date libraries, HTTP clients and logging — not cryptography, which every modern platform provides natively.

Why constrain the format so tightly? Every option is code in every verifier forever. One algorithm and one canonicalisation is what keeps it readable.

Why does error detail matter? An expired receipt, a revoked key and a statement mismatch mean different things. A boolean discards what an investigation needs.

Where does verifier bloat come from? Schema validators, date libraries, HTTP clients and logging — not cryptography, which platforms provide natively.

What should be published alongside the code? Test vectors with negative cases, a reimplementable specification, and an explicit statement of what verification does not establish.

Where this fits in Manav

Manav's receipt is deliberately small: RFC 8785 canonicalisation, one signature algorithm, keys published at a well-known URL, and a verifier short enough to audit or reimplement.

Read the architecture →

Sources and further reading