The fleet key a paired browser was never allowed to be given - #44
Merged
Conversation
A browser pins the fleet public key from the QR at pairing time and from nowhere else. That was right and it was also final: a browser that paired while its machine could not sign pinned nothing, so it read no directory and listed the single machine it paired with, for good. The device certificate had exactly this problem and rides the welcome to solve it. The fleet key belongs on the same path, for the same reason: a relayed connection is a Noise IK session opened against a static key the browser pinned out of band, so what arrives on it is an authenticated statement from a party the browser already trusts. Sent whenever this daemon holds a key — read live, so a machine joined under a running daemon answers with what it can sign under now — and omitted when it holds none. The public half only; the seed never leaves the machines. Which sessions may *keep* it is a property of the channel, and the client is the end that knows which channel it is on. That half is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last forced re-pairing. A browser that paired before its machine had a working fleet key pinned none, so it read no directory, listed the one machine it paired with, and the only cure was another ceremony — on a fleet where every machine had been joined correctly. The prevailing claim was that a fleet key cannot be re-supplied without becoming trust-on-first-use. True of an arbitrary connection, too strong for this one: a browser that pinned a machine's daemon static key has an authenticated channel to it, because IK names that key as the responder's static and message B decrypts only for the peer holding its private half. A relay cannot forge the session, and a daemon that could lie holds the fleet seed already and can mint a certificate for any key it likes. So the key is adopted from a welcome, under one condition and no other: the session must be keyed to a daemon key this browser pinned itself. Never from a machine reached on a certificate — that certificate verified under the very key being replaced — and never from loopback, which authenticates a cookie and no key at all. FleetSource.pinned carries the fact from the only place that knows it, the code that built the transport, and relayBoot now states it in its return type so a second way of reaching a machine cannot acquire it by accident. The key is taken before the certificate on the same welcome, because the certificate verifies under it — so the browser that has been stuck longest is whole again from one message — and the fleet re-expands in session, the way a re-supplied certificate already does. The band that said "pair it again from any machine on the fleet" now says the two things that are still true: nothing has answered with a key yet, or this tab never paired with anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document said a browser pins the fleet key from the QR and stopped there, which left the browser the QR reached too early with no described way back — and no statement of why the wire is now allowed to carry one. Both stated: the rule, and the reason it is not trust on first use. Also what it excludes, since that is the load-bearing half — a machine reached on a machine cert, whose Noise key the fleet key itself vouched for, and loopback, which authenticates a cookie and no key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
A browser that paired before its machine had a working fleet key pinned no
fleet public key. That was terminal:
fleet.tsreturns an empty fleet withoutone, so the browser read no directory, listed the single machine it paired
with, and the only cure was another pairing ceremony — on a fleet where every
machine had been joined correctly with
--secret/--fleet. Machines werefine; devices were stuck.
Why "the fleet key cannot be re-supplied" was too strong
The claim in the code was that a key learned from a connection is a key the
connection chose, so accepting one would be trust-on-first-use. True of an
arbitrary connection. Not true of this one.
A browser that pinned a machine's daemon static key at a ceremony has an
authenticated channel to that machine. Noise IK names the pinned key as the
responder's static and mixes it into
esandss, so message B only decryptsfor a peer holding its private half (
web/src/crypto/noise.ts,internal/crypto/handshake.go). A key arriving on that session is anauthenticated statement from a party the browser already trusts out of band —
not an assertion from an unknown peer.
the session; a welcome it wrote never decrypts.
relay.jsononevery machine, so a machine that could lie about this key can already mint a
device certificate for any key it likes. It holds full fleet power already;
handing over a wrong key is not an escalation.
unchanged, and correct.
The precedent was already in the tree: the device certificate rides the welcome
for exactly this reason. The fleet key belongs on the same path.
The exact trust condition
It is a parameter (
adoptFleetKey(w, pinnedDaemon)), not an inference, becauseby the time a welcome arrives every session looks alike.
FleetSource.pinnedcarries the fact from the only place that knows it — whoever built the
transport:
pinnedrelayBoot,fleetSourcespin)/ws(session cookie, no key)relayBoot's return type now sayspinned: trueexplicitly, so a future secondway of reaching a machine cannot inherit the privilege by accident.
An existing pin is never overwritten. Two keys differ only if the fleet was
set up again, and this browser's device certificate is then signed by a key
nothing honours — adopting the new one would swap a browser that lists what it
can reach for one that lists what it cannot. The mismatch goes to the console
rather than being swallowed; pairing again (the only thing that mints a
certificate) stays the way out.
What heals now, and what still needs a ceremony
Heals automatically, in-session, no reload:
machine. The welcome carries the key and the certificate; the key is taken
first because the certificate verifies under it, so a browser holding neither
is whole from one message. The fleet then re-expands the way
#43'scertificate adoption does (
resupplied, one rebuild per epoch) and the othermachines appear while the tab is open.
Still needs a ceremony, legitimately:
points at the relay's address rather than its own.
The band retired for the healed case. It used to read "Pair it again from any
machine on the fleet and they all appear", which is now false. It splits on
FleetGaps.pinned— the count of ceremonies this browser performed: at zero itpoints at the relay's address, above zero it says a machine will hand the key
over as soon as it answers, and that nothing has yet. The stale comments in
fleet.tsandcrypto/keys.tsthat described a path which could not work nowdescribe the one that does.
Test evidence
Adversarial, because the refusals are the feature:
asserted through the real builder, which marks that source
pinned: false;no-op that warns about nothing.
End to end (
a browser paired before its machine had a fleet key): a browserwith a pinned daemon key for one machine and no fleet key connects, and ends up
holding the fleet key and seeing every machine in the directory — real builder,
real key store, real certificate verification, only the transport and the
directory response stubbed. The mirror case asserts a ride this browser never
pinned keeps nothing and reads no directory.
Fails against the un-fixed code: the end-to-end heal, plus the two
adoptFleetKeycases that assert a key is kept. Verified by stubbing theadoption out — 3 failed, 55 passed. Every refusal case passes either way, which
is the point of them.
Go:
TestWelcomeCarriesTheFleetPublicKey— omitted before the fleet exists,carried after a key arrives under a running daemon (the live read
#43added),public half only, never the seed.
🤖 Generated with Claude Code