package identity 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 — four things: where the broker is, what certificate to expect there, whose // signature to believe afterwards, and a one-time right to join. // // THIS IS A WIRE FORMAT SHARED WITH THE CONTROL PLANE, which writes it. The two definitions are // separate on purpose — the host requires nothing present and does not import the control plane — // so they are held together by a test on each side asserting the exact field names rather than by // a shared type. If a field is renamed here and not there, that test fails on both sides. type Token struct { Version int `json:"v"` // Node is what the mesh calls this machine, and it arrives here because the node cannot work // it out. The broker account it must authenticate as is named after it, so it has to be known // before the mesh can say anything — and without it enrolment is a connection refused with an // empty username, which names nothing. Node string `json:"node,omitempty"` Broker string `json:"broker,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` Signer []byte `json:"signer,omitempty"` Secret string `json:"secret"` } // ParseToken reads a token a person pasted. // // Every refusal here says *this is not a token* rather than *this is the wrong token*. The // difference matters once there is a mesh: a host must tell "this is not from the mesh I joined" // apart from "this is malformed" (novox/hq ADR 0004), and the first is a signature check later, // not a parse failure here. 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) } 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 control plane's signing key") } if strings.TrimSpace(t.Secret) == "" { missing = append(missing, "the one-time secret") } if len(missing) > 0 { // Refused whole rather than used partially. A token missing the fingerprint would have // this node connect to whatever answers at that address, and one missing the signing key // would leave it unable to tell a declaration from a forgery — so an incomplete token is // not a reduced capability, it is an unsafe one. return Token{}, fmt.Errorf( "this token is missing %s, so it cannot be used to join anything", strings.Join(missing, ", ")) } return t, nil } // SignerKey is the control plane's public signing key, as a key. func (t Token) SignerKey() ed25519.PublicKey { return ed25519.PublicKey(t.Signer) }