overlay.social
↯ RSS· Draft · specs in the open

SPEC · IDN Identity · v1 draft

Identity & signing-key binding.

How a human-meaningful, multi-key, cert-bearing identity is rooted in BSV keys and resolved over an open overlay — additively, on top of standard AIP. BRC-100 identity key, ProfileToken, handles, and the signing-key binding that makes them cohere.

Last revised2026-06-08
Editoroverlay.social wg
StatusWorking draft
LicenseOpen BSV v5
Raw markdown →

peck-identity-v1 — DRAFT

Status: Draft v1, implementation-ready. The unifying identity model for the overlay.social protocol family. Author: Thomas (kryp2), drafted with Claude. Started: 2026-06-08. Companion specs: peck-bio-profile-token-v1.md (ProfileToken on-chain shape), peck-social-protocol.md (post/social vocabulary), peck-social-overlay.md (overlay discovery). Design rationale: KEY_BINDING_DESIGN_2026-06-08.md. License: Open BSV License v5. (Chain-bound protocol spec — see §0.4.)

0. What this spec is

0.1

This is the identity model for overlay.social: how a human-meaningful, multi-key, cert-bearing identity is rooted in BSV keys and resolved over an open overlay — additively, on top of standard AIP, without changing how content is signed.

It ties together four pieces that already exist in isolation — the BRC-100 identity key, the ProfileToken (peck-bio-profile-token-v1.md), handles (handle_tokens), and AIP-signed posts (peck-social-protocol.md) — and adds the one missing primitive that makes them cohere: the signing-key binding.

0.2 — The problem it solves

BRC-100 wallets derive per-protocol/counterparty keys (BRC-42/43). So the key that AIP-signs a post (pecks.author) is normally not the wallet's root identity key, and users rotate wallets over time. Today the overlay assumes address == P2PKH(ProfileToken.subject) — exactly one signing key per identity — so posts signed by any other key become "ghost authors": silently dropped from identity resolution. This spec removes that assumption.

0.3 — What it is NOT

  • Not a change to AIP. Posts stay raw AIP/Bitcoin Schema; pecks.author is untouched. Any existing indexer reads posts as before.
  • Not a handle-namespace spec. Handle ownership/uniqueness is Zooko's Triangle and is deliberately decoupled (§10) into its own future spec.
  • Not the ProfileToken on-chain shape — that is peck-bio-profile-token-v1.md. This spec leaves the ProfileToken UTXO format unchanged (subject stays immutable, 1:1).

0.4 — Why Open BSV License

This spec is intrinsically chain-bound: binding attestations are on-chain BSV transactions, verification is BRC-52/BRC-3, resolution is over a BSV overlay. Per the ecosystem licensing policy (license by chain-binding, not name), it is Open BSV License v5 — the same as the rest of the overlay.social protocol family.


1. Design principles

  1. Identity = pubkey. The universal, immutable root of an identity is a BSV public key — the BRC-100 identity key (identity_subject). Everything else (profile, handles, signing keys, posts) hangs off it. The key is the only globally-unique, decentralized, secure identifier (Zooko); human-meaningful names are a separate, pluggable layer (§10).

  2. Binding is additive over AIP. Posts remain raw AIP. The binding layer is a new, optional attestation + index. Apps that don't understand it still read posts; apps that do get the richer, multi-key identity. Progressive, backwards-compatible.

  3. Mutual attestation — no key hijacking. A binding is valid only if both the identity key (certifier) and the signing key (subject) sign it. The identity says "this key is mine"; the key says "I consent." Neither alone is trusted. (BRC-103 mutual-auth, distilled into one self-contained, publicly-verifiable record.)

  4. Federated trust, not walled garden. Any party can be a certifier — the verification is issuer-agnostic (BRC-52 signature check). Self-issued (you vouch for your own keys) is the default; third parties MAY also vouch. Trust is a consumer policy (which certifiers to accept), not a protocol capability gate.

  5. Public, not encrypted. Unlike identity attribute certs (email, KYC — encrypted for selective disclosure per BRC-52/53), a binding attestation is a public claim ("this key is mine"). Its fields are plaintext, so it is directly MAP-discoverable and overlay-servable without keyrings.

  6. Self-contained on-chain. A binding is small (~400–500 bytes) and lives directly in its transaction (not UHRP-referenced). The chain is the universal source every overlay reads, so a binding has zero off-chain availability dependency. (UHRP is reserved for large content — avatars, encrypted attribute certs.)

  7. Overlay is the trustless public truth; wallet is the private/peer path. Public resolution is served by the overlay index (no wallet/handshake). In authenticated contexts, discoverByIdentityKey (BRC-100) complements it for the user's own + peer certs with selective disclosure. Both resolve to the same identity_subject.


