Stop verifying composition signatures: the node applies only its own
ADR 0593 §6: a composition read from composed-<node> is a record never applied, and the engine applies only what it composes itself, so no composition carries a signature and nothing is verified. The agent's trusted signers, its signed-composition test and the unused signing helper of the integration bed go, so mesh-sdk can drop SignComposed, VerifyComposed and Composed.Signature. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
+3
-20
@@ -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))
|
||||
}
|
||||
|
||||
+12
-102
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user