Canonicalisation et hachage
Tout ce qu'Agreely hache ou signe est d'abord réduit à une seule chaîne d'octets déterministe. Si deux implémentations peuvent diverger sur ces octets, chaque signature et chaque engagement en aval ne valent plus rien. Les règles ici sont donc délibérément étroites et échouent bruyamment.
JCS / RFC 8785, rendu sans nombres
Avant tout hachage, la valeur est canonicalisée avec JCS (RFC 8785) : clés d'objet triées par ordre d'octets, aucun espace blanc non significatif, échappement de chaîne minimal, sortie UTF-8. JCS existe précisément pour que le même JSON logique produise les mêmes octets dans tout langage.
Agreely ajoute une contrainte stricte par-dessus la RFC 8785 : aucun nombre.
Le canonicaliseur n'accepte que des chaînes, des objets, des tableaux, des
booléens et null, et lève une exception sur tout entier ou flottant.
Les autres règles figées : les clés d'objet doivent être uniquement en ASCII et
sont triées par ordre d'octets strcmp (identique au tri UTF-16 de la RFC 8785
pour l'ASCII); l'ordre des tableaux est préservé; les chaînes utilisent
l'échappement minimal de la RFC 8259 (n'échapper que ", \ et la plage de
contrôle U+0000-U+001F, avec les formes abrégées \b \t \n \f \r), et /
n'est pas échappé; tous les autres caractères, y compris non-ASCII, sont émis
en octets UTF-8 bruts. Un objet vide est {}, un tableau vide est [].
encode({ "b": "2", "a": "1" }) -> {"a":"1","b":"2"}
encode({ "n": 1 }) -> throws (numbers forbidden)
keccak256 : la variante Ethereum, pas le SHA3 du NIST
Agreely hache avec keccak256 en utilisant le bourrage d'origine 0x01, ce
qu'utilisent Ethereum, le keccak256 de Solidity, et viem/ethers. Ce n'est
pas le SHA3-256 du NIST, qui utilise le bourrage 0x06 et produit une sortie
différente pour la même entrée.
L'engagement par cellule
Chaque cellule de consentement est liée par un engagement sur sa déclaration et un sel secret :
commitment = keccak256( JCS(claim) || salt32 )
Le schéma figé de la déclaration est sans nombres par construction :
{
"issuer": "did:web:agreely.ca:c:acme",
"subject": "did:agreely:citizen:9F8K2M4P7Q1R3T5V8W0X2Y4Z6B",
"item": {
"itemId": "0x4b08...",
"category": "phone number",
"purpose": "billing"
},
"grantedAt": "2026-06-26T15:04:05Z"
}
item.category et item.purpose portent les clés normalisées (valeurs
canoniques en minuscules), les mêmes chaînes que stocke le catalogue et sur
lesquelles s'indexe la vérification, de
sorte que l'engagement, le reçu et l'application concordent octet pour octet. Le
sel est ce qui rend l'engagement indevinable, et détruire le sel est exactement ce
que détruit l'effacement.
L'arbre de Merkle : compatible OpenZeppelin
Un consentement peut couvrir plusieurs cellules. Leurs engagements sont combinés
en une seule racine de Merkle, de sorte qu'une seule signature (et un seul ancrage
sur la chaîne) couvre chaque cellule, et que n'importe quelle cellule puisse être
détruite pendant que les autres se vérifient toujours. La construction est un
portage manuel du SimpleMerkleTree d'OpenZeppelin, de sorte que la même preuve
se vérifie sur la chaîne par rapport à la racine ancrée. Les formules exactes
(engagement, feuille, appariement et cas N = 1) vivent dans la
recette de vérification.
Ensuite
- Signatures et clés d'accès : la
signature d'entreprise
eddsa-jcs-2022et l'assertion WebAuthn du citoyen. - Registre sur la chaîne : là où la racine est ancrée.