175 lines
5.5 KiB
Go
175 lines
5.5 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())
|
|
}
|
|
}
|