package delivery import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "strings" ) // Token is what a person carries to a machine that is joining (novox/hq ADR 0004): where the bus is, what certificate // to expect there, whose signature to believe afterwards, and a one-time right to join; and, when it was issued for the // machine's tunnel key, the machine's first tunnel (ADR 0169). // // Moved here from the node-engine (mesh-host internal/identity, which keeps an alias) so the vault, which makes the // token from the lease write on (novox/hq ADR 0587 §3, issue 774), mesh-cli, which opens and prints it, and the // node-engine, which parses it, read one shape. The field names are the wire format the controller's `token issue` // writes, unchanged, so a token of either issuer reads the same; the tunnel stays in it (the lead's settlement of // ADR 0587 §3, 2026-11-02). type Token struct { Version int `json:"v"` // Node is what the mesh calls this machine: the bus user it dials as is named after it, so it must be known before // the mesh can say anything. Node string `json:"node,omitempty"` Broker string `json:"broker,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` Signer []byte `json:"signer,omitempty"` Secret string `json:"secret"` // Adopted says this node joins adopted (novox/hq ADR 0100); absent for a converged node. Adopted bool `json:"adopted,omitempty"` // Tunnel is this machine's first tunnel, when the token was issued for the key it made (novox/hq ADR 0169): its own // address and the hub to reach, so it reaches the bus over the private network and the bus never faces the internet. Tunnel *TokenTunnel `json:"tunnel,omitempty"` } // TokenTunnel is the joining machine's side of its first tunnel, in the field names the controller writes. type TokenTunnel struct { Key string `json:"key"` Address string `json:"address"` Range string `json:"range"` HubKey string `json:"hub_key"` HubEndpoint string `json:"hub_endpoint"` } // ParseToken reads a token a person pasted. Every refusal says *this is not a token* rather than *this is the wrong // token*: telling "not from the mesh I joined" apart from "malformed" is a signature check later (ADR 0004). A token // missing a part is refused whole, naming every part, never used partially: without the fingerprint a node connects // to whatever answers, without the signing key it cannot tell a declaration from a forgery. func ParseToken(encoded string) (Token, error) { raw, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(encoded)) if err != nil { return Token{}, fmt.Errorf("this is not a token: %w", err) } var t Token if err := json.Unmarshal(raw, &t); err != nil { return Token{}, fmt.Errorf("this is not a token: %w", err) } if t.Version != 1 { return Token{}, fmt.Errorf( "this token says it is version %d, and this host understands version 1", t.Version) } if missing := t.Missing(); len(missing) > 0 { return Token{}, fmt.Errorf( "this token is missing %s, so it cannot be used to join anything", strings.Join(missing, ", ")) } return t, nil } // Missing names every part of t a joining machine needs and t lacks. func (t Token) Missing() []string { var missing []string if strings.TrimSpace(t.Broker) == "" { missing = append(missing, "the broker's address") } if strings.TrimSpace(t.Fingerprint) == "" { missing = append(missing, "the broker certificate's fingerprint") } if len(t.Signer) != ed25519.PublicKeySize { missing = append(missing, "the vault's signing key") } if strings.TrimSpace(t.Secret) == "" { missing = append(missing, "the one-time secret") } if tt := t.Tunnel; tt != nil { for _, part := range []struct{ value, says string }{ {tt.Key, "the tunnel key it was issued for"}, {tt.Address, "this machine's address"}, {tt.Range, "the private network's range"}, {tt.HubKey, "the hub's tunnel key"}, {tt.HubEndpoint, "where the hub's tunnel is dialled"}, } { if strings.TrimSpace(part.value) == "" { missing = append(missing, part.says) } } } return missing } // Encode is t as one line a person carries: base64 (URL alphabet, no padding) of its JSON at version 1, as the // controller's `token issue` writes it. Refused when a part is missing. func (t Token) Encode() (string, error) { t.Version = 1 if missing := t.Missing(); len(missing) > 0 { return "", fmt.Errorf("delivery: the token is missing %s", strings.Join(missing, ", ")) } raw, err := json.Marshal(t) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(raw), nil } // SignerKey is the signing key the token carries, as a key: the controller's before the lease write, the vault's after. func (t Token) SignerKey() ed25519.PublicKey { return ed25519.PublicKey(t.Signer) }