Files

87 lines
3.4 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"`
// Adopted says this node joins adopted (novox/hq ADR 0100). The host checks it speaks the
// firewall found here before enrolling, because an adopted node keeps that firewall in force.
// Absent for a converged node.
Adopted bool `json:"adopted,omitempty"`
}
// 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) }