package enroll import ( "crypto/ed25519" "encoding/base64" "encoding/json" "strings" "testing" ) // The two artefacts have to agree, and that agreement is the whole reason this is a package rather // than two commands: the config carries the private signing key, the registration carries the public // one, and a node whose relay entry was minted from a different key pair authenticates nowhere. func TestTheConfigAndTheRegistrationDescribeTheSameNode(t *testing.T) { result, err := Generate(Options{ RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/var/log/msa-agent/audit.log", }) if err != nil { t.Fatalf("Generate: %v", err) } var cfg struct { NodeID string `json:"nodeId"` RelayURL string `json:"relayUrl"` AgentToken string `json:"agentToken"` LocalPort int `json:"localPort"` SignKey string `json:"signKey"` KxKey string `json:"kxKey"` AuditPath string `json:"auditPath"` Devices []any `json:"devices"` } if err := json.Unmarshal(result.Config, &cfg); err != nil { t.Fatalf("the config is not valid JSON: %v", err) } var reg Registration if err := json.Unmarshal(result.Registration, ®); err != nil { t.Fatalf("the registration is not valid JSON: %v", err) } if cfg.NodeID != reg.NodeID || cfg.NodeID != result.NodeID { t.Fatalf("node ids disagree: config=%q registration=%q result=%q", cfg.NodeID, reg.NodeID, result.NodeID) } if cfg.AgentToken != reg.Token { t.Fatal("the config's bearer and the registration's token differ; the agent would 401") } if cfg.RelayURL != "https://relay.example" || cfg.LocalPort != 3000 { t.Fatalf("relay or port lost: %+v", cfg) } if cfg.Devices == nil || len(cfg.Devices) != 0 { t.Fatalf("devices must start empty, got %v", cfg.Devices) } // The private key is 64 bytes; its last 32 are the public key, which is what the relay verifies. private, err := base64.StdEncoding.DecodeString(cfg.SignKey) if err != nil || len(private) != ed25519.PrivateKeySize { t.Fatalf("signKey is not a base64 ed25519 private key: %v (%d bytes)", err, len(private)) } public, err := base64.StdEncoding.DecodeString(reg.SignKey) if err != nil || len(public) != ed25519.PublicKeySize { t.Fatalf("the registration's signKey is not a base64 ed25519 public key: %v (%d bytes)", err, len(public)) } if string(private[ed25519.PrivateKeySize-ed25519.PublicKeySize:]) != string(public) { t.Fatal("the relay's registration carries a key that does not belong to the config's private key") } } // The node password is the one secret this tool must not invent. An unprotected node needs no value; // a protected one needs the real one, or every command fails with node_auth_failed and the cause is // not obvious from the phone. func TestTheNodePasswordIsOnlyWrittenWhenGiven(t *testing.T) { without, err := Generate(Options{RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a"}) if err != nil { t.Fatal(err) } if strings.Contains(string(without.Config), "nodePassword") { t.Fatal("a config with no password claims one") } with, err := Generate(Options{ RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a", NodePassword: "hunter2", }) if err != nil { t.Fatal(err) } if !strings.Contains(string(with.Config), `"nodePassword": "hunter2"`) { t.Fatal("the given password was not written") } } // Re-enrolling a node has to be able to keep the identifiers the relay already knows, or the operator // has two edits to make instead of one. func TestGivenIdentifiersAreKept(t *testing.T) { result, err := Generate(Options{ RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a", NodeID: "nd_keepme0001", AgentToken: strings.Repeat("a", 64), ProvisionSecret: "kept-secret", }) if err != nil { t.Fatal(err) } var reg Registration if err := json.Unmarshal(result.Registration, ®); err != nil { t.Fatal(err) } if reg.NodeID != "nd_keepme0001" || reg.Token != strings.Repeat("a", 64) { t.Fatalf("identifiers were regenerated: %+v", reg) } if result.ProvisionSecret != "kept-secret" { t.Fatal("the pairing secret was regenerated") } } // Two enrollments must not produce the same keys or ids. A generator that repeated a key would make // two nodes able to impersonate each other, and it is the kind of bug a seeded RNG introduces once. func TestTwoEnrollmentsDiffer(t *testing.T) { first, err := Generate(Options{RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a"}) if err != nil { t.Fatal(err) } second, err := Generate(Options{RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a"}) if err != nil { t.Fatal(err) } if first.NodeID == second.NodeID { t.Fatal("two enrollments produced the same node id") } if string(first.Config) == string(second.Config) { t.Fatal("two enrollments produced the same config, so the same keys") } } func TestMissingInputIsRefused(t *testing.T) { cases := map[string]Options{ "no relay": {LocalPort: 3000, AuditPath: "/a"}, "bad port": {RelayURL: "https://relay.example", LocalPort: 0, AuditPath: "/a"}, "huge port": {RelayURL: "https://relay.example", LocalPort: 70000, AuditPath: "/a"}, "no audit": {RelayURL: "https://relay.example", LocalPort: 3000}, } for name, opts := range cases { if _, err := Generate(opts); err == nil { t.Fatalf("%s was accepted", name) } } } // The node records how it got into the relay, because `uninstall` cannot work it out any other way: the // agent holds no admin token, so it cannot remove its own entry, and the operator has to be told which // of the relay's two records mentions this node. func TestEnrollmentRecordsItsOwnProvenance(t *testing.T) { withCode, err := Generate(Options{ RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a", SelfEnrolled: true, }) if err != nil { t.Fatal(err) } if !strings.Contains(string(withCode.Config), `"selfEnrolled": true`) { t.Fatal("a self-enrolled node does not say so") } // And a node an operator wrote into agents.json does not claim to have enrolled itself, or the // uninstall would send the operator to the app to remove an entry that is not there. manual, err := Generate(Options{RelayURL: "https://relay.example", LocalPort: 3000, AuditPath: "/a"}) if err != nil { t.Fatal(err) } if strings.Contains(string(manual.Config), "selfEnrolled") { t.Fatal("a manually registered node claims to be self-enrolled") } }