2. The identity model

                         identity_subject               ← BRC-100 identity key (pubkey)
                         (universal root)                  the ONE immutable identifier
                          /     |      \
              ___________/      |       \___________________
             /                  |                           \
      ProfileToken          handle_tokens               signing_keys
      (profile + cert_refs) (human name → subject)      (bound posting keys → subject)
      subject == root        points AT root §10           certifier == root, §4
             |                                                   |
        attributes,                                         each post is AIP-signed
        avatar, certs                                       by a bound signing key
                                                                 |
                                                            pecks.author (raw AIP addr)
                                                                 |
                                              resolve: pecks.author → signing_keys → identity_subject

Reading it: the root is a pubkey. The ProfileToken carries the mutable profile + attribute certs for that root (subject == identity_subject). Handles point at the root (§10). Signing keys are bound to the root (§4). A post is plain AIP, authored by a signing key; resolution maps that key back to the root, so every surface — follow, @mention, notifications, feed — keys on identity_subject, coherent across all bound keys and all rotations.


3. The canonical identity key (normative)

The identity_subject of a user is the pubkey returned by the BRC-100 wallet as its identity key:

identitySubject = wallet.getPublicKey({ identityKey: true }).publicKey   // 33-byte compressed hex
  • A ProfileToken's subject MUST equal the identity_subject it represents.
  • A binding's certifier MUST equal the identity_subject it binds to.
  • Consumers and issuers MUST use the BRC-100 identity key as the root, NOT a connection/session key, a derived posting key, or a stored address. (The historical failure that motivated this spec was capturing a non-identity wallet pubkey as the root.)

Note on counterparty: BRC-100 getPublicKey/createSignature default counterparties differ (self vs anyone). Implementations MUST set counterparty explicitly when deriving the identity key and when verifying signatures, or verification will fail.


4. Signing-key binding attestation (normative)

A binding attestation proves that one signing key belongs to one identity. It is a BRC-52-shaped certificate with a mandatory mutual counter-signature, published directly on-chain as a Bitcoin Schema (MAP) output so it is both indexer-discoverable and TopicManager-parseable.

4.1 — On-chain shape (one OP_RETURN output)

OP_FALSE OP_RETURN
  MAP_PREFIX "SET"
  "app"            "overlay.social"
  "type"           "keybinding"
  "schema_version" "1"
  "subject"        <signing-pubkey hex>     // 33-byte compressed — the key being bound
  "certifier"      <identity-pubkey hex>    // == identity_subject — the root claiming it
  "serial"         <serialNumber>           // 16+ random bytes, hex
  "purpose"        "social-posting"         // binding scope
  "revocation"     "<txid>.<vout>"          // BRC-52 revocation UTXO (spend ⇒ revoked)
  "boundAt"        <blockHeightHint | iso>  // advisory
  "sig"            <certifier signature>    // identity (certifier) over preimage, DER hex
  "subjectsig"     <subject signature>      // signing key (subject) over SAME preimage, DER hex

Values are pushed as plaintext (§1.5). A binding output carries 1 satoshi (ordinal convention) or may be a 0-value OP_RETURN; the revocation UTXO is a separate spendable output the owner retains.

4.2 — Preimage (what both keys sign)

preimage = utf8( canonicalJSON({
  app, type, schema_version, subject, certifier, serial, purpose, revocation, boundAt
}) )

canonicalJSON = keys sorted lexicographically, no whitespace, UTF-8. sig and subjectsig are excluded. Both signatures are BRC-3 ECDSA over these exact preimage bytes (the BRC-100 createSignature/verifySignature methods apply their own hashing — issuer and verifier MUST pass the same raw preimage bytes).

Why BRC-3, not raw ECDSA. A BRC-100 wallet never signs with the root identity key directly (createSignature({identityKey:true}) is rejected). It signs with BRC-42-derived child keys, verified against the root + invoice number (BRC-3). The two binding signatures follow this, confirmed against a live BSV-Desktop wallet 2026-06-08.

