FR EN

Vérifier un reçu

La vérifiabilité indépendante ne compte que si vous pouvez réellement le faire. Le moyen le plus court est Agreely.verifyReceipt : une méthode statique, sans aucune clé d'API, qui rend un verdict sur toute entrée, jamais une exception. La CLI expose exactement les mêmes statuts par agreely verify.

Vérifier avec le SDK ou la CLI

Passez le dossier de vérification qu'émet un consentement par clé d'accès (le reçu, l'offre signée et la racine de Merkle, dans un seul fichier JSON), ou un reçu sur papier ou par téléphone. Les contrôles sur la chaîne (les ancrages et le retrait) sont facultatifs et n'ont lieu que si vous fournissez une URL RPC.

import { readFile } from "node:fs/promises";
import { Agreely } from "@agreely/sdk";

const file = JSON.parse(await readFile("./agreely-verification-bundle.json", "utf8"));
const v = await Agreely.verifyReceipt(file, {
  rpcUrl: "https://base-rpc.publicnode.com", // optional: the on-chain anchors and the withdrawal
});

v.receiptType; // "company_attested" | "company_documented" | "citizen"
if (v.overall === "verified") {
  // the checks listed held; v.withdrawal reads "active" once the chain reads the consent in force
} else if (v.overall === "withdrawn") {
  // authentic, and the chain records its withdrawal: no longer in force, NOT a forgery
} else if (v.overall === "partial") {
  // a telephone receipt, the organisation's word. A
  // partial does not on its own show consent: verify its bundle at app.agreely.ca/verify
} else if (v.overall === "incomplete") {
  // a bare passkey receipt: verify the verification bundle issued with it
} else if (v.overall === "unavailable") {
  // inconclusive (a DID, a gateway or an RPC did not answer), NOT a forgery: retry later
} else {
  // "failed" or "malformed": read which check failed, and the notes, before calling it a tamper
}
use Agreely\Sdk\Agreely;

$v = Agreely::verifyReceipt(file_get_contents('agreely-verification-bundle.json'), [
    'rpcUrl' => 'https://base-rpc.publicnode.com', // optional: the on-chain anchors and the withdrawal
]);

$v->receiptType; // "company_attested" | "company_documented" | "citizen"
$v->overall;     // "verified" | "partial" | "incomplete" | "failed" | "unavailable" | "malformed" | "withdrawn"
$v->withdrawal;  // "active" | "withdrawn" | "not_checked"
# zero-install (no global install needed):
npx @agreely/cli verify ./agreely-verification-bundle.json

# or, if the CLI is installed globally:
agreely verify ./agreely-verification-bundle.json --json
# human mode prints the check matrix, the withdrawal line, an overall verdict and notes
# exit 0 = verified (or partial on a telephone receipt), 4 = a check could not complete,
# 6 = failed or malformed, 11 = a bare passkey receipt, 12 = withdrawn (with --onchain)

# air-gapped: supply the DID documents locally, taken from the issuer and the citizen
# tier, never out of the file under test:
agreely verify ./agreely-verification-bundle.json --did-doc ./issuer-did.json --did-doc ./citizen-did.json

# opt-in cross-checks: the IPFS copy, the on-chain anchors and the withdrawal:
agreely verify ./agreely-verification-bundle.json --ipfs --onchain --rpc-url https://base-rpc.publicnode.com

Le résultat est un ReceiptVerification : un input (bundle ou receipt), un receiptType, un statut par contrôle, un overall, un withdrawal (active, withdrawn ou not_checked), et des notes en langage clair, écrites pour les humains : un programme lit les champs.

Les contrôles

  1. companySignature : la signature Ed25519 de l'entreprise, sur le corps d'un reçu sur papier ou par téléphone, ou sur l'offre signée d'un dossier par clé d'accès.
  2. receiptIntegrity : sur un dossier, le reçu correspond à l'offre signée champ par champ, et sa date d'acceptation (grantedAt) tombe dans la fenêtre de l'offre.
  3. citizenAssertion : l'assertion WebAuthn de la clé d'accès de la personne. Sur un dossier, son défi se recalcule à partir de l'offre et de la racine de Merkle, et le document DID du citoyen confirme la clé à grantedAt. Sur un reçu citoyen seul, l'assertion se vérifie sur le défi que porte le reçu, sans relier ce défi au contenu du reçu : elle rend unbound, et à elle seule ne montre pas que cette personne a consenti à ces finalités. Un reçu citoyen se vérifie avec son dossier de vérification.
  4. disclosureCopy : recoupement facultatif qui récupère la copie de divulgation sur IPFS et la compare au disclosureHash signé (option verifyDisclosure: true dans le SDK, --ipfs à la CLI); autrement skipped.
  5. documentAnchor : recoupement facultatif sur la chaîne qui confirme que le document a été ancré. Il exige une URL RPC (rpcUrl dans le SDK, --onchain avec --rpc-url ou AGREELY_RPC_URL à la CLI); autrement skipped.
  6. consentAnchor : recoupement facultatif sur la chaîne, sur un dossier par clé d'accès seulement. Il lit l'enregistrement de chaque consentement sous la racine du dossier, et c'est lui qui fonde withdrawal.

La matrice affiche aussi cellLabelBinding : un résumé dérivé indiquant si les étiquettes lisibles (catégorie / finalité) sont liées cryptographiquement. C'est le champ à lire avant de faire confiance à une étiquette affichée. Sur un reçu sur papier ou par téléphone, elle suit companySignature (les étiquettes sont dans le corps signé, donc les modifier casse la vérification); sur un dossier, elle passe quand l'offre signée et l'intégrité du reçu passent; sur un reçu citoyen seul, elle est unsupported.

Essayez-le

Un régulateur à qui l'on dit « c'est vérifiable de façon indépendante » a besoin de quelque chose à essayer. Téléchargez un vrai reçu attesté par l'entreprise et le document DID de son émetteur, tirés des vecteurs de référence du SDK, puis vérifiez le reçu hors ligne (aucun réseau, aucune clé d'API) :

