Vérifier un reçu
Un reçu de consentement est conçu pour être vérifiable par quiconque, sans aucune confiance en Agreely. Cette page est la recette au niveau des champs; pour une marche à suivre exécutable, voir vérifier un reçu vous-même.
Essayez-le
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 avec la CLI : aucun réseau, aucune clé d'API.
- sample-receipt.json : le reçu attesté par l'entreprise
- sample-did.json : le document DID de l'émetteur
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 ligne cellLabelBinding est le champ à lire avant de faire confiance à une
étiquette affichée : ici, pass, car les étiquettes sont dans le corps signé par
l'entreprise. Le reçu ne contient que des données de démonstration (l'entreprise
fictive « Acme »).
Ce dont vous avez besoin
Pour vérifier une cellule de bout en bout, un vérificateur détient :
- le reçu VC (le reçu de consentement);
- l'objet offre d'origine (pour contrôler
companySig, qui a signé l'offre, et recalculer le défi avec sonnonce); - l'enveloppe de la cellule : son
saltde 32 octets et sa preuve d'inclusion de Merkle (divulgués par Agreely; le sel est détruit à l'effacement); - la racine de Merkle ancrée (issue du consentement du reçu, recoupable sur le registre sur la chaîne);
- les deux documents DID (résolus de façon indépendante).
Les quatre contrôles
1. Recalculer l'engagement à partir de la déclaration et du sel
Reconstruisez la déclaration figée à partir des champs du reçu, canonicalisez-la en JCS, concaténez brutalement le sel, et passez au keccak256. Le résultat doit égaler l'engagement stocké.
claim = {
"issuer": receipt.issuer,
"subject": receipt.credentialSubject.id,
"item": { itemId, category, purpose }, // from receipt.credentialSubject.consent.items[i]
"grantedAt": receipt.credentialSubject.consent.grantedAt
}
commitment == keccak256( JCS(claim) || salt )
2. Vérifier l'inclusion de Merkle par rapport à la racine
Prenez leaf = keccak256(commitment) et repliez-la avec les frères de la preuve à
l'aide de l'appariement commutatif. Le résultat doit égaler la racine de Merkle du
consentement, qui est la valeur ancrée sur la chaîne.
leaf = keccak256(commitment)
fold(leaf, proof) == merkleRoot // keccak256(min||max) at each step
3. Vérifier companySig par rapport à la clé du DID de l'entreprise
Résolvez le document DID de l'entreprise et
prenez la méthode que nomme proof[0].verificationMethod : son id est le DID de
l'émetteur suivi de # et d'un fragment, elle est listée sous assertionMethod, et
ce n'est jamais simplement la première méthode du document. Décodez son
publicKeyMultibase vers la clé Ed25519 brute, et vérifiez
proof[0].proofValue (décodé depuis base58btc) par rapport aux octets JCS de
l'offre moins ses champs d'enveloppe (companySig, subject, status,
result). Le signedOffer du dossier de vérification les a déjà retirés.
method = companyDoc.verificationMethod[ id == proof[0].verificationMethod ] // listed under companyDoc.assertionMethod
pubKey = decodeEd25519( method.publicKeyMultibase )
ed25519_verify( decodeBytes(proof[0].proofValue), JCS(offer\{companySig,subject,status,result}), pubKey )
4. Vérifier l'assertion WebAuthn par rapport à la clé valide à grantedAt
Résolvez le document DID du citoyen. Recalculez
d'abord le défi à partir des champs du reçu, de la racine de Merkle et du nonce de
l'offre signée : il doit égaler proof[1].challenge, et le clientDataJSON.challenge
intégré aussi. Prenez la méthode que nomme proof[1].verificationMethod et vérifiez
l'assertion (authenticatorData, clientDataJSON, signature) par rapport à sa clé
COSE. Cette clé n'est celle de la personne que si une méthode du DID du sujet, listée
sous authentication, porte exactement cette clé COSE et que sa fenêtre
[validFrom, validUntil) couvre grantedAt (validUntil exclu). Enfin, grantedAt
doit tomber dans la fenêtre de l'offre, createdAt et expiresAt inclus.
challenge = keccak256( JCS({ issuer, subject, requestId, action, grantedAt, merkleRoot }) || hexBytes(offer.nonce) )
assert challenge == proof[1].challenge // clientDataJSON.challenge carries the same bytes
method = citizenDoc.verificationMethod[ id == proof[1].verificationMethod ]
webauthn_verify( proof[1], method.publicKeyCose )
assert method.id under the subject DID, listed in citizenDoc.authentication
assert method.validFrom <= grantedAt and (method.validUntil is null or grantedAt < method.validUntil)
assert offer.createdAt <= grantedAt <= offer.expiresAt
Pourquoi ces quatre, et rien au sujet d'Agreely
Le contrôle 1 prouve que la déclaration est exactement ce qui a été engagé. Le contrôle 2 prouve que cet engagement est dans le consentement qui a été ancré. Le contrôle 3 prouve que l'entreprise a fait l'offre. Le contrôle 4 prouve que la personne l'a approuvée, avec une clé que le document DID dit être la sienne à ce moment-là. Chaque intrant est un artéfact publié ou une résolution indépendante; rien de tout cela ne vous demande de croire Agreely sur parole.
La frontière honnête de la vérification
Les quatre contrôles ci-dessus supposent qu'on vous a remis l'offre d'origine et l'enveloppe de la cellule. Ce que vous pouvez vérifier avec le seul reçu hors ligne dépend de sa variante.
Un reçu attesté par l'entreprise est entièrement vérifiable hors ligne : sa
signature Ed25519 d'entreprise se vérifie par rapport à la clé did:web de
l'entreprise résolue. Il n'y a pas de moitié citoyen à contrôler.
Un reçu citoyen n'est que partiellement vérifiable hors ligne :
- L'assertion de la clé d'accès du citoyen se vérifie sur le défi que porte le
reçu, sans le relier à son contenu : recalculer ce défi demande le
noncede l'offre signée, que le reçu ne reproduit pas. Elle montre la possession de la clé d'accès, pas que cette personne a consenti à ces cellules. - La moitié signature de l'entreprise n'est pas vérifiable à partir du
reçu seul. L'entreprise a signé l'offre d'origine, qui porte la divulgation
complète, le nonce et les dates de l'offre, et le reçu ne la reproduit pas.
Aucun point de terminaison serveur ne refait ce contrôle. Il se fait hors ligne à
partir du dossier de vérification que la personne télécharge avec son reçu :
receipt,signedOffer(l'offre exacte que l'entreprise a signée, sanssubjectni les autres champs d'enveloppe),merkleRoot,consentRefsetdidDocuments(issueretcitizen). La référence client de l'entreprise n'y figure jamais.
« Indisponible » n'est pas « échoué »
Distinguez toujours indisponible d'échoué. Un document DID qui ne s'est pas résolu est indisponible : le résultat est non concluant, ce n'est pas une falsification. Un échec cryptographique actif (une signature qui ne vérifie pas contre la bonne clé) est échoué : c'est une altération ou une mauvaise clé. Ne rapportez jamais l'un pour l'autre.
Cette matrice est mise en œuvre par la
vérification d'un reçu en TypeScript, la
vérification d'un reçu en PHP et la commande
verify de la CLI.
Référence PHP
Ceci reflète le propre test d'intégration d'Agreely, qui effectue exactement ces contrôles.
use App\Models\Crypto\{Canonicalizer, Commitment, Keccak, Merkle, Multibase};
$canon = new Canonicalizer();
$commitment = new Commitment();
// 1 + 2: per item, recompute the commitment and verify Merkle inclusion.
foreach ($receipt['credentialSubject']['consent']['items'] as $item) {
$claim = [
'issuer' => $receipt['issuer'],
'subject' => $receipt['credentialSubject']['id'],
'item' => ['itemId' => $item['itemId'], 'category' => $item['category'], 'purpose' => $item['purpose']],
'grantedAt' => $receipt['credentialSubject']['consent']['grantedAt'],
];
$recomputed = $commitment->commit($claim, $salt); // == stored commitment
assert(Merkle::verify(Merkle::leafForCommitment($recomputed), $proofSiblings, $merkleRoot));
}
// 3: companySig over the offer (envelope fields removed) vs the key proof[0] names.
// This test's document holds one method, the one proof[0] names; a verifier selects it by id.
$pubKey = Multibase::decodeEd25519PublicKey($companyDoc['verificationMethod'][0]['publicKeyMultibase']);
unset($offer['subject'], $offer['status'], $offer['companySig'], $offer['result']);
$sig = Multibase::decodeBytes($receipt['proof'][0]['proofValue']);
assert(sodium_crypto_sign_verify_detached($sig, $canon->encode($offer), $pubKey));
// 4: recompute the challenge, then verify the WebAuthn assertion with the COSE key valid at grantedAt
// (use a WebAuthn library; the method's [validFrom, validUntil) must cover grantedAt).
Ensuite
- Vérifier un reçu vous-même : un guide exécutable.