// Package token is the thing a person carries to a machine that is joining. // // novox/hq ADR 0004: it carries four things, and it is the only thing a joining node needs — // where the broker is, what certificate to expect there, whose signature to believe afterwards, // and a one-time right to join. // // Its authenticity comes from the channel it travelled, not from anything the node can check // afterwards: trust on first use, with the first use moved out of band. Which makes the token // security-critical, because it carries the pin. Tampering with it substitutes the mesh — still // better than the alternative, where there is nothing to tamper with and a node trusts the first // answer it gets. package token import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "strings" ) // Token is everything a joining node needs, assembled by whatever holds all of it. // // Two contexts contribute: `inventory` owns the node record and mints the secret, `identity` owns // the signing key. Neither reads the other's store — the process holding both grants asks each // for its part (novox/hq ADR 0008). type Token struct { Version int `json:"v"` // Node is what the mesh calls this machine. // // Not a secret and not the node's to choose — the record was created before the token was // issued, and the broker account the node must authenticate as is named after it. So the node // has to know it *before* the mesh can tell it anything, which is why it travels here. // // It was not here at first, and enrolment then needed a separate flag while its own help said // the token was the only thing required. The failure that produced was a connection refused // with an empty username, which says nothing about the cause. Node string `json:"node,omitempty"` // Broker is an address and not a name. There is no resolution before joining, which is why // this is the one place in the mesh where an address is carried deliberately. Broker string `json:"broker,omitempty"` // Fingerprint is the broker certificate's, checked before anything is sent. Fingerprint string `json:"fingerprint,omitempty"` // Signer is the control plane's public signing key: whose declarations to believe. Signer []byte `json:"signer,omitempty"` // Secret is the one-time right to join. Useless once used, useless after it expires. Secret string `json:"secret"` } // Missing names the parts that are not filled in. // // Returned as a list rather than a bool, because "this token cannot be used" is not an answer // anybody can act on and "it has no broker address" is. Everything here is required: a token // missing the fingerprint cannot verify what it connects to, and one missing the signer makes // the control plane's authority transitive through the broker — which ADR 0004 rejects, because // a compromised broker could then forge declarations, and that is the whole machine. func (t Token) Missing() []string { var missing []string if strings.TrimSpace(t.Node) == "" { missing = append(missing, "the node's name — the broker account is named after it") } if strings.TrimSpace(t.Broker) == "" { missing = append(missing, "the broker's address — there is nowhere to connect to") } if strings.TrimSpace(t.Fingerprint) == "" { missing = append(missing, "the broker certificate's fingerprint — nothing to check the connection against") } if len(t.Signer) != ed25519.PublicKeySize { missing = append(missing, "the control plane's signing key — declarations could not be told from forgeries") } if strings.TrimSpace(t.Secret) == "" { missing = append(missing, "the one-time secret — nothing to present") } return missing } // Complete reports whether this token could actually be used to join. func (t Token) Complete() bool { return len(t.Missing()) == 0 } // Encode renders the token as one line a person can carry. // // Base64 of JSON: self-describing, so a token from an older control plane says what it is rather // than being misread by a newer one; and one line, because it is copied by hand between a // terminal and a machine. func (t Token) Encode() (string, error) { t.Version = 1 raw, err := json.Marshal(t) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(raw), nil } // Decode reads a token a person pasted. func Decode(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. It was made "+ "by a different control plane than the one this was built against", t.Version) } return t, nil } // base64Decode and encodeBase64 exist for the tests, which construct a token by hand to prove a // version this build does not understand is refused. Kept beside the encoding they mirror. func base64Decode(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(strings.TrimSpace(s)) } func encodeBase64(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s)) }