agreely verify sample-receipt.json --did-doc sample-did.json
✓ VERIFIED  (company_attested, read as a receipt)
  companySignature  pass
  receiptIntegrity  pass
  citizenAssertion  unsupported
  disclosureCopy    skipped
  documentAnchor    skipped
  consentAnchor     skipped
  cellLabelBinding  pass
  withdrawal        not checked

  ! WITHDRAWAL NOT CHECKED: this verify did not read the chain, so it says nothing of a withdrawal since the consent was given. For a passkey consent's verification bundle, add --onchain --rpc-url https://base-rpc.publicnode.com to read it.

  · Company signature verified against issuer DID did:web:api.agreely.ca:c:acme (key did:web:api.agreely.ca:c:acme#kms-1).
  · This proves the company ATTESTED to a hand-signed PDF (hash 0xabababababababababababababababababababababababababababababababab); it does NOT prove a human signed.
  · A company-attested receipt carries no citizen passkey assertion, so citizenAssertion is not applicable.
  · Cell labels bound: the category/purpose labels sit inside the company-signed body, so the verified signature covers them. Mutating any item label breaks it.
  · disclosureCopy skipped: the receipt carries no document.ipfsCid + disclosureHash pair to fetch and compare.
  · documentAnchor skipped: pass an rpcUrl to check the on-chain document anchor.
  · consentAnchor skipped: this verifier reads a consent's on-chain record from a passkey bundle only, so the withdrawal of a paper or telephone consent is not checked.

La sortie commence par VERIFIED avec un code de sortie 0. C'est un reçu attesté par l'entreprise, donc la vérification est propre et non partial. La ligne WITHDRAWAL NOT CHECKED rappelle que cette vérification ne dit rien d'un retrait : ce vérificateur lit le retrait sur la chaîne à partir du dossier d'un consentement par clé d'accès.

La matrice d'honnêteté

Le meilleur verdict possible dépend du type de reçu, et c'est le point d'honnêteté porteur de cette page.

Type de reçu Meilleur verdict Pourquoi
company_attested verified la signature de l'entreprise porte sur le corps du reçu et se vérifie hors ligne
company_documented partial un consentement verbal : la signature de l'entreprise se vérifie, mais la parole de l'organisation est tout ce qu'il y a
citizen, dans son dossier verified la signature de l'offre, l'intégrité du reçu et l'assertion liée, avec une clé que le DID du citoyen confirme
citizen, reçu seul incomplete l'assertion n'est reliée à aucun contenu (unbound, voir plus bas)

Un reçu attesté par l'entreprise (typiquement un PDF signé à la main) atteint verified hors ligne : sa signature d'entreprise porte sur le corps du reçu. Un consentement par clé d'accès atteint verified à partir de son dossier de vérification. Sur un reçu citoyen seul, l'assertion par clé d'accès du citoyen se vérifie sur le défi que porte le reçu, sans le relier à son contenu : citizenAssertion rend unbound et overall rend incomplete. Un verified veut dire que les contrôles énumérés tiennent; ce n'est pas une preuve juridique.

Pourquoi un reçu téléphonique s'arrête à partial, et un reçu citoyen seul à incomplete

Un reçu téléphonique porte la signature de l'entreprise, mais aucune signature de la personne : il documente ce que l'organisation dit avoir recueilli. Un partial ne montre donc pas à lui seul que cette personne a consenti : c'est la parole de l'organisation, authentifiée. Le vérificateur public à app.agreely.ca/verify lit en plus, avec son dossier de vérification, si un reçu de version 4 est encore en vigueur. Sur un reçu citoyen, l'entreprise a signé l'offre d'origine, qui porte la divulgation complète; le reçu ne la reproduit pas, pour préserver la non-liaison. Seul le dossier de vérification porte l'offre signée, le requestId et la racine de Merkle qui relient l'assertion au reçu : passez-le au SDK, à la CLI ou au vérificateur public.

Échec contre indisponible

verifyReceipt sépare une vérification qui a échoué (failed : la cryptographie ou la chaîne contredit activement le fichier) d'une vérification qui n'a pas pu aboutir (unavailable : un DID, une passerelle IPFS ou un RPC n'a pas répondu, ou le dossier nomme un registre que le vérificateur ne lit pas). Un fail actif l'emporte toujours sur un unavailable : un vrai négatif n'est jamais masqué par une indisponibilité, et un reçu valide durant une panne ne ressemble jamais à une contrefaçon. À la CLI, c'est sortie 6 (échec) contre sortie 4 (indisponible).

Un recoupement demandé qui n'a pas pu aboutir (disclosureCopy, documentAnchor ou consentAnchor à unavailable) laisse le verdict global à verified dans le SDK; la CLI sort alors en 4. Lisez chaque contrôle et notes avant de vous fier au verdict.

withdrawn n'est ni un échec ni une indisponibilité : le reçu est authentique, et la chaîne consigne le retrait du consentement, qui n'est plus en vigueur (sortie 12 à la CLI). Sans URL RPC, rien ne lit le retrait : withdrawal rend not_checked.

Les clés viennent des documents DID que lit le vérificateur. Un résolveur ou un --did-doc qui sert un document citoyen sans authentication ni validFrom, ou un document d'émetteur qui ne liste pas la clé sous assertionMethod, fait rendre failed à un reçu authentique. Un failed seul n'est donc pas une preuve de falsification : lisez le contrôle qui a échoué et notes, vérifiez les documents DID fournis, ou vérifiez le reçu à app.agreely.ca/verify avec son dossier de vérification.

Ensuite