Vérifier un ConsentReceipt

Agreely.verifyReceipt(receipt, opts) est statique, ne demande aucune clé d'API, et résout toujours. Un reçu falsifié rend un verdict, jamais une exception. En TypeScript, les seams réseau s'injectent par opts.fetch.

import { Agreely } from "@agreely/sdk";

const v = await Agreely.verifyReceipt(receipt, {
  rpcUrl: process.env.BASE_SEPOLIA_RPC, // required for the on-chain anchor check, else skipped
});

Le point d'honnêteté porteur : verifyReceipt sépare une vérification qui a échoué (fail, la cryptographie a activement infirmé le reçu) d'une vérification qui n'a pas pu aboutir (unavailable, le DID de l'émetteur ou du citoyen n'a pas pu être résolu). Un reçu valide pendant une panne DID ne ressemble donc jamais, octet pour octet, à une contrefaçon.

Les deux types de reçu

  • company_attested : l'entreprise a attesté un consentement (typiquement un PDF signé à la main). La signature de l'entreprise se vérifie entièrement hors ligne. Meilleur verdict possible : verified.
  • citizen : le citoyen a signé avec une clé d'accès (WebAuthn). Meilleur verdict possible : partial. Un reçu citoyen ne peut jamais être verified hors ligne (voir la limite honnête plus bas).

Les quatre contrôles (plus la liaison des étiquettes)

verifyReceipt renvoie un ReceiptVerification. Chaque contrôle est une chaîne CheckStatus nue (pas un objet { status }); les explications lisibles vivent toutes dans un seul tableau notes plat, valable pour l'ensemble du résultat.

