Repo : bip-353-identity-binding. Spec V1 figée : BIP-XXX.md. Expérience V2 : docs/SCHNORR_V2_MINI_SPEC.md.
BIP-353 permet de payer alice@domaine.com. J'ai exploré une couche qui lie cette adresse à une identité cryptographique, en deux variantes (OpenPGP puis Schnorr). Voici ce que ça apporte, ce que ça ne fait pas, comment ça a été testé, et pourquoi je ne suis pas sûr que ce soit nécessaire.
1. Le point de départ
BIP-353 résout une adresse lisible en instructions de paiement (URI BIP-321) via un enregistrement DNS validé par DNSSEC. DNSSEC garantit que la réponse provient bien de la zone signée. Il ne garantit pas que la zone soit contrôlée par la même personne qu'hier. Un accès au compte registrar ou au fournisseur DNS, un transfert de domaine ou une expiration suffisent pour publier un enregistrement parfaitement valide, avec une autre destination. Rien ne permet ensuite de distinguer ce changement d'un changement légitime.
2. Modèle de menace
L'attaquant contrôle la zone DNS. Il ne contrôle ni le wallet, ni le nœud, ni la machine du payeur. Deux situations très différentes :
- Le wallet connaît déjà l'identité (il a mémorisé la clé racine, comportement de wallet, non défini par la spec) : une destination modifiée doit être autorisée par la chaîne de clés qu'il connaît.
- Premier contact : le wallet n'a aucune référence. L'attaquant publie sa propre clé, un document signé par elle et une destination cohérente. Toutes les vérifications passent, car elles contrôlent la cohérence interne du paquet, pas l'identité du propriétaire. Ce cas n'est pas résolu.
3. Architecture
textclé racine (KROOT)
└─ délégation (SubkeyBinding, fenêtre de validité)
└─ clé de signature (KSIGN)
└─ IdentityDocument {domaine, identifiant, payment_hash, …}
├─ payment_hash = hash(PaymentBinding)
└─ (option) ancre Bitcoin : OP_RETURN = hash(domaine, identifiant, racine)
La vérification produit quatre constats indépendants, jamais un seul booléen « vérifié » :
| Constat | Ce qu'il signifie |
|---|---|
identity_verified | délégation racine→signature et signature du document valides, dans la fenêtre de validité |
payment_verified | le hash du PaymentBinding correspond à celui signé dans le document (cohérence, pas paiement effectué) |
identity_anchored | preuve Bitcoin complète (voir section 6) |
continuity_verified | ancré et même clé racine, égalité d'octets, sans notion de fraîcheur |
Le nom « continuité » est un piège : il ne veut pas dire « toujours contrôlé par la même personne ».
Champs CBOR et layout OP_RETURN : voir la mini-spec / le repo.
4. Encodage canonique et séparation des domaines
Tout ce qui est signé ou haché passe par un CBOR déterministe (sous-ensemble de RFC 8949 §4.2.1). Les entiers non minimaux, longueurs indéfinies, clés dupliquées et octets en trop sont rejetés. Deux encodages différents du même contenu ne sont volontairement pas équivalents, faute de quoi le hash serait malléable.
En V2, chaque usage a son propre tagged hash BIP-340 : TAG_SUBKEY_BINDING, TAG_IDENTITY, TAG_PAYMENT, TAG_ANCHOR. Réutiliser une signature d'un contexte dans un autre est testé et rejeté.
5. V1 OpenPGP, V2 Schnorr
V1 (figée) utilise OpenPGP pour la racine et la sous-clé. V2 (expérimentale) utilise des clés x-only BIP-340. V2 n'est pas une correction de V1 : c'est une hypothèse produit différente.
| Aspect | V1 OpenPGP | V2 Schnorr |
|---|---|---|
| Primitive côté wallet | rarement présente | déjà là (Taproot) |
| Parsing | format de paquets RFC 9580 | signature de 64 octets |
| Gestion de clé | clé PGP dédiée | clé x-only ordinaire, dérivation depuis la seed = piste future, non-objectif de la mini-spec |
| Interop PGP existante | oui | non |
| Garanties contre un DNS corrompu | identiques | identiques |
| Tag OP_RETURN | B353ID | B353S2 |
Les deux versions sont volontairement incompatibles : tags distincts, dossiers distincts, aucune compatibilité de preuves revendiquée.
6. L'ancrage Bitcoin, précisément
L'ancre est une sortie OP_RETURN qui engage {version, domaine, identifiant, racine}. La vérification suit une chaîne complète : raw_tx → txid (SHA256d de la sérialisation sans witness) → branche Merkle → merkle_root de l'en-tête → sortie unique du tag V2 → égalité avec le commitment recalculé. Plusieurs sorties du même tag sont rejetées plutôt qu'arbitrées par position.
Trois bornes importantes :
- L'inclusion est vérifiée sous l'en-tête fourni par le wallet. V2 ne fait ni contrôle de preuve de travail ni parcours de chaîne : s'assurer que l'en-tête est sur la bonne chaîne est à la charge du wallet.
- « Ancré » n'est pas « finalisé » (reorgs, profondeur de confirmation : politique du wallet).
- La vérification n'est démontrée que sur des fixtures regtest synthétiques, pas sur mainnet. Dans le langage des claims du repo : TESTED sous ces hypothèses, pas « PROVEN » au sens d'une preuve formelle ou d'un audit externe.
Ce que ça apporte : une trace publique et datée de l'association clé/nom, indépendante du DNS.
7. Ce que ça ne fait pas
Pas de fraîcheur, pas de protection contre le rollback (sequence est optionnel, sans effet anti-rollback), pas de détection de split-view, pas de révocation à jour, pas d'identité humaine, pas de confidentialité. Et les compromissions comptent : une clé de signature volée permet de forger des documents pendant sa fenêtre de validité, une clé racine volée permet l'usurpation complète.
Surface résiduelle côté API (finding LOW du red-team simulé, RT-L2 / F-S2) : une vérif exposée sur des dicts Python plutôt que sur les seuls octets CBOR peut ouvrir des chemins non-canoniques si on l'utilise mal. Ce n'est pas une faille du modèle cryptographique, la piste de durcissement est une API qui ne prend que les octets.
8. Méthode, et ce que les preuves valent
Spec, code, tests et cet article ont été rédigés avec l'aide d'une IA. Le repo contient :
- une implémentation de référence Python (embit) et une reproduction indépendante en JavaScript (
@noble/curves, décodeur CBOR écrit à la main), comparées sur tous les vecteurs, ainsi que le vecteur officiel BIP-340 - des vecteurs valides et invalides, avec checksums et tags de snapshot
- des revues de sécurité, dont un red-team simulé (pas un tiers)
Deux épisodes illustrent la démarche :
- F-S1 (HIGH) : la première revue a fait échouer la phase parce que l'inclusion Bitcoin n'était pas réellement vérifiée (seule la cohérence logique de l'OP_RETURN l'était). Correctif : preuve complète, et
identity_anchoredn'est plus positionné sur une simple correspondance logique. - ADV-M1 : le test différentiel a révélé une divergence UTF-8 en CBOR. Cause racine : dans l'implémentation JS indépendante,
TextDecoderétait non-fatal et acceptait un text-string CBOR invalide (61ff). Correctif (Phase 12) :
jsnew TextDecoder("utf-8", { fatal: true })
Défaut d'implémentation, pas un changement de protocole ni du snapshot expérimental. La couche concernée est la validité UTF-8 au décodage CBOR, la normalisation Unicode (NFC/NFD, etc.) n'est pas dans V2.
Sur le vocabulaire des claims : le document de claims parle de TESTED (vecteurs / fixtures sous hypothèses explicites). Mon red-team simulé a relevé que la mini-spec §12 dit encore PROVEN par endroits, écart de documentation (RT-L1), pas une montée en grade de la garantie. Ici j'utilise TESTED.
Limites de ces preuves : les deux implémentations partagent le même auteur et la même IA, donc le test différentiel attrape des bugs d'implémentation, pas une mauvaise lecture commune de la spec. Après la phase 12, les 22 cas différentiels CBOR/crypto et les 31 cas de la matrice UTF-8 concordent entre les deux implémentations, aucun constat ouvert de sévérité critique, haute ou moyenne (2 LOW et quelques constats informatifs restent ouverts). Il s'agit de reproductibilité, pas d'un audit.
9. Est-ce vraiment nécessaire ?
L'alternative simple : le wallet mémorise la dernière destination vue et alerte si elle change. Sans nouvelle clé ni protocole.
| Critère | Mémoire + alerte | Ma proposition |
|---|---|---|
| Détecte un changement de destination | oui | oui, si non signé par la chaîne connue |
| Distingue changement légitime et hijack | non, l'utilisateur tranche | oui, si signé par la chaîne connue |
| Premier contact | non | non |
| Nouveaux risques | aucun | garde de la clé, complexité, surface d'implémentation |
| Coût d'adoption | très faible | élevé |
Les deux approches exigent que le wallet garde un état. La différence : l'alerte mémorise une destination, qui change à chaque modification légitime, ma proposition mémorise une clé, qui reste stable. Ce stockage n'est pas défini par la spec, c'est une politique de wallet. Sans référence mémorisée, un paquet cohérent présenté par l'attaquant passe toutes les vérifications.
Les gains possibles restent conditionnels. Contre un DNS pris seul, l'attaquant doit compromettre aussi une clé, mais uniquement pour un wallet qui la connaît déjà. Un changement légitime de destination, signé par la chaîne de clés connue, passe sans alerte. La racine peut aussi lier une nouvelle clé de signature sans changer d'identité. La rotation de la racine elle-même n'est pas couverte : piste de conception future.
Hypothèses à tester, plutôt que des certitudes : la fatigue d'alerte quand les changements légitimes sont fréquents, le besoin d'une trace publique en cas de litige, les acteurs qui veulent une politique automatisable. Et une piste non spécifiée : chercher sur la chaîne des ancrages plus anciens pour un même nom, pour réduire le problème du premier contact.
10. Questions ouvertes
Un wallet qui pin et alerte suffit-il ? Existe-t-il un scénario concret où une couche signée fait mieux ? L'ancre on-chain justifie-t-elle son empreinte ? OpenPGP ou Schnorr comme racine ? Retours bienvenus, y compris « ce n'est pas utile ».
Repo : https://github.com/cyphertux/bip-353-identity-binding