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
typeporte en plus"CompanyAttestedConsentReceipt"; - il porte, à la racine du reçu,
assuranceLevel: "company_attested", jamais omis, et un blocevidencequi décrit la feuille signée; - son
credentialSubjectn'a aucunidcitoyen (il n'y a pas de DID citoyen à lier); - son
consentne porte nirequestIdnidisclosureHash, 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, dansconsent,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
proofest 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
typeporte"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 typeVerbalConsentDocumentedByOrganisationet nomme lescriptVersionlu et ladocumentVersion; il n'y a aucune empreinte de PDF, parce qu'il n'y a aucun PDF; - son
consentportechannel: "telephone", ladocumentVersion, legrantedAt(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
proofne 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
- Vérifier un reçu : les quatre contrôles qui confirment ce document de bout en bout.