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 et les registres de l'entreprise sont intègres, 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é
Six 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, la révocation et la réintégration. (registerCompanyDid/rotateCompanyDid/revokeCompanyDid/reinstateCompanyDid, émettantCompanyDidRegistered/CompanyDidRotated/CompanyDidRevoked/CompanyDidReinstated) - 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) - Engagements d'identité citoyenne : à la création d'un DID citoyen, et à
chaque rotation de ses clés par récupération, un engagement salé et opaque
(
keccak256( JCS(revendication) || sel32 )sur le DID et les empreintes de ses clés). Le sel est détruit dès que l'engagement est calculé : l'engagement ne peut jamais être ouvert, ne révèle rien et ne renvoie à aucun consentement. (anchorIdentity, émettantIdentityAnchored) - Racines d'époque et sceaux de déclaration de l'entreprise : une racine à
l'épreuve de la falsification sur chaque époque périodique du journal d'accès de
l'entreprise, de son journal d'attestations et de ses registres scellés
(incidents, diffusions, horloge de réception des demandes d'exercice de droits,
purges déclarées), plus le sceau de chaque déclaration isolée (une attestation
d'anonymisation, une destruction, une version publiée d'une politique, une
réévaluation d'anonymisation). Le journal d'accès documente qui a atteint quels
renseignements personnels, à l'appui de la responsabilité de l'organisation
(P-39.1, art. 3.1). (
anchorAuditRoot, émettantAuditRootAnchored) Chaque racine 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 de filtrer eth_getLogs sur toutes les racines
d'une entreprise (les deux champs sont indexés), sans recoupement hors chaîne. Une
entreprise sans DID enregistré sur la chaîne reçoit l'étiquette bytes32(0), qui
signifie « non attribuée », et non « invalide ».
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é : un identifiant citoyen inscrit sur une chaîne publique romprait la non-corrélation, et il ne pourrait plus jamais être rectifié ni détruit. Les engagements de création et de rotation décrits plus haut sont salés et ne portent aucun identifiant.
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. Seule une ref
jamais ancrée est écrite : une ref déjà Active, Revoked ou Superseded est
ignorée (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, ni ramener un
consentement révoqué. Un nouvel octroi après un retrait est un nouveau
consentement, ancré sous ses propres nouvelles refs. 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 salés issus de la création d'un
DID citoyen et de sa rotation par récupération. 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 : anchorAuditRoot pour les racines et les sceaux de
déclaration de l'entreprise énumérés plus haut, anchorCookieRoot pour le
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. Le contrat refuse de réintégrer la clé exacte qui a été
révoquée.
Le relayeur d'Agreely est le seul à écrire, mais il ne se fie pas à la base de
données pour aucune de ces quatre écritures. Avant d'en signer une, il lit
l'enregistrement TXT _agreely.<domaine> de l'entreprise par DNS sur HTTPS, et le
registre lui-même :
| Écriture | Ce que le relayeur exige |
|---|---|
registerCompanyDid |
Aucun enregistrement sur la chaîne, et le DNS déclare ce DID et cette clé. |
rotateCompanyDid |
Un enregistrement Active, et soit le DNS déclare la nouvelle clé, soit (dès que le DNS publie root=, uniquement ainsi) une assertion de la clé d'accès racine de l'entreprise nommant la nouvelle clé. |
revokeCompanyDid |
Un enregistrement Active, et une assertion de la clé d'accès racine de l'entreprise (agreely.didRevoke.v1) pour ce domaine, cette racine étant celle que publie le DNS. |
reinstateCompanyDid |
Un enregistrement Revoked sous une autre clé, et soit le DNS déclare ce DID et cette clé, soit (dès que le DNS publie root=, uniquement ainsi) une assertion de la clé d'accès racine. |
Une clé que le registre a déjà révoquée pour un domaine ne revient jamais. Une assertion racine de plus de 72 heures, ou signée avant le dernier changement de l'enregistrement, est refusée.
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 seul chemin de Revoked vers Active, 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.
Vérifier une racine à émission seule
registry(root) répond par des zéros pour une racine d'époque, un sceau de
déclaration, un ancrage de document ou un engagement d'identité : ces écritures
sont à émission seule et ne touchent jamais la table de correspondance, qui ne
contient que des refs de consentement. Une réponse nulle à cet endroit ne signifie
pas « non ancrée ». Lisez plutôt le journal d'événements :
- Récupérez le reçu de la transaction d'ancrage (
eth_getTransactionReceipt) pour connaître son numéro de bloc. - Appelez
eth_getLogssur l'adresse du registre avecfromBlock=toBlock= ce bloc ettopic1= la racine. Les points d'accès RPC publics plafonnent la plage de blocs deeth_getLogs(https://mainnet.base.orgrefuse plus de 10 000 blocs), de sorte qu'une requête sans borne échoue. - Contrôlez
topic0:keccak256("AuditRootAnchored(bytes32,bytes32,uint64)"), ouCookieConsentRootAnchored(bytes32,bytes32,uint64)pour une racine de témoins, ouIdentityAnchored(bytes32,uint64)pour un engagement de document ou d'identité. Hachez la signature vous-même plutôt que de vous fier à une valeur précalculée.topic2est l'étiquette d'attribution ; ne l'intervertissez jamais avectopic1, les deux sont desbytes32.
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 à des positions différentes sur le spectre de l'auto-gestion de l'identité.
DID opérationnel (actif aujourd'hui) : did:web:app.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://app.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:app.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://app.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 rend
le cliché du did.json 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:app.agreely.ca:c:<slug>) |
keyId |
Empreinte SHA-256 en bytes32 de la clé de signature (identique au DNS TXT) |
publicKey |
La clé publique Ed25519 brute de 32 octets |
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) :
| Élément | Valeur |
|---|---|
| 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.