Confiance et preuves
Ce que chaque preuve établit, où la lire, et comment un tiers refait la vérification de l'ancrage, de la copie IPFS et de la signature sans nous faire confiance.
Sur cette page
- Le principe
- Ce que chaque preuve établit
- Où lire toutes les preuves d'un coup
- Vérifier la copie IPFS
- Vérifier l'ancrage
- Étape 1 : recalculer la racine
- Étape 2 : comparer la racine à la chaîne
- Étape 3 : relier la racine au contenu
- Vérifier l'appartenance d'un article à un lot ancré
- Vérifier la signature de l'attestation
- Quand une preuve est absente
- Plafonds d'appels et erreurs
- Ce qu'il faut retenir
En quittant cette page, vous saurez quelles preuves accompagnent un produit et son passeport, ce que chacune établit exactement, ce qu'aucune n'établit, et comment un tiers refait la vérification lui-même, avec ses propres outils.
Toutes les adresses citées ici sont publiques et ne demandent aucune clef
d'API. Chacune existe sous deux formes, avec le préfixe /v1 et sans préfixe.
Les exemples utilisent /v1, c'est la forme à préférer.
#Le principe
Une preuve n'a de valeur que si elle se refait sans son émetteur. Deux des éléments décrits ici tiennent hors de nos serveurs : la copie IPFS et l'ancrage.
La copie IPFS est adressée par son contenu : son adresse est le résultat du calcul d'empreinte sur le fichier lui-même, donc modifier le fichier change l'adresse.
L'ancrage est une inscription sur une chaîne publique, Base, chain_id 8453.
N'importe qui la lit sur un explorateur de blocs ou par un nœud.
Nous signons l'attestation avec une clef dont nous ne publions que la partie publique. Vous vérifiez la signature contre cette clef publique. Par défaut, nous publions cette clef sur notre API. Une marque qui pose son identifiant d'émetteur sur son propre domaine sort aussi cette étape de nos serveurs.
Le reste, l'empreinte SHA-256 du contenu et le sceau de version, sont des contrôles de cohérence que nous calculons. Ils sont utiles, et cette page dit précisément jusqu'où ils portent.
#Ce que chaque preuve établit
| Preuve | Ce qu'elle établit | Ce qu'elle laisse ouvert |
|---|---|---|
Empreinte SHA-256 du contenu (data_hash) | le contenu enregistré correspond à l'empreinte enregistrée | les deux valeurs sont chez nous, c'est un contrôle de cohérence interne |
Sceau de version (seal) | une version publiée puis modifiée se détache de la chaîne des versions suivantes | ne donne aucune date opposable à un tiers |
Copie IPFS (ipfs_uri) | le document publié est figé : son adresse est son empreinte | ne dit rien de la date de publication |
Ancrage du document (passport_anchor) | ce contenu de passeport existait au plus tard à la transaction | ne rend pas le contenu exact |
Ancrage de l'article (anchor) | cet article appartient à un lot dont la racine est inscrite sur Base | ne dit rien du contenu du passeport |
Attestation signée (vc) | le document a bien été émis par la marque annoncée | ne dit rien de l'exactitude des données décrites |
Les deux mécanismes ont la même forme et ne se croisent nulle part.
Deux colonnes séparées, une par mécanisme. À gauche, l'ancrage d'un lot
d'articles : la feuille d'un article réunit son identifiant de jeton,
l'empreinte de son identifiant d'unité et l'empreinte de l'adresse de ses
métadonnées. À droite, l'ancrage d'une version de passeport : la feuille réunit
l'identifiant du passeport, le numéro de version et l'empreinte SHA-256 du
contenu. De chaque côté, les feuilles forment un arbre, l'arbre donne une
racine, et cette racine part dans une transaction sur Base, chain_id 8453. La
première se lit sur GET /v1/verify/merkle/{identifier} et dans le bloc
anchor, la seconde dans le bloc passport_anchor de
GET /v1/passport/{identifier}/proof. Les deux colonnes ne se rejoignent
jamais : aucune inscription faite pour l'article ne porte sur le contenu du
passeport.
La composition exacte de la feuille d'un article suit le contrat sur lequel son
lot a été frappé, et vous n'avez pas à la reconstituer : le point d'entrée
GET /v1/verify/merkle/{identifier} vous rend leaf et proof prêts à
l'emploi, et le contrat sait recalculer l'appartenance lui-même avec
verifyAnchored. La feuille d'une version de passeport, elle, est décrite champ
par champ plus bas, avec le code qui la recalcule.
#Où lire toutes les preuves d'un coup
Le point d'entrée est GET /v1/passport/{identifier}/proof. Il rassemble
l'empreinte, la copie IPFS, les deux ancrages, le sceau et l'état de
l'attestation.
Le champ identifier accepte l'empreinte d'UID (0x suivi de 64 caractères
hexadécimaux), l'identifiant de jeton (une suite de chiffres) ou le numéro de
série imprimé sur l'étiquette. Un numéro de certificat n'est pas résolu par
cette adresse et renvoie 404. Pour un numéro de certificat, passez par
GET /v1/verify/merkle/{identifier} ou par /resolve.
curl "https://api.sealtrust.io/v1/passport/000000000000/proof"const reponse = await fetch(
"https://api.sealtrust.io/v1/passport/000000000000/proof",
);
const preuves = await reponse.json();
console.log(preuves.anchor?.anchored, preuves.passport_anchor?.data_hash_matches);import requests
reponse = requests.get(
"https://api.sealtrust.io/v1/passport/000000000000/proof",
timeout=10,
)
reponse.raise_for_status()
preuves = reponse.json()
print(preuves.get("anchor", {}).get("anchored"))
print(preuves.get("passport_anchor", {}).get("data_hash_matches")){
"passport_version": 3,
"data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"ipfs_uri": "ipfs://bafyexemple0000000000000000000000000000000000000000000",
"ipfs_gateway_url": "https://ipfs.io/ipfs/bafyexemple0000000000000000000000000000000000000000000",
"anchor": {
"chain": "base",
"chain_id": 8453,
"type": "merkle_batch",
"tx_hash": "0x5555555555555555555555555555555555555555555555555555555555555555",
"basescan_url": "https://basescan.org/tx/0x5555555555555555555555555555555555555555555555555555555555555555",
"merkle_root": "0x4444444444444444444444444444444444444444444444444444444444444444",
"anchored": true,
"proves": "batch_inclusion"
},
"passport_anchor": {
"chain": "base",
"chain_id": 8453,
"tx_hash": "0x6666666666666666666666666666666666666666666666666666666666666666",
"basescan_url": "https://basescan.org/tx/0x6666666666666666666666666666666666666666666666666666666666666666",
"merkle_root": "0x7777777777777777777777777777777777777777777777777777777777777777",
"leaf": "0x1111111111111111111111111111111111111111111111111111111111111111",
"leaf_index": 0,
"proof": [
"0x2222222222222222222222222222222222222222222222222222222222222222",
"0x3333333333333333333333333333333333333333333333333333333333333333"
],
"anchored_at": "2026-08-01T10:00:00+00:00",
"data_hash_matches": true,
"proves": "content_existed_at_or_before_tx"
},
"seal": {
"sealed": true,
"sealed_at": "2026-08-01T09:00:00+00:00",
"algorithm": "st-dpp-chain-v1",
"version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"prev_version_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"linked": true,
"chain_link_match": true
},
"vc": {
"issued": true,
"vct": "https://schema.sealtrust.io/vct/digital-product-passport",
"issued_at": "2026-08-01T09:00:00+00:00"
},
"verifications": {
"count": 12,
"last_verified_at": "2026-08-12T14:32:00+00:00"
}
}Toutes les valeurs de cet exemple sont fictives.
Un passeport rattaché à un modèle a son propre résumé, à
GET /v1/passport/01/{gtin}/proof. Deux blocs y sont absents, et
cette absence est la réponse juste : anchor date un article, or un modèle n'en
est pas un, et verifications compte des vérifications d'UID, or un modèle n'a
pas d'UID. La réponse porte alors "level": "model" et le GTIN.
#Vérifier la copie IPFS
Nous déposons la copie sur IPFS au moment de la publication. Depuis le passage au dépôt de la projection publique, cette copie ne contient que les champs du niveau public. Pour les passeports plus anciens, nous ne servons le lien que si le contenu déposé correspond à cette projection.
Deux choses sont à votre portée.
Récupérer la copie. Le champ ipfs_gateway_url donne un lien direct. Le
champ ipfs_uri donne l'identifiant de contenu, que vous pouvez ouvrir par la
passerelle de votre choix ou par votre propre nœud IPFS. Ne dépendez pas de la
passerelle que nous indiquons.
Comparer dans le temps. L'identifiant de contenu est le résultat du calcul d'empreinte sur le fichier. Deux récupérations du même identifiant rendent les mêmes octets, sinon l'identifiant aurait changé.
L'identifiant vous permet de prouver plus tard que la copie que vous détenez est bien celle qui était publiée. Il ne garantit pas que le fichier sera encore servi par une passerelle. Nous n'annonçons aucune durée de conservation. Conservez l'identifiant de contenu et une copie des octets que vous avez lus.
Le point d'entrée GET /v1/passport/{identifier}/verify fait cette comparaison
pour vous et rend ipfs_match. Trois valeurs, trois sens : true la copie
correspond, false elle diffère, null elle n'a pas pu être récupérée. Une
passerelle injoignable donne null, jamais false.
curl "https://api.sealtrust.io/v1/passport/000000000000/verify"La même réponse porte db_hash_match, qui compare une donnée que nous détenons
à une empreinte que nous détenons, et le bloc seal, dont chain_link_match à
false signale qu'une version scellée a été modifiée après publication.
#Vérifier l'ancrage
L'ancrage repose sur un arbre de Merkle. Nous réduisons chaque document à dater à une empreinte, appelée feuille. Nous combinons les feuilles deux à deux jusqu'à une valeur unique, la racine. Nous inscrivons la racine seule sur la chaîne. Une preuve d'appartenance vous donne la liste des empreintes voisines qui vous permettent de remonter d'une feuille jusqu'à la racine.
Vous n'avez pas à nous faire confiance pour conclure. Les valeurs de départ viennent de notre réponse, et la comparaison finale se fait contre la chaîne, que nous ne contrôlons pas. Notez ces valeurs le jour où vous les lisez : elles suffisent ensuite à refaire la vérification sans nous rappeler.
#Étape 1 : recalculer la racine
Prenez leaf et proof dans le bloc passport_anchor. À chaque étape, les
deux valeurs sont rangées dans l'ordre croissant avant d'être concaténées et
hachées en keccak256, la fonction d'empreinte utilisée par la chaîne. Le
résultat final doit égaler merkle_root.
Les deux exemples ci-dessous demandent une bibliothèque qui calcule keccak256 :
eth-utils en Python, ethers en TypeScript.
from eth_utils import keccak
def racine_depuis_preuve(feuille: str, preuve: list[str]) -> str:
courant = bytes.fromhex(feuille[2:])
for voisin in preuve:
autre = bytes.fromhex(voisin[2:])
gauche, droite = (courant, autre) if courant < autre else (autre, courant)
courant = keccak(gauche + droite)
return "0x" + courant.hex()
print(
racine_depuis_preuve(
"0x1111111111111111111111111111111111111111111111111111111111111111",
[
"0x2222222222222222222222222222222222222222222222222222222222222222",
"0x3333333333333333333333333333333333333333333333333333333333333333",
],
)
)import { keccak256 } from "ethers";
function racineDepuisPreuve(feuille: string, preuve: string[]): string {
let courant = feuille.toLowerCase();
for (const voisin of preuve) {
const autre = voisin.toLowerCase();
courant =
courant < autre
? keccak256("0x" + courant.slice(2) + autre.slice(2))
: keccak256("0x" + autre.slice(2) + courant.slice(2));
}
return courant;
}
console.log(
racineDepuisPreuve(
"0x1111111111111111111111111111111111111111111111111111111111111111",
[
"0x2222222222222222222222222222222222222222222222222222222222222222",
"0x3333333333333333333333333333333333333333333333333333333333333333",
],
),
);#Étape 2 : comparer la racine à la chaîne
Ouvrez basescan_url. La transaction émet un événement RootAnchored, qui
porte quatre valeurs : l'identifiant de lot, la racine, le nombre de feuilles et
une empreinte de métadonnées. Comparez la racine de l'événement à celle que vous
venez de recalculer.
Si les deux correspondent, la racine que nous vous avons servie est bien celle inscrite sur Base, à la date du bloc qui contient la transaction. Cette date est le seul élément que la chaîne ajoute, et c'est celui que ni l'empreinte ni la copie IPFS ne peuvent donner.
#Étape 3 : relier la racine au contenu
La feuille d'une version de passeport est l'empreinte keccak256 de trois
valeurs encodées au format ABI, la mise en forme binaire attendue par un contrat
sur la chaîne : passport_id en uint256, passport_version en uint256, et
data_hash en bytes32.
data_hash est rendu sans préfixe 0x : lisez-le comme 32 octets hexadécimaux.
from eth_abi import encode
from eth_utils import keccak
passport_id = 7
passport_version = 3
data_hash = "0000000000000000000000000000000000000000000000000000000000000000"
feuille = "0x" + keccak(
encode(
["uint256", "uint256", "bytes32"],
[passport_id, passport_version, bytes.fromhex(data_hash)],
)
).hex()
print(feuille)import { AbiCoder, keccak256 } from "ethers";
const passportId = 7;
const passportVersion = 3;
const dataHash =
"0000000000000000000000000000000000000000000000000000000000000000";
const feuille = keccak256(
AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256", "bytes32"],
[passportId, passportVersion, "0x" + dataHash],
),
);
console.log(feuille);Les valeurs de cet exemple sont fictives. Comparez la feuille que vous obtenez
au champ leaf du bloc passport_anchor.
passport_version figure dans le résumé des preuves. data_hash aussi.
passport_id est le champ id de la réponse de
GET /v1/passport/{identifier}, ou de GET /v1/passport/01/{gtin} pour un
passeport de modèle. Ces deux réponses sont publiques et existent dès qu'une
version est publiée. Le numéro de version fait partie de la feuille pour qu'une
preuve désigne une version précise, même si deux versions portaient des données
identiques.
Le champ data_hash_matches dit si l'empreinte enregistrée aujourd'hui est
celle qui a été inscrite. La valeur false signifie que les données
enregistrées ne correspondent plus à ce qui a été ancré. C'est le signal que ce
mécanisme existe pour lever, et il est publié.
#Vérifier l'appartenance d'un article à un lot ancré
Le point d'entrée est GET /v1/verify/merkle/{identifier}. Il rend la preuve
d'appartenance d'un article au lot dont la racine a été inscrite sur Base. Ici,
identifier accepte l'empreinte d'UID, l'identifiant de jeton, le numéro de
série imprimé sur l'étiquette ou un numéro de certificat.
curl "https://api.sealtrust.io/v1/verify/merkle/000000000000"{
"anchor_batch_id": 42,
"merkle_root": "0x4444444444444444444444444444444444444444444444444444444444444444",
"leaf": "0x1111111111111111111111111111111111111111111111111111111111111111",
"leaf_index": 0,
"proof": [
"0x2222222222222222222222222222222222222222222222222222222222222222",
"0x3333333333333333333333333333333333333333333333333333333333333333"
],
"anchor_tx_hash": "0x5555555555555555555555555555555555555555555555555555555555555555",
"basescan_url": "https://basescan.org/tx/0x5555555555555555555555555555555555555555555555555555555555555555",
"contract_address": "0x0000000000000000000000000000000000000000",
"network": "Base",
"token_id": "1",
"verified": true,
"lifecycle_status": "mined",
"withdrawn": false,
"leaf_set": "frozen"
}La preuve se recalcule avec la même fonction que plus haut. Deux vérifications
supplémentaires sont possibles sur la chaîne, sur le contrat dont l'adresse est
donnée par contract_address.
| Lecture sur le contrat | Ce qu'elle rend |
|---|---|
anchoredRootByBatch(anchor_batch_id) | la racine inscrite pour ce lot, à comparer à merkle_root |
verifyAnchored(anchor_batch_id, token_id, proof) | un booléen : la chaîne recalcule elle-même l'appartenance |
Les deux fonctions sont en lecture seule et ouvertes à tous. Le champ verified
de la réponse est le résultat de cette même lecture faite par nos soins. Il vaut
null quand la lecture n'a pas pu aboutir, ce qui n'enlève rien à la preuve
locale.
Trois champs demandent une lecture attentive.
leaf_set vaut frozen quand l'ensemble des feuilles utilisé est celui qui a
été enregistré au moment de l'ancrage. Il vaut recomputed pour les lots
ancrés avant l'existence de cet enregistrement : l'arbre est alors reconstruit à
partir des lignes actuelles du lot, et une modification ultérieure du lot peut
le faire diverger de la racine inscrite.
lifecycle_status et withdrawn décrivent l'état de l'article aujourd'hui. Un
ancrage affirme qu'un ensemble de feuilles donnait cette racine ce jour-là, et
cela reste vrai quoi qu'il arrive ensuite. La preuve d'un article retiré du
catalogue continue donc de se vérifier. Ces deux champs existent pour que vous
n'ayez pas à deviner l'état courant à partir d'une preuve valide.
#Vérifier la signature de l'attestation
La publication d'un passeport déclenche l'émission d'une attestation signée, au format SD-JWT-VC. Elle établit que le document vient de la marque annoncée. Si l'émission échoue, la publication aboutit quand même et le passeport reste sans attestation.
Le chemin qui ne dépend pas de nous tient en quatre gestes.
Récupérez l'attestation avec GET /v1/passport/{identifier}/vc. La réponse
porte sd_jwt_vc, la présentation elle-même, et issuer, l'identifiant de
l'émetteur au format did:web.
Récupérez le document de l'émetteur. Un identifiant de la forme
did:web:<domaine> se résout à https://<domaine>/.well-known/did.json. Un
identifiant de la forme did:web:<hote>:brand:<numero> se résout à
https://<hote>/brand/<numero>/did.json. Le document liste les clefs publiques
non révoquées de la marque, chacune au format JsonWebKey2020.
Choisissez la bonne clef. L'en-tête de l'attestation porte un champ kid de
la forme <did>#key-<version>. Prenez la clef qui porte cet identifiant dans le
document. Cette numérotation permet à une attestation ancienne de rester
vérifiable après une rotation de clef, tant que l'ancienne clef n'est pas
révoquée.
Vérifiez la signature. L'algorithme est ES256. Le type déclaré dans
l'en-tête est dc+sd-jwt. N'importe quelle bibliothèque did:web et SD-JWT-VC
standard convient.
curl "https://api.sealtrust.io/v1/passport/000000000000/vc"Une marque peut porter son identifiant d'émetteur sur son propre domaine. Elle conserve alors la propriété de son identité d'émetteur, et la vérification de ses attestations ne passe plus par nos serveurs.
Si vous préférez une réponse directe, GET /v1/passport/{identifier}/vc/verify
fait la vérification et rend verified. En cas d'échec, verified vaut false
et error vaut verification_failed, sans le message d'origine.
#Quand une preuve est absente
Une preuve absente est une information. Nous la rendons telle quelle.
| Situation | Ce que vous observez | Lecture juste |
|---|---|---|
| Version publiée il y a peu | passport_anchor absent | l'ancrage d'un passeport est une opération que SealTrust déclenche à la main, et beaucoup de passeports ne sont jamais ancrés. Ne l'attendez pas. Le sceau de version tient l'intégrité sans lui |
| Lot de l'article jamais ancré | anchor.anchored à false, type à mint_transaction, proves à token_minted | la transaction prouve que le jeton existe. Elle ne dit rien du lot ni du passeport |
| Article jamais inscrit sur la chaîne | bloc anchor absent en entier | l'article n'a ni lot ancré ni transaction de frappe. Lisez passport_anchor, qui date le document et ne dépend pas de l'article |
| Dépôt IPFS échoué à la publication | ipfs_uri absent | la publication a abouti quand même, le passeport reste sans copie IPFS |
| Passerelle IPFS injoignable | ipfs_match à null, ipfs_uri et ipfs_gateway_url absents | inconnu. Réessayez plus tard, ou par une autre passerelle |
| Passeport publié avant l'existence de la chaîne de versions | seal.linked à false, reason à sealed_before_chain | aucun maillon n'est fabriqué après coup |
| Aucune attestation émise | vc.issued à false | republiez le passeport pour déclencher l'émission |
| Lot modifié depuis son ancrage | GET /v1/verify/merkle/{identifier} répond 409 | la racine recalculée diffère de la racine inscrite. Nous refusons de servir une preuve qui échouerait sur la chaîne |
#Plafonds d'appels et erreurs
Les adresses commençant par /passport partagent un plafond de 60 appels par
tranche de 60 secondes et par adresse IP. Celles commençant par /verify
partagent un plafond de 30 appels par tranche de 60 secondes et par adresse IP.
Les formes avec et sans /v1 comptent sur le même compteur.
Un dépassement renvoie 429, avec les en-têtes Retry-After, X-RateLimit-Limit,
X-RateLimit-Remaining et X-RateLimit-Reset. Ces trois derniers accompagnent
aussi les réponses qui passent. Lisez X-RateLimit-Remaining pour espacer vos
appels avant d'atteindre le plafond.
Toute réponse porte Cache-Control: no-store, max-age=0, y compris les résumés
de preuves /proof et /verify/merkle. Ne placez aucune de ces réponses
derrière un cache partagé. Pour réduire le nombre de vos appels, gardez le
résultat dans votre propre cache applicatif, avec la durée de fraîcheur que
votre usage tolère.
| Code | Condition | Que faire |
|---|---|---|
| 404 | aucun produit ne correspond à l'identifiant | vérifiez l'identifiant |
| 404 | le produit existe, aucun passeport publié | publiez une version |
| 404 | l'article n'appartient à aucun lot ancré, sur /verify/merkle | cas normal, ne le lisez pas comme un échec |
| 404 | aucune attestation émise pour ce passeport, sur /vc | republiez le passeport pour déclencher l'émission |
| 409 | le lot a changé depuis son ancrage, sur /verify/merkle | la preuve serait invalide sur la chaîne, contactez la marque |
| 429 | plafond d'appels dépassé | attendez la durée indiquée par Retry-After |
#Ce qu'il faut retenir
Chaque preuve établit une chose et une seule, et la réponse la nomme. Lisez
anchored, proves, data_hash_matches, chain_link_match et ipfs_match
avant d'affirmer quoi que ce soit.
Ce qui ne dépend pas de nous : la copie IPFS, adressée par son contenu, et
l'inscription sur Base chain_id 8453, lisible par tous. La signature de
l'attestation se vérifie contre une clef publique. Par défaut, nous publions
cette clef sur notre API. Une marque qui pose son identifiant d'émetteur sur son
propre domaine sort aussi cette vérification de nos serveurs.
Ce qui dépend de nous : l'empreinte enregistrée et le sceau de version. Ce sont des contrôles de cohérence utiles, et cette page en dit la portée exacte.
Un ancrage établit une date. Il n'établit pas que le contenu daté est exact. Il n'empêche pas une marque de publier une correction, qui devient la version suivante et laisse la version ancrée vérifiable.
Cette page vous a-t-elle été utile ?
Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.