diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index e5518af..68d8b5e 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -163,28 +163,6 @@ func fpIt(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 6 func fpDigest(c string) string { return "sha256:" + strings.Repeat(c, 64) } -// compose writes a module's signed composition of one file into the node's bucket and returns its digest. -func (b *itBed) compose(module, fp, content string, data bool, signer ed25519.PrivateKey) string { - b.t.Helper() - path := filepath.Join(b.dir, module+".conf") - res, _ := json.Marshal(map[string]any{"id": module + ".conf", "type": "file", "path": path, "content": content, "module": module}) - c := delivery.Composed{Node: itNode, Module: module, FP: fp, Data: data, Resources: []json.RawMessage{res}} - signed, err := delivery.SignComposed(c, signer) - if err != nil { - b.t.Fatal(err) - } - raw, err := delivery.EncodeComposed(signed) - if err != nil { - b.t.Fatal(err) - } - dig, _ := delivery.ComposedDigestOf(signed) - key, _ := delivery.ComposedKey(itNode, module, fp) - if _, err := b.composed.Put(key, raw); err != nil { - b.t.Fatal(err) - } - return dig -} - func (b *itBed) want(module, fp, digest, backup string) { b.t.Helper() raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest, Backup: backup}) @@ -521,7 +499,7 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str return false } d, err := delivery.ComposedDigestOf(c) - return err == nil && d == first && c.Signature == nil + return err == nil && d == first }) if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil { t.Fatal(err) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 9674f23..c392947 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -15,7 +15,6 @@ package agent import ( "bytes" "context" - "crypto/ed25519" "encoding/json" "fmt" "sort" @@ -120,10 +119,6 @@ type Env struct { // whose have names a composition the bucket keeps runs from that composition, never from the kept declaration // (issue 924): the kept declaration is older than every composition applied since the switch. KeptFilled func() map[string][]string - // Trusted is the public keys a composition may be signed by: the controller's and the operator's (ADR 0539 §3). A - // composition unsigned, signed by another key, or changed after it was signed is refused and nothing of it is - // applied. Nil trusts no key, so nothing is applied. - Trusted func() []ed25519.PublicKey // Recorded says the engine holds a record of every resource of the composition, so it was applied and not removed // since; nil says yes. A composition the kept declaration or a have says runs is applied again when it says no. Recorded func(ctx context.Context, c delivery.Composed) bool @@ -686,26 +681,14 @@ func (a *Agent) takeComposition(u Update) string { } now := compNow{Raw: u.Value, Rev: u.Revision} c, err := delivery.DecodeComposed(u.Value) - if err == nil && a.env.SelfComposed { - // The node composed it itself: nothing crossed the bus, so nothing is signed (ADR 0547 §2). + if err == nil { + // The node composed it itself: it is applied as composed, never verified against a signer, since no composition + // read from the bus is applied (novox/hq ADR 0547 §2, ADR 0593 §6). now.C = c now.Dig, err = delivery.ComposedDigestOf(c) if err != nil { now.C = delivery.Composed{} } - } else if err == nil { - var trusted []ed25519.PublicKey - if a.env.Trusted != nil { - trusted = a.env.Trusted() - } - // The signature before anything of the composition is used: its digest is what the want names. - now.C = c - now.Dig, err = delivery.VerifyComposed(c, trusted) - if err != nil { - now.C = delivery.Composed{} - a.sayOnce("verify:"+u.Key, fmt.Sprintf("the composition %s is refused and nothing of it is applied: %v", u.Key, err)) - err = fmt.Errorf("refused: %w", err) - } } else { a.sayOnce("decode:"+u.Key, fmt.Sprintf("the composition %s cannot be read, and nothing of it is applied: %v", u.Key, err)) } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 2ce56b1..f2b3306 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -2,7 +2,6 @@ package agent import ( "context" - "crypto/ed25519" "encoding/json" "fmt" "maps" @@ -18,19 +17,6 @@ import ( const node = "n1" -// The keys of the tests: the controller's and the operator's are trusted, a stranger's is not. -var controllerPub, controllerKey = newKey() -var operatorPub, operatorKey = newKey() -var _, strangerKey = newKey() - -func newKey() (ed25519.PublicKey, ed25519.PrivateKey) { - pub, key, err := ed25519.GenerateKey(nil) - if err != nil { - panic(err) - } - return pub, key -} - func fpOf(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) } // A watcher the test writes to; it says Caught once the keys written before the watch began are out. @@ -183,8 +169,7 @@ func newBed(t *testing.T) *bed { b.settled = append(b.settled, m) b.settledRan = append(b.settledRan, ran) }, - Kept: func() map[string][]json.RawMessage { return b.kept }, - Trusted: func() []ed25519.PublicKey { return []ed25519.PublicKey{controllerPub, operatorPub} }, + Kept: func() map[string][]json.RawMessage { return b.kept }, Say: func(s string) { t.Log(s) b.mu.Lock() @@ -223,21 +208,17 @@ func composition(t *testing.T, module, fp string, data bool, rs ...json.RawMessa if err != nil { t.Fatal(err) } - return signedBy(t, c, key, controllerKey) + return encodedAs(t, c, key) } -// signedBy is c signed with signer, as the writer's code signs it: its key, its bytes and its digest. -func signedBy(t *testing.T, c delivery.Composed, key string, signer ed25519.PrivateKey) (string, []byte, string) { +// encodedAs is c as the node's composer hands it: unsigned (ADR 0593 §6), its key, its bytes and its digest. +func encodedAs(t *testing.T, c delivery.Composed, key string) (string, []byte, string) { t.Helper() - signed, err := delivery.SignComposed(c, signer) + raw, err := delivery.EncodeComposed(c) if err != nil { t.Fatal(err) } - raw, err := delivery.EncodeComposed(signed) - if err != nil { - t.Fatal(err) - } - digest, err := delivery.ComposedDigestOf(signed) + digest, err := delivery.ComposedDigestOf(c) if err != nil { t.Fatal(err) } @@ -735,76 +716,6 @@ func TestTheNodesOwnCompositionComesBeforeAnyModules(t *testing.T) { } } -// R6-A AC9: a composition signed by either trusted key is applied; one signed by another key, one changed after it was -// signed and one unsigned are refused and nothing of them is applied (ADR 0539 §3). -func TestOnlyACompositionSignedByATrustedKeyIsApplied(t *testing.T) { - t.Run("with no trusted key", testWithNoTrustedKeyNothingIsApplied) - b := newBed(t) - stop := b.run() - defer stop() - - plain := func(module, fp, content string) delivery.Composed { - return delivery.Composed{Node: node, Module: module, FP: fp, Resources: []json.RawMessage{file(module, "x", content)}} - } - put := func(c delivery.Composed, signer ed25519.PrivateKey, tamper func([]byte) []byte) { - key, _ := delivery.ComposedKey(node, c.Module, c.FP) - var raw []byte - var dig string - if signer == nil { - raw, _ = delivery.EncodeComposed(c) - dig, _ = delivery.ComposedDigestOf(c) - } else { - _, raw, dig = signedBy(t, c, key, signer) - } - if tamper != nil { - raw = tamper(raw) - } - b.comps.put(key, raw) - wk, wraw := wantFor(t, c.Module, c.FP, dig, "") - b.wants.put(wk, wraw) - } - - // Refused: signed by a stranger, changed after signing, unsigned. - put(plain("stranger", fpOf("1"), "x"), strangerKey, nil) - put(plain("changed", fpOf("2"), "before"), controllerKey, func(raw []byte) []byte { - return []byte(strings.Replace(string(raw), "before", "after!", 1)) - }) - put(plain("unsigned", fpOf("3"), "x"), nil, nil) - time.Sleep(200 * time.Millisecond) - if got := b.appliedList(); len(got) != 0 { - t.Fatalf("a composition that is not signed by a trusted key was applied: %v", got) - } - - // Applied: signed by the controller, signed by the operator. - put(plain("bycontroller", fpOf("4"), "x"), controllerKey, nil) - put(plain("byoperator", fpOf("5"), "x"), operatorKey, nil) - eventually(t, "the trusted signers' compositions were not applied", func() bool { return len(b.appliedList()) == 2 }) - got := b.appliedList() - if !(got[0] == "bycontroller" && got[1] == "byoperator") && !(got[0] == "byoperator" && got[1] == "bycontroller") { - t.Fatalf("applied %v", got) - } - if h := b.haves.last("byoperator"); h.Phase != string(delivery.PhaseRunning) || h.FP != fpOf("5") { - t.Fatalf("the operator's module has no running have: %+v", h) - } -} - -// With no key trusted, nothing is applied: a node that does not know its signers fails closed. -func testWithNoTrustedKeyNothingIsApplied(t *testing.T) { - b := newBed(t) - b.env.Trusted = nil - stop := b.run() - defer stop() - fp := fpOf("a") - ck, craw, dig := composition(t, "web", fp, false, file("web", "x", "1")) - b.comps.put(ck, craw) - wk, wraw := wantFor(t, "web", fp, dig, "") - b.wants.put(wk, wraw) - time.Sleep(200 * time.Millisecond) - if got := b.appliedList(); len(got) != 0 { - t.Fatalf("applied with no trusted key: %v", got) - } -} - // What a module's apply must not take is every other module that runs, those the agent holds and those still running from // the kept declaration (ADR 0429 §4): the resources of the kept declaration reach Apply as the others. func TestApplyingOneModuleHoldsOutTheModulesStillRunningFromTheKeptDeclaration(t *testing.T) { @@ -860,7 +771,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { // and is what runs from before the first composition is watched; an apply keeps, a removal forgets. t.Run("what this engine applied is what runs before the node composes its own", func(t *testing.T) { b := newBed(t) - b.env.SelfComposed, b.env.Trusted, b.kept = true, nil, nil + b.env.SelfComposed, b.kept = true, nil store := &memApplied{kept: map[string][]byte{}} b.env.Applied = store // A folded module's resource in the node's own composition: its id names the module, as the controller folds it. @@ -930,7 +841,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { h.Write(context.Background(), Have{Module: NodeModule, Composed: "sha256:" + strings.Repeat("c", 64)}) firstPass := func(kept map[string][]json.RawMessage) []delivery.Composed { b := newBed(t) - b.env.SelfComposed, b.env.Trusted, b.kept, b.env.Applied, b.env.Haves = true, nil, kept, store, h + b.env.SelfComposed, b.kept, b.env.Applied, b.env.Haves = true, kept, store, h firstCh := make(chan []delivery.Composed, 1) comps := b.comps var ag *Agent @@ -1155,8 +1066,7 @@ func testARemovedModulesCompositionHoldsNothingBack(t *testing.T) { } } -// The node's own composition is not applied while a module's composition in the node's bucket cannot be read or is not -// signed by a trusted key: what that one carries may be what left the node's own, and the node's own apply would remove +// The node's own composition is not applied while a module's composition in the node's bucket cannot be read: what that one carries may be what left the node's own, and the node's own apply would remove // it (issue 1945, the review's gap 2). It is said, naming the composition, and applied once that one reads. func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) { b := newBed(t) @@ -1284,11 +1194,11 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) { ck, _ := delivery.ComposedKey(node, "power", fp) applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true, Resources: []json.RawMessage{filledFile("settings"), filledFile("bound")}} - _, appliedRaw, appliedDig := signedBy(t, applied, ck, controllerKey) + _, appliedRaw, appliedDig := encodedAs(t, applied, ck) newer := applied newer.Resources = append([]json.RawMessage{}, applied.Resources...) newer.Resources = append(newer.Resources, file("power", "later", "1")) - _, newerRaw, _ := signedBy(t, newer, ck, controllerKey) + _, newerRaw, _ := encodedAs(t, newer, ck) for _, tc := range []struct { name string // have is the module's have; bucket the value its composition key holds now, history the revisions kept. @@ -1371,7 +1281,7 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) { // the yes for that composition, and is then applied. func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(t *testing.T) { b := newBed(t) - b.env.SelfComposed, b.env.Trusted = true, nil + b.env.SelfComposed = true b.env.Policy = func(_ context.Context, m string) string { if m == "db" || m == "rec" { return catalogue.PolicyRecord diff --git a/internal/agent/composition.go b/internal/agent/composition.go index 1e5c70d..938c2aa 100644 --- a/internal/agent/composition.go +++ b/internal/agent/composition.go @@ -2,7 +2,6 @@ package agent import ( "context" - "crypto/ed25519" "errors" "fmt" @@ -194,8 +193,8 @@ func (a *Agent) compositionKey(module, fp string) (string, error) { return delivery.ComposedKey(a.env.Node, module, fp) } -// revisionsOf is every revision of a composition key the bucket keeps, oldest first, each with its digest as this node -// trusts it (its own unsigned with SelfComposed, else signed by a trusted key), or why it is refused. Nil Revisions +// revisionsOf is every revision of a composition key the state keeps, oldest first, each with its digest (unsigned, ADR +// 0593 §6), or why it is refused. Nil Revisions // reads none. func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error) { if a.env.Revisions == nil { @@ -205,10 +204,6 @@ func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error) if err != nil { return nil, err } - var trusted []ed25519.PublicKey - if a.env.Trusted != nil { - trusted = a.env.Trusted() - } var out []compNow for _, u := range revs { if u.Deleted || u.Key != key { @@ -219,12 +214,7 @@ func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error) out = append(out, compNow{Raw: u.Value, Rev: u.Revision, Err: err}) continue } - var dig string - if a.env.SelfComposed { - dig, err = delivery.ComposedDigestOf(c) // the node's own: unsigned (ADR 0547 §2) - } else { - dig, err = delivery.VerifyComposed(c, trusted) - } + dig, err := delivery.ComposedDigestOf(c) // unsigned: a record, never verified (ADR 0593 §6) out = append(out, compNow{Raw: u.Value, C: c, Dig: dig, Rev: u.Revision, Err: err}) } return out, nil diff --git a/internal/compose/composed.go b/internal/compose/composed.go index d7a4661..9dc317d 100644 --- a/internal/compose/composed.go +++ b/internal/compose/composed.go @@ -21,8 +21,8 @@ import ( // gets that one; but for a module the node runs inside its own composition (Node.InOwn, and the // private network's always), which fps names at no fingerprint of the new form: its resources go into the node's own, // their module taken off and their ids still naming it, and its filled paths with them, as the controller's composed -// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are not -// signed and not yet by digest: the writer does both (delivery.SignComposed, Composed.ByDigest). +// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned +// (ADR 0593 §6) and not yet by digest (Composed.ByDigest). func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) { out, skipped, _, err = compositions(in, node, fps, mode, false) return out, skipped, err diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index 665e370..0ea6d69 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -123,7 +123,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { } if raw := state.kv[lemursKey].Value; raw != nil { comp, err := delivery.DecodeComposed(raw) - if err != nil || comp.Module != "lemurs" || comp.FP != fp || comp.Signature != nil || len(comp.Resources) == 0 { + if err != nil || comp.Module != "lemurs" || comp.FP != fp || len(comp.Resources) == 0 { t.Errorf("the published composition of lemurs: %+v, %v", comp, err) } }