FR EN

Le reçu de consentement

Le reçu est l'artéfact présenté au citoyen et vérifiable de façon indépendante. C'est un justificatif vérifiable (Verifiable Credential) du W3C qui porte les éléments consentis et les deux preuves : le companySig réutilisé de l'entreprise et l'assertion WebAuthn du citoyen sur le défi keccak d'Agreely.

Il ne porte délibérément jamais le sel, le consentRef ni l'engagement. Ceux-ci restent dans l'enveloppe destructible afin que le reçu ne défasse pas la non-corrélation et ne bloque pas l'effacement. Comme l'offre, il est sans nombres : chaque valeur est une chaîne.

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://agreely.ca/credentials/consent/v1"
  ],
  "type": ["VerifiableCredential", "ConsentReceipt"],
  "id": "urn:agreely:receipt:6f9619ff-8b86-d011-b42d-00cf4fc964ff",
  "issuer": "did:web:app.agreely.ca:c:acme",
  "validFrom": "2026-06-26T15:04:05Z",
  "validUntil": "2031-01-01T00:00:00Z",
  "credentialSubject": {
    "id": "did:agreely:citizen:9F8K2M4P7Q1R3T5V8W0X2Y4Z6B",
    "consent": {
      "action": "grant",
      "items": [
        {
          "itemId": "0x9a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081",
          "category": "phone number",
          "purpose": "billing"
        }
      ],
      "requestId": "0x4b08245219c0e1a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081234",
      "grantedAt": "2026-06-26T15:04:05Z",
      "document": {
        "code": "acme-billing-v3",
        "name": "Consentement facturation v3",
        "version": "3",
        "effectiveDate": "2026-01-01",
        "ipfsCid": "bafybeib..."
      },
      "disclosureHash": "0x…sha256…"
    }
  },
  "proof": [
    {
      "type": "DataIntegrityProof",
      "cryptosuite": "eddsa-jcs-2022",
      "created": "2026-06-26T15:00:00Z",
      "verificationMethod": "did:web:app.agreely.ca:c:acme#kms-1",
      "proofPurpose": "assertionMethod",
      "proofValue": "z3Ge8wQ5...base58btc..."
    },
    {
      "type": "WebAuthnAssertion",
      "verificationMethod": "did:agreely:citizen:9F8K2M4P7Q1R3T5V8W0X2Y4Z6B#passkey-1",
      "nonce": "0x7081234b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f",
      "challenge": "0xc0ffee...keccak256...",
      "authenticatorData": "SZYN5YgOjGh0NBcP...base64url...",
      "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0Iiw...base64url...",
      "signature": "MEUCIQD...base64url..."
    }
  ]
}

Champ par champ

Champ Signification
@context Le contexte VC v2 plus le contexte du justificatif de consentement d'Agreely.
type ["VerifiableCredential", "ConsentReceipt"].
id urn:agreely:receipt:{uuid}.
issuer Le DID de l'entreprise (repris de l'issuer de l'offre).
validFrom grantedAt, le moment où le citoyen a approuvé.
validUntil Le validUntil de l'offre, la durée de vie du consentement.
credentialSubject.id Le DID du citoyen, did:agreely:citizen:....
credentialSubject.consent.action grant.
credentialSubject.consent.items Les éléments consentis (itemId, category normalisée, purpose normalisée).
credentialSubject.consent.requestId Le requestId de l'offre d'origine.
credentialSubject.consent.grantedAt Quand le consentement a été donné (RFC 3339).
credentialSubject.consent.document Le descripteur de document : code, name, nameEn quand l'entreprise a nommé le document en anglais, version, effectiveDate et, lorsque la version de la divulgation est épinglée sur IPFS, l'ipfsCid de la copie immuable sous laquelle le consentement a été donné (voir intégrité du document sur IPFS). L'ipfsCid est omis quand la version n'est pas épinglée.
credentialSubject.consent.disclosureHash 0x + le SHA-256 de la carte de divulgation canonicalisée.
credentialSubject.consent.declinedItems Les éléments que la personne a refusés. Présent seulement quand elle en a refusé au moins un : le reçu montre alors chaque fin demandée, pas seulement les fins acceptées.
credentialSubject.consent.disclosureVersion et legalBasis Présents seulement quand une cellule de l'offre repose sur un motif autre que le consentement : disclosureVersion vaut "2", legalBasis associe chaque itemId à son motif et à sa justification, et disclosureHash porte alors sur {disclosure, legalBasis}. Absents, l'empreinte porte sur la seule divulgation.
credentialSubject.consent.minor Présent seulement quand la personne concernée est mineure de moins de 14 ans : le fondement du consentement parental que portait l'offre.

Le reçu citoyen ne porte aucun champ assuranceLevel : son palier, citizen_signed, se lit à son type (ConsentReceipt seul) et à ses deux preuves. Les variantes attestée et verbale, elles, le portent toujours (voir plus bas).