4.3 — Validity rules (a binding is valid iff ALL hold)

  1. type == "keybinding", schema_version == "1".
  2. subject and certifier are well-formed compressed pubkeys; subject != certifier.
  3. sig (certifier attests): the identity ROOT signs via createSignature({data:preimage, protocolID:[1,'keybinding'], keyID:serial}) (counterparty defaults to anyone → publicly verifiable). Verify with ProtoWallet('anyone').verifySignature({data:preimage, signature:sig, protocolID:[1,'keybinding'], keyID:serial, counterparty:certifier}).valid. Proves the identity root signed, without exposing its private key.
  4. subjectsig (subject consents): the posting key signs via createSignature({data:preimage, protocolID:[1,'identity'], keyID:'1', counterparty:'self'}). Verify RAW: PublicKey(subject).verify(preimage, subjectsig). Proves control of the post-author key (= pecks.author).
  5. revocation outpoint is unspent (see §7).

Rule 4 prevents claiming a key you do not control. Rule 3 prevents binding a key to someone else's identity. Together: the mutual proof.


5. Overlay mechanics (normative)

5.1 — KeyBindingTopicManager (admit on tm_key-binding)

Admits an output iff it parses as a §4.1 binding and passes all §4.3 rules (signatures verified at admit-time). A binding MAY be admitted before/without a ProfileToken existing for certifier (the profile hydrates later); the binding itself is self-contained.

5.2 — KeyBindingLookupServicesigning_keys

CREATE TABLE IF NOT EXISTS signing_keys (
  signing_pubkey      TEXT NOT NULL,          -- subject
  signing_address     TEXT NOT NULL,          -- P2PKH(subject) — JOIN key vs pecks.author
  identity_subject    TEXT NOT NULL,          -- certifier == ProfileToken.subject
  binding_outpoint    TEXT NOT NULL,          -- where admitted
  revocation_outpoint TEXT,                   -- spend ⇒ revoked
  bound_at            BIGINT,
  spent               BOOLEAN DEFAULT FALSE,   -- UTXO lifecycle / revoked
  PRIMARY KEY (signing_pubkey, identity_subject)
);
CREATE INDEX IF NOT EXISTS idx_signing_keys_address  ON signing_keys (signing_address);
CREATE INDEX IF NOT EXISTS idx_signing_keys_identity ON signing_keys (identity_subject);

5.3 — Canonical rule (one key → one identity)

A signing_pubkey MUST resolve to at most ONE identity_subject at a time. If two valid bindings exist for the same signing_pubkey, earliest-admitted-unspent wins (the ProfileToken canonical pattern). Competing later bindings are ignored until the first is revoked.

5.4 — Lifecycle

Spending the binding_outpoint OR the revocation_outpoint sets spent = true; the binding stops resolving.


6. Resolution contract (normative)

pecks.author is unchanged (raw AIP address). Identity resolution joins through signing_keys:

pecks.author = signing_keys.signing_address   WHERE NOT spent
            → identity_subject
            → ProfileToken (profile, certs) + handles

6.1 — Endpoints (extend existing)

  • POST /v1/identities/resolve — for each input address: first look up signing_keys.signing_address → identity_subject and hydrate; fall back to the legacy address == P2PKH(subject) match. Ends "ghost authors."
  • GET /identity/:pubkey — add signingKeys: [{ pubkey, address, boundAt, bindingOutpoint }] to the response.
  • GET /identity/:subject/signing-keys — list bound keys for an identity.

6.2 — Social surfaces

Follow / @mention / notifications / following-feed MUST key on identity_subject (resolved via §6) rather than raw pecks.author, so they cohere across all bound keys.

6.3 — Dual discovery

The overlay endpoints are the trustless public truth. In authenticated contexts, consumers MAY additionally use the wallet's discoverByIdentityKey (BRC-100) for the user's own and peer certs with selective disclosure. Both resolve to the same identity_subject.


7. Revocation

Each binding carries a revocation outpoint (a UTXO the identity owner controls). Spending it revokes the binding (BRC-52 standard). The overlay marks the row spent and stops resolving it. Reuses the existing checkRevocation path (WoC /tx/<txid>/<vout>/spent, cached). To disconnect a key (e.g. after a wallet change), burn its revocation UTXO.


