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.

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 salt de 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