Found by raising a mesh end to end for the first time. Enrolment's own help says the token "is the only thing it needs", and it also needed --name, with no default. Without it the failure is: cannot reach the broker at 192.0.2.10:5671 as : username or password not allowed An empty username, and nothing about the cause. The node cannot work its own name out. The broker account it authenticates as is named after it and exists before this machine has been told anything, so the name has to arrive with the rest. It is not a secret and the issuer already knows it. --name stays, as an override for a token issued before the name travelled in one, and says so when it is needed rather than failing at the broker. Also corrects the bundle example, which claimed to stop before the control plane runs and has raised one for some time. A comment about what something does not do is a comment nobody updates.
82 lines
3.2 KiB
Go
82 lines
3.2 KiB
Go
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) }
|