// {
//   receiptType: "company_attested" | "citizen",
//   companySignature: "pass" | "fail" | "unavailable" | "skipped" | "unsupported",
//   citizenAssertion: "pass" | "fail" | "unavailable" | "skipped" | "unsupported",
//   disclosureCopy:   "pass" | "fail" | "unavailable" | "skipped" | "unsupported",
//   documentAnchor:   "pass" | "fail" | "unavailable" | "skipped" | "unsupported",
//   cellLabelBinding: "pass" | "fail" | "unavailable" | "skipped" | "unsupported",
//   overall: "verified" | "partial" | "failed" | "unavailable",
//   notes: string[]   // caveats en langage clair, un seul tableau plat
// }
  1. companySignature : signature Ed25519 de l'entreprise. Sur un reçu company_attested, vérifiée hors ligne (pass / fail / unavailable si le DID de l'entreprise est irrésolvable). Sur un reçu citizen, unsupported hors ligne.
  2. citizenAssertion : assertion WebAuthn du citoyen. Sur un reçu citizen, vérifiée (pass / fail / unavailable). Sur un reçu company_attested, unsupported.
  3. disclosureCopy (recoupement optionnel, verifyDisclosure défaut true) : récupère la copie de divulgation sur IPFS, calcule "0x"+sha256(JCS(disclosure)) et la compare au disclosureHash du reçu (pass / fail; skipped si le reçu n'a pas de document.ipfsCid + disclosureHash, ou si la récupération échoue).
  4. documentAnchor (recoupement optionnel sur la chaîne, exige opts.rpcUrl, sinon skipped) : commitment = keccak256(utf8(cid)), puis eth_getLogs sur l'AgreelyRegistry pour IdentityAnchored + ce commitment (pass / fail).

À ces quatre contrôles s'ajoute un verdict dérivé :

  1. cellLabelBinding : est-ce que les étiquettes lisibles des cellules (category / purpose) sont liées cryptographiquement dans cette vérification hors ligne? C'est le champ à lire avant de faire confiance à une étiquette affichée. Sur un reçu company_attested, il suit companySignature (les étiquettes sont dans le corps signé, donc une mutation la brise). Sur un reçu citizen, il vaut unsupported hors ligne : le reçu omet l'engagement salé et la racine de Merkle qui lient les étiquettes (non-liaison / crypto-broyage), donc une étiquette altérée ne peut pas être détectée hors ligne. Utilisez alors le point de terminaison serveur receipts/verify (qui détient le sel) pour un contrôle probant des étiquettes.

Légende des statuts

Statut par contrôle (CheckStatus) :

Statut Sens
pass le contrôle a été mené et a vérifié cryptographiquement
fail le contrôle a été mené et n'a activement pas vérifié (falsification ou mauvaise clé)
unavailable le contrôle n'a pas pu aboutir : DID irrésolvable, hôte injoignable, panne réseau, clé introuvable
skipped l'entrée est absente ou le contrôle n'a pas été demandé
unsupported le contrôle ne s'applique pas à ce type de reçu

Statut global (OverallStatus) :

Statut Sens
verified un reçu company_attested, entièrement sain
partial un reçu citoyen vérifié hors ligne : honnête, pas un échec
failed un contrôle décisif a activement échoué
unavailable un contrôle décisif n'a pas pu aboutir : peu concluant, PAS une contrefaçon

Règle porteuse : un fail actif l'emporte toujours sur un unavailable. Un vrai négatif n'est jamais masqué par une indisponibilité.

Ce qui est prouvé contre ce qui est présumé

Contrôle Reçu company_attested Reçu citizen Un pass PROUVE Ce qui reste PRÉSUMÉ
companySignature Ed25519, hors ligne (pass/fail/unavailable) unsupported hors ligne l'entreprise a attesté un PDF signé à la main de hachage H, sous une clé que son DID publie qu'un humain a bien signé le PDF (le SDK voit l'attestation, pas la signature manuscrite); que l'hôte du DID est bien l'entreprise
citizenAssertion unsupported WebAuthn (pass/fail/unavailable) la clé d'accès du citoyen a signé un défi engagé : la possession de la clé que la possession équivaut au consentement au niveau des cellules (elle ne prouve pas quelles cellules ont été consenties)
disclosureCopy copie IPFS contre disclosureHash (pass/fail/skipped) idem la copie IPFS publique correspond au disclosureHash engagé que la passerelle IPFS a servi la vraie copie (un pass ne lie que les octets récupérés au hachage)
documentAnchor engagement sur la chaîne (pass/fail/skipped) idem le document a existé sur la chaîne (keccak256(cid) consigné dans le registre) que l'existence sur la chaîne équivaut à un consentement (l'ancrage prouve le document, pas un octroi)
cellLabelBinding suit companySignature (pass/fail/unavailable) unsupported hors ligne les étiquettes category/purpose affichées sont dans le corps signé, donc couvertes par la signature vérifiée rien de plus sur un reçu attesté; sur un reçu citoyen, ne présumez pas l'exactitude des étiquettes hors ligne (utilisez receipts/verify)

Meilleur verdict global par type de reçu :

Type de reçu Meilleur verdict Pourquoi
company_attested verified la signature se vérifie entièrement hors ligne
citizen partial companySignature est unsupported hors ligne (voir plus bas)

La limite honnête : companySignature sur un reçu citoyen

Sur un reçu citoyen, companySignature est unsupported hors ligne, et ce n'est pas un oubli. L'entreprise a signé l'offre d'origine, qui inclut la référence du sujet et la divulgation complète; le reçu omet ces champs pour préserver la non-liaison. Un contrôle sain de la signature d'entreprise exige donc le point de terminaison serveur receipts/verify, qui n'existe pas encore. Ne présumez pas qu'il existe. C'est précisément pour cela qu'un reçu citoyen plafonne à partial hors ligne.

Le SDK est le vérificateur allégé; le modèle complet vit à verify.agreely.ca

Agreely.verifyReceipt est le vérificateur allégé à quatre contrôles (signature d'entreprise, assertion citoyenne, copie de divulgation, ancrage de document) plus le verdict dérivé cellLabelBinding. Son seul contrôle sur la chaîne est documentAnchor (l'événement IdentityAnchored : il prouve que le document a existé, pas qu'un consentement a été donné). Le modèle complet côté client (authenticité de l'émetteur par le DID d'entreprise sur la chaîne, validation des clés au point-dans-le-temps à travers les rotations, revérification DNS en direct de la liaison de domaine did:web, cycle de vie actif/révoqué du consentement, et vérification de dossier par lot) vit dans le vérificateur côté client à verify.agreely.ca, pas (encore) dans le SDK. Ne présumez pas que le SDK effectue ce modèle plus riche.

Lire le résultat

const v = await Agreely.verifyReceipt(receipt, { rpcUrl: process.env.BASE_SEPOLIA_RPC });

v.receiptType;        // "company_attested" | "citizen"
v.overall;            // "verified" | "partial" | "failed" | "unavailable"
v.companySignature;   // "pass" | "fail" | "unavailable" | "skipped" | "unsupported" (a bare string)
v.citizenAssertion;   // same CheckStatus string
v.disclosureCopy;
v.documentAnchor;
v.cellLabelBinding;   // the field to read before trusting a displayed label
v.notes;              // string[] of plain-language caveats (one flat array)

if (v.overall === "failed") {
  // a decisive check ACTIVELY failed: treat as tamper/forgery, reject.
} else if (v.overall === "unavailable") {
  // inconclusive (a DID or RPC could not be reached), NOT a forgery: retry later.
} else if (v.overall === "partial") {
  // a citizen receipt, verified offline as far as offline can go. Honest, not a failure.
}

Options de vérification

VerifyReceiptOptions :

Option Défaut Rôle
resolver résolveur did:web par défaut injectez votre propre résolveur DID
companyDidHost agreely.ca hôte apex du did:web de l'entreprise (sert /c/{slug}/did.json), utilisé par resolveCompanyDid
citizenResolverBaseUrl https://api.agreely.ca base de résolution du DID citoyen
verifyDisclosure true mène le recoupement de la copie de divulgation
ipfsGateway https://gateway.lighthouse.storage/ipfs/ passerelle IPFS (Lighthouse)
rpcUrl (aucun) requis pour le contrôle d'ancrage sur la chaîne, sinon skipped
registryAddress 0x3532a9DfF3C910152543afCa49De7c66113C5312 AgreelyRegistry (Base Sepolia)
chainId 84532 (Base Sepolia) le mainnet 8453 vaut null, non déployé
fetch fetch global injectez votre propre fetch

Confiance et SSRF : injectez votre propre résolveur pour un reçu non fiable

Le résolveur did:web par défaut récupère un hôte tiré du reçu lui-même (HTTPS uniquement). Il ne peut jamais rendre un faux verified (la clé doit tout de même vérifier), mais pour des reçus non fiables, injectez votre propre resolver ou fournissez les documents DID localement, plutôt que de laisser le reçu diriger une requête sortante.

Ensuite