Deux émetteurs, ne les confondez pas

L'issuer est le DID de l'entreprise (did:web:app.agreely.ca:c:{slug}); le credentialSubject.id est le DID du citoyen (did:agreely:citizen:...). Ils utilisent des méthodes DID différentes à dessein. Voir documents DID.

La liaison de divulgation

consent.document lie le reçu à la version exacte de la divulgation sous laquelle le consentement a été donné, par son code, sa version et, lorsque la version est épinglée, son ipfsCid sur IPFS. consent.disclosureHash est 0x + le SHA-256 de la carte de divulgation canonicalisée (JCS).

disclosureHash est du SHA-256, pas du keccak256

Soyez précis sur l'algorithme : disclosureHash est du SHA-256, tandis que les engagements par cellule, l'arbre de Merkle et le défi de la clé d'accès sont du keccak256. Ne mélangez pas les deux quand vous recalculez.

La divulgation prévue à l'article 8 de P-39.1 (article 65 de A-2.1 pour un organisme public) couvre les fins, les moyens de collecte, les catégories, la conservation, le retrait du consentement, les destinataires, la communication hors Québec, les droits, le responsable et toute décision automatisée et, pour un organisme public, son nom, le caractère obligatoire ou facultatif de la demande et les conséquences d'un refus ou d'un retrait (A-2.1, art. 65 al. 1 (1°), (4°) et (5°)). Sa copie publique et récupérable à l'ipfsCid est décrite sous intégrité du document sur IPFS.

Le tableau des preuves

proof est un tableau d'exactement deux preuves, et l'ordre est fixe.

proof[0] : la moitié entreprise

La preuve Data Integrity eddsa-jcs-2022, réutilisée mot pour mot depuis le companySig de l'offre. Son verificationMethod se résout dans le document DID de l'entreprise; elle signe l'offre d'origine (voir la demande de consentement).

proof[1] : la moitié citoyen

Une WebAuthnAssertion sur le défi d'Agreely.

