Primary live updates are on ngit: ngit repository
This project will be updated more actively on ngit than GitHub.
PoC web client showing key rotation from old_npub -> new_npub using guardian quorum attestations.
- NIP-17 DMs (gift-wrap) for request + partial replies
- 2-of-3 guardian threshold demo
- Minimal 3-role UI:
- Requester (new key)
- Guardian
- Observer/Verifier
- Rotation proof event published publicly (custom kind
39089) - Verification against guardian policy group public key
- Trusted dealer setup generates 2-of-3 shares of one group secret.
- Guardians produce partials over canonical rotate message hash:
H("rotate|old_npub|new_npub|nonce")
- Requester aggregates partials into one Schnorr-style proof
(R, z). - Verifier checks:
z*G == R + c*XwhereXis guardian group pubkey.
This is a PoC threshold Schnorr model, not hardened production FROST.
npm install
npm run dev
Open local URL from Vite.
Default relay set in UI is now:
wss://nos.lolwss://relay.primal.netwss://relay.snort.socialwss://relay.nostr.bandwss://purplepag.es
If one relay is blocked/unreachable (e.g. regional/network issues), keep multiple relays enabled.
- Click Run scripted demo.
- See state transition and verified proof in the proof panel.
- Open Demo Walkthrough tab to view:
- trusted guardian box
- step-by-step status with checkmarks
- compact demo state summary
⚠️ Demo-only. Do not use real recovery phrases, real identity keys, or important accounts.
Open the app in 5 browser tabs/windows:
- 1 tab = Requester
- 3 tabs = Guardian (one per guardian)
- 1 tab = Observer
Use the same relay list in all tabs.
Use these words only as operator labels (human memory aid):
- Guardian #1 → Pancho
- Guardian #2 → Climbing
- Guardian #3 → Yolo
These words are not the cryptographic share themselves. They just help you track which guardian tab is which.
- Example old npub:
npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2u6x9q
- Example group pubkey:
02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Requester → Guardian npubs field:
1,npub15epzy875zp9gpsjvlzzgjesmewfe7zr75vpyvq982qy23ey5n02qx60kj4,02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2,npub1mtd0v34lycdf9xvny4fy3y0wrng6mag7geurfp0q854lway63uls7vnzrs,02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
3,npub1ykcexcmxxxwf57vx5z9j459fse24ath5dw7qj2h74va8w65ylrgq624ddz,02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Guardian #1 (Pancho) share JSON:
{"id":1,"share":"1111111111111111111111111111111111111111111111111111111111111111","threshold":2,"groupPubkey":"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
Guardian #2 (Climbing) share JSON:
{"id":2,"share":"2222222222222222222222222222222222222222222222222222222222222222","threshold":2,"groupPubkey":"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
Guardian #3 (Yolo) share JSON:
{"id":3,"share":"3333333333333333333333333333333333333333333333333333333333333333","threshold":2,"groupPubkey":"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
### guardian-share (automatic provisioning)
Requester -> guardian secret share delivery is done via an encrypted NIP-17 DM:
```json
{"type":"guardian-share","version":1,"group_id":"g_<opaque>","guardian_id":2,"threshold":2,"group_pubkey":"<hex>","share":"<64-hex>","created_at":1772800000}
Guardians ingest this from DM history and persist it into localStorage["guardian-share-map-v1"]["<group_id>:<guardian_id>"] so you can simulate multiple guardians in one browser profile.
---
### Step 1 — Requester sends rotation request
In **Requester** tab:
1. Paste `old npub`
2. Click **Generate new key** (or paste new nsec)
3. Paste guardian lines into **Guardian npubs**
4. Set reason (e.g. `key compromise`)
5. Click **Send rotation request via NIP-17 DM**
Expected result:
- Status shows request sent.
### Step 2 — Guardians receive request message
In each **Guardian** tab:
1. Set that guardian’s nsec
2. Paste that guardian’s share JSON
3. Click **Refresh incoming requests**
Expected result:
- You should see a `rotation-request` in guardian inbox.
- This is the “message came through” check.
### Step 3 — Guardian confirm or deny decision
Current UI behavior:
- **Confirm path supported directly**: click **Confirm first pending request** (sends a partial signature).
- **Deny path for now**: do not click confirm in that guardian tab (treated as no approval).
For a 2-of-3 demo:
- Confirm in any 2 guardian tabs (e.g. Pancho + Climbing)
- Leave the 3rd unconfirmed (Yolo)
### Step 4 — Requester collects partials
Back in **Requester** tab:
1. Click **Refresh DMs**
2. Verify partials appear under **Collected partials**
3. Click **Aggregate 2-of-3 proof**
Expected result:
- Proof panel shows `valid_local: true`.
### Step 5 — Publish proof
In **Requester** tab:
1. Click **Publish proof event**
Expected result:
- Status says proof published.
### Step 6 — Observer verifies
In **Observer** tab:
1. Paste policy JSON (or use one generated in Requester)
2. Click **Fetch proof events**
Expected result:
- Output includes proof entries with `valid: true` when verification succeeds.
---
> Note: Example keys/shares above are intentionally fake for walkthrough UX. Use generated, matching values for realistic crypto tests.
## Mini glossary (technical terms)
- **old_npub**: the current/old public identity you are rotating away from.
- **new_npub**: the new public identity you want others to follow after rotation.
- **nsec**: private key in Nostr bech32 format. Keep secret.
- **nonce**: one-time unique value to prevent replay and make each rotation request unique.
- **guardian**: a trusted account participating in rotation approval.
- **threshold (2-of-3)**: minimum number of guardian approvals required (2 out of 3).
- **share**: a guardian’s private threshold key share (secret scalar). Each guardian has a different one.
- **groupPubkey**: public key of the guardian threshold group (derived from group secret). Verifiers use this to check the final aggregate proof.
- **partial signature**: a guardian’s contribution toward the final threshold signature.
- **aggregate signature/proof**: final combined signature built from enough partials.
- **rotation proof event**: public Nostr event containing old/new npub mapping + proof data.
- **NIP-17 DM**: private Nostr DM transport using gift-wrap events.
## Tests
```bash
npm test
- Uses Vitest
- Covers threshold signing/verification and Nostr helper utilities
implementation/ contains vendored client integrations for demo UX work:
implementation/coracle(Coracle)- demo route:
/key-rotation-demo
- demo route:
implementation/yakihonne(official YakiHonne web app)- demo route:
/key-rotation-demo - supports end-to-end demo flow:
- send
rotation-request - guardian DM Confirm
- collect matching partials
- aggregate proof
- publish proof event
- send
- run:
cd implementation/yakihonne pnpm install pnpm dev
- demo route:
index.htmlUIsrc/main.jsrole workflowssrc/nostr.jsNIP-17 and relay opssrc/frost.jsthreshold Schnorr PoC mathtest/Vitest suite