Manav.id
Spec

Receipt v2 reference

Receipt members, signed bytes, lifecycle and the verifier algorithm.

Receipt

Last updated 24 September 2026

A receipt is the issuer's countersigned record of a seen-and-signed ceremony. Two versions exist. The canonical form and the signature scheme are chosen by the receipt's v field, never by configuration.

Version 2 (current)

{
  "v": 2,
  "issuer": "manav.id",
  "kid": "k_2026_09",                       // one kid names a hybrid key pair (Ed25519 + ML-DSA-87)
  "principal": "manav:human:7f3a9c1d4b8ee21b",
  "action": "payment.release",
  "stmt_hash": "sha256:…",                  // SHA-256 of JCS(statement without rp_sig) — also the WebAuthn challenge
  "render": [["Action","RELEASE PAYMENT"], ["Amount","$42,000.00 USD"], …],   // exactly what the human was shown
  "rp": { "id": "ap.acme.com", "kid": "acme-2026-09" },
  "presence": { "tier": "P2", "aaguid": "…", "uv": true, "dwell_ms": 1840, "rendered_by": "issuer" },
  "assertion_hash": "sha256:…",             // SHA-256(authenticatorData || clientDataJSON || signature) of the stored assertion
  "nonce": "Q1fTk2p9Yw3bC7dE",
  "issuedAt": "2026-09-05T14:23:51Z",
  "sig": { "ed25519": "<b64url 64 B>", "mldsa87": "<b64url 4,627 B>" }
}

Signed bytes: "manav-receipt/2" || 0x0A || JCS(receipt without sig) (see Canonical reference). Both signatures MUST verify; pq_signature_missing and pq_signature_invalid are distinct reject codes from bad_signature.

presence.tier is set from what actually ran: P1 no user verification, P2 platform authenticator with UV, P3 a roaming authenticator recognised in the FIDO Metadata Service (core/data/fido-mds-aaguids.json) with UV, P4 reserved for trusted-display devices (not issued in v1). Unknown AAGUIDs are P2, never higher.

Unknown fields are rejected. All times are RFC 3339 UTC with Z, no fractional seconds. Numbers are integers only.

Version 1 (frozen)

{v:1, issuer, kid, principal, action, payloadHash, assurance, nonce, issuedAt, sig} signed with Ed25519 alone over the legacy canonical form (core/canonical.php). Nothing new is issued as v1; the format exists so that anything signed before the migration keeps verifying (test group 17).

Publication and lifecycle

Verifier algorithm (reference: core/Verifier.php; ports: js/manav-crypto.js, packages/verify-*)

  1. Resolve kid in the cached JWKS → unknown_kid.
  2. Verify the revocation documents' own signatures; check kid and nonce → revocation_list_unverifiable, kid_revoked, nonce_revoked.
  3. Canonicalise without sig (form by version); verify Ed25519, and for v2 also ML-DSA-87 → bad_signature, pq_signature_missing, pq_signature_invalid.
  4. If the statement is supplied: recompute stmt_hash; compare render byte for byte → stmt_hash_mismatch, render_mismatch.
  5. Policy: tier_too_low, stale.

Returns { ok, reason, principal, action, render, presence, issuedAt, kid } — identical across implementations (tests/vectors/receipts).

← All specification documents