Repo: bip-353-identity-binding. Frozen V1 spec: BIP-XXX.md. V2 experiment: docs/SCHNORR_V2_MINI_SPEC.md.
BIP-353 lets you pay alice@domain.com. I explored a layer that binds that address to a cryptographic identity, in two variants (OpenPGP, then Schnorr). Here is what it adds, what it does not do, how it was tested, and why I am not sure it is necessary.
1. Starting point
BIP-353 resolves a human-readable address into payment instructions (BIP-321 URI) via a DNS record validated by DNSSEC. DNSSEC guarantees that the answer comes from the signed zone. It does not guarantee that the zone is controlled by the same person as yesterday. Access to the registrar account or DNS provider, a domain transfer, or an expiry is enough to publish a perfectly valid record with a different destination. Nothing then distinguishes that change from a legitimate one.
2. Threat model
The attacker controls the DNS zone. They control neither the wallet, nor the node, nor the payer's machine. Two very different situations:
- The wallet already knows the identity (it remembered the root key, wallet behavior, not defined by the spec): a modified destination must be authorized by the key chain it already knows.
- First contact: the wallet has no prior reference. The attacker publishes their own key, a document signed by it, and a coherent destination. Every check passes, because the checks validate internal consistency of the bundle, not who owns the name. This case is not solved.
3. Architecture
textroot key (KROOT)
└─ delegation (SubkeyBinding, validity window)
└─ signing key (KSIGN)
└─ IdentityDocument {domain, identifier, payment_hash, …}
├─ payment_hash = hash(PaymentBinding)
└─ (optional) Bitcoin anchor: OP_RETURN = hash(domain, identifier, root)
Verification produces four independent findings, never a single “verified” boolean:
| Finding | Meaning |
|---|---|
identity_verified | root→signing delegation and document signature are valid, within the validity window |
payment_verified | the PaymentBinding hash matches the one signed in the document (consistency, not a settled payment) |
identity_anchored | full Bitcoin proof (see section 6) |
continuity_verified | anchored and same root key, byte equality, with no notion of freshness |
The word “continuity” is a trap: it does not mean “still controlled by the same person.”
CBOR fields and OP_RETURN layout: see the mini-spec / the repo.
4. Canonical encoding and domain separation
Everything that is signed or hashed goes through deterministic CBOR (a subset of RFC 8949 §4.2.1). Non-minimal integers, indefinite lengths, duplicate keys, and trailing bytes are rejected. Two different encodings of the “same” content are intentionally not equivalent, otherwise the hash would be malleable.
In V2, each use has its own BIP-340 tagged hash: TAG_SUBKEY_BINDING, TAG_IDENTITY, TAG_PAYMENT, TAG_ANCHOR. Reusing a signature from one context in another is tested and rejected.
5. V1 OpenPGP, V2 Schnorr
V1 (frozen) uses OpenPGP for the root and subkey. V2 (experimental) uses BIP-340 x-only keys. V2 is not a fix for V1: it is a different product hypothesis.
| Aspect | V1 OpenPGP | V2 Schnorr |
|---|---|---|
| Wallet-side primitive | rarely present | already there (Taproot) |
| Parsing | RFC 9580 packet format | 64-byte signature |
| Key management | dedicated PGP key | ordinary x-only key, seed derivation = future track, non-goal of the mini-spec |
| Existing PGP interop | yes | no |
| Guarantees against a corrupted DNS | identical | identical |
| OP_RETURN tag | B353ID | B353S2 |
The two versions are intentionally incompatible: distinct tags, distinct trees, no claimed proof compatibility.
6. Bitcoin anchoring, precisely
The anchor is an OP_RETURN output that commits to {version, domain, identifier, root}. Verification follows a full chain: raw_tx → txid (SHA256d of the non-witness serialization) → Merkle branch → header merkle_root → unique V2-tag output → equality with the recomputed commitment. Multiple outputs with the same tag are rejected rather than arbitrated by position.
Three important bounds:
- Inclusion is checked under the header supplied by the wallet. V2 does neither proof-of-work checks nor chain walking: ensuring the header is on the right chain is the wallet's job.
- “Anchored” is not “finalized” (reorgs, confirmation depth: wallet policy).
- Verification is demonstrated only on synthetic regtest fixtures, not on mainnet. In the repo's claim language: TESTED under those assumptions, not “PROVEN” in the sense of a formal proof or an external audit.
What it adds: a public, dated trace of the key/name association, independent of DNS.
7. What it does not do
No freshness, no rollback protection (sequence is optional and has no anti-rollback effect), no split-view detection, no up-to-date revocation, no human identity, no confidentiality. Compromises matter: a stolen signing key can forge documents within its validity window, a stolen root key enables full impersonation.
Residual API surface (LOW finding from the simulated red-team, RT-L2 / F-S2): a verify path exposed on Python dicts rather than CBOR bytes alone can open non-canonical paths if misused. That is not a break of the cryptographic model, the hardening path is an API that only accepts bytes.
8. Method, and what the evidence is worth
Spec, code, tests, and this article were written with AI assistance. The repo contains:
- a Python reference implementation (embit) and an independent JavaScript reproduction (
@noble/curves, hand-written CBOR decoder), compared on all vectors, plus the official BIP-340 vector - valid and invalid vectors, with checksums and snapshot tags
- security reviews, including a simulated red-team (not a third party)
Two episodes illustrate the approach:
- F-S1 (HIGH): the first review failed the phase because Bitcoin inclusion was not actually verified (only logical consistency of the OP_RETURN was). Fix: full proof, and
identity_anchoredis no longer set on a mere logical match. - ADV-M1: differential testing found a UTF-8 CBOR divergence. Root cause: in the independent JS implementation,
TextDecoderwas non-fatal and accepted an invalid CBOR text-string (61ff). Fix (Phase 12):
jsnew TextDecoder("utf-8", { fatal: true })
An implementation defect, not a protocol or experimental-snapshot change. The layer at stake is UTF-8 well-formedness at CBOR decode, Unicode normalization (NFC/NFD, etc.) is not part of V2.
On claim wording: the claims document uses TESTED (vectors / fixtures under explicit assumptions). My simulated red-team noted that mini-spec §12 still says PROVEN in places, a documentation mismatch (RT-L1), not an upgrade of the guarantee. This article uses TESTED.
Limits of this evidence: both implementations share the same author and the same AI, so differential tests catch implementation bugs, not a shared misreading of the spec. After phase 12, the 22 CBOR/crypto differential cases and the 31 UTF-8 matrix cases agree across both implementations, no open finding of critical, high, or medium severity remains (2 LOW findings and a few informational notes stay open). This is reproducibility, not an audit.
9. Is it actually necessary?
The simple alternative: the wallet remembers the last destination it saw and alerts if it changes. No new key, no new protocol.
| Criterion | Memory + alert | This proposal |
|---|---|---|
| Detects a destination change | yes | yes, if not signed by the known chain |
| Distinguishes legitimate change from hijack | no, the user decides | yes, if signed by the known chain |
| First contact | no | no |
| New risks | none | key custody, complexity, implementation surface |
| Adoption cost | very low | high |
Both approaches require the wallet to keep state. The difference: the alert remembers a destination, which changes on every legitimate update, this proposal remembers a key, which stays stable. That storage is not defined by the spec, it is wallet policy. Without a remembered reference, a coherent bundle presented by the attacker passes every check.
Possible gains remain conditional. Against DNS alone, the attacker must also compromise a key, but only for a wallet that already knows that key. A legitimate destination change, signed by the known key chain, passes without an alert. The root can also bind a new signing key without changing identity. Root-key rotation itself is not covered: a future design track.
Hypotheses to test, not certainties: alert fatigue when legitimate changes are frequent, the need for a public trace in a dispute, actors who want an automatable policy. And an unspecified track: search the chain for older anchors for the same name, to shrink the first-contact problem.
10. Open questions
Is a wallet that pins and alerts enough? Is there a concrete scenario where a signed layer does better? Does the on-chain anchor justify its footprint? OpenPGP or Schnorr as the root? Feedback welcome, including “this is not useful.”