Champ Signification
type WebAuthnAssertion.
verificationMethod Le fragment de clé d'accès du DID citoyen, p. ex. ...#passkey-1.
nonce Le nonce à usage unique de l'offre.
challenge 0x-hex `keccak256( JCS(context)
authenticatorData Données brutes de l'authentificateur WebAuthn, base64url.
clientDataJSON Données client WebAuthn brutes, base64url. Son challenge intégré égale le champ challenge ci-dessus.
signature La signature de l'authentificateur, base64url.

Effacer une seule cellule invalide la signature partagée

Comme l'entreprise a signé sur l'ensemble complet des éléments, effacer un élément d'un reçu à plusieurs éléments invalide companySig sur l'ensemble survivant : il n'y a pas de preuve à divulgation sélective dans la v1. Les survivants restent vérifiables de façon indépendante via leur commitment + la racine de Merkle + l'ancrage sur la chaîne, et non via le reçu caviardé. La divulgation sélective SD-JWT / BBS+ est le correctif différé.

La variante attestée par l'entreprise

Tous les consentements ne passent pas par une clé d'accès. Lorsqu'une personne a signé un formulaire papier à la main, l'entreprise émet un reçu attesté par l'entreprise (le flux manuel ou hors ligne). Il diffère d'un reçu citoyen sur ces points :

  • son type porte en plus "CompanyAttestedConsentReceipt";
  • il porte, à la racine du reçu, assuranceLevel: "company_attested", jamais omis, et un bloc evidence qui décrit la feuille signée;
  • son credentialSubject n'a aucun id citoyen (il n'y a pas de DID citoyen à lier);
  • son consent ne porte ni requestId ni disclosureHash, parce qu'aucune offre n'a été signée en ligne : il nomme plutôt la version du document (documentVersion) et la date de la signature (effectiveDate);
  • un reçu récent porte receiptVersion: "2" et, dans consent, recordedAt, l'instant où l'organisation l'a consigné dans Agreely et où la clé de l'entreprise a signé. Un reçu plus ancien n'a ni l'un ni l'autre; les deux se vérifient de la même façon;
  • son proof est un tableau d'un seul élément : la moitié entreprise seulement, sans assertion WebAuthn.
{
  "receiptVersion": "2",
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://agreely.ca/credentials/consent/v1"
  ],
  "type": ["VerifiableCredential", "ConsentReceipt", "CompanyAttestedConsentReceipt"],
  "id": "urn:agreely:receipt:6f9619ff-8b86-d011-b42d-00cf4fc964ff",
  "issuer": "did:web:app.agreely.ca:c:acme",
  "assuranceLevel": "company_attested",
  "validFrom": "2026-06-26T15:04:05Z",
  "validUntil": "2031-01-01T04:59:59Z",
  "credentialSubject": {
    "consent": {
      "action": "grant",
      "items": [
        {
          "itemId": "0x9a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081",
          "category": "phone number",
          "purpose": "billing"
        }
      ],
      "documentVersion": "e4d41dc3-c8a5-44b1-96c5-be740c45cc6d",
      "document": {
        "code": "acme-billing-v3",
        "name": "Consentement facturation v3",
        "version": "3",
        "effectiveDate": "2026-01-01",
        "ipfsCid": "bafybeib..."
      },
      "effectiveDate": "2026-06-26T15:04:05Z",
      "grantedAt": "2026-06-26T15:04:05Z",
      "recordedAt": "2026-06-27T13:12:40Z"
    }
  },
  "evidence": {
    "type": "HandSignedPaperConsent",
    "pdfHash": "0x…sha256…",
    "pdfHeldBy": "company",
    "documentVersion": "e4d41dc3-c8a5-44b1-96c5-be740c45cc6d"
  },
  "proof": [
    {
      "type": "DataIntegrityProof",
      "cryptosuite": "eddsa-jcs-2022",
      "created": "2026-06-27T13:12:40Z",
      "verificationMethod": "did:web:app.agreely.ca:c:acme#kms-1",
      "proofPurpose": "assertionMethod",
      "proofValue": "z3Ge8wQ5...base58btc..."
    }
  ]
}
Champ Signification
receiptVersion "2" sur un reçu récent; absent sur un reçu de la première forme.
assuranceLevel "company_attested", toujours présent, à la racine du reçu.
credentialSubject.consent.documentVersion L'identifiant de la version du document sous laquelle la feuille a été signée.
credentialSubject.consent.effectiveDate La date de la signature de la feuille.
credentialSubject.consent.grantedAt La date du consentement. Elle égale effectiveDate, sauf quand la feuille confirme un consentement donné par téléphone : le consentement date alors de l'appel, et consent.confirms nomme le consentement verbal confirmé (assuranceLevel, merkleRoot, grantedAt).
credentialSubject.consent.recordedAt L'instant de la consignation dans Agreely et de la signature de l'entreprise, couvert par la signature. Le created de la preuve est informatif : la signature porte sur le corps du reçu, pas sur les options de la preuve.
evidence.type HandSignedPaperConsent.
evidence.pdfHash 0x + le SHA-256 du PDF signé à la main.
evidence.pdfHeldBy "company" ou "agreely-escrow", qui détient le PDF.
evidence.documentVersion L'identifiant de la version du document, le même que consent.documentVersion.
evidence.documentVersionMatch Présent seulement quand la feuille a été signée avant la publication de cette version dans Agreely : "attested-by-issuer", l'organisation atteste que la feuille portait le texte de cette version. La valeur nomme qui atteste; ce n'est pas une vérification par Agreely.

Ce qu'un reçu attesté prouve, et ce qu'il ne prouve pas

Un reçu attesté par l'entreprise documente que l'entreprise a attesté détenir un PDF signé à la main dont le hachage correspond à evidence.pdfHash. Il ne prouve pas qu'un humain a signé : il n'y a aucune assertion de clé d'accès, seulement la signature de l'entreprise. C'est une attestation d'entreprise, pas une preuve cryptographique de la présence du citoyen.

La variante verbale

Un consentement donné par téléphone produit un troisième reçu, plus faible encore, qui le dit sur l'artéfact lui-même :

  • son type porte "VerbalConsentReceipt", jamais "CompanyAttestedConsentReceipt" : un vérificateur qui ne lit que le type ne doit pas croire qu'une feuille signée existe;
  • il porte, à la racine du reçu, assuranceLevel: "company_documented", la même valeur que renvoie /v1/check;
  • son bloc evidence, lui aussi à la racine, est de type VerbalConsentDocumentedByOrganisation et nomme le scriptVersion lu et la documentVersion; il n'y a aucune empreinte de PDF, parce qu'il n'y a aucun PDF;
  • son consent porte channel: "telephone", la documentVersion, le grantedAt (l'instant de l'appel) et, sur un reçu récent, recordedAt;
  • les finalités auxquelles la personne a répondu « non » figurent dans declinedItems, pour que le reçu montre chaque question posée;
  • son proof ne porte que la moitié entreprise.

Il consigne que l'organisation a déclaré, à l'instant consigné (l'empreinte du reçu est ensuite ancrée), que la personne a consenti par téléphone à ces finalités sous ce script. Il ne prouve pas que l'appel a eu lieu. Le nom du membre du personnel qui a pris l'appel n'y figure pas.

Les vocabulaires d'assurance

Il y a deux vocabulaires, et ils ne se mélangent pas :

Palier de stockage Étiquette publique (reçu et /v1/check)
full citizen_signed
manual company_attested
verbal company_documented

/v1/check renvoie les deux, sous tier et assurance. Faites la correspondance, ne mélangez jamais les deux vocabulaires.

Ensuite