Private preview: this is rolling out and may not be available on your account yet.
- The chain. Every larger mandate it names was signed, unaltered, and the one at the top was signed by the person.
- The key. Whoever presents the token holds the key it is bound to.
sumvin-verify Python package, or implement them yourself from this page.
What a narrowed mandate’s token carries
A narrowed mandate’s token carries every claim the offline check reads, plus two more. A mandate the person signed directly carries neither.
The digests make the chain checkable without trusting whoever serves it. The token is signed by Sumvin, so a signed request that hashes to the digest it names is the one the mandate was narrowed from.
What you need
The
x-sumvin-mandate value is the unpadded base64url encoding of this JSON, with keys sorted and no whitespace:
payload is the narrowed mandate’s own signed request, as EIP-712 typed data. Pass the header value on unchanged: Sumvin compares the signed request and the signature with the ones it stored, exactly, and re-encoding the typed data can change it.
Fetch the larger mandates
$JTI is the token’s jti claim. No API key is needed: the route answers only a request that presents the signed request stored for that token. A mismatched pair returns the same 404 as a token that doesn’t exist, and a header that can’t be decoded returns 400. The route is rate limited per IP address and per token; past the limit it returns 429 with Retry-After. Answers are never cached.
Don’t trust any of this because Sumvin served it. Every check below recomputes it from the token and the signatures.
The checks, in order
A failure at any step is a refusal.1
Check the token
Run the offline check:
ES256 against Sumvin’s key set, the issuer https://sis.sumvin.com, your audience, and expiry. Then require cnf and parent. A token without them is not a narrowed mandate.2
Check the narrowed mandate's own signed request
Decode
x-sumvin-mandate. Its message.resources must include parent.uri, and its message.wallet must equal the token’s wallet. Its signature must verify either as an agent signature against the token’s cnf, or as the person’s signature.3
Match the larger mandates to the token
Fetch the ancestors. There must be exactly one for each level of
parent, and each one’s pint_uri must equal that level’s uri.4
Recompute every digest
Compute the EIP-712 signing hash of each ancestor’s
typed_data: keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message)), using the typed data’s own types and domain. It must equal that level’s digest. A mismatch means the signed request was altered, or is not the one the mandate was narrowed from.5
Check that each mandate names the one above it
Every ancestor except the last must include the next ancestor’s
pint_uri in its message.resources.6
Check every signature
An ancestor with
signed_by: agent must verify as an agent signature against the cnf the token gives for that level. If the token gives that level no cnf, refuse. The last ancestor must be signed_by: user and verify as the person’s signature. A chain whose top was signed only by an agent has no signature from the person, so refuse it.The person’s signature
The person signs with a secp256k1 key. The signature is 65 bytes,r ‖ s ‖ v, with v either 27 or 28. Refuse any other v.
- Recover the signing address from the digest.
- If it equals
message.wallet, the person’s own key signed. Accept. - Otherwise the wallet must be a Safe the person’s key owns. Require
domain.verifyingContractto equalmessage.wallet. Then callgetOwners()(selector0xa0e67e2b) on that address, witheth_callon the chaindomain.chainIdnames. Accept only if the recovered address is one of the owners.
Agent signatures
An agent key signs the way a passkey does, so the same signature can be checked on chain. Its signature is 65 bytes,r ‖ s ‖ 0x00. The final byte is a fixed placeholder, not a recovery id. The rest of what was signed is fixed, so you rebuild it from the digest:
client_data_json is exactly those bytes, with no whitespace. 0x05 means the user was present and verified, and the counter is always zero. Verify ECDSA P-256 with SHA-256 over the signed message, using r and s against the cnf key’s x and y. s is always in the lower half of the curve order, so a verifier that insists on low-s accepts it.
Check the agent holds the key
The token alone doesn’t prove the presenter is the agent it was issued to. Ask for a proof made with the bound key, for you, now:- Issue a nonce for this request: a random, single-use value you generate, at least 128 bits, for example 32 hex characters. Hand it to the agent and remember it until the request completes.
- The agent signs a proof over that nonce, your audience and the mandate’s
pint_uri, and returns it with the request. - Check the proof offline against the token’s
cnf.jwk, then forget the nonce so the same proof can’t be presented twice.
The AgentProof signed request
Sumvin’s agent keys sign proofs the same way they sign mandates, so the second kind is EIP-712 typed data with exactly this domain and struct:
chainId and no verifyingContract: a proof is checked offline by you, never on chain, and pintUri already ties it to one mandate. issuedAt may arrive as a JSON integer or a decimal string.
primaryType to be AgentProof and the domain to be exactly the one above. Compute the EIP-712 signing hash over that domain and struct, with the message’s four fields. Verify the signature over it as an agent signature against the token’s cnf.jwk, then check the four bindings.
In Python
verify_delegation_chain runs every check on this page up to the agent’s proof, and returns the verified chain. verify_agent_proof checks the proof: nonce is required, and max_age (default 300 seconds) and leeway (default 30 seconds) set the freshness window. rpc takes a JSON-RPC URL, or a function eth_call(chain_id, to, data) that returns the call’s hex result. jwks takes the key set URL, or a jwt.PyJWKClient.
Nothing returns False: every failure raises a subclass of DelegationVerificationError. Refuse on any of them. The subclass tells you which check failed:
Next
Checks and errors
The offline check, the website token check and their errors.
Limits and narrowing
What a narrowed mandate may and may not permit.