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)
companySignature pass
citizenAssertion unsupported
disclosureCopy skipped
documentAnchor skipped
cellLabelBinding pass
· 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.
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); - 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,
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, status, result).
pubKey = decodeEd25519( companyDoc.verificationMethod[0].publicKeyMultibase )
ed25519_verify( decodeBytes(proof[0].proofValue), JCS(offer\{companySig,status,result}), pubKey )
4. Vérifier l'assertion WebAuthn par rapport à la clé valide à grantedAt
Résolvez le document DID du citoyen. Trouvez la
méthode de vérification dont l'id se termine par le fragment du
proof[1].verificationMethod, confirmez que sa fenêtre [validFrom, validUntil]
contient grantedAt, et vérifiez l'assertion (authenticatorData,
clientDataJSON, signature) par rapport à la clé COSE de cette méthode. Le
clientDataJSON.challenge intégré doit égaler proof[1].challenge.
method = citizenDoc.verificationMethod[ ends-with proof[1].verificationMethod fragment ]
assert method.validFrom <= grantedAt and (method.validUntil is null or grantedAt <= method.validUntil)
webauthn_verify( proof[1], method.publicKeyCose )
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 est vérifiable hors ligne. Elle prouve la possession de la clé d'accès sur le défi keccak engagé, lié à la racine de Merkle. Elle ne prouve pas la sémantique du consentement au niveau de la cellule.
- La moitié signature de l'entreprise n'est pas vérifiable de façon
probante hors ligne sur un reçu citoyen. L'entreprise a signé l'offre
d'origine, qui comprend la référence au sujet et la divulgation complète, et le
reçu omet ces éléments pour la non-corrélation. Un contrôle probant de la
signature d'entreprise nécessite un point de terminaison serveur
receipts/verify, qui n'existe pas encore.
« 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 company DID key.
$pubKey = Multibase::decodeEd25519PublicKey($companyDoc['verificationMethod'][0]['publicKeyMultibase']);
unset($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: resolve the COSE key valid at grantedAt and verify the WebAuthn assertion
// (use a WebAuthn library; the method's validFrom/validUntil must bracket grantedAt).
Ensuite
- Vérifier un reçu vous-même : un guide exécutable.