Vérifier un receipt Tessaliq soi-même, sans coordination avec nous

Par Olivier Meunier — développeur indépendant, je construis Tessaliq, un vérificateur de portefeuille EUDI focalisé sur la vérification d’âge dans le cadre ARCOM français.


TL;DR

Quand un éditeur soumis à ARCOM demande à Tessaliq de vérifier l’âge d’un utilisateur, Tessaliq renvoie un receipt JWT signé. Ce receipt n’est pas un numéro de réservation — c’est un artefact d’audit. Un inspecteur CNIL, un auditeur interne, le service juridique d’un RP, ou un développeur curieux peut prendre ce receipt et le vérifier cryptographiquement lui-même, en utilisant uniquement le endpoint JWKS public.

La vérification lit une seule ressource HTTPS publique : https://api.tessaliq.com/.well-known/jwks.json. Elle est publique, cacheable, non authentifiée, et c’est le seul appel que fait la vérification. Pas de ticket, pas de coordination, pas d’appel API authentifié. Trois lignes de code avec la librairie jose, ou une page web où vous collez le JWT et la vérification s’exécute entièrement côté client dans votre navigateur.

(Si vous avez besoin d’une vérification totalement air-gapped — sans réseau du tout — vous pouvez pré-télécharger le JWKS une fois et le passer à la librairie. Détails plus bas.)

Cet article couvre :

  • À quoi ressemble un receipt Tessaliq et quels champs il porte
  • Comment le vérifier — trois voies (one-liner, CLI, web)
  • Ce que la vérification prouve cryptographiquement, et ce qu’elle ne prouve pas
  • La spec, la librairie open-source, et la page interactive

Rien dans la spec ne dépend d’un endpoint privé Tessaliq. Un des objectifs de conception est que le receipt se suffise à lui-même.

À quoi ressemble un receipt

Un receipt Tessaliq est un JWS en forme compacte — trois segments base64url séparés par des points :

eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3NhbGlxLXJlY2VpcHQtdjEiLCJ0eXAiOiJ0ZXNzYWxpcS1yZWNlaXB0K2p3dCJ9
.<payload-base64url>
.<signature-base64url>

Le header porte toujours trois champs — alg: ES256, kid: tessaliq-receipt-v1, et typ: tessaliq-receipt+jwt. Un vérifieur rejette tout token où ces valeurs divergent.

Le payload porte les claims JWT standard (iss, iat, jti) plus des claims spécifiques Tessaliq qui décrivent la vérification :

{
  "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", "…": "…" }
}

L’objet verification est le cœur audit-facing : quelle policy a été appliquée, quel résultat, quand c’est arrivé, quel niveau d’assurance avait le credential sous-jacent (quand disponible — voir limitations). L’objet dpv embarque une déclaration W3C Data Privacy Vocabulary, signée avec le reste — un autre article couvre pourquoi.

Aucun identifiant personnel du wallet user n’est présent. C’est par design : sous l’exigence de double anonymat ARCOM, Tessaliq n’apprend jamais qui est l’utilisateur, donc il ne peut pas mettre cette information dans le receipt.

Comment vérifier — one-liner

Avec la librairie jose (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',
});

C’est tout. Si la signature est fausse, si l’algorithme est altéré, ou si l’émetteur ne correspond pas, jwtVerify lève une exception. Si ça retourne, vous avez un payload authentifié.

Pour quelque chose de plus packagé, il y a @tessaliq/receipt-verifier — une librairie MIT qui enveloppe l’appel avec des valeurs de retour typées et une CLI :

tessaliq-receipt-verify ./receipt.jwt
# ✓ Receipt is valid
# { "iss": "https://api.tessaliq.com", ... }

La librairie accepte aussi un JWKS pré-téléchargé via l’option jwks, pour une vérification totalement air-gapped : récupérez la clé publique une fois, placez-la à côté de votre outillage, et ne touchez plus jamais le réseau sur le chemin de vérification.

Comment vérifier — dans un navigateur

Si vous préférez une UI, il y a une page interactive sur ce site. Collez un JWT ou déposez un fichier ; la page fait la vérification entièrement dans votre navigateur — le contenu du receipt ne quitte pas votre machine, la seule requête sortante est le fetch du JWKS. La page détecte si vous êtes en production ou en staging et utilise le endpoint JWKS correspondant. Elle montre à la fois un découpage structuré (policy, résultat, timestamps, session id) et tous les claims décodés pour qui veut creuser.

La page est assez petite pour qu’un développeur curieux puisse lire son code source et voir exactement ce qui s’exécute. Pas d’appel API caché.

Ce que la vérification prouve cryptographiquement

