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