token issue --overlay-key records the key the machine made, binds the token to it, gives the machine its address and makes it a peer of the hub, pushing the hub before the token is shown. The token carries the hub's tunnel and the bus at its address on the private network, and enrolment refuses any other key (novox/hq ADR 0169). Tokens without a key enrol as before until the bus is closed. Also a token verb.
210 lines
6.8 KiB
Go
210 lines
6.8 KiB
Go
package token
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func complete(t *testing.T) Token {
|
|
t.Helper()
|
|
public, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Token{
|
|
Node: "anchor",
|
|
Broker: "192.0.2.10:5671",
|
|
Fingerprint: "sha256:" + strings.Repeat("ab", 32),
|
|
Signer: public,
|
|
Secret: "a-one-time-secret",
|
|
}
|
|
}
|
|
|
|
func TestATokenSurvivesBeingCarried(t *testing.T) {
|
|
// It is copied by hand out of a terminal and into a machine. Whatever comes back must be
|
|
// exactly what went in, including the key — a signing key that changed in transit is a node
|
|
// that refuses every declaration it is later sent.
|
|
original := complete(t)
|
|
encoded, err := original.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.ContainsAny(encoded, " \n\t") {
|
|
t.Error("the encoded token contains whitespace; it is copied by hand as one line")
|
|
}
|
|
|
|
back, err := Decode(encoded)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back.Broker != original.Broker || back.Fingerprint != original.Fingerprint ||
|
|
back.Secret != original.Secret || string(back.Signer) != string(original.Signer) {
|
|
t.Errorf("the token changed in transit:\n sent %+v\n got %+v", original, back)
|
|
}
|
|
}
|
|
|
|
func TestSurroundingWhitespaceIsTolerated(t *testing.T) {
|
|
// It arrives pasted. A trailing newline is not a corrupted token, and refusing one would
|
|
// send somebody hunting for a fault that is not there.
|
|
encoded, err := complete(t).Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Decode(" " + encoded + "\n"); err != nil {
|
|
t.Errorf("a pasted token was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGarbageIsRefusedAsNotBeingAToken(t *testing.T) {
|
|
for _, bad := range []string{"", "not-base64-!!!", "aGVsbG8"} {
|
|
if _, err := Decode(bad); err == nil {
|
|
t.Errorf("%q was accepted as a token", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestATokenFromAnotherVersionIsRefusedClearly(t *testing.T) {
|
|
// The host must tell "this is not from the mesh I joined" apart from "this is malformed"
|
|
// (novox/hq ADR 0004). A version it does not understand is the first case.
|
|
future := complete(t)
|
|
encoded, err := future.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Re-encode by hand at a version this build does not know.
|
|
raw := strings.Replace(string(mustDecodeBase64(t, encoded)), `"v":1`, `"v":99`, 1)
|
|
if _, err := Decode(encodeBase64(raw)); err == nil {
|
|
t.Fatal("a token from an unknown version was accepted")
|
|
}
|
|
}
|
|
|
|
func TestEveryMissingPartIsNamed(t *testing.T) {
|
|
// "This token cannot be used" is not something anybody can act on. "It has no broker
|
|
// address" is. And all of them at once, not the first: fixing one at a time turns a single
|
|
// decision into four.
|
|
empty := Token{}
|
|
missing := empty.Missing()
|
|
if len(missing) != 5 {
|
|
t.Fatalf("an empty token named %d missing parts, expected 5: %v", len(missing), missing)
|
|
}
|
|
if empty.Complete() {
|
|
t.Error("an empty token reported itself complete")
|
|
}
|
|
}
|
|
|
|
func TestAShortSigningKeyIsNotASigningKey(t *testing.T) {
|
|
// The one that would pass a nil check and fail at the moment a declaration is verified —
|
|
// which is on a node, in production, long after this.
|
|
t1 := complete(t)
|
|
t1.Signer = []byte("too short")
|
|
if t1.Complete() {
|
|
t.Error("a truncated signing key was accepted as present")
|
|
}
|
|
}
|
|
|
|
func TestACompleteTokenIsComplete(t *testing.T) {
|
|
if got := complete(t); !got.Complete() {
|
|
t.Errorf("a token with every part reported missing: %v", got.Missing())
|
|
}
|
|
}
|
|
|
|
func mustDecodeBase64(t *testing.T, s string) []byte {
|
|
t.Helper()
|
|
raw, err := base64Decode(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
|
|
// The contract with the host, which defines this format separately because it requires
|
|
// nothing present and does not import this (novox/hq ADR 0005). There is a matching test on
|
|
// that side. Rename a field on either and both fail — the alternative is a rename that only
|
|
// shows up at enrolment, on a real machine.
|
|
raw, err := json.Marshal(complete(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fields map[string]any
|
|
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{"v", "node", "broker", "fingerprint", "signer", "secret"} {
|
|
if _, ok := fields[want]; !ok {
|
|
t.Errorf("the token has no %q field; the host reads that name", want)
|
|
}
|
|
}
|
|
if len(fields) != 6 {
|
|
t.Errorf("the token has %d fields, expected 6: %v", len(fields), fields)
|
|
}
|
|
|
|
// An adopted node's token says so, under exactly this name, and a converged one does not
|
|
// carry it at all (novox/hq ADR 0100).
|
|
adopted := complete(t)
|
|
adopted.Adopted = true
|
|
raw, err = json.Marshal(adopted)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fields = nil
|
|
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fields["adopted"] != true || len(fields) != 7 {
|
|
t.Errorf("an adopted token does not carry \"adopted\": true: %v", fields)
|
|
}
|
|
}
|
|
|
|
func TestATokenWithNoNameIsRefused(t *testing.T) {
|
|
// The broker account a joining node authenticates as is named after the node, so the node has
|
|
// to know its name before the mesh can tell it anything. Without this the connection is
|
|
// refused with an empty username, which says nothing about the cause — which is exactly how
|
|
// it went the first time a mesh was raised end to end.
|
|
without := complete(t)
|
|
without.Node = ""
|
|
if without.Complete() {
|
|
t.Fatal("a token with no node name reported itself usable")
|
|
}
|
|
if !strings.Contains(strings.Join(without.Missing(), " "), "node's name") {
|
|
t.Fatalf("the refusal does not say what is missing: %v", without.Missing())
|
|
}
|
|
}
|
|
|
|
// A token issued for a tunnel key carries the one peer a joining machine needs (novox/hq ADR 0169),
|
|
// and says which part is missing rather than producing a tunnel that never answers.
|
|
func TestATokenThroughTheTunnelCarriesThePeerOrSaysWhatIsMissing(t *testing.T) {
|
|
whole := Token{Node: "n", Broker: "10.42.0.1:4222", Fingerprint: "sha256:x", Signer: make([]byte, 32), Secret: "s",
|
|
Tunnel: &Tunnel{Key: "k", Address: "10.42.0.9/32", Range: "10.42.0.0/16", HubKey: "h", HubEndpoint: "198.51.100.1:51820"}}
|
|
if !whole.Complete() {
|
|
t.Fatalf("a whole token through the tunnel reads as missing %v", whole.Missing())
|
|
}
|
|
encoded, err := whole.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := Decode(encoded)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back.Tunnel == nil || *back.Tunnel != *whole.Tunnel {
|
|
t.Fatalf("the tunnel did not survive the round trip: %+v", back.Tunnel)
|
|
}
|
|
|
|
part := whole
|
|
part.Tunnel = &Tunnel{Key: "k", Address: "10.42.0.9/32"}
|
|
if len(part.Missing()) != 3 {
|
|
t.Errorf("a tunnel without the hub and the range should name three missing parts: %v", part.Missing())
|
|
}
|
|
|
|
// And a token issued without a key carries no tunnel at all, byte for byte as before.
|
|
plain := whole
|
|
plain.Tunnel = nil
|
|
raw, _ := plain.Encode()
|
|
if strings.Contains(raw, "tunnel") {
|
|
t.Error("a token without a key mentions a tunnel")
|
|
}
|
|
}
|