Files
ng-eventually/docs/readcap-and-nuri-model.md
T
Sylvain Duchesne 1791c31f42 docs: vision du polyfill (shape-fidelity), modèle ReadCap/NURI réel, brief caps
vision.md — la charte : le polyfill n'est PAS une couche de sécurité (wallet
partagé + pas de crypto = insécurité ACCEPTÉE) ; seul objectif = exposer la
BONNE FORME des primitives futures pour que les consommateurs n'aient rien à
réécrire. Invariant tenu par une simulation crypto légère : un `did` nu (sans
ReadCap) ne permet PAS de lire ; un NURI avec ReadCap est suffisant et requis.

readcap-and-nuri-model.md — le vrai modèle, VÉRIFIÉ par lecture de
`nextgraph-rs` : ReadCap = ObjectRef {id BLAKE3, clé ChaCha20} = possession de
clé, PAS une ACL ; grant = sceller la clé à l'inbox du destinataire ;
révocation = re-key grossier et non-rétroactif ; grammaire NURI cap-less vs
cap-porteur (le segment `:k:` est le discriminant) ; table des divergences avec
l'émulation `caps.ts` (aujourd'hui une ACL — l'inversion exacte).

briefs/2026-07-20-caps-emulation-alignment.md — le chantier d'alignement :
spike P0 keyless-resolve (verdicts vérifiés : existence sans clé OUI,
détection de suppression sans clé NON, confidentialité OUI), puis P1 cap-less
vs cap-porteur, P2 possession, P3 re-key, P4 migration d'API. Inclut la revue
adverse (WriteCap = membership et non possession ; sans crypto la privacy de
lecture n'est pas applicable ; migration = re-architecture consommateur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 11:30:33 +02:00

134 lines
7.3 KiB
Markdown

# Modèle ReadCap & NURI de NextGraph — et l'émulation caps du polyfill
**Établi 2026-07-20**, VÉRIFIÉ par lecture directe du cœur Rust `nextgraph-rs`
(sauf points marqués INFÉRÉ). Les `file:line` sont datés — les numéros de ligne
sont volatils, se repérer par symbole/regex.
But : donner la vérité-terrain du modèle de droits d'accès NextGraph, pour
aligner l'émulation `caps.ts` du polyfill (aujourd'hui une ACL — l'inverse du
modèle réel). C'est la base de l'item « aligner ReadCap/WriteCap avec NextGraph ».
---
## 1. Un ReadCap = possession d'une clé, PAS une ACL par-identité
Un **ReadCap est fondamentalement une clé cryptographique que l'on détient**, pas
une entrée d'ACL liée à un wallet. « Qui détient la clé peut lire. »
- Structure : `ReadCap = ObjectRef = BlockRef { id: BlockId, key: SymKey }`
(`engine/repo/src/types.rs:461, 463-471, 557, 565`).
- `id: BlockId` = digest **BLAKE3** (adresse de l'objet chiffré).
- `key: SymKey = ChaCha20Key([u8;32])` = la **clé de déchiffrement**.
Détenir le couple → le broker sert les blocs chiffrés par `id`, on déchiffre
**localement** avec `key`.
- Granularité : par commit/objet l'`ObjectRef` **est** le cap ; pour une branche
→ commit de définition ; pour un repo → RootBranch ; pour un store → cap du
repo racine (`types.rs:559-565`). `ReadCapSecret` = la moitié clé (`:567-570`).
- **Il n'y a PAS de read-ACL.** L'appartenance/permissions d'un repo
(`RootBranch`, `AddMember`, `AddPermission`) gouvernent l'**écriture/admin**,
pas la lecture. La lecture n'est gardée que par la possession de la clé.
## 2. Accorder la lecture = sceller la clé au destinataire
« Grant » = livrer le cap **scellé** (`crypto_box seal`, chiffrement à clé
publique anonyme) à la **pubkey d'inbox** du destinataire — seul lui l'ouvre avec
sa clé privée.
- Message d'inbox scellé : `InboxMsgBody.msg` = `crypto_box::seal(... to_inbox ...)`,
ouvert avec la clé secrète d'inbox (`engine/net/src/types.rs:4272, 4299, 4319`).
- Le payload peut porter un cap : `ContactDetails.read_cap: Option<ReadCap>`
(« if user wants to share the content of profile ») (`net/types.rs:4232-4233`)
**grant dirigé** (scellé à un destinataire).
- Variante **non-dirigée** : `RepoLinkV0.read_cap` = un lien partageable que
**quiconque le reçoit** peut ouvrir (`net/types.rs:5061-5078`).
Le « ciblage wallet » vit donc dans **l'enveloppe de scellage**, pas dans le cap :
le cap reste `{id, clé}`, possession-based.
## 3. Révocation = re-key (grossier, non-rétroactif)
On ne « reprend » pas une clé livrée. Révoquer = **re-chiffrer** avec une nouvelle
clé et ne la re-sceller qu'aux autorisés restants.
- « Capabilities are not durable: they can be refreshed by members and previously
shared Caps become obsolete/revoked… if [a member] doesn't subscribe, they lose
access after the refresh » (`net/types.rs:5055-5058`).
- Mécanisme : `RootCapRefresh` / `BranchCapRefresh` (`repo/src/commit.rs:616,630`;
perms `types.rs:1748-1749`).
- Conséquences : **grossier** (échelle repo/branche), **non-rétroactif** (ce qui a
été lu avant reste connu de l'ex-détenteur ; il ne déchiffre que les versions
**antérieures** au refresh).
- Livraison **durable** d'un cap = `PermaCap` — encore **TODO** (`repo/types.rs:578`).
## 4. Grammaire NURI : cap-less vs cap-porteur (le segment `:k:`)
Le discriminant est le segment **`:k:{clé}`** : présent = cap-porteur ; **absent =
cap-less** (nomme/localise **sans** donner le droit de lire). C'est de **première
classe** dans le type : `NuriV0.target` (des ids) et `access`/`objects` (le cap)
sont des **champs séparés** — un NURI d'id parse avec `access: vec![]`
(`engine/net/src/app_protocol.rs:53-62, 99-118, 181-195, 659-677`).
**Cap-less** (id + overlay éventuel, pas de clé) — formatters `app_protocol.rs`,
regexes `net/types.rs` :
- `did:ng:o:{repo_id}` (`:315`, `RE_REPO_O` types.rs:52)
- `did:ng:o:{repo_id}:v:{overlay_id}` (`:263`, `RE_REPO` types.rs:55)
- `did:ng:o:{repo_id}:v:{overlay_id}:b:{branch_id}` (`RE_BRANCH` types.rs:58)
- `did:ng:o:{repo_id}:c:{commit_id}` (`:355`)
- `did:ng:b:{branch}` / `h:{topic}` / `v:{overlay}` / `d:{inbox}` (`:327,323,319,359`)
**Cap-porteur** (embarque la clé) :
- `did:ng:j:{id}:k:{clé}` — read cap d'objet/fichier (`repo/types.rs:511`,
`RE_FILE_READ_CAP` types.rs:49)
- `did:ng:o:{repo}:c:{commit}:k:{clé}` (`RE_COMMIT` types.rs:73)
- liste `RE_OBJECTS` `…:[cj]:{id}:k:{clé}…:l:{locator}` (types.rs:64)
L'`overlay` est **dérivé publiquement** de l'id du repo (`OverlayId::outer(store_id)`,
`:259`) → id+overlay ne fuitent **aucun secret**.
INFÉRÉ : un détenteur **sans clé** peut vraisemblablement **récupérer les blocs
chiffrés** (avec l'overlay, toujours cap-less) → vérifier l'**existence** d'un doc
sans lire son **contenu**. Le chemin d'autorisation de fetch broker pour un
détenteur sans clé n'a **pas** été tracé — à confirmer avant de s'en servir.
## 5. Ce que le polyfill émule (caps.ts) — et où ça diverge
`packages/client/src/caps.ts` modélise `readers: Map<Nuri, Set<PrincipalId>>` +
`grantRead(doc, grantee)` (`:29-30, 41-42`) — **une ACL de principals par
document, soit l'INVERSION exacte du modèle réel** (clé). Divergences :
| | Réel NextGraph | Émulation caps.ts |
|---|---|---|
| Nature | possession de **clé** | **ACL** (set de principals) |
| Grant | sceller la clé (crypto_box) à l'inbox | ajouter un principal au set |
| Durabilité | **durable** (clé livrée une fois) | **éphémère** (Map vide à chaque session → re-déclarée) |
| Révocation | **re-key** grossier, non-rétroactif | retrait du set : **instantané et total** |
| Granularité | repo / branche / commit / objet | **un cap par doc-NURI** |
| Réf. sans droit | **NURI cap-less** (sans `:k:`) | pas de notion (l'ACL dit qui peut) |
**Face app** : `declareConnections` (côté consommateur) qui re-déclare « mes
connexions lisent mes entités protected » **à chaque session** est un **artefact
de cette ACL éphémère** — sans objet dans le modèle réel (les scellages y sont
durables ; on scelle par-doc au partage, pas par-session).
## 6. Implications pour les consommateurs (ex. Festipod)
- « **scope protected = mon réseau peut lire** » n'est **pas** une ACL vérifiée
par le broker : c'est « j'ai **scellé ma read key** à chacune de mes
connexions ». Le modèle mental « scope = ACL » est faux au niveau NextGraph.
- **Références anonymes possibles** : mettre un **NURI cap-less** dans une
collection tierce laisse le tiers **nommer/compter** sans **lire l'identité** ;
sceller le cap-porteur séparément aux seuls autorisés. (Base d'un modèle de
présence « participation auto-possédée + Set curé cap-less + cap scellé aux
connexions ».)
- **Alignement à faire** : quand les vraies opérations de cap seront disponibles,
remplacer l'ACL émulée par du scellage de clé durable par-doc, et
`declareConnections`-comme-ACL-ré-déclarée disparaît.
## Réserves / lacunes
- `file:line` datés (2026-07) — re-vérifier par symbole ; le core bouge.
- INFÉRÉ : fetch broker keyless (existence sans clé) — non tracé au runtime.
- Non tracé : exécution complète de `RootCapRefresh` côté verifier
(`verifier/src/commits/mod.rs:616`), stockage wallet de `private_store_read_cap`
(`repo/types.rs:945,976`).