Trustvice est en bêta restreinte. À l’inscription ou à la connexion, vous devez saisir le mot de passe de bêta-testeur que vous avez reçu ou en demander un à preview@trustvice.com. Merci.
Au cœur du format de reçu : JSON canonique, Ed25519 et clés publiées
Une visite champ par champ de l’enveloppe de reçu Trustvice, de pourquoi nous signons une forme canonique RFC 8785 plutôt que les octets transmis, de la façon dont les id de clé dérivent des clés elles-mêmes, et de ce que promet le jeu de clés publié.
Par The Trustvice team··Lecture 6 min
Un reçu n’a qu’une mission : être contrôlé, par n’importe qui, dans des années, avec du code que nous n’avons pas écrit. Cette exigence a guidé chaque décision du format. Cet article parcourt l’enveloppe, la canonicalisation, la signature et le jeu de clés, et explique les choix derrière chacun.
Les reçus de ce format partagent une enveloppe ; seul le body change d’un type de reçu à l’autre. Les preuves signées comme un Proof-of-Erasure utilisent une enveloppe sœur, avec subject et claims à la place de body, sous la même canonicalisation, la même signature et le même jeu de clés décrits ci-dessous.
Déclarez les règles. Faites vérifier chaque exécution.
Agents, preuves, gouvernance, commerce et divulgation sont dans Trustvice 360. Inscrivez-vous gratuitement et ouvrez toutes les suites depuis un seul tableau de bord.
Toujours "1". La version du format de reçu, pas une version d’API.
receiptId
Un ULID de 26 caractères, triable par date de création.
issuedAt
Instant ISO 8601 en UTC avec millisecondes, fixé par Trustvice à la signature.
type
Le type de reçu. body.type lui est toujours égal.
signatureKeyId
Le kid de la clé de signature dans le jeu publié : 22 caractères de base64url.
body
Les faits propres au type.
signature
La signature Ed25519, en base64url sans remplissage : 86 caractères.
Deux propriétés découlent de la construction de la signature, et toutes deux comptent pour qui écrit un vérifieur.
Chaque membre sauf signature est signé. Cela inclut tout membre qu’ajoutera une version future. Un vérifieur ne doit pas écarter les membres qu’il ne reconnaît pas avant le contrôle, sinon des reçus valides échoueront. Les nouveaux champs facultatifs s’ajoutent au sein de la version 1 : les lecteurs doivent ignorer ce qu’ils ne connaissent pas, mais le garder pour vérifier.
Le reçu ne nomme ni votre workspace ni votre compte. Le workspace est le périmètre sous lequel vous listez et récupérez les reçus, mais il n’est pas écrit dans l’enveloppe signée. Un reçu est un énoncé autonome sur un fait, pas la preuve de qui l’a demandé.
Un id de reçu est un ULID : 26 caractères en base32 de Crockford. Les dix premiers forment un horodatage de 48 bits en millisecondes, les seize autres sont aléatoires. Les id se trient donc par date de création.
L’heure contenue dans l’id est générée à quelques millisecondes d’issuedAt. Traitez issuedAt comme l’heure signée et l’id comme un identifiant.
Une signature couvre des octets, pas un sens. JSON offre bien des écritures d’une même valeur : clés dans n’importe quel ordre, espaces facultatifs, 4 ou 4.0, "é" ou "\u00e9". Si nous signions les octets envoyés tels quels, un proxy qui réindente la réponse, ou une bibliothèque qui réordonne les clés à l’enregistrement, casserait un reçu parfaitement valide.
La signature couvre donc une forme canonique, définie par la RFC 8785, le JSON Canonicalization Scheme (JCS). Toute implémentation correcte transforme une même valeur parsée en mêmes octets. Pour un reçu, les règles se résument à cinq :
Les clés sont triées à chaque niveau d’imbrication, par unité de code UTF-16.
Aucun espace entre les jetons.
Les nombres suivent la conversion nombre-vers-chaîne d’ECMAScript : les entiers s’écrivent sans point décimal ni exposant. Les corps de reçu ne contiennent que des entiers.
Les chaînes utilisent l’échappement JSON minimal ; les caractères non ASCII sont émis en UTF-8 brut, jamais en \uXXXX, et la barre oblique n’est pas échappée.
Les tableaux gardent leur ordre, et les valeurs null et false sont conservées, pas écartées.
Le message signé est la forme JCS de l’enveloppe sans le membre signature, encodée en UTF-8. Côté signature, les valeurs qui ne peuvent être représentées partout à l’identique, comme NaN, les infinis, undefined ou bigint, sont rejetées plutôt que converties.
La conséquence pratique pour vous : vous pouvez mettre en forme un reçu, le stocker dans une base qui réordonne les clés, ou le passer par tout outil JSON qui préserve les valeurs, et il se vérifie toujours. Ce que vous ne pouvez pas faire : changer une valeur, retirer un membre null ou arrondir un nombre. Ce sont justement les changements qu’un vérifieur doit attraper.
La signature est du pur Ed25519 tel que spécifié par la RFC 8032 : sans pré-hachage ni chaîne de contexte. Nous l’avons choisi parce qu’il est compact, rapide, déterministe, et implémenté dans les bibliothèques standard que chacun possède déjà. Node 20 et plus, les navigateurs actuels, Deno, Bun et Cloudflare Workers le vérifient avec WebCrypto seul ; Python avec le paquet cryptography ; OpenSSL 3 depuis le shell. Un vérifieur de reçu Trustvice ne devrait jamais avoir besoin d’une bibliothèque Trustvice.
Une signature fait 64 octets, soit les 86 caractères de base64url de l’enveloppe. Une clé publique fait 32 octets.
Trustvice conserve les clés privées dans des modules matériels de sécurité d’AWS KMS, une clé par région de données, qu’elles ne quittent jamais. Le signataire envoie les octets canoniques à KMS et reçoit une signature en retour ; il peut signer mais pas exporter une clé.
Le signatureKeyId d’un reçu correspond au kid d’une clé du jeu publié. Ce kid n’est pas un libellé choisi par nous. Il est calculé à partir de la clé :
text
kid = base64url( first 16 bytes of SHA-256( the 32 raw bytes of x ) )
sans remplissage, ce qui donne 22 caractères. Deux conséquences utiles en découlent.
D’abord, une entrée du jeu de clés est auto-vérifiable : chacun peut recalculer le kid à partir de x, et une entrée où ils ne concordent pas n’est pas valide. En JavaScript :
Ensuite, comme le signatureKeyId se trouve dans les octets signés, un reçu ne peut pas être réorienté vers une autre clé sans être invalidé. Et comme le kid dérive de x, un x modifié pour un kid que vous détenez déjà ne peut pas être légitime.
Les clés publiques forment un JSON Web Key Set sur https://trustvice.com/.well-known/receipt-keys.json, qui redirige en HTTP 308 vers https://trustvice.com/api/v1/receipts/keys. Il est servi en JSON, mis en cache une heure, et avec Access-Control-Allow-Origin: * : une page sur votre propre domaine peut donc le récupérer et vérifier des reçus dans le navigateur.
Pour vérifier, il ne faut que kid, kty, crv et x. L’ensemble peut contenir plusieurs clés, et les clés retirées restent publiées. Les reçus d’un même workspace peuvent porter différents id de clé au fil du temps : cherchez donc toujours la clé reçu par reçu.
Le jeu est construit à partir d’une configuration publiée : la vérification ne dépend jamais d’un appel à AWS. Seuls des champs publics y figurent.
Par défaut, un vérifieur se fie à ce que renvoie l’endpoint de clés au moment où il le demande, ce qui ne vaut pas plus que cette connexion TLS. Pour une preuve qui compte, épinglez le jeu : téléchargez-le une fois depuis un réseau de confiance, recalculez chaque kid à partir de son x, stockez les paires là où rien ne change sans revue, et vérifiez contre ce fichier. Actualisez-le selon un calendrier et examinez les nouvelles clés avant de les ajouter.
Stockez le jeu de clés utilisé à côté de tout reçu gardé comme preuve. Un reçu, avec le jeu de clés qui l’a vérifié, se vérifie seul.
Un reçu nomme la clé qui l’a signé : ajouter une clé au jeu n’affecte donc jamais les anciens reçus. Il n’y a aujourd’hui ni rotation automatique ni expiration de clé. La rotation est une cérémonie manuelle, hors bande, et le jeu ne porte aucun marqueur de clé retirée ou révoquée. La rotation est additive : une clé retirée reste publiée pour que les reçus qu’elle a signés restent vérifiables. C’est une politique, pas une contrainte appliquée par l’endpoint de clés, raison de plus pour garder votre propre copie épinglée.
Il n’y a pas non plus de liste de révocation. Si une clé de signature était un jour compromise, le remède serait de cesser de la publier, ce qui ferait échouer chaque reçu signé par elle avec unknown_key, vrais reçus compris. Garder votre propre copie du jeu de clés datant de l’acceptation d’un reçu, et consigner le reçu dans un système que vous maîtrisez, préserve dans tous les cas l’usage de vos preuves.