Documentation
¶
Overview ¶
Package crypto owns flue's Noise IK handshake, the secure channel framing, and the key material on the daemon side. Pure: no HTTP, no WebSockets, no knowledge of sessions or transports.
Index ¶
- Variables
- func DeviceID(publicKey []byte) string
- func LoadOrCreateStaticKey(configDir string) (noise.DHKey, error)
- func Suite() noise.CipherSuite
- type Channel
- type Device
- type DeviceStore
- func (s *DeviceStore) Add(label string, publicKey, cert []byte) (Device, error)
- func (s *DeviceStore) AddFromFleetCert(label string, publicKey, cert []byte, pairedAt time.Time) (Device, error)
- func (s *DeviceStore) AddRevocation(publicKey, cert []byte) error
- func (s *DeviceStore) FindByID(id string) (Device, bool, error)
- func (s *DeviceStore) FindByKey(publicKey []byte) (Device, bool, error)
- func (s *DeviceStore) IsRevoked(publicKey []byte) (bool, error)
- func (s *DeviceStore) List() ([]Device, error)
- func (s *DeviceStore) Relabel(publicKey []byte, label string, cert []byte) (bool, error)
- func (s *DeviceStore) Remove(id string) (Device, bool, error)
- func (s *DeviceStore) RemoveByKey(publicKey []byte) (Device, bool, error)
- func (s *DeviceStore) Revocations() ([]StoredRevocation, error)
- func (s *DeviceStore) SetCert(publicKey, cert []byte) (bool, error)
- func (s *DeviceStore) SetOrigin(publicKey []byte, origin string) (bool, error)
- func (s *DeviceStore) UpdateLastSeen(id string, now time.Time) (bool, error)
- type StoredRevocation
Constants ¶
This section is empty.
Variables ¶
var ErrDeviceRevoked = errors.New("crypto: this device key is revoked")
ErrDeviceRevoked is AddFromFleetCert refusing a key on the revocation list. A revocation permanently outranks a device cert for the same key, whatever either one's timestamp says (spec/fleet-trust.md): the check is membership, never a time comparison, and it happens in the same critical section as the add so a concurrent revoke cannot slip between them.
Functions ¶
func DeviceID ¶
DeviceID derives the identity from the key itself, so an entry cannot claim to be a key it does not hold.
func LoadOrCreateStaticKey ¶
LoadOrCreateStaticKey returns the daemon's static keypair, creating it on first run. A file that exists but cannot be parsed is an error, never a regenerate: a fresh key would silently invalidate every pairing.
func Suite ¶
func Suite() noise.CipherSuite
Suite is the one cipher suite flue speaks: Noise_IK_25519_ChaChaPoly_SHA256.
Types ¶
type Channel ¶
type Channel struct {
// contains filtered or unexported fields
}
Channel is one authenticated, ordered, end-to-end encrypted pipe. Nonces are the CipherStates' own strictly incrementing counters; the transport underneath is a WebSocket, which is ordered and reliable, so any Open failure means tampering or replay and the caller must tear the connection down — there is no recovery path by design.
func InitiatorHandshake ¶
func InitiatorHandshake(static noise.DHKey, peerStatic []byte, payload []byte, rng io.Reader, recv func() ([]byte, error), send func([]byte) error) (*Channel, error)
InitiatorHandshake runs the browser side of the handshake, sending payload in message A (nil for none). It exists in Go for tests and the vector generator; production initiators are TypeScript. rng == nil means crypto/rand.
func ResponderHandshake ¶
func ResponderHandshake(static noise.DHKey, rng io.Reader, recv func() ([]byte, error), send func([]byte) error) (*Channel, []byte, []byte, error)
ResponderHandshake runs the daemon side. The returned public key is the initiator's static — the device identity the caller authorizes against the device store — and payload is message A's decrypted payload, empty when the initiator sent none. Callers must treat an error as fatal to the connection and process no frames before the handshake completes.
type Device ¶
type Device struct {
ID string `json:"id"`
Label string `json:"label"`
PublicKey []byte `json:"publicKey"`
PairedAt time.Time `json:"pairedAt"`
LastSeen time.Time `json:"lastSeen"`
// Cert is the fleet device certificate for this key — the signed blob
// exactly as internal/fleet produces it — when there is one: minted by
// this machine's own pairing ceremony, or presented by the device in
// its handshake after pairing elsewhere (spec/fleet-trust.md). Opaque
// to this package, kept so the machine can hand the cert onward — the
// fleet directory will publish it — and empty for devices paired
// before the fleet key existed.
Cert []byte `json:"cert,omitempty"`
// Origin is where this device first reached this machine from — the
// Origin header on its enrolment POST, e.g. "http://localhost:7719".
// A hint for telling otherwise-identical rows apart, never an identity:
// it is whatever the browser sent, it is only known for devices that
// enrolled over loopback, and empty is the ordinary state of every
// other row — one paired by ceremony, one admitted on a fleet cert, or
// one from before origins were recorded. First seen wins (SetOrigin),
// and it stays out of the certificate: it is a fact about how this
// machine was reached, not about the device.
Origin string `json:"origin,omitempty"`
}
type DeviceStore ¶
type DeviceStore struct {
// contains filtered or unexported fields
}
DeviceStore is the paired-device registry: devices.json in the config dir, 0600, re-read under the lock on every call so concurrent daemon paths (pairing, revocation, the devices op) serialize on the file's truth.
func NewDeviceStore ¶
func NewDeviceStore(configDir string) *DeviceStore
func (*DeviceStore) Add ¶
func (s *DeviceStore) Add(label string, publicKey, cert []byte) (Device, error)
Add registers a freshly paired device. cert is its fleet device certificate — nil on a daemon with no fleet key, which pairs exactly as it always did.
func (*DeviceStore) AddFromFleetCert ¶ added in v0.2.0
func (s *DeviceStore) AddFromFleetCert(label string, publicKey, cert []byte, pairedAt time.Time) (Device, error)
AddFromFleetCert registers a device this machine never paired itself — it arrived with a fleet certificate another machine's ceremony minted — and is what makes the Devices screen show it and LastSeen work.
It is idempotent where Add is not: two channels racing the same new device both succeed, the second finding the entry the first wrote. That is the honest semantic — "make sure this certified key is registered" — where Add's is "record the ceremony that just happened", which must never happen twice. An existing entry is returned untouched, label and cert included: the registry is this machine's record, and a reconnect is not an edit.
pairedAt is the cert's iat — when the device joined the fleet, which is the fact the column shows — while LastSeen starts now.
func (*DeviceStore) AddRevocation ¶ added in v0.2.0
func (s *DeviceStore) AddRevocation(publicKey, cert []byte) error
AddRevocation records a dead key with its signed revocation. Idempotent by key — revoking twice, or hearing the same revocation again from the directory one stage on, is one entry — and the first blob recorded wins, which is safe because any verifying revocation for a key is exactly as dead as any other.
func (*DeviceStore) FindByID ¶ added in v0.2.0
func (s *DeviceStore) FindByID(id string) (Device, bool, error)
FindByID looks a device up by its display identity — what the revocation path holds — without changing anything.
func (*DeviceStore) FindByKey ¶
func (s *DeviceStore) FindByKey(publicKey []byte) (Device, bool, error)
FindByKey answers the acceptance rule's first question (spec/fleet-trust.md): is this static key one this machine paired, and still paired?
A key on the revocation list is never a yes, whatever devices.json still says — and the two questions are one call, under one lock, for the reason AddFromFleetCert checks revocation inside the same critical section as its write. Revoking is two writes in a fixed order (daemon.removeDevice): the signed revocation first, the registry entry second. A revoke that landed the first and failed the second leaves an entry here that must not be honoured, and a caller that asked the two questions separately would have a window between them in which it was.
It also simplifies the gossip handler the next stage brings: a revocation arriving from the fleet directory for a device this machine never paired has only to be recorded (AddRevocation), and every acceptance path — this one and AddFromFleetCert both — is already refusing that key. Nothing has to remember to ask a second question.
An unreadable revocation list is an error rather than a false, the same direction every other refusal here takes: a caller deciding whether to admit a key must refuse when it cannot know.
One edge this leaves standing, deliberately and not comfortably: Add does not consult the revocation list, so re-pairing a revoked *key* — which the browser does whenever its IndexedDB survives the revoke, since it reuses its device key — writes an entry this function will then refuse. The ceremony reports success and the relay channel closes anyway. Making Add refuse, or making a deliberate re-pairing clear the revocation, is a design call about what "un-revoking" means (the spec says: a new key, and the old one stays dead) and is deliberately not decided here.
func (*DeviceStore) IsRevoked ¶ added in v0.2.0
func (s *DeviceStore) IsRevoked(publicKey []byte) (bool, error)
IsRevoked reports whether publicKey is on the revocation list. An unreadable list is an error, not a false: the caller deciding whether to admit a key must refuse when it cannot know, the same direction FindByKey errors push.
func (*DeviceStore) List ¶
func (s *DeviceStore) List() ([]Device, error)
func (*DeviceStore) Relabel ¶ added in v0.4.0
Relabel renames a device and replaces its certificate together, reporting whether anything changed. Missing key, empty label or empty cert: no change, no error.
The two move as one because they are one fact. A device's display name is minted *into* its fleet certificate — the name a sibling machine reads it under comes out of that signed blob and nowhere else (relay/channel.go) — so a row renamed without re-minting would be a machine calling a device one thing locally while vouching for it as another everywhere else.
It is deliberately not SetCert, which back-fills a missing certificate and refuses to touch one that exists. That refusal is right for the back-fill: re-minting on every reconnect would hand a browser different bytes each time and defeat the comparison it makes against what it holds. This is the other case — a name that has actually changed, which happens when a machine is renamed or when the way flue words these labels changes under an upgrade — and there the stale bytes are the bug, so the new ones are written once and then stay put, because the next call finds the label already matching.
Keyed on the bytes rather than the id, for the reason RemoveByKey gives.
func (*DeviceStore) RemoveByKey ¶ added in v0.2.0
func (s *DeviceStore) RemoveByKey(publicKey []byte) (Device, bool, error)
RemoveByKey unpairs whichever device holds publicKey, reporting whether there was one. Missing is not an error: it is the ordinary answer for a revocation that arrived from the fleet directory naming a device this machine never paired, and for the second delivery of one it did.
By key rather than by id, unlike Remove, because its caller holds the key and the id is a 48-bit digest of it (DeviceID). Removing by id would let a key ground out to collide with a paired device's digest unpair that device — the mirror of the attack FindByKey's byte comparison exists to refuse — and here the attacker's blob has already been checked for a fleet signature, so the only thing standing between a hostile *fleet member* and unpairing someone else's device is this comparison being on the bytes.
func (*DeviceStore) Revocations ¶ added in v0.2.0
func (s *DeviceStore) Revocations() ([]StoredRevocation, error)
Revocations is the whole set — what the directory publisher one stage on pushes for the machines that were not in the room when the revoke happened.
func (*DeviceStore) SetCert ¶ added in v0.2.1
func (s *DeviceStore) SetCert(publicKey, cert []byte) (bool, error)
SetCert gives a device the fleet certificate its own pairing never minted, reporting whether it wrote one.
It exists for the devices paired while their machine held no fleet key — before the key existed at all, or in the window between a `flue relay setup` and the daemon restart that used to be needed before the daemon could sign. Those entries were stuck: Add writes Cert exactly once, at the ceremony, and AddFromFleetCert needs a certificate the device already holds. A device with neither reaches the machine it paired with and no other, for as long as the pairing lasts, with nothing anywhere able to repair it.
Three refusals, and each is the same rule stated somewhere else in this file:
- A key that is not registered gets nothing. This mints for pairings this machine performed, not for strangers; AddFromFleetCert is the door a device that paired elsewhere comes in by.
- A key on the revocation list gets nothing, checked inside the same critical section as the write, exactly as AddFromFleetCert checks it: a revocation permanently outranks a certificate, and a concurrent revoke must not be able to slip between the question and the answer.
- A device that already holds a certificate keeps it. The stored blob is one artifact for the life of a pairing — that is what lets a browser compare what it holds against what it is offered — and re-minting would hand it different bytes on every reconnect.
The comparison is on the key bytes rather than on the id, for the reason RemoveByKey gives: the id is a 48-bit digest, and this writes a credential.
func (*DeviceStore) SetOrigin ¶ added in v0.8.0
func (s *DeviceStore) SetOrigin(publicKey []byte, origin string) (bool, error)
SetOrigin records where a device enrolled from, once, reporting whether it wrote. A row that already has an origin keeps it — first seen wins, so a key that reaches this daemon from more than one place is labelled by where it turned up rather than by wherever its tab last loaded — and an empty origin, an unregistered key or a repeat are quiet no-ops, because the enrolment endpoint calls this on every page load.
No revocation check, unlike SetCert and Relabel, and the asymmetry is the point: those write a credential, this writes a display hint. Revoking removes the row itself, so a revoked key has nothing here to stamp.
func (*DeviceStore) UpdateLastSeen ¶
UpdateLastSeen stamps the device's LastSeen to now, reporting whether the device exists. Missing devices are not an error: a connection may race its own revocation, and the registry is the truth either way — a device that was unpaired a moment ago is not brought back by having connected.
The whole file is rewritten, like every other mutation here, because that is what makes a concurrent revoke and a concurrent stamp serialise on the same lock rather than on two partial views of the same JSON.
type StoredRevocation ¶ added in v0.2.0
StoredRevocation is one dead key: the key itself, and the signed fleet revocation for it.