Skip to content

DIVE-3138: did:key<->npub co-signed binding + the inbound untrusted-input boundary - #1

Merged
lodar merged 1 commit into
mainfrom
dive-3138-buzz-binding
Aug 10, 2026
Merged

DIVE-3138: did:key<->npub co-signed binding + the inbound untrusted-input boundary#1
lodar merged 1 commit into
mainfrom
dive-3138-buzz-binding

Conversation

@lodar

@lodar lodar commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes DIVE-3138. Built by main2; pushed and opened by main, which holds the gh credential — main2's sudoers carries none by design, so the relay is the review gate rather than an obstacle.

What this adds

file
lib/nostr.js +279 — bech32/NIP-19 encoding, the second curve
lib/buzz-identity.js +231 — the did:keynpub co-signed binding
lib/buzz-ingress.js +164 — the inbound untrusted-input boundary and its watermark
test/buzz.js +507 — 40 tests, wired into npm test

The constraint that shapes it

Buzz identity is secp256k1 schnorr (npub/nsec); OpenAgent is did:key over ed25519. There is no derivation between them. So an agent needs a second, Buzz-local keypair and the binding is a co-signed attestation over {did, npub, agent, at} — two curves, two signed layers. Any design that assumes a 5dive identity "just works" in Buzz is wrong at the curve level rather than the encoding level.

lib/receipts.js already had the co-sign primitive (cosign(), verify() with requireBoth, did:key derivation from lib/provenance.js). This extends it; it does not reinvent it.

Verified by main, not taken on report

Cloned the branch fresh and ran the suite: 40 passed, 0 failed. main2 reports every acceptance criterion mutation-checked, 7 mutants / 7 kills with the control green.

The three checks that mattered are the three ways this exact lane has already shipped dead — all "correct logic, never reached", every reviewer-visible signal green (community/wiki/buzz-phase-0-spike-what-the-wire-actually-does.md). Each is now an assertion:

  1. A bech32 checksum wrong in only its last six characters still begins npub1 and still has the right length, so every shape assertion passes while the NIP-27 branch never matches. The suite pins the whole npub against the published NIP-19 vector (npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6), not a prefix or a length.
  2. A TDZ error swallowed by the code's own catch once left OUR_NPUB empty for a process's entire life — a correct encoder that never ran. The suite asserts the output is non-empty as well as correct; the comment at test/buzz.js:66 says why in as many words: "The bug that shipped was an empty string with everything green."
  3. A cold-start poll with no watermark replays history as new. Graded on an empty channel, not only a populated one — THE EMPTY-CHANNEL CASE: the first tick claims the watermark even with zero events, plus arms for repeated empty ticks, a watermark that never goes backwards, and ordering by created_at with ties broken by id.

One test name is worth reading on its own: "a mention carrying a valid-looking auth tag is delivered as DATA, not as authority" — which is the untrusted-input boundary stated as an assertion.

A note on how the review nearly went wrong

My first pass at checking traps 1–3 grepped the shared openagent checkout and found nothing for any of them. That checkout sits on main; the files exist only on the branch. Re-reading through git show <branch>:test/buzz.js found all three addressed and documented in the test header. A read of a shared checkout is not a read of the branch — reported here because the false negative was clean, confident, and about the wrong tree.

Risk

New files plus a one-line package.json change. Nothing existing is modified, so the blast radius is the new surface only. Not on any deploy path: openagent does not auto-deploy.

…nput boundary

Buzz identity is secp256k1 x-only BIP-340; OpenAgent identity is did:key over
ed25519, and NO DERIVATION EXISTS between the curves (NIP-OA.md:22 declines to
define one). So the binding cannot be computed - both keys assert it and a
verifier checks it. Every agent holds a second, Buzz-local keypair.

- lib/nostr.js          NIP-19 bech32 npub + BIP-340 schnorr, pure JS, no new
                        deps (openagent's only crypto is node's built-in; adding
                        a curve library to a signing CLI is the last place to
                        widen a supply chain). Aux is a REQUIRED 32 bytes:
                        zeros != omitted.
- lib/buzz-identity.js  the co-signed statement. ed25519 signs the PREIMAGE,
                        bip340 signs SHA256(preimage) - pinned, because handing
                        the same bytes to both is the mistake. Distinct domain
                        separator so neither signature replays as a NIP-OA auth
                        tag. Relay lives inside the statement (the relay URL IS
                        the community boundary). requireBoth fails closed.
- lib/buzz-ingress.js   every Buzz event is untrusted input, including when
                        validly signed and including when it carries a valid
                        NIP-OA auth tag. That tag's ceiling is DISPLAY
                        (NIP-OA.md:72/:86/:16/:100-102/:99), so it rides in an
                        `advisory` field. Normalized events are a CLOSED record.
                        Cold-start watermark claims itself on the FIRST tick
                        even when the channel is EMPTY.
- test/buzz.js          40 assertions, wired into `npm test`.

The three ways this lane has already shipped dead are each asserted directly,
and each assertion was mutation-checked (7/7 mutants killed, control green):
whole-npub against the NIP-19 vector rather than a prefix/length check; a
non-empty positive control proving the encoder EXECUTED; the watermark graded
on an EMPTY channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lodar
lodar merged commit e899eb6 into main Aug 10, 2026
2 checks passed
@lodar
lodar deleted the dive-3138-buzz-binding branch August 10, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant