docs: capture offline root signing design

This commit is contained in:
Jordan Mecom
2026-07-10 15:41:00 -04:00
parent 19919e1810
commit c34028ad3b
+140
View File
@@ -0,0 +1,140 @@
# WIP: Offline-root signing
Status: implementation paused while we confirm what “generic delegated
signing” should mean.
## What we are trying to protect
A Buzz account has a routine key that remains available to the desktop app and
may also be accessible to agents. That key should continue to sign ordinary
Buzz events. For a sensitive action such as approving a pull request, we want
an additional signature from an offline root.
The original PR event and its updates are already signed and content-addressed.
The offline root therefore does not need to sign another copy of the payload or
participate in a separate request/policy workflow. It needs to sign the exact
event ID it reviewed.
A Nostr event has one author and one signature, and its ID commits to that
author, its tags, and its content. We cannot append a second signature to an
existing event without changing it. The root must publish a new signed event
that references the original event.
## Delegation and countersigning are different
Tyler's comment that we will probably want this generically for delegated
signing changes where the primitive should live, but it does not bring back the
generic approval system.
There are two related operations:
- **Delegation:** a root authorizes another key to sign some class of future
events on its behalf.
- **Countersigning:** a root reviews an event that already exists and signs a
reference to that exact event.
Ordinary delegation alone does not solve the high-assurance problem. If the
routine key is compromised, an attacker can use whatever authority was
delegated to it. High-assurance approval requires the second operation: the
offline root must countersign each exact sensitive event.
The two operations can share a generic representation of the relationship
between an account and its signing authority, but their verification rules and
security properties must remain distinct.
## Proposed minimal primitive
The reusable protocol needs only two signed objects in addition to the event
being approved.
First, the routine account publishes a root link. The link contains a root
signature over the routine pubkey, and the routine profile signs the link in
turn. This proves that both keys agree on the relationship.
Second, the root publishes an endorsement with:
- the exact subject event ID;
- the exact account/root-link event ID;
- the routine account represented by the root; and
- a short signed purpose such as `approve`.
The generic verifier checks the profile, subject, and endorsement signatures;
the root-to-account link; the exact references; and the signed purpose. It does
not know anything about pull requests.
The relay may use that same verifier before storing an endorsement. It should
validate the signing relationship and referenced events, but it should not
implement PR-specific rules.
## Pull requests become a thin consumer
For a pull request, the subject is the current signed revision:
- the latest trusted kind `1619` update, when one exists; or
- otherwise the original kind `1618` pull-request event.
The PR code treats a valid endorsement with purpose `approve` as an approval by
the routine account. If a new update is published, its event ID is different,
so an endorsement of the previous revision no longer counts. No separate
staleness or payload-digest mechanism is required.
The desktop should export the subject event and account link as an offline
signing package. The root secret remains on the offline device. The desktop
only imports the resulting public endorsement event.
## What Tyler's comment changes
The underlying SDK and wire format should use generic event-signing language,
not PR-specific types. The offline CLI should likewise sign an arbitrary exact
event package. PR code supplies the subject and interprets `approve`.
The current direction is therefore mostly right, but any checks for PR kinds or
the literal `approve` action belong in the PR feature, not in the generic
endorsement builder or publisher.
This does **not** justify adding:
- approval policies;
- request and decision records;
- thresholds or quorum calculation;
- a generic approval inbox;
- copied payloads and payload digests; or
- a separate proof-object hierarchy.
Those features solve different problems and were responsible for most of the
earlier size and complexity.
## Root rotation and revocation
Changing or removing the root must itself require authorization from the
current root. Otherwise a compromised routine key could remove the protection
before performing a sensitive action.
We still need to decide how rotation affects old endorsements. There are two
reasonable meanings:
- The endorsement proves that the root was valid when it signed. It remains
valid historical evidence after rotation.
- An endorsement may authorize a pending action only while its referenced root
link is current. Rotation immediately prevents old-root endorsements from
authorizing new execution.
For PR admission, the second rule is safer. We can preserve the old event as
historical evidence while requiring the current account/root link when deciding
whether the PR may merge.
## Questions to settle before continuing
1. Did Tyler mean classic future-event delegation, exact-event countersigning,
or a shared protocol that supports both?
2. Should the account/root relationship remain a tag on the routine profile,
or become a dedicated replaceable event reusable by other signing features?
3. Is the first version intentionally one root per account, or must it support
multiple scoped delegates now?
4. Should active authorization always require the currently linked root, while
preserving older endorsements only as audit history?
My recommendation is to ship one root per account and one generic exact-event
endorsement. Use it first for PR approvals, keep PR interpretation outside the
generic verifier, and defer policies, quorum, and broader delegation until a
concrete second consumer requires them.