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 PHP, les seams réseau s'injectent par des appelables httpGet /
httpPost (et non par fetch).
use Agreely\Sdk\Agreely;
$v = Agreely::verifyReceipt($receipt, [
'rpcUrl' => getenv('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 êtreverifiedhors 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
propriété string nue (readonly string, pas un objet avec ->status); les
explications lisibles vivent toutes dans un seul tableau $v->notes plat.
// $v->receiptType "company_attested" | "citizen"
// $v->companySignature "pass" | "fail" | "unavailable" | "skipped" | "unsupported"
// $v->citizenAssertion "pass" | "fail" | "unavailable" | "skipped" | "unsupported"
// $v->disclosureCopy "pass" | "fail" | "unavailable" | "skipped" | "unsupported"
// $v->documentAnchor "pass" | "fail" | "unavailable" | "skipped" | "unsupported"
// $v->cellLabelBinding "pass" | "fail" | "unavailable" | "skipped" | "unsupported"
// $v->overall "verified" | "partial" | "failed" | "unavailable"
// $v->notes string[] (caveats en langage clair, un seul tableau plat)
// $v->toArray() la même forme que le SDK TS (discipline des vecteurs témoins)
companySignature: signature Ed25519 de l'entreprise. Sur un reçucompany_attested, vérifiée hors ligne (pass/fail/unavailablesi le DID de l'entreprise est irrésolvable). Sur un reçucitizen,unsupportedhors ligne.citizenAssertion: assertion WebAuthn du citoyen. Sur un reçucitizen, vérifiée (pass/fail/unavailable). Sur un reçucompany_attested,unsupported.disclosureCopy(recoupement optionnel,verifyDisclosuredéfauttrue) : récupère la copie de divulgation sur IPFS, calcule"0x"+sha256(JCS(disclosure))et la compare audisclosureHashdu reçu (pass/fail;skippedsi le reçu n'a pas dedocument.ipfsCid+disclosureHash, ou si la récupération échoue).documentAnchor(recoupement optionnel sur la chaîne, exigeopts['rpcUrl'], sinonskipped) :commitment = keccak256(utf8(cid)), puiseth_getLogssur l'AgreelyRegistrypourIdentityAnchored+ cecommitment(pass/fail).
À ces quatre contrôles s'ajoute un verdict dérivé :
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çucompany_attested, il suitcompanySignature. Sur un reçucitizen, il vautunsupportedhors ligne : le reçu omet l'engagement salé et la racine de Merkle qui lient les étiquettes, donc une étiquette altérée ne peut pas être détectée hors ligne (utilisez alors le point de terminaison serveurreceipts/verify).
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
$v = Agreely::verifyReceipt($receipt, ['rpcUrl' => getenv('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.
} elseif ($v->overall === 'unavailable') {
// inconclusive (a DID or RPC could not be reached), NOT a forgery: retry later.
} elseif ($v->overall === 'partial') {
// a citizen receipt, verified offline as far as offline can go. Honest, not a failure.
}
Options de vérification
VerifyReceiptOptions (un tableau associatif) :
| 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é |
httpGet / httpPost |
appelables curl par défaut | injectez vos propres seams réseau |
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 vos appelables httpGet / httpPost) ou fournissez les documents DID localement, plutôt que de laisser le reçu diriger une requête sortante.
Ensuite
- Vérifier un reçu vous-même : la recette à quatre contrôles au niveau des champs, sans le SDK.
- SDK TypeScript · Vérifier un ConsentReceipt.