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

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

PreuveCe qu'elle établitCe qu'elle laisse ouvert
Empreinte SHA-256 du contenu (data_hash)le contenu enregistré correspond à l'empreinte enregistréeles 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 suivantesne donne aucune date opposable à un tiers
Copie IPFS (ipfs_uri)le document publié est figé : son adresse est son empreintene dit rien de la date de publication
Ancrage du document (passport_anchor)ce contenu de passeport existait au plus tard à la transactionne rend pas le contenu exact
Ancrage de l'article (anchor)cet article appartient à un lot dont la racine est inscrite sur Basene dit rien du contenu du passeport
Attestation signée (vc)le document a bien été émis par la marque annoncéene 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.

Les deux ancrages : le lot d'articles et la version de passeport

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"
200 OK
{
  "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
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",
        ],
    )
)

#É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)

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
curl "https://api.sealtrust.io/v1/verify/merkle/000000000000"
200 OK
{
  "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 contratCe 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
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.

SituationCe que vous observezLecture juste
Version publiée il y a peupassport_anchor absentl'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_mintedla transaction prouve que le jeton existe. Elle ne dit rien du lot ni du passeport
Article jamais inscrit sur la chaînebloc anchor absent en entierl'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 publicationipfs_uri absentla publication a abouti quand même, le passeport reste sans copie IPFS
Passerelle IPFS injoignableipfs_match à null, ipfs_uri et ipfs_gateway_url absentsinconnu. Réessayez plus tard, ou par une autre passerelle
Passeport publié avant l'existence de la chaîne de versionsseal.linked à false, reason à sealed_before_chainaucun maillon n'est fabriqué après coup
Aucune attestation émisevc.issued à falserepubliez le passeport pour déclencher l'émission
Lot modifié depuis son ancrageGET /v1/verify/merkle/{identifier} répond 409la 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.

CodeConditionQue faire
404aucun produit ne correspond à l'identifiantvérifiez l'identifiant
404le produit existe, aucun passeport publiépubliez une version
404l'article n'appartient à aucun lot ancré, sur /verify/merklecas normal, ne le lisez pas comme un échec
404aucune attestation émise pour ce passeport, sur /vcrepubliez le passeport pour déclencher l'émission
409le lot a changé depuis son ancrage, sur /verify/merklela preuve serait invalide sur la chaîne, contactez la marque
429plafond 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.

Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.

Proposer une correctionSignaler un problème