Registre sur la chaîne
La chaîne est la couche à l'épreuve de la falsification d'Agreely. Elle consigne qu'un consentement a existé, qu'une révocation s'est produite, qu'une identité d'entreprise a été créée ou a fait l'objet d'une rotation, qu'un document de consentement existait, et que le journal d'accès est intègre, d'une manière que personne (y compris Agreely) ne peut réécrire en douce. C'est une preuve, jamais le chemin de lecture.
La chaîne est une preuve, pas le chemin de lecture
La vérification synchrone lit une ligne dans le backend d'Agreely et ne touche jamais à la chaîne, à l'indexeur ni à aucun état d'ancrage. Un octroi ou une révocation fait autorité à l'instant où il est validé dans la base de données; l'ancrage se produit ensuite de façon asynchrone. Ainsi, une révocation refuse dès la toute prochaine vérification, sans aucune attente de la chaîne, et la chaîne donne tout de même à un auditeur une preuve indépendante et à l'épreuve de la falsification de ce qui s'est passé.
Ce qui est ancré
Cinq types de données sont écrits sur Base, le réseau principal (chain id
8453). Chaque charge utile est une valeur bytes32 opaque -- une racine, un
hachage ou un engagement -- jamais des renseignements personnels, jamais un
identifiant ou une chaîne DID citoyen.
- Consentement -- la racine de Merkle d'un consentement signé ainsi que le
consentRefpar cellule couvrant chaque portée de collecte; les révocations font passer une ref deActiveàRevoked. (anchorBatch/revoke) - Registre DID d'entreprise -- une table de correspondance de
domainHash(keccak256(domaine-d-entreprise)) vers la chaîne DID opérationnelle de l'entreprise, l'empreinte de la clé de signature et le CID IPFS du did.json courant. Prend en charge l'enregistrement, la rotation et la révocation. (registerCompanyDid/rotateCompanyDid/revokeCompanyDid, émettantCompanyDidRegistered/CompanyDidRotated/CompanyDidRevoked) - Document de consentement --
keccak256( utf8(cid) )du CID IPFS du document de divulgation publié. Un ancrage à émission seule et sans arête prouvant que le document existait à un instant donné, sans lien vers quelque consentement ou citoyen que ce soit. (anchorIdentityutilisé comme ancrage de document, émettantIdentityAnchored) - Racines d'époque du journal d'accès -- une racine à l'épreuve de la
falsification sur l'époque périodique du journal d'accès, outillant
l'obligation de piste d'audit de la Loi 25. (
anchorAuditRoot, émettantAuditRootAnchored) Chaque racine d'époque est aussi signée au scellement avec la clé DID enregistrée de l'entreprise, additivement; voir signature d'époque et délégation. - Racines d'époque du consentement par témoin -- la même construction pour le
journal de consentement de la bannière de témoins, par site web, dans son
propre espace de noms d'événement pour que l'indexeur ne les confonde jamais
avec les racines du journal d'accès. (
anchorCookieRoot, émettantCookieConsentRootAnchored)
Les racines d'audit et de témoins portent toutes deux une étiquette
d'attribution companyDidDomainHash (le même keccak256(domaine vérifié en minuscules) que le registre DID). C'est une étiquette de lecture, jamais une
autorisation : elle permet d'interroger toutes les racines d'une entreprise en
un seul eth_getLogs (les deux champs sont indexés), sans recoupement hors
chaîne.
Le DID citoyen n'est PAS ancré
L'identité du citoyen -- l'identifiant did:agreely:citizen:..., le document DID, les clés publiques -- n'est jamais écrite sur la chaîne. L'identité citoyenne est hors chaîne et pseudonyme par conception. Seuls des hachages et des racines opaques atteignent la chaîne : jamais un identifiant pouvant créer un lien, jamais des renseignements personnels. Le lien du citoyen dans un reçu est sa signature WebAuthn, vérifiable par le résolveur DID citoyen hors chaîne à /did/{did}, et non un enregistrement sur la chaîne. C'est délibéré : inscrire un identifiant citoyen sur la chaîne romprait la non-corrélation et pourrait constituer un traitement interdit de renseignements personnels au sens de la Loi 25.
AgreelyRegistry
AgreelyRegistry est un petit contrat Solidity : une table de correspondance de
clés opaques, une liste d'autorisation onlyRelayer régie par Ownable2Step, et
des événements indexés que l'indexeur hors chaîne décode mot pour mot. Il n'y a
aucun ether, aucun appel externe et aucun delegatecall, de sorte que la
réentrance est structurellement impossible. renounceOwnership est désactivé :
le contrat ne peut pas se retrouver sans propriétaire, et le transfert de
propriété se fait en deux étapes (proposition, puis acceptation).
Un portefeuille chaud relayeur d'Agreely approvisionné (jamais le citoyen) soumet
les écritures. Chacune ne porte que des clés opaques : une racine, un engagement
ou un bytes32 sans arête, jamais de renseignements personnels, jamais une chaîne
DID.
anchorBatch
function anchorBatch(bytes32 merkleRoot, bytes32[] calldata consentRefs) external onlyRelayer
Ancre un consentement : sa racine de Merkle signée plus les consentRefs par
cellule qu'il couvre. Le merkleRoot est stocké contre chaque ref de sorte qu'un
reçu détenu puisse vérifier l'inclusion d'une cellule sur la chaîne. Une ref déjà
Active est sans effet (aucune écriture d'état, aucun événement), de sorte qu'une
tâche de relayeur re-livrée ne peut ni agiter l'état ni dupliquer un journal. Une
ref Revoked peut être ré-ancrée vers Active, ce qui est le chemin de
ré-octroi. L'argument consentRefs[] existe pour qu'une future fenêtre de
regroupement réservée au relayeur puisse rassembler plusieurs consentements sous
une seule racine sans aucune agitation du contrat.
revoke
function revoke(bytes32 consentRef) external onlyRelayer
Fait passer une ref Active à Revoked. Elle annule la transaction si la ref
n'est pas Active (jamais ancrée, ou déjà révoquée), de sorte qu'une tâche de
révocation re-livrée est interceptée par le contrôle préalable du relayeur plutôt
que d'émettre un second ConsentRevoked parasite.
anchorIdentity
function anchorIdentity(bytes32 commitment) external onlyRelayer
Ancre un engagement bytes32 opaque. Émission seule : aucune écriture dans
la table de correspondance, de sorte que l'ancrage ne renvoie jamais à un
quelconque consentRef. Achemine actuellement deux types d'engagements :
l'ancrage de document de consentement (keccak256( utf8(cid) ) pour le CID IPFS
d'une divulgation publiée), et les engagements opaques issus des événements de
cycle de vie de création ou de rotation de DID citoyen. Aucune de ces écritures
ne porte un identifiant DID, un identifiant client ni aucune donnée pouvant créer
un lien.
anchorAuditRoot / anchorCookieRoot
function anchorAuditRoot(bytes32 root, bytes32 companyDidDomainHash) external onlyRelayer
function anchorCookieRoot(bytes32 root, bytes32 companyDidDomainHash) external onlyRelayer
Ancrent une racine d'époque, respectivement du journal d'accès et du consentement par témoin. Émission seule, deux espaces de noms d'événement distincts. Le second argument est l'étiquette d'attribution décrite plus haut : le contrat l'accepte même lorsque le DID est inconnu, révoqué ou nul, précisément parce que ce n'est pas une autorisation. L'autorisation, elle, vit dans la signature d'époque et la délégation, hors chaîne et vérifiable.
registerCompanyDid / rotateCompanyDid / revokeCompanyDid / reinstateCompanyDid
Ces méthodes constituent le registre DID d'entreprise. registerCompanyDid
stocke la chaîne DID opérationnelle, l'empreinte de la clé de signature (keyId),
la clé publique brute et le CID IPFS du did.json, indexé par
domainHash = keccak256(domaine vérifié en minuscules). rotateCompanyDid met à
jour la clé et le CID pour un domaine existant. revokeCompanyDid marque
l'enregistrement d'un domaine comme révoqué. reinstateCompanyDid réactive un
domaine révoqué sous une nouvelle clé (une nouvelle preuve DNS est exigée hors
chaîne) et émet son propre événement, distinct d'un premier enregistrement, pour
qu'un vérificateur ne puisse jamais confondre une réintégration avec une
inscription initiale.
Un vérificateur dérive le même domainHash depuis le champ alsoKnownAs du
did.json du reçu (le domaine de l'entreprise) et interroge le registre pour
confirmer que la clé, la chaîne DID et le CID du did.json sont durablement sur la
chaîne.
Le CID didDocCid sur la chaîne est aussi l'ancre qui lie le did.json (et donc la
clé d'accès racine qu'il publie sous #root-1) : un vérificateur confirme que le
did.json qu'il lit hache bien vers ce CID. Et rotateCompanyDid est la manière dont
la révocation d'une délégation se réalise : faire tourner la clé opérationnelle
la sort de la fenêtre de clés sur la chaîne, de sorte que les époques qu'elle signe
après le bloc de rotation ne se vérifient plus comme autorisées. Voir
signature d'époque et délégation.
État et événements
enum State { None, Active, Revoked, Superseded }
enum Status { None, Active, Revoked }
struct Anchor {
bytes32 merkleRoot;
uint64 anchoredAt;
uint64 revokedAt;
State state;
}
event AnchorBatch(bytes32 indexed merkleRoot, uint256 refCount, uint64 timestamp);
event ConsentAnchored(bytes32 indexed consentRef, bytes32 indexed merkleRoot, uint64 timestamp);
event ConsentRevoked(bytes32 indexed consentRef, uint64 timestamp);
event ConsentReanchored(bytes32 indexed consentRef, bytes32 indexed newMerkleRoot, uint64 previousRevokedAt, uint64 timestamp);
event IdentityAnchored(bytes32 indexed commitment, uint64 timestamp);
event AuditRootAnchored(bytes32 indexed root, bytes32 indexed companyDidDomainHash, uint64 timestamp);
event CookieConsentRootAnchored(bytes32 indexed root, bytes32 indexed companyDidDomainHash, uint64 timestamp);
event CompanyDidRegistered(bytes32 indexed domainHash, string did, bytes32 keyId, string didDocCid, uint64 verifiedAt);
event CompanyDidRotated(bytes32 indexed domainHash, bytes32 newKeyId, string newDidDocCid, uint64 rotatedAt);
event CompanyDidRevoked(bytes32 indexed domainHash, uint64 timestamp);
event CompanyDidReinstated(bytes32 indexed domainHash, string did, bytes32 keyId, bytes32 previousKeyId, string didDocCid, uint64 verifiedAt);
event RelayerSet(address indexed relayer, bool authorized);
State.None est la valeur par défaut, de sorte qu'une ref inconnue se relit comme
None. Superseded est réservé au remplacement lors d'un renouvellement.
Status est l'énumération parallèle du registre DID d'entreprise, avec la même
convention de défaut à zéro : un domaine inconnu se relit comme None.
Quatre méthodes de lecture ouvertes permettent à tout vérificateur d'interroger
directement le contrat : registry(consentRef) et stateOf(consentRef) pour un
consentement, getCompanyDid(domainHash) et companyDidStatus(domainHash) pour
un DID d'entreprise.
L'indexeur hors chaîne décode AnchorBatch, ConsentAnchored, ConsentRevoked,
IdentityAnchored, AuditRootAnchored, CookieConsentRootAnchored,
CompanyDidRegistered, CompanyDidRotated, CompanyDidRevoked,
CompanyDidReinstated et RelayerSet. reanchorConsent / ConsentReanchored
existent dans le contrat déployé (le chemin de ré-octroi explicite sous une
nouvelle racine) mais ne sont pas utilisés par l'application aujourd'hui. Les
charges utiles d'ancrage ne portent que des valeurs bytes32 opaques (des racines
ou des engagements), jamais de renseignements personnels.
Modèle DID d'entreprise : trois couches
Plusieurs utilisateurs ont été confondus par le fonctionnement de l'identité d'entreprise. Voici l'explication en langage courant des deux DID d'entreprise et des trois couches vérifiables qui les ancrent.
Les deux DID
Une entreprise dans Agreely possède deux DID a des positions différentes sur le spectre de l'auto-gestion de l'identité.
DID opérationnel (actif aujourd'hui) : did:web:agreely.ca:c:<slug>
C'est le DID qu'Agreely héberge pour le compte de l'entreprise. C'est le id
dans le did.json de l'entreprise, l'issuer sur chaque reçu signé, et la chaîne
DID stockée dans le registre sur la chaîne. Son document DID (le did.json
contenant la clé publique de l'entreprise) est servi à
https://agreely.ca/c/<slug>/did.json. Tout vérificateur le résout par une simple
requête HTTPS GET, sans infrastructure particulière.
DID réservé (futur, pas encore actif) : did:web:<domaine-d-entreprise>
Il s'agit d'un espace réservé pour une future mise à niveau vers l'auto-hébergement.
Dès qu'une entreprise sert son propre did.json à
https://<domaine-d-entreprise>/.well-known/did.json, elle peut prendre l'entière
garde de son identifiant. Aujourd'hui ce DID n'est ni résolu ni utilisé pour
signer. Il n'apparaît que dans le champ alsoKnownAs du did.json opérationnel,
en tant qu'ancre de domaine dont le vérificateur se sert pour dériver le
domainHash. Aucun did.json n'est servi sur le domaine propre de l'entreprise
aujourd'hui. Ne prétendez pas que le modèle auto-hébergé est actif; il est réservé
pour une future mise à niveau de souveraineté.
Le domaine de l'entreprise est l'ancre d'identité dans les deux cas : l'enregistrement
DNS TXT, le domainHash sur la chaîne et le champ alsoKnownAs du did.json font
tous référence au domaine de l'entreprise.
Couche 1 : L'enregistrement DNS TXT (preuve de contrôle du domaine)
L'entreprise publie un enregistrement TXT permanent dans son propre DNS :
Hôte : _agreely.<domaine-d-entreprise>
Valeur : agreely-did=did:web:agreely.ca:c:<slug>; key=<empreinte-sha256>[; root=<0x + keccak de la clé COSE racine>]
Cet enregistrement est la déclaration propre de l'entreprise indiquant quel
DID et quelle clé lui appartiennent. Agreely vérifie l'enregistrement avant que
quoi que ce soit n'arrive sur la chaîne : pas d'enregistrement TXT valide, pas
d'inscription sur la chaîne, pas de résolution du document DID. L'empreinte de
clé dans le TXT (key=...) est le même keyId stocké dans le registre sur la
chaîne, de sorte que la déclaration DNS, le document DID et le registre désignent
tous une seule et même clé.
Le champ optionnel root= n'apparaît qu'une fois que l'entreprise a enregistré
une clé d'accès racine : il engage l'empreinte keccak de cette clé racine dans la
propre zone DNS de l'entreprise, ce qui lie la racine à l'entreprise (et non à une
simple assertion d'Agreely). Une entreprise sans clé d'accès racine ne publie aucun
root= et se vérifie exactement comme avant. Voir
signature d'époque et délégation.
Couche 2 : Le document DID (did.json)
Le did.json est le Document DID W3C contenant la clé publique de signature de
l'entreprise. Agreely l'héberge à https://agreely.ca/c/<slug>/did.json -- c'est
le chemin de résolution did:web. Une copie IPFS est également épinglée via
Lighthouse et son CID est enregistré sur la chaîne.
IPFS est une copie ancrée pour la durabilité et l'intégrité, pas le chemin de
résolution. did:web se résout toujours via HTTPS. L'épinglage IPFS garantit
que le cliché du did.json est récupérable et à l'épreuve de la falsification
indépendamment du serveur d'Agreely, et le CID sur la chaîne permet à un
vérificateur de confirmer que la copie épinglée n'a pas changé depuis
l'enregistrement.
Le champ alsoKnownAs du did.json porte did:web:<domaine-d-entreprise> (le DID
réservé). Un vérificateur utilise le domaine de l'entreprise à l'intérieur de
alsoKnownAs pour dériver le domainHash et confirmer qu'il correspond à
l'enregistrement sur la chaîne. Il s'agit d'un contrôle de liaison de domaine,
pas d'une étape de résolution DID.
Couche 3 : Le registre sur la chaîne
Lorsque l'enregistrement DNS TXT est vérifié, Agreely met en file d'attente une
transaction registerCompanyDid qui stocke :
| Champ | Valeur |
|---|---|
domainHash |
keccak256(domaine-d-entreprise en minuscules) -- la clé de domaine opaque |
did |
La chaîne DID opérationnelle (did:web:agreely.ca:c:<slug>) |
keyId |
Empreinte SHA-256 en bytes32 de la clé de signature (identique au DNS TXT) |
didDocCid |
CID IPFS du cliché du did.json au moment de l'enregistrement |
Le vérificateur dérive le domainHash depuis le domaine alsoKnownAs du reçu et
interroge le registre. Une correspondance prouve que l'entreprise a enregistré
cette clé et ce DID de façon durable sur la chaîne, indépendamment du backend
d'Agreely.
Déploiement
Le registre de production est en service sur Base, le réseau principal
(chain id 8453) :
| Chaîne | Base (réseau principal), chain id 8453 |
| Contrat | 0x23577fAffA306375028D33a559D0F95Ced9424DB |
| Explorateur | basescan.org |
| RPC public de lecture | https://mainnet.base.org |
Les ancres de production sont donc sur un réseau public réel, non sur un réseau
de test. Base Sepolia (chain id 84532) a servi pendant la construction et reste
disponible pour vos essais d'intégration; ce n'est plus l'endroit où vivent les
ancres de production.
La chaîne est une preuve, jamais le chemin de lecture, et elle ne conditionne jamais la vérification synchrone.
Ensuite
- Signature d'époque et délégation : la clé DID qui signe chaque racine d'époque, et la délégation par clé d'accès racine liée dans le DNS.
- Intégrité du document sur IPFS : d'où vient le CID de document ancré, et pourquoi IPFS est une copie ancrée et non le chemin de résolution did:web.
- Vérifier un reçu de bout en bout par rapport à la racine ancrée.
- Documents DID : les formes des documents DID d'entreprise et citoyen qu'un vérificateur résout.