Vérifier un ConsentReceipt
Agreely::verifyReceipt($receipt, $opts) est statique, ne demande aucune clé
d'API, et résout sur toute entrée : un fichier falsifié ou illisible rend un
verdict, jamais une exception. Il prend le dossier de vérification qu'émet un
consentement par clé d'accès (version "1" : le reçu, son signedOffer et sa
merkleRoot) ou un reçu seul (sur papier ou par téléphone), sous forme de texte
JSON (recommandé : le fichier est lu exactement tel qu'écrit), des objets que rend
json_decode(), ou d'un tableau. ReceiptVerifier::isVerificationBundle($value)
distingue un dossier d'un reçu. En PHP, les seams réseau s'injectent par des
appelables httpGet / httpPost (et non par fetch). Cette page décrit le SDK PHP
0.6.2.
use Agreely\Sdk\Agreely;
use Agreely\Sdk\Verify\ReceiptVerifier;
$json = file_get_contents('agreely-verification-bundle.json');
ReceiptVerifier::isVerificationBundle(json_decode($json)); // true for a verification bundle
$v = Agreely::verifyReceipt($json, [
'rpcUrl' => 'https://base-rpc.publicnode.com', // optional: the on-chain checks, else skipped
]);
Le point d'honnêteté porteur : verifyReceipt sépare une vérification qui a
échoué (fail, la cryptographie ou la chaîne contredit activement le fichier)
d'une vérification qui n'a pas pu aboutir (unavailable : un DID n'a pas pu être
résolu, une passerelle IPFS ou un RPC a échoué, refusé ou expiré). Un reçu valide
pendant une panne ne ressemble donc jamais, octet pour octet, à une contrefaçon.
Les trois types de reçu
company_attested: l'entreprise a attesté un consentement (typiquement un PDF signé à la main). Sa signature porte sur le corps du reçu. Meilleur verdict possible :verified, que le reçu soit seul ou dans son dossier.company_documented: l'organisation a documenté un consentement donné par téléphone (unVerbalConsentReceipt). La signature de l'entreprise se vérifie, mais il n'existe aucun document signé : le meilleur verdict possible estpartial.citizen: la personne a signé avec une clé d'accès (WebAuthn). Meilleur verdict possible :verified, à partir de son dossier de vérification seulement. Un reçu citoyen seul rendincomplete: passez le dossier émis avec lui (voir la limite honnête plus bas).
Les contrôles (plus la liaison des étiquettes)
verifyReceipt renvoie un ReceiptVerification aux propriétés readonly. Chaque
contrôle est une chaîne CheckStatus nue. Les explications lisibles vivent toutes
dans un seul tableau notes plat, écrit pour les humains : un programme lit les
propriétés, jamais les notes. toArray() rend la même forme que le SDK TypeScript.
// $v->input 'bundle' | 'receipt'
// $v->receiptType 'company_attested' | 'company_documented' | 'citizen'
// $v->companySignature 'pass' | 'fail' | 'unavailable' | 'unbound' | 'withdrawn' | 'skipped' | 'unsupported'
// $v->receiptIntegrity, $v->citizenAssertion, $v->disclosureCopy,
// $v->documentAnchor, $v->consentAnchor, $v->cellLabelBinding (same CheckStatus strings)
// $v->overall 'verified' | 'partial' | 'incomplete' | 'failed' | 'unavailable' | 'malformed' | 'withdrawn'
// $v->withdrawal 'active' | 'withdrawn' | 'not_checked'
// $v->withdrawnAt ?int, unix seconds, when the chain records it
// $v->documentAnchorRecord ?array { anchoredAt, anchoredBlock }
// $v->consentAnchorRecords ?array of { consentRef, state, anchoredAt, revokedAt, anchoredBlock }
// $v->consentAnchorCounts ?array { cells, active, withdrawn, pending, unreadable }
// $v->registryRefused bool
// $v->notes string[], plain-language caveats, one flat array
companySignature: la signature Ed25519 de l'entreprise. Sur un reçu sur papier ou par téléphone, elle porte sur le corps du reçu. Sur un dossier par clé d'accès, c'estproof[0]sur l'offre signée. Seule la méthode que nommeproof[0].verificationMethodla contrôle : une méthode de l'émetteur (son id est le DID de l'émetteur suivi de#et d'un fragment), listée sousassertionMethoddans le document DID de l'émetteur. Un document résolu sans cette clé rendfail; un DID irrésolvable rendunavailable. Sur un reçu citoyen seul,unsupported: l'offre ne voyage que dans le dossier.receiptIntegrity: sur un dossier par clé d'accès, le reçu correspond à l'offre signée champ par champ (ses clés de premier niveau,type,@contextet l'idurn:agreely:receipt:compris), ledisclosureHashse recalcule à partir de l'offre,validFromégalegrantedAt, etgrantedAttombe dans la fenêtre de l'offre, decreatedAtàexpiresAt(en secondes entières). Le tableauproofa la forme qu'écrit l'émetteur : deux preuves sur un reçu par clé d'accès (leDataIntegrityProofde l'entreprise, puis leWebAuthnAssertionde la personne), une seule sur un reçu sur papier ou par téléphone. Sur un reçu sur papier ou par téléphone, ce contrôle suitcompanySignature(la signature couvre le corps) et rendfailsi la preuve n'a pas sa forme émise. Sur un reçu citoyen seul,unsupported.citizenAssertion: sur un dossier, l'assertion WebAuthn se vérifie, son défi se recalcule commekeccak256(JCS({issuer, subject, requestId, action, grantedAt, merkleRoot}) || nonce), et le document DID du citoyen (dont l'idest le sujet) liste cette clé exacte sousauthentication, avec une fenêtre[validFrom, validUntil)qui couvregrantedAt. Un document qui ne confirme pas la clé rendfail; un document qui ne peut pas être lu comme celui de cette identité rendunavailable. QuandgrantedAttombe hors de la fenêtre de l'offre, la clé n'est pas évaluée à cette date :unavailable. La moitié citoyenne n'est contrôlée que pour un sujetdid:agreely:citizen:, quel que soit le résolveur; tout autre sujet rendunavailable. Sur un reçu citoyen seul, l'assertion se vérifie sur le défi que porte le reçu, mais rien ne relie ce défi à son contenu (il se calcule à partir durequestId, de la racine de Merkle et du nonce, que seul le dossier porte) :unbound, qui ne prouve rien du reçu. Sur un reçu sur papier ou par téléphone,unsupported.disclosureCopy(recoupement optionnel,'verifyDisclosure' => true, sinonskippedsans contacter la passerelle) : récupère la copie de divulgation sur IPFS et la compare audisclosureHashsigné. Sur un dossier, c'est celui de l'offre signée. Quand le reçu déclaredisclosureVersion: "2", le hachage couvre{disclosure, legalBasis}, et un reçu v2 sans sonlegalBasisrendfail. Une passerelle qui ne rend pas le document (404,502), qui échoue ou qui expire rendunavailable. Sur un reçu citoyen seul,unsupported: son hachage n'est signé par personne.documentAnchor(recoupement optionnel sur la chaîne, exigerpcUrl, sinonskipped) : un seuleth_callrootOf(keccak256(cid))sur l'AgreelyRegistry. Un enregistrement dans la voie des documents de consentement passe et remplitdocumentAnchorRecord(anchoredAt,anchoredBlock). Aucun enregistrement encore rendunavailable: le document s'ancre après sa publication, son absence n'est pas une contradiction. Un enregistrement dans une autre voie, ou étiqueté, rendfail. Ce contrôle prouve que le document a existé, pas qu'un consentement a été donné.consentAnchor(recoupement optionnel sur la chaîne, exigerpcUrl, dossier par clé d'accès seulement) : uneth_callregistry(consentRef)par cellule, rattaché au reçu par la racine de Merkle que lie l'assertion vérifiée.passquand chaque cellule a été lue sous cette racine et est en vigueur;withdrawnquand la chaîne consigne le retrait d'une cellule sous cette racine (le consentement est authentique, et n'est plus en vigueur);failquand la chaîne consigne une cellule sous une autre racine;unavailableautrement, une cellule pas encore ancrée ou un RPC en échec compris. Chaque cellule acceptée que liste le reçu compte : une cellule dont le dossier ne nomme pas laconsentRefcompte comme illisible, car personne ne signe cette liste.consentAnchorRecordsdonne chaque cellule lue sous cette racine (state,anchoredAt,revokedAt,anchoredBlock), etconsentAnchorCountsle décompte (cells,active,withdrawn,pending,unreadable). Sur un reçu sur papier, par téléphone ou citoyen seul,skipped.
À ces contrôles s'ajoute un verdict dérivé :
cellLabelBinding: est-ce que les étiquettes lisibles des cellules (category/purpose) 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, il suitcompanySignature(les étiquettes sont dans le corps signé). Sur un dossier, il vautpassquand la signature de l'offre et l'intégrité du reçu passent (chaque cellule est un élément exact de l'offre signée),failquand l'une échoue, etunavailablequand le DID de l'émetteur ne se résout pas. Sur un reçu citoyen seul, il vautunsupported: ne vous fiez pas aux étiquettes affichées.
Légende des statuts
Statut par contrôle (CheckStatus) :
| Statut | Sens |
|---|---|
pass |
le contrôle a été mené et a vérifié |
fail |
le contrôle a été mené et n'a activement pas vérifié : une altération, une mauvaise clé, ou une chaîne qui contredit le fichier |
unavailable |
le contrôle n'a pas pu aboutir : DID irrésolvable, passerelle IPFS ou RPC en échec, en refus ou expiré, appelable httpGet / httpPost qui lève une exception, ou registre inconnu refusé avant tout appel |
unbound |
la signature se vérifie, mais rien ne la relie au contenu du reçu (l'assertion d'un reçu citoyen seul). Ne prouve rien du reçu |
withdrawn |
le consentement est authentique et sous la racine du reçu, mais la chaîne consigne son retrait. Ni un succès, ni une contrefaçon |
skipped |
l'entrée nécessaire est absente, ou le contrôle n'a pas été demandé |
unsupported |
le contrôle ne s'applique pas à cette entrée |
Statut global (overall) :
| Statut | Sens |
|---|---|
verified |
un reçu company_attested sain, seul ou dans son dossier, ou un dossier par clé d'accès dont la signature de l'entreprise, l'intégrité du reçu et l'assertion du citoyen passent. Les contrôles énumérés tiennent; ce n'est pas une preuve juridique |
partial |
un reçu téléphonique (company_documented) dont la signature de l'entreprise se vérifie : la parole de l'organisation, authentifiée. Il ne montre pas à lui seul que la personne a consenti, faute de toute signature de sa part. Le vérificateur à app.agreely.ca/verify lit en plus, avec son dossier de vérification, si un reçu de version 4 est encore en vigueur |
incomplete |
un reçu citoyen seul : il ne peut pas être vérifié sans son dossier. Ni un succès, ni une falsification |
failed |
un contrôle a activement échoué |
unavailable |
un contrôle décisif n'a pas pu aboutir : peu concluant, PAS une contrefaçon. Aussi quand le dossier nomme un registre ou une chaîne que le SDK ne lit pas (registryRefused) |
malformed |
un fichier que le vérificateur ne peut pas lire : pas un objet, un dossier d'une autre version, un receipt ou un signedOffer qui n'est pas un objet, un dossier par clé d'accès sans son signedOffer ou sa merkleRoot, un reçu par clé d'accès, dans un dossier, qui porte un champ que l'émetteur n'écrit jamais, une valeur que la forme canonique refuse, ou une imbrication de plus de 64 niveaux. Aucun contrôle n'a eu lieu : ce verdict ne dit rien d'une altération |
withdrawn |
le reçu est authentique et la chaîne consigne le retrait du consentement : chaque autre contrôle décisif aurait rendu verified, et consentAnchor rend withdrawn |
Règle porteuse : un fail actif l'emporte toujours sur un unavailable, et
withdrawn n'est rendu que si rien d'autre n'a échoué ni manqué. Un vrai négatif
n'est jamais masqué par une indisponibilité.
Un recoupement demandé qui n'a pas pu aboutir (disclosureCopy, documentAnchor
ou consentAnchor à unavailable) ne fait pas baisser un verified : lisez ces
contrôles avant de vous fier au verdict. La CLI sort alors en 4.
Les clés viennent des documents DID que lit le vérificateur. Un resolver 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 que sert votre
resolver, ou vérifiez le reçu à app.agreely.ca/verify
avec son dossier de vérification.
Le retrait : withdrawal
overall juge le reçu; withdrawal dit si le consentement est encore en vigueur,
tel que la chaîne le consigne. Il vaut active quand chaque cellule acceptée a été
lue sur la chaîne sous la racine de Merkle du dossier et qu'aucune n'est retirée,
withdrawn quand la chaîne consigne le retrait d'une cellule (withdrawnAt donne
le plus récent, en secondes unix, quand l'enregistrement le porte), et not_checked
autrement. Sans rpcUrl, rien ne le lit : un consentement retiré, vérifié hors
ligne, rend overall verified et withdrawal not_checked. Un reçu sur papier,
par téléphone ou citoyen seul rend toujours not_checked, car ce SDK lit
l'enregistrement sur la chaîne d'un dossier par clé d'accès seulement. Pour
n'accepter un consentement que tant qu'il est en vigueur :
$result = Agreely::verifyReceipt($bundleJson, ['rpcUrl' => 'https://base-rpc.publicnode.com']);
$inForce = $result->overall === 'verified' && $result->withdrawal === 'active';
Ce qui est prouvé contre ce qui est présumé
| Contrôle | Reçu sur papier ou par téléphone | Dossier par clé d'accès (citizen) |
Un pass PROUVE |
Ce qui reste PRÉSUMÉ |
|---|---|---|---|---|
companySignature |
Ed25519 sur le corps du reçu | proof[0] sur l'offre signée |
l'entreprise a signé ce corps (sur papier : elle atteste un PDF signé à la main de hachage H) ou cette offre, sous une clé que son document DID liste sous assertionMethod |
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 que vous attendez |
receiptIntegrity |
suit companySignature, plus la forme de la preuve |
le reçu contre l'offre, champ par champ, et grantedAt dans la fenêtre de l'offre |
le reçu dit ce que l'entreprise a offert, à une date que l'offre permet | rien de plus sur le contenu |
citizenAssertion |
unsupported |
WebAuthn, défi recalculé, clé confirmée par le DID du citoyen à grantedAt |
une clé d'accès que le document DID du citoyen lui attribue à grantedAt a approuvé ce reçu, tel que le relie le dossier |
que la personne était seule à détenir cette clé (l'assertion prouve la possession); que le document DID du citoyen dit vrai |
disclosureCopy |
copie IPFS contre le disclosureHash du reçu |
contre celui de l'offre signée | la copie IPFS récupérée correspond au hachage signé | que la passerelle a servi la vraie copie (un pass ne lie que les octets récupérés au hachage) |
documentAnchor |
rootOf sur la chaîne |
idem | le document a été ancré dans la voie des documents de consentement | qu'un consentement a été donné (l'ancrage prouve le document, pas un octroi) |
consentAnchor |
skipped |
registry(consentRef) sous la racine liée |
chaque cellule est consignée sur la chaîne sous la racine de ce dossier, en vigueur au moment de la lecture | que la liste des consentRef du dossier est complète (personne ne la signe; une cellule acceptée sans référence compte comme illisible) |
cellLabelBinding |
suit companySignature |
pass quand l'offre et l'intégrité passent |
les étiquettes category/purpose affichées sont dans un corps ou une offre signés par l'entreprise |
rien de plus; sur un reçu citoyen seul (unsupported), ne présumez pas l'exactitude des étiquettes |
Meilleur verdict global par entrée :
| Entrée | Meilleur verdict | Pourquoi |
|---|---|---|
company_attested, seul ou dans son dossier |
verified |
la signature de l'entreprise porte sur le corps du reçu |
company_documented |
partial |
la signature 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 |
rien ne relie l'assertion au contenu du reçu (voir plus bas) |
La limite honnête : un reçu citoyen seul
Sur un reçu citoyen seul, companySignature et receiptIntegrity sont
unsupported et citizenAssertion est unbound, et ce n'est pas un oubli.
L'entreprise a signé l'offre d'origine, qui porte la divulgation complète, le
nonce et les dates de l'offre; le reçu ne la reproduit pas, pour préserver la
non-liaison. La référence client de l'entreprise n'a jamais fait partie de ce que
l'entreprise a signé. L'offre voyage dans le dossier de vérification que la
personne télécharge avec le reçu (receipt, signedOffer, merkleRoot) : passez
ce dossier à verifyReceipt, contrôlez-le à la main
(la recette) ou dans le vérificateur public à
app.agreely.ca/verify. C'est pour cela qu'un reçu citoyen seul rend incomplete.
Ce que lit le SDK, et ce que fait en plus app.agreely.ca/verify
Sur un dossier par clé d'accès, Agreely::verifyReceipt mène la signature de l'offre, l'intégrité du reçu, l'assertion liée avec la clé que le DID du citoyen confirme à grantedAt, et, avec un rpcUrl, les deux ancrages et le retrait. Le vérificateur côté client à app.agreely.ca/verify fait en plus la revérification DNS en direct de la liaison de domaine did:web, la validation de la clé de l'entreprise au point-dans-le-temps à travers les rotations, la lecture du retrait d'un reçu sur papier ou par téléphone de version 4, et la vérification de dossiers par lot à app.agreely.ca/verify/batch. Ne présumez pas que le SDK effectue ces contrôles-là.
Lire le résultat
$v = Agreely::verifyReceipt($json, ['rpcUrl' => 'https://base-rpc.publicnode.com']);
switch ($v->overall) {
case 'verified':
// the checks listed held: read $v->withdrawal before treating the consent as in force
break;
case 'withdrawn':
// authentic, and the chain records its withdrawal: no longer in force, NOT a forgery
break;
case 'partial':
// a telephone receipt: the organisation's word, its signature verified. A
// partial does not on its own show consent: verify its bundle at app.agreely.ca/verify
// to read whether a version 4 receipt is still in force
break;
case 'incomplete':
// a bare passkey receipt: verify the verification bundle issued with it
break;
case 'unavailable':
// inconclusive (a DID, a gateway or an RPC did not answer, or an unknown registry), NOT a forgery
break;
case 'failed':
// a check ACTIVELY failed: read which one, and the notes, before calling it a tamper
break;
case 'malformed':
// not a file this verifier can read: no check ran
break;
}
Options de vérification
Les options (un tableau associatif) :
| Option | Défaut | Rôle |
|---|---|---|
resolver |
résolveur did:web par défaut, et résolveur citoyen |
un appelable fn(string $did) qui rend le document DID, de préférence en texte JSON pour qu'il soit lu tel que publié; s'il lève une exception, la résolution compte comme unavailable. Les clés ne sont jamais tirées des didDocuments du dossier |
companyDidHost |
app.agreely.ca |
hôte du did:web de l'entreprise : le niveau Web d'Agreely, le seul qui sert /c/{slug}/did.json (pas l'apex agreely.ca), utilisé par resolveCompanyDid |
citizenResolverBaseUrl |
https://my.agreely.ca |
base de résolution du DID citoyen : le palier citoyen d'Agreely, qui sert /did/{did} |
verifyDisclosure |
false |
mène le recoupement de la copie de divulgation sur IPFS; autrement skipped, sans contacter la passerelle |
ipfsGateway |
passerelle Lighthouse (https://gateway.lighthouse.storage/ipfs/ suivi du CID) |
un callable fn(string $cid): string qui rend l'URL à récupérer. Une chaîne (une URL) est ignorée sans erreur : la passerelle par défaut sert alors |
rpcUrl |
(aucun) | l'URL JSON-RPC des contrôles sur la chaîne (documentAnchor, consentAnchor) et du retrait; sans elle, skipped |
registryAddress |
le registre connu de chainId |
l'adresse d'AgreelyRegistry, lue par eth_call sur toute chaîne; hors de Base et de Base Sepolia, passez-la, sinon les contrôles d'ancrage rendent unavailable |
chainId |
8453 (Base, le réseau principal) |
la chaîne d'ancrage; passez 84532 pour essayer contre Base Sepolia |
timeoutMs |
10000 |
délai de chaque appel HTTP des appelables par défaut, en millisecondes |
rpcDeadlineMs |
30000 |
délai de toute la séquence sur la chaîne d'une vérification, depuis son premier appel RPC; passé ce délai, les contrôles d'ancrage encore en attente rendent unavailable |
httpGet / httpPost |
appelables curl par défaut | fn(string $url): ?string et fn(string $url, string $body): ?string; un appelable qui lève une exception compte comme unavailable |
Le registre que lit le SDK
Sans registryAddress, le SDK lit le registre connu de la chaîne : sur Base, le réseau principal (chain id 8453), 0x6F2cF94045cF0cbDD94338882c34D183575023E9, déployé au bloc 53113531; sur Base Sepolia (84532), 0xf25D3F8E73648C797Be2CCCD428061f357567b9A, déployé au bloc 50213158. Les contrôles sur la chaîne lisent le registre par eth_call seulement, sans requête de journaux. Un dossier dont la délégation de la clé de l'émetteur nomme un autre registre ou une autre chaîne que celui lu est refusé avant tout appel RPC : les contrôles d'ancrage rendent unavailable, registryRefused vaut true, et le verdict n'est jamais verified. Ce résultat est peu concluant, pas une contrefaçon. L'adresse en vigueur est toujours celle que publie le vérificateur public à app.agreely.ca/verify.
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). Un verified montre donc que le reçu a été signé par qui contrôle le document DID de cet hôte, pas que cet hôte est bien l'entreprise : un reçu qui nomme un hôte de son choix, signé par une clé que cet hôte publie, peut rendre verified. Pour un reçu que vous n'avez pas récupéré vous-même, injectez votre propre resolver (ou vos appelables httpGet / httpPost) ou fournissez les documents DID localement : vous choisissez alors les émetteurs que vous croyez, plutôt que de laisser le reçu les choisir et diriger une requête sortante. Les clés ne sont jamais tirées des didDocuments du dossier, que n'importe qui peut écrire.
Ensuite
- Vérifier un reçu vous-même : la marche à suivre avec le SDK ou la CLI, et un reçu à essayer.
- La recette de vérification au niveau des champs, sans le SDK.
- SDK TypeScript · Vérifier un ConsentReceipt.