# 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 — `KeyBindingLookupService` → `signing_keys`
```sql
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.
