A node holds its link open, and applies what the mesh signs
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.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user