The loop the whole thing exists for: told, apply, report. `run` holds one outbound connection open and consumes the node's own queue. Every declaration is verified against the control plane's signing key before a byte of it is read as an instruction -- not once at connect, every time. The transport being pinned is a different question from the instruction being genuine, and pinning only the first would make the second transitive: a compromised broker could forge declarations, and this host applies whatever the link delivers. Malformed and forged are reported differently, because ADR 0004 requires a host to tell "this is not from the mesh I joined" from "this is broken". One means somebody is trying and the other means something needs fixing. A node now keeps what it needs to come back on its own: the broker's address and fingerprint, the signing key it believes, and its own broker password -- which the mesh issues at enrolment to replace the token's secret, so the one-time thing stays one-time and the credential it holds for years is not the one that was pasted into a terminal. Verified in the lab end to end. The node enrolled, held its link, received a signed declaration and applied it -- the file is on the machine with the right contents, and the host's own record lists both resources. That run also found issue 010, which is recorded in novox/hq: the declaration removed every container on the machine, including the control plane that sent it. Correct reconciliation, shared store, and the first thing that happens.
137 lines
4.2 KiB
Go
137 lines
4.2 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// verified runs what Run does to a delivery body, without a broker: unmarshal, check the
|
|
// signature, and only then apply. Isolating it keeps this test about the check rather than about
|
|
// AMQP, which is tested against a real broker in the lab.
|
|
func verified(t *testing.T, signer ed25519.PublicKey, body []byte) (Report, bool) {
|
|
t.Helper()
|
|
applied := false
|
|
report := handleBody(context.Background(), Membership{Node: "anchor", Signer: signer}, body,
|
|
func(context.Context, []byte) Report {
|
|
applied = true
|
|
return Report{Applied: []string{"something"}}
|
|
})
|
|
return report, applied
|
|
}
|
|
|
|
func signedBody(t *testing.T, private ed25519.PrivateKey, declaration string) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(Signed{
|
|
Declaration: []byte(declaration),
|
|
Signature: ed25519.Sign(private, []byte(declaration)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func TestTheMeshsOwnDeclarationIsApplied(t *testing.T) {
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, applied := verified(t, public, signedBody(t, private, `{"declaration":1}`))
|
|
if !applied {
|
|
t.Fatalf("a declaration the mesh signed was not applied: %s", report.Refused)
|
|
}
|
|
}
|
|
|
|
func TestAForgedDeclarationIsNeverApplied(t *testing.T) {
|
|
// The check that stands between "the mesh changes this machine" and "anybody does". The host
|
|
// applies whatever the link delivers, so a forged declaration is the whole machine.
|
|
public, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, other, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
report, applied := verified(t, public, signedBody(t, other, `{"declaration":1}`))
|
|
if applied {
|
|
t.Fatal("a declaration signed by another key was applied")
|
|
}
|
|
if report.Refused != ErrForged.Error() {
|
|
t.Errorf("refused, but not as a forgery: %q", report.Refused)
|
|
}
|
|
}
|
|
|
|
func TestATamperedDeclarationIsNeverApplied(t *testing.T) {
|
|
// A broker that changed the declaration in flight, keeping the signature. This is what makes
|
|
// pinning the transport insufficient on its own.
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := json.Marshal(Signed{
|
|
Declaration: []byte(`{"declaration":1,"resources":["something else entirely"]}`),
|
|
Signature: ed25519.Sign(private, []byte(`{"declaration":1}`)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
report, applied := verified(t, public, raw)
|
|
if applied {
|
|
t.Fatal("a declaration altered after signing was applied")
|
|
}
|
|
if report.Refused != ErrForged.Error() {
|
|
t.Errorf("refused, but not as a forgery: %q", report.Refused)
|
|
}
|
|
}
|
|
|
|
func TestAMalformedMessageIsToldApartFromAForgery(t *testing.T) {
|
|
// novox/hq ADR 0004 requires these to be distinguishable: one means somebody is trying, the
|
|
// other means something is broken, and they need different responses from a person.
|
|
public, _, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, applied := verified(t, public, []byte("this is not a message"))
|
|
if applied {
|
|
t.Fatal("something unparseable was applied")
|
|
}
|
|
if report.Refused == ErrForged.Error() {
|
|
t.Error("a malformed message was reported as a forgery; those must be distinguishable")
|
|
}
|
|
}
|
|
|
|
func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
|
|
// The contract with the control plane, which defines these separately. A matching test lives
|
|
// there; rename a field on either side and both fail.
|
|
for _, c := range []struct {
|
|
value any
|
|
expect []string
|
|
}{
|
|
{Signed{Declaration: []byte("{}"), Signature: []byte("x")}, []string{"declaration", "signature"}},
|
|
{Report{Node: "n", Applied: []string{"a"}, Failed: map[string]string{"k": "v"}, Refused: "r"},
|
|
[]string{"node", "applied", "failed", "refused"}},
|
|
} {
|
|
raw, err := json.Marshal(c.value)
|
|
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 c.expect {
|
|
if _, ok := fields[want]; !ok {
|
|
t.Errorf("%T has no %q field; the control plane uses that name", c.value, want)
|
|
}
|
|
}
|
|
if len(fields) != len(c.expect) {
|
|
t.Errorf("%T has %d fields, expected %d: %v", c.value, len(fields), len(c.expect), fields)
|
|
}
|
|
}
|
|
}
|