Trois affirmations tiennent, prouvablement, à tout tiers qui fait la vérification :

  1. Le JWT a été signé par la clé privée dont la contrepartie publique est publiée au endpoint JWKS sous le key id tessaliq-receipt-v1.
  2. Chaque claim du payload — policy, résultat, session id, timestamps, assurance level, déclaration DPV — est exactement ce que Tessaliq a signé. Personne n’a modifié un octet depuis.
  3. Le type de token est tessaliq-receipt+jwt et l’algorithme est ES256 (ECDSA sur P-256). Toute autre combinaison est rejetée.

Ces garanties sont liées à la cryptographie, pas à la parole de Tessaliq. Si la clé privée était compromise, Tessaliq ferait pivoter le kid et publierait une note de sécurité identifiant la période affectée — et chaque receipt de cette période nécessiterait une ré-évaluation.

Ce que la vérification ne prouve pas

Je veux être précis ici parce que ça compte pour les auditeurs.

Le receipt ne prouve pas que la session de vérification existe dans la base Tessaliq. Il n’y a pas de endpoint public de lookup en v1. En théorie, une instance Tessaliq compromise pourrait émettre un receipt qui paraît valide cryptographiquement mais ne correspond à aucune session réelle. En pratique, les signaux autour du vérifieur (plans de conformance OIDF, code source open-core, posture d’observabilité, notes de sécurité publiées le cas échéant) donnent le contexte pour juger si un receipt qui ressemble à une forgery est plausible — mais la vérification cryptographique du receipt seul n’exclut pas ce cas.

Le receipt n’identifie pas le wallet user. C’est intentionnel. Sous l’exigence de double anonymat ARCOM SREN, Tessaliq n’apprend jamais qui est l’utilisateur. Un receipt qui porterait un identifiant utilisateur serait hors spec.

Le receipt ne prouve pas que le moteur de policies interne de Tessaliq a appliqué correctement la policy déclarée. C’est un autre type de confiance, établi par :

Le receipt est l’attestation par vérification. Les plans OIDF sont l’attestation par version de code. Ensemble, un tiers peut reconstruire la piste d’audit sans avoir à faire confiance à Tessaliq sur parole.

Pourquoi ça compte maintenant

La plupart des produits de vérification-as-a-service renvoient un booléen oui/non sur un appel API et stockent une ligne de log de leur côté. Quand un régulateur demande une preuve de vérification, la partie régulée doit revenir vers le vérifieur demander un extrait de log — ce qui signifie faire confiance au fait que le vérifieur a gardé des logs honnêtes.

Un receipt cryptographiquement signé déplace cette hypothèse de confiance. Le RP garde le receipt. L’auditeur le vérifie lui-même en utilisant uniquement la clé publique publiée. Puisque le receipt est signé, ses claims ne peuvent pas être modifiés après coup sans invalider la signature.

Ce n’est pas un pattern propre à Tessaliq — les receipts de conformité signés apparaissent dans plusieurs stacks de vérification régulée. Mais d’après ce que j’ai pu inspecter publiquement sur les produits vérifieurs EUDI Wallet actuellement dans le paysage, c’est notablement absent. Une partie de l’intérêt de publier la spec et la librairie MIT est de poser le pattern quelque part que tout le monde peut copier, critiquer ou ignorer.

Limitations actuelles — en toute franchise

La spec est en v1-draft, pas gelée. Le tag v1.0 est conditionné à l’exécution d’un flux end-to-end complet contre un vrai wallet EUDI (France Identité via le Playground, ou l’app EU AV en pilote quand le blueprint public sera intégré), pas à une fenêtre de feedback. Faire passer un receipt signé par mock dans un test unitaire n’est pas la même chose que lire un champ dans une vraie présentation de credential et signer un receipt pour.

Le champ assurance_level vaut unknown par défaut aujourd’hui. Les wallets ne propagent pas encore de manière cohérente le LoA eIDAS du PID à travers le flow OID4VP. Quand ils le feront, le champ portera la vraie valeur. D’ici là, les auditeurs ne doivent pas lire unknown comme équivalent à low — ça signifie que le vérifieur n’a pas eu l’information, pas que le LoA était bas.

La révocation n’est pas implémentée en v1. Les receipts sont permanents par défaut. Si un receipt doit être invalidé — parce que la session sous-jacente est contestée, par exemple — le mécanisme pour ça reste une question de design ouverte.

La librairie n’a pas encore de release npm publiée — un choix de politique intentionnel lié au calendrier d’incorporation du projet. Les consommateurs installent via une dépendance git ou un lien local pour l’instant. Le code est stable au sens où les tests passent et le format est documenté, mais considérez ça comme v0.1.0-draft jusqu’au tag v1.0.

Essayez

Si vous auditez, régulez ou intégrez des produits vérifieurs pour gagner votre vie, un retour ciblé sur la spec est réellement utile — ouvrez une issue sur Tessaliq/tessaliq-open, ou écrivez. Le tag v1.0 lui-même est conditionné à un vrai flux end-to-end, pas à une fenêtre de feedback, mais plus tôt les regards affûtés atterrissent, mieux c’est.


← Tous les articles