By Olivier Meunier — independent developer building Tessaliq, an EUDI Wallet verifier focused on age verification under the French ARCOM framework.
TL;DR
When an ARCOM-obliged editor asks Tessaliq to verify a user’s age, Tessaliq returns a signed JWT receipt. That receipt is not a booking reference — it is an audit artifact. A CNIL inspector, an internal auditor, a Relying Party’s counsel, or a curious developer can take that receipt and verify it cryptographically themselves, using only the public JWKS endpoint.
The verification reads one public HTTPS resource: https://api.tessaliq.com/.well-known/jwks.json. It’s public, cacheable, unauthenticated, and the only call the verification makes. No ticket, no coordination, no authenticated API call. Three lines of code with the jose library, or a web page where you paste the JWT and the verification runs entirely client-side in your browser.
(If you need fully air-gapped verification — no network at all — you can pre-fetch the JWKS once and pass it to the library. More on that below.)
This post covers:
- What a Tessaliq receipt looks like and what fields it carries
- How to verify it — three ways (one-liner, CLI, web)
- What the verification cryptographically proves, and what it does not
- The spec, the open-source library, and the interactive page
Nothing in the spec is load-bearing on any private Tessaliq endpoint. One design goal is that the receipt stands on its own.
What a receipt is
A Tessaliq receipt is a JWS in compact form — three base64url segments joined by dots:
eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3NhbGlxLXJlY2VpcHQtdjEiLCJ0eXAiOiJ0ZXNzYWxpcS1yZWNlaXB0K2p3dCJ9
.<base64url-encoded-payload>
.<base64url-signature>
The header always carries three fields — alg: ES256, kid: tessaliq-receipt-v1, and typ: tessaliq-receipt+jwt. A verifier rejects any token where these values drift.
The payload carries standard JWT claims (iss, iat, jti) plus Tessaliq-specific claims that describe the verification:
{
"iss": "https://api.tessaliq.com",
"iat": 1713607335,
"jti": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "7f3d2e1a-8b4c-4d5e-9f6a-1b2c3d4e5f6a",
"verification": {
"policy": "av_age_18_plus",
"policy_version": 1,
"result": true,
"state": "verified",
"created_at": "2026-04-20T12:00:00.000Z",
"completed_at": "2026-04-20T12:00:02.145Z",
"assurance_level": "unknown"
},
"proof": null,
"dpv": { "@type": "dpv:PersonalDataHandling", "dpv:hasPurpose": "https://w3id.org/dpv#AgeVerification", "…": "…" }
}
The verification object is the audit-facing core: which policy was applied, what result, when it happened, what level of assurance the underlying credential had (when available — see limitations). The dpv object embeds a W3C Data Privacy Vocabulary declaration, signed along with the rest — a separate post covers why.
No personal identifier of the wallet user is present. That is by design: under ARCOM’s double-anonymity requirement, Tessaliq never learns who the user is, so it cannot put that information in the receipt.
How to verify — one-liner
Using the jose library (Node.js, Deno, Bun):
import { jwtVerify, createRemoteJWKSet } from 'jose';
const jwks = createRemoteJWKSet(
new URL('https://api.tessaliq.com/.well-known/jwks.json')
);
const { payload, protectedHeader } = await jwtVerify(jwt, jwks, {
issuer: 'https://api.tessaliq.com',
algorithms: ['ES256'],
typ: 'tessaliq-receipt+jwt',
});
That’s it. If the signature is wrong, the algorithm is tampered with, or the issuer does not match, jwtVerify throws. If it returns, you have an authenticated payload.
For something more packaged, there’s @tessaliq/receipt-verifier — an MIT-licensed library that wraps the call with typed return values and a CLI:
tessaliq-receipt-verify ./receipt.jwt
# ✓ Receipt is valid
# { "iss": "https://api.tessaliq.com", ... }
The library also accepts a pre-fetched JWKS object via its jwks option, for fully offline / air-gapped verification: grab the public key once, put it next to your tooling, and never hit the network on the verification path again.
How to verify — in a browser
If you prefer a UI, there’s an interactive page on this site. Paste a JWT or drop a file; the page runs the verification entirely in your browser — the receipt contents never leave your machine, the only outbound request is the JWKS fetch. The page detects whether you’re on production or staging and uses the matching JWKS endpoint. It shows both a structured breakdown (policy, result, timestamps, session id) and the full decoded claims for anyone who wants to dig in.
The page is small enough that a curious developer can read its source and see exactly what runs. No hidden API call.
What the verification cryptographically proves
Three statements hold, provably, to any third party who runs the check:
- The JWT was signed by the private key whose public counterpart is published at the JWKS endpoint under the key id
tessaliq-receipt-v1. - Every claim in the payload — policy, result, session id, timestamps, assurance level, DPV declaration — is exactly what Tessaliq signed. No one has modified a byte since.
- The token type is
tessaliq-receipt+jwtand the algorithm isES256(ECDSA over P-256). Any other combination is rejected.
These are guarantees tied to the cryptography, not to Tessaliq’s word. If the private key were compromised, Tessaliq would rotate the kid and publish a security note identifying the affected period — and any receipt from that period would need re-evaluation.
What the verification does not prove
I want to be specific here because it matters for auditors.
The receipt does not prove that the verification session exists in the Tessaliq database. There is no public lookup endpoint in v1. In theory, a compromised Tessaliq instance could mint a receipt that looks valid cryptographically but does not correspond to any real session. In practice, the surrounding signals around the verifier (OIDF conformance plans, open-core source code, observability posture, published security notices if any) give context to judge whether a forged-looking receipt is plausible — but the cryptographic check on the receipt alone does not rule that out.
The receipt does not identify the wallet user. That is intentional. Under ARCOM SREN’s double-anonymity requirement, Tessaliq never learns who the user is. A receipt that carried a user identifier would be out of spec.
The receipt does not prove that Tessaliq’s internal policy engine applied the declared policy correctly. That is a separate kind of trust, established by:
- The four public OIDF conformance plans (24/24 modules passed, immutable, inspectable by anyone on
demo.certification.openid.net). - The open-source parts of the stack — the SDK, the Noir circuits, the SD-JWT parser — published under MIT on
Tessaliq/tessaliq-open. - The ENISA position paper submitted on 2026-04-17.
The receipt is the per-verification attestation. The OIDF plans are the per-code-version attestation. Together, a third party can reconstruct the audit trail without having to trust Tessaliq on its own word.
Why this matters now
Most verifier-as-a-service products hand back a boolean yes/no over an API call and store a log line on their side. When a regulator asks for proof of verification, the regulated party has to go back to the verifier and request a log excerpt — which means trusting that the verifier kept honest logs.
A cryptographically signed receipt shifts that trust assumption. The Relying Party can keep the receipt. The auditor can verify it themselves using only the published public key. Since the receipt is signed, its claims cannot be altered after the fact without invalidating the signature.
This is not a Tessaliq-only pattern — signed compliance receipts show up in several regulated-verification stacks. But from what I’ve been able to inspect publicly on the EUDI Wallet verifier products currently in the landscape, it is notably absent. Part of the goal of publishing the spec and the MIT library is to put the pattern somewhere everyone can copy, critique, or ignore.
Current limitations — honestly
The spec is v1-draft, not frozen. The v1.0 tag is conditioned on running a complete end-to-end flow against a real EUDI wallet (France Identité via the Playground, or the EU AV app pilot when the public blueprint is integrated), not on a feedback window. Running through a mock-signed receipt in a unit test is not the same as reading a field in a real credential presentation and signing a receipt for it.
The assurance_level field defaults to unknown today. Wallets are not consistently propagating the eIDAS LoA of the PID through the OID4VP flow yet. When they do, the field will carry the real value. Until then, auditors should not read unknown as equivalent to low — it means the verifier did not get the information, not that the LoA was low.
Revocation is not implemented in v1. Receipts are permanent by default. If a receipt needs to be invalidated — because the underlying session is disputed, for example — the mechanism for that is still an open design question.
The library does not yet have a published npm release — an intentional policy choice tied to the project’s incorporation timeline. Consumers install via a git dependency or a local link for now. The code is stable in the sense that the tests pass and the format is documented, but consider it v0.1.0-draft until the v1.0 tag.
Try it
- Verify a receipt in your browser
- Read the spec
- Install the library
- Open an issue if a third-party implementation runs into a rough edge
If you audit, regulate, or integrate verifier products for a living, targeted feedback on the spec is genuinely useful — open an issue on Tessaliq/tessaliq-open, or reach out. The v1.0 tag itself is gated on a real end-to-end flow, not on a feedback window, but the earlier sharp eyes land the better.