8. Backwards-compatibility

  • Posts are unchanged raw AIP; pecks.author and all existing indexers are unaffected.
  • The binding layer is additive: an app ignorant of it still renders posts (falling back to raw address / paymail). An app that understands it resolves the full identity.
  • Adoption is per-key and opt-in: a user attests the keys they want bound, when they want.
  • An unbound key is simply unresolved (renders as raw address) — an honest, graceful degradation, not an error.

9. Security

  • No key hijack: mutual subjectsig (§4.3 rule 4).
  • No identity hijack: certifier signature (§4.3 rule 3) — you can't bind to a root you don't own.
  • No double-claim: canonical earliest-unspent rule (§5.3).
  • Trustless serving: the TopicManager verifies both signatures before admit; the LookupService indexes only verified bindings. Consumers trust the overlay, not any single app.
  • Revocable: UTXO-spend, the BRC-52 trust model.
  • Federated trust: certifier is open; consumers choose which certifiers they accept. Self-certification is the floor; third-party vouching composes on the same structure.

10. Handles & Zooko — deliberately decoupled

Handle ownership/uniqueness is Zooko's Triangle (human-meaningful / decentralized / globally-unique — pick 2). It is a fundamental tradeoff, not solvable here, and MUST NOT block key-binding.

Principle this spec commits to: identity = pubkey (root); handle = a pluggable layer that points AT an identity; value/trust comes from attestation + reputation, not a global ownership lock. A handle is claimed by an identity and MAY be vouched by certifiers; multiple claims across namespaces can coexist, and the canonical one is decided by attestation/reputation — the same shape as key-binding.

handle_tokens (first-come on-chain) is one namespace and MAY coexist with others (paymail, third-party registries); all resolve handle → identity_subject. The full namespace model (authority, dispute, reputation) is a separate future spec. The bridge: once key-binding yields a coherent identity_subject, handle_tokens anchors on the identity rather than a single key.


11. Consumer integration contract

To resolve the identity behind a post: 1. Read pecks.author (raw AIP address) — unchanged. 2. POST /v1/identities/resolve { addresses: [author] }{ identity_subject, handle, displayName, avatarRef, ... } or empty. 3. If empty: render the raw address / paymail (graceful fallback). 4. For richer identity (certs, all signing keys): GET /identity/:identity_subject. 5. Key all relational logic (follow/mention/block/feed) on identity_subject, never raw address.

To bind a key (issuer/wallet flow): 1. certifier = getPublicKey({ identityKey: true }).publicKey (the identity root). subject = getPublicKey({ protocolID:[1,'identity'], keyID:'1', counterparty:'self' }).publicKey (the posting key the wallet AIP-signs with = pecks.author). 2. Build §4.1 fields; compute the §4.2 preimage bytes. 3. sig = createSignature({ data:preimage, protocolID:[1,'keybinding'], keyID:serial }) (certifier attests). subjectsig = createSignature({ data:preimage, protocolID:[1,'identity'], keyID:'1', counterparty:'self' }) (subject consents). Both are DER hex. 4. createAction: one OP_RETURN binding output (bindingToScript) + one retained revocation UTXO → wallet funds + broadcasts → submit BEEF to overlay /submit with x-topics: tm_key-binding (or let the broadcaster forward it) → admitted.


12. Conformance — what an implementer MUST do

  • Indexer/overlay: implement KeyBindingTopicManager (verify both sigs at admit) + KeyBindingLookupService (signing_keys, canonical rule, revocation lifecycle); extend the three §6.1 endpoints.
  • Wallet/issuer: implement the §11 binding flow with dual signing and explicit counterparty.
  • Consumer app: resolve via §11 and key relational logic on identity_subject.
  • Everyone: treat pecks.author as raw AIP; never assume address == P2PKH(subject) — always resolve through signing_keys.

Core: identity is a pubkey; everything human (profile, name, the posts you write across many keys and many wallets) hangs off it by public, mutual, revocable, federated attestation — served trustless over an open overlay, additive over plain AIP. That is BRC > AIP made real: not a signature, but an open, multi-key, verifiable identity layer over Bitcoin Schema content.

Documentpeck-identity — identity model, v1 draft
Drawing №OS-IDN-01
Rev.A
Date2026-06-08
LicenseOpen BSV v5