Repository navigation
Conversation
The handshake signature covered identity only. nests, hosts, can_relay and free_slots are parsed after verification and were never covered by it, so any peer completing a signed handshake could over-claim relay rights or capacity, and both feed real decisions. Adds a second signature over the claims under a separate domain. The identity signature is untouched, so a peer verifying the old message still verifies it and existing peers keep working. A hello with no capsSignature is admitted and marked unsigned — backward compatible. A hello with one that fails verification is rejected outright rather than degraded, since a peer that signs the wrong thing is broken or lying and either way the claims cannot be trusted. free_slots is not signed: it is capacity telemetry that changes after the handshake, so binding it would make a valid signature stale immediately. can_relay, nests and hosts are the authority claims and are the ones now bound.
A socket hello declares `hid` as a bare string and humd parses it. There is no proof it belongs to the bee claiming it. A bee could send another bee's hid and inherit its reconnect-dedup slot, and the registry keyed by identity has no way to tell a real hid from a typed one. Same shape as the peer handshake gap fixed earlier, one transport down. Bees already hold an Ed25519 key — BeeKey, whose pubkey hashes to the hid they already send. It was just never used to sign anything on this path. BeeKeys can now sign a socket hello. humd verifies when the fields are present, under its own domain so a peer-handshake signature cannot be replayed onto a socket hello. Checks: hid parses, carries a bee prefix, is the sha256 of the signing pubkey, timestamp is inside the skew window, signature verifies. Unsigned stays admitted. Most hives send no signature today and this would break every one of them if it required the fields; marked hid_verified: false instead so callers routing on identity can tell proven from self-declared. A signature that is present and wrong is a hard reject — a bee that signs the wrong thing is broken or lying. Wire naming stays socket-flavoured (`hid`, `pubkey`, `signedAt`, `signature`) rather than borrowing the peer handshake's. Different transport, and renaming would churn every hive for nothing. Rule 0: removing the signature check fails 2 tests; removing the hid/pubkey match first failed none, which is how the test for it got written — a valid signature over a hid the signing key does not own was previously uncaught. Both now fail when their gate is removed. No hive sends a signature yet. The path is inert until one does.
Five tests driving humd's hello arm with a real signature: a valid one is admitted, a forged one is refused, one signed over a hid the key does not own is refused, a stale one is refused, and an unsigned one is still admitted. The unsigned case is the one that protects every existing hive, and the claiming-another's-hid case is the one the pubkey/hid binding exists for — neither was reachable from the unit tests, since they assert on the decision humd actually makes rather than on the parse. Rule 0: neutering humd's reject branch fails exactly the three refusal tests and leaves both admission tests passing.
thrum_broadcast sent to anyone owning a sigil, and to everyone owning none when nobody matched. Neither looked at what the client said it speaks. A forager that only advertises humfs_* still received every prompt, chunk and tool-call the session produced — declared scope was recorded in the manifest and then ignored for delivery. Records the declared chis on the Reach at hello time and filters both the sigil pass and the no-owner fallback on them. A client that declares nothing is unfiltered, not starved. Every bee predating this field sends an empty set, and an empty list means "unspecified" rather than "wants nothing" — refusing it traffic would be a wire break dressed as a security fix. Re-declaring replaces the previous set rather than merging, so a re-hello that narrows scope actually narrows it. Scoped thrum_all too: it has no callers today, but leaving it unfiltered would make it a bypass for anyone who reaches for it next. Rule 0: neutering declares_chi fails the scoped-rejection test and leaves both delivery tests passing. Workspace 381 -> 393, no failures.
A bee's hid is the hash of its public key. That is pseudonymity without accountability — no authority behind a name, and no way to withdraw one that behaves badly. Nothing was revocable. The operator lists hids in $XDG_STATE_HOME/hum/revoked.json and humd refuses them at hello: closed connection, error echo, not registered and not announced. Not merely hidden from discovery, which leaves the socket open to the bee it was meant to exclude. Checked whether or not the bee proved the hid. The evasion this closes is the obvious one: stop signing, claim the same string, and watch a proof-only check pass by. The revoked string is refused however it is presented. A corrupt file revokes nothing rather than everything. Failing closed would lock every bee out of the socket — a daemon that admits nobody is a worse failure than one that admits a revoked bee, and the operator can act on the parse warning. The list is read once at boot, not per hello. Re-reading puts disk on the admission path, which is the one path that must not stall; a revocation takes effect on next start. Path is on DaemonConfig so tests can point at a tempdir instead of the developer's real state. What this does not do: bind across the mesh. A bee refused here can still reach another daemon. That gap needs the on-chain registry to carry revocations, which is a contract change. Worth noting the issue describes HumdRegistry as "currently used for optional manifest anchoring" — it has no callers anywhere in the tree, behind an `onchain` feature flag. So there was nothing to hook into. Rule 0: neutering the check fails the two refusal tests and leaves the control passing. Workspace 393 -> 400, no failures.
A revocation list nobody can reach is not a feature. The list was written by the test harness and by nothing an operator has. humctl revoke <hid>..., humctl unrevoke <hid>..., humctl revocations. Idempotent: revoking a listed hid says so rather than duplicating the entry, and unrevoking an absent one is not an error. The on-disk type moves to hum-paths, next to RuntimeInfo which is already the shared serde state file both sides read. humd keeps a thin wrapper for the admission check; humctl gets the edits without linking the daemon's dependency tree. Both verbs say restart humd afterwards, because the daemon reads the list once at boot. That is a deliberate trade — re-reading per hello puts disk on the admission path — but it is a footgun if left implicit. Workspace 400 -> 404, no failures.
This was referenced Oct 18, 2026
Removed 77 inline comment lines. The behaviour is unchanged; 404 tests still pass and no gate moved. Kept the doc comments. Those describe the contract rather than the code — what `SocketIdentity::Unsigned` permits, why the two signature domains must not cross, what a corrupt revocation file costs. `hives.rs` runs 105 doc-comment lines against 161 lines of code, so this is the house style and removing it would leave the new security surface undocumented at exactly the points a reader cannot infer. Removed the rest: "the peer signed can_relay: false, then ships can_relay: true" restates the next two lines. "a re-hello that narrows scope must actually narrow" restates the test name above it. Where a rationale was real but is now expressed by placement — revocation is checked outside the identity match, so "checked whether or not the bee proved the hid" is visible in the structure — the structure carries it. Also collapsed a nested if in `Revocations::write_to` to match the existing `RuntimeInfo::write` three lines above it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First slice of #66 — the cheapest of the four, and the one that was already a live gap.
The hole
The handshake signature covers identity only:
nests,hosts,can_relayandfree_slotsare all parsed afterverifying_key.verify(...)returns. Nothing checks them against the signature. So any peer that completes a valid signed handshake can claim any capacity, any relay rights, and any hostname — and those feed real decisions.Same class of defect as the model claim in #67, one layer down. #67 stopped prompts routing on an unverified claim; this stops the peer handshake carrying unverified claims.
What changes
A second signature over the claims, under a separate domain (
hum-ensemble-caps-v1).The identity signature is untouched. Changing
handshake_messagewould break every existing peer's verification — that's a wire break, not a patch. An old peer verifyingsignaturesees exactly the message it always did.Backward compatible. A hello with no
capsSignatureis admitted and markedcaps_signed: false. An old peer sends none and keeps working.A bad signature is a hard reject, not a degrade. A peer that signs the wrong claims is either broken or lying. Either way the claims can't be trusted, so the hello fails rather than falling back to "unsigned but admitted".
What is and isn't signed
Signed:
nests,hosts,can_relay. These are authority claims.Not signed:
free_slots. It's capacity telemetry that changes after the handshake — binding it would make a valid signature stale the moment a slot is used. Worth a decision if anyone wants it covered; I left it out deliberately rather than quietly.Proof
Six tests in
ensemble/src/handshake.rs. Rule 0 — verification removed → 4 of 6 fail:Restored: 6/6. They cover the cases that matter:
can_relayafter signinghostsafter signingFull workspace: 55 binaries, 0 failures.
ensemble --lib108 → 114.Pre-existing, not touched
clippy -D warningsis red onhum-nest,thehumandhum-identity— 8 + 5 errors on cleanmain, unchanged by this branch. CI only lints-p ensemble -p hum-paths, so none of it blocks. Flagging because it's a release-readiness item, not because it belongs here.Refs #66