From the lease write the vault makes the token (ADR 0587 §3), mesh-cli opens and prints it, and the node-engine parses it: three repositories, so the type leaves mesh-host's internal/identity (which keeps an alias) for go/delivery, field names unchanged from the controller's token issue, the tunnel kept (the lead's settlement 7). Encode is added, refusing a token with a part missing. The test reads a token the controller's own code made and encodes it back to the same line. Paid by merging TestGateBuckets into TestKeysOfPlainNamesAreUnchanged. (issue 774, 774/S36) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
119 lines
4.8 KiB
Go
119 lines
4.8 KiB
Go
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 control plane'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) }
|