From ab077d8080266af1a0901a38766f3b276fd57e3c Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Mon, 27 Jul 2026 12:31:55 +0200 Subject: [PATCH] =?UTF-8?q?docs(concept):=20r=C3=A9serve=20durable=20sur?= =?UTF-8?q?=20le=20pseudonyme=20permanent=20de=20l'overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app-security/caveat_stable-overlay-pseudonym (nouveau) — toute référence cap-less vers un document protected expose le `:v:` du store, identique partout et pour toujours. BLAKE3 non inversible le rend OPAQUE, d'où la tentation de le croire INOFFENSIF : ce sont deux choses différentes. C'est la constance qui expose, pas la lisibilité. Un seul recoupement, une seule fois, et tout l'historique bascule — y compris ce qui a été publié des années plus tôt. Aucune porte de sortie, vérifié sur quatre axes : pas de rotation d'overlay, store id généré une fois pour toutes, aucune migration de contenu, aucune forme de référence n'évitant d'exposer l'overlay. Le renouvellement de capabilities ne toucherait que l'inner ; l'outer y survit. Placé en app-security et non dans le brief inscriptions : un brief se dissout à sa graduation, la réserve doit lui survivre. Le brief n'en garde qu'un résumé et pointe dessus. Déclencheurs élargis (anonymat, pseudonyme, traçage, corrélation, overlay, cap-less) pour qu'elle remonte quand on s'apprête à concevoir de l'« anonyme ». Consigne pratique qui en découle : ne jamais présenter une action comme « anonyme » si elle fait circuler une référence cap-less — c'est pseudonyme, et le pseudonyme est permanent. Dédup par `:v:` validée par le PO ; le brief le note. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg --- .project/concepts/app-security/_overview.md | 6 ++- .../caveat_stable-overlay-pseudonym.md | 41 +++++++++++++++++++ .../brief_2026-07-20_attendance-set-model.md | 6 ++- 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .project/concepts/app-security/caveat_stable-overlay-pseudonym.md diff --git a/.project/concepts/app-security/_overview.md b/.project/concepts/app-security/_overview.md index c8a58aa..8f08774 100644 --- a/.project/concepts/app-security/_overview.md +++ b/.project/concepts/app-security/_overview.md @@ -2,7 +2,7 @@ type: _overview summary: Sécurité & confidentialité de Festipod — l'isolation entre périmètres est assurée par le SDK de données, l'app lui fait confiance et ne porte aucune logique d'autorisation dans les écrans ; authentification par wallet ; matrice d'autorisations cible en incubation triggers: - keywords: [sécurité, security, confidentialité, privacy, accès, "access control", contrôle d'accès, trust, confiance, authz, autorisation, permission, wallet, auth, authentification, anonyme, identité, login, scope, isolation] + keywords: [sécurité, security, confidentialité, privacy, accès, "access control", contrôle d'accès, trust, confiance, authz, autorisation, permission, wallet, auth, authentification, anonyme, anonymat, pseudonyme, traçage, corrélation, overlay, cap-less, identité, login, scope, isolation] paths: ["src/modules/auth/**", "src/shared/context/NextGraphContext.tsx"] --- @@ -13,6 +13,10 @@ Le modèle de **sécurité, confidentialité et autorisations** de Festipod. - **Modèle appliqué** — l'**isolation entre périmètres** (public / protected / private) est **assurée par le SDK de données** (`@ng-eventually/client`), qui n'expose à chaque utilisateur que ce à quoi il a droit. L'app **fait confiance** au SDK : aucun écran ne porte de logique d'autorisation. Voir [[knowledge_trust-model]]. - **Matrice d'autorisations cible** — le détail *qui peut faire quoi* par acteur × verbe (données personnelles = réseau, anonymat via inbox de notification) : [[brief_2026-05-18_authorization-matrix]]. **Incubation.** Graduera en `rule_`/`behavior_` à mesure que le produit se cale. +## Pièges (lire AVANT de concevoir quoi que ce soit d'« anonyme ») + +- [[caveat_stable-overlay-pseudonym]] — une référence cap-less expose un **pseudonyme permanent** de la personne ; un seul recoupement dé-anonymise **rétroactivement** tout son historique, et aucune rotation n'est connue + ## Liens - [[knowledge_trust-model]] — l'app délègue l'isolation au SDK, pas de contrôle d'accès dans les écrans diff --git a/.project/concepts/app-security/caveat_stable-overlay-pseudonym.md b/.project/concepts/app-security/caveat_stable-overlay-pseudonym.md new file mode 100644 index 0000000..ecfff0d --- /dev/null +++ b/.project/concepts/app-security/caveat_stable-overlay-pseudonym.md @@ -0,0 +1,41 @@ +--- +type: caveat +summary: Toute référence cap-less vers un document protected d'une personne expose le `:v:` de son store — un pseudonyme STABLE ET PERMANENT, identique partout et pour toujours. Ne dit pas qui, mais un seul recoupement dé-anonymise RÉTROACTIVEMENT toutes ses références passées et futures. C'est le même bit d'information qui permet la dédup anonyme. Aucune rotation connue. +last_checked: 2026-07-27 +--- + +# Piège : le `:v:` d'une référence cap-less est un pseudonyme permanent + +**À lire avant de concevoir quoi que ce soit qui fasse circuler des références cap-less** (inscriptions, invitations, mentions, index, notifications). + +## Le fait + +Un NURI s'écrit `did:ng:o:{document}:v:{overlay}`. Le segment `:v:` ne vient **pas du document** mais de **son store** — et une personne a **un seul** store *protected*. Donc : + +> **Toutes** les références cap-less vers **n'importe lequel** des documents protected d'une personne portent le **même** `:v:`. Partout, et pour toujours. + +VÉRIFIÉ dans `nextgraph-rs` (le détail et les pointeurs vivent côté polyfill, `docs/readcap-and-nuri-model.md`) : la valeur injectée à la création d'un document est l'overlay du store contenant ; un `Repo` ne porte aucun overlay propre, et les accès blocs d'un `Store` passent tous par **son** `overlay_id` — un overlay par-document est donc structurellement impossible, pas seulement absent. + +## Pourquoi c'est un piège et pas juste une limite + +Ce `:v:` **ne dit pas qui** — c'est un `BLAKE3` non inversible du store id. La tentation est donc de le traiter comme opaque, donc inoffensif. Il ne l'est pas : c'est un **handle constant**. + +- **Corrélation** — quiconque collecte des références cap-less relie entre elles toutes celles d'une même personne, sans jamais l'identifier. Présence récurrente, appartenances, rythme. +- **Dé-anonymisation rétroactive** — c'est le vrai danger. Il suffit d'**un seul** recoupement, **une seule fois** (une personne qui se nomme, un canal qui fuit, un croisement avec une donnée externe) pour que `:v:X` soit attaché à une identité. À cet instant, **tout** l'historique lié à ce `:v:` bascule d'un coup — y compris ce qui a été publié des années plus tôt en croyant à l'anonymat. +- **Aucune porte de sortie** — VÉRIFIÉ, sur quatre axes : pas de rotation d'overlay (l'outer est un hash pur du store id, sans secret) ; le store id est généré une seule fois à la création de l'identité et jamais régénéré ; aucun chemin de migration de contenu vers un nouveau store ; et aucune forme de référence ne permet de localiser un document sans exposer l'overlay de son store. Le renouvellement de capabilities ne changerait que l'overlay *inner* — l'outer, seul présent dans les NURIs cap-less, y survivrait. **La seule sortie est d'abandonner l'identité entière**, ce qui n'emporte aucun contenu. Signalé en amont comme possible défaut de conception (`orm-tests/INBOX/2026-07-27-outer-overlay-permanent-pseudonym-no-rotation.md`, cf. [[rule_nextgraph-inbox]]). + +## Le couplage à ne pas espérer défaire + +Ce même `:v:` est ce qui permet de **dédupliquer sans lire** — deux références de même `:v:` viennent de la même personne, c'est la base du compteur de participants anonyme ([[brief_2026-07-20_attendance-set-model]] côté `data-layer`). + +**C'est le même bit d'information.** Dédup anonyme et non-traçabilité ne sont pas deux exigences à concilier : ce sont deux lectures d'une seule et même donnée. On ne peut pas obtenir l'une en supprimant l'autre. Le seul curseur réel est le **découpage en stores** — qui déplace l'arbitrage sans le faire disparaître. + +Et ce n'est **pas** un artefact du polyfill : la propriété survit au vrai NextGraph. + +## Ce qu'il faut en faire + +- **Ne jamais présenter à l'utilisateur** une action comme « anonyme » sans réserve si elle fait circuler une référence cap-less. Elle est **pseudonyme**, et le pseudonyme est permanent. +- **Compter** les occurrences d'un `:v:` qu'on expose : chaque contexte supplémentaire où il apparaît augmente la surface de recoupement. +- **Revérifier** ce caveat si NextGraph introduit une rotation d'overlay ou une forme de référence indirecte — il deviendrait alors caduc, ce qui serait une bonne nouvelle. + +Liens : [[knowledge_trust-model]], [[brief_2026-05-18_authorization-matrix]], data-layer ([[brief_2026-07-20_attendance-set-model]], [[rule_capture-nextgraph-findings]], [[rule_nextgraph-inbox]]). diff --git a/.project/concepts/data-layer/brief_2026-07-20_attendance-set-model.md b/.project/concepts/data-layer/brief_2026-07-20_attendance-set-model.md index 5d994df..95a89d6 100644 --- a/.project/concepts/data-layer/brief_2026-07-20_attendance-set-model.md +++ b/.project/concepts/data-layer/brief_2026-07-20_attendance-set-model.md @@ -33,13 +33,15 @@ Vérifiés par lecture de `nextgraph-rs`. Le détail et les pointeurs vivent cô ## La dédup : sur quoi exactement -**Hypothèse de travail — à confirmer par le PO.** Le mécanisme que j'identifie derrière « il a normalement assez d'informations pour dédupliquer » : +**Validé par le PO (2026-07-27).** Le mécanisme derrière « il a normalement assez d'informations pour dédupliquer » : Le segment `:v:` d'un NURI ne vient **pas du document** mais de **son store** (`ProtectedStore(id) → outer(id)`). Or une personne a **un seul** store protected. Donc **toutes ses Participations portent le même `:v:`**, quel que soit le nombre d'objets qu'elle crée. Le créateur déduplique là-dessus : deux références de même `:v:` dans le Set d'un même événement = la même personne. **Sans jamais savoir qui.** Conséquence de conception : le Set est **indexé par `:v:`** — au plus une référence par `:v:`. `Set.size` = nombre de `:v:` distincts = nombre de personnes distinctes. -### La contrepartie, à connaître avant de la découvrir plus tard +### La contrepartie — réserve durable, à ne pas perdre + +> **Elle vit dans `app-security/caveat_stable-overlay-pseudonym`**, pas ici. Ce brief a vocation à être dissous à sa graduation ; la réserve, elle, doit survivre. Résumé ci-dessous, référence là-bas. Ce `:v:` est un **pseudonyme stable et permanent de la personne**, présent dans **toute** référence cap-less vers **n'importe lequel** de ses documents protected. Il ne dit pas *qui* (`BLAKE3(store_id)` n'est pas inversible), mais c'est un **handle constant**, le même partout et pour toujours.