From f5ccbb2700e4bdb5519010439bd09c641025a83d Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:24:01 +0100 Subject: [PATCH] Open every link with no signer instead of following the controller's key The controller that signed the hand-over, search, judgement, access and move asks ended (issue 1126), so a link believes none of them by having no signer, and the per-ask key it was told to follow goes with the lease. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- cmd/mesh-host/main.go | 3 --- cmd/mesh-host/trust.go | 4 ---- internal/link/handover.go | 15 +++----------- internal/link/handover_test.go | 38 ++++++++-------------------------- internal/link/hearing_nats.go | 32 +++++++++------------------- internal/link/run.go | 6 ------ internal/link/session.go | 4 ---- 7 files changed, 22 insertions(+), 80 deletions(-) diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 8ca4e66..697b692 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1120,9 +1120,6 @@ func runLink(ctx context.Context, opts options) error { Membership: membership, // And the public halves and certificate its record says (novox/hq ADR 0555 §3), read at each facts report. Keys: recordKeys(opts.state, trust.Trusts), - // And no controller's key: every ask the controller would sign is refused, never verified with the membership's - // signer (novox/hq ADR 0563 §7; issue 1126, the controller ended by its unassignment). - ControllerKey: noControllerKey, // And the first have this build writes is what proves it to the launcher (ADR 0539 §9, ADR 0227 §8). HaveWritten: proof.haveWritten, // And the operator's hand-over of a directory used as found, carried by the controller from the diff --git a/cmd/mesh-host/trust.go b/cmd/mesh-host/trust.go index 23a5ec4..29f2702 100644 --- a/cmd/mesh-host/trust.go +++ b/cmd/mesh-host/trust.go @@ -36,10 +36,6 @@ type meshTrust struct { // newMeshTrust trusts nothing until the vault's key is read. func newMeshTrust() *meshTrust { return &meshTrust{} } -// noControllerKey is the controller's key as this engine trusts it: none. A link told it refuses every ask signed as -// the controller's, where a link told nothing would verify it with the membership's signer (link.HandOverCheck). -func noControllerKey() ed25519.PublicKey { return nil } - // onChange names who is told when what it trusts changed. func (t *meshTrust) onChange(f func()) { t.mu.Lock() diff --git a/internal/link/handover.go b/internal/link/handover.go index e6103f0..d45ad23 100644 --- a/internal/link/handover.go +++ b/internal/link/handover.go @@ -76,13 +76,9 @@ const MaxHandOverBy = 200 // HandOverCheck is what an ask is held to: the mesh's key, this node's name, the clock, and the nonces taken. type HandOverCheck struct { Signer ed25519.PublicKey - // SignerNow, when set, is the controller's key as the engine trusts it at this ask, read per ask in place of Signer: - // nil once the controller's lease is handed away, which refuses every ask signed by its key (novox/hq ADR 0563 §7, - // ADR 0550 §2). - SignerNow func() ed25519.PublicKey - Node string - Now func() time.Time - Nonces *Nonces + Node string + Now func() time.Time + Nonces *Nonces } // Nonces are the nonces of asks taken, each kept until its ask expires: an ask is taken once. Kept in memory — @@ -162,11 +158,6 @@ func believedBytesWithin(check HandOverCheck, context string, withPath bool, bod return HandOverAsk{}, nil, "the ask is not a signed ask: " + err.Error() } signer := check.Signer - if check.SignerNow != nil { - if signer = check.SignerNow(); signer == nil { - return HandOverAsk{}, nil, "the controller's lease is handed away, and no ask signed by its key is taken" - } - } if len(signer) != ed25519.PublicKeySize || !ed25519.Verify(signer, append([]byte(context), signed.Ask...), signed.Signature) { return HandOverAsk{}, nil, "the ask is not signed by the mesh" diff --git a/internal/link/handover_test.go b/internal/link/handover_test.go index 8be95ba..be1b363 100644 --- a/internal/link/handover_test.go +++ b/internal/link/handover_test.go @@ -148,20 +148,14 @@ func TestAHandOverAskIsBelievedOnlySignedForThisNodeUnexpiredAndOnce(t *testing. if strings.Join(given, "; ") != "/srv/notes by jo through mesh-cli on anchor; /refused by jo through mesh-cli on anchor" { t.Fatalf("the handler was given %v", given) } - // ADR 0563 §7, ADR 0550 §2: after the lease write the engine answers no ask signed by the controller's key. The key - // is read per ask from what the engine trusts now; nil (the lease handed away) refuses, and nothing is handed over. - trusted := m.Signer - following := HandOverCheck{Signer: m.Signer, SignerNow: func() ed25519.PublicKey { return trusted }, Node: m.Node, - Now: func() time.Time { return now }, Nonces: &Nonces{}} + // Issue 1126: a link opened on the mesh has no signer (the controller that signed these asks ended), so an ask signed + // by the membership's key, the controller's, is refused and nothing is handed over. before := len(given) - trusted = nil - late := signedAsk(t, key, with(func(a *HandOverAsk) { a.Expires = now.Add(time.Minute); a.Nonce = strings.Repeat("cd", 16) })) - if a := AnswerHandOver(following, handle, late); !strings.Contains(a.Refused, "lease is handed away") || len(given) != before { - t.Fatalf("after the lease write an ask signed by the controller was answered %+v", a) - } - trusted = m.Signer - if a := AnswerHandOver(following, handle, late); a.Refused != "" || len(given) != before+1 { - t.Fatalf("while the controller holds the lease its ask was answered %+v", a) + opened := (&natsLink{node: m.Node}).check() + opened.Now, opened.Nonces = func() time.Time { return now }, &Nonces{} + late := signedAsk(t, key, with(func(a *HandOverAsk) { a.Nonce = strings.Repeat("cd", 16) })) + if a := AnswerHandOver(opened, handle, late); !strings.Contains(a.Refused, "not signed by the mesh") || len(given) != before { + t.Fatalf("a link opened on the mesh answered an ask signed by the controller's key: %+v", a) } // A nonce is forgotten once its ask has expired, so the set does not grow for ever. now = now.Add(HandOverGood + time.Second) @@ -192,23 +186,17 @@ func TestAHandOverIsAnsweredOnlyWhenItCarriesAReply(t *testing.T) { // handingLink is a quiet link that hears hand-overs, keeping the handler it was given. type handingLink struct { *quietLink - given chan HandOverFunc - signer chan func() ed25519.PublicKey + given chan HandOverFunc } func (l handingLink) HearHandOvers(h HandOverFunc) { l.given <- h } -// FollowSigner keeps what the link is told to verify the controller's asks with. -func (l handingLink) FollowSigner(f func() ed25519.PublicKey) { l.signer <- f } - // **The link is given the queue's hand-over** when it is served: what the controller asks reaches the engine's // judgement, and nothing else. func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) { m, _ := aMember(t) - l := handingLink{quietLink: newQuietLink(), given: make(chan HandOverFunc, 1), signer: make(chan func() ed25519.PublicKey, 1)} + l := handingLink{quietLink: newQuietLink(), given: make(chan HandOverFunc, 1)} q := &Session{Membership: m, Timeout: time.Second} - // The link verifies the controller's asks with the key the engine trusts now (ADR 0563 §7). - q.ControllerKey = func() ed25519.PublicKey { return nil } q.HandOver = func(path, by string) (string, error) { return "the queue's: " + path + " by " + by, nil } ctx, stop := context.WithCancel(context.Background()) served := make(chan error, 1) @@ -221,14 +209,6 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) { case <-time.After(5 * time.Second): t.Fatal("the link was never given the queue's hand-over") } - select { - case f := <-l.signer: - if f() != nil { - t.Fatal("the link was given another key than the queue's") - } - case <-time.After(5 * time.Second): - t.Fatal("the link was never given the key the engine trusts now") - } stop() <-served // 774 (ADR 0563 §1): a call of the node seat reaches its verb with its caller as the subject proves it, unproven on diff --git a/internal/link/hearing_nats.go b/internal/link/hearing_nats.go index 06cedf2..960fd20 100644 --- a/internal/link/hearing_nats.go +++ b/internal/link/hearing_nats.go @@ -8,7 +8,6 @@ import ( "os" "regexp" "strings" - "sync" "time" "github.com/nats-io/nats.go" @@ -39,14 +38,11 @@ func natsURL(address string) string { // natsLink is this node's connection as a JetStream subscription. type natsLink struct { - // mu guards signerNow (FollowSigner). - mu sync.Mutex - signerNow func() ed25519.PublicKey - conn *nats.Conn - js nats.JetStreamContext - asking *nats.Subscription - asked chan struct{} - handing *nats.Subscription + conn *nats.Conn + js nats.JetStreamContext + asking *nats.Subscription + asked chan struct{} + handing *nats.Subscription // searching hears the controller's terminal asking for a fresh setuid search (novox/hq issue 361). searching *nats.Subscription // judging hears the controller asking for a read-only judgement of this node's directories (issue 358). @@ -117,7 +113,9 @@ func dialNats(ctx context.Context, m Membership, timeout time.Duration) (Link, e } l := &natsLink{ - conn: conn, js: js, node: m.Node, user: user, signer: m.Signer, + // No signer: the asks these subjects carry were signed by the controller, which ended by its unassignment + // (issue 1126), so a link opened on the mesh believes none of them (ADR 0563 §7, ADR 0550 §2). + conn: conn, js: js, node: m.Node, user: user, lost: make(chan error, 1), asked: make(chan struct{}, 1), refused: make(chan string, 64), @@ -163,19 +161,9 @@ func (l *natsLink) hearAsks() { } } -// FollowSigner has every signed ask heard on this link verified with the key the engine trusts at that ask, the -// controller's while it holds its lease and none from its hand-over (novox/hq ADR 0563 §7, ADR 0550 §2). -func (l *natsLink) FollowSigner(f func() ed25519.PublicKey) { - l.mu.Lock() - l.signerNow = f - l.mu.Unlock() -} - -// check is how a signed ask is believed on this link: the static membership signer until FollowSigner names the trust. +// check is how a signed ask is believed on this link: by the link's signer, which a link opened on the mesh has none of. func (l *natsLink) check() HandOverCheck { - l.mu.Lock() - defer l.mu.Unlock() - return HandOverCheck{Signer: l.signer, SignerNow: l.signerNow, Node: l.node} + return HandOverCheck{Signer: l.signer, Node: l.node} } // HearHandOvers listens for the controller asking this node to hand a directory used as found to the mesh diff --git a/internal/link/run.go b/internal/link/run.go index 968d504..825373d 100644 --- a/internal/link/run.go +++ b/internal/link/run.go @@ -199,12 +199,6 @@ func serve(ctx context.Context, link Link, m Membership, queue *Session, say Ann } // And the controller asking this node to hand a directory used as found to the mesh (novox/hq issue // 356), answered on the link the question came over. - // Every signed ask is verified with the key trusted at that ask, none after the lease write (ADR 0563 §7). - if f, ok := link.(interface { - FollowSigner(func() ed25519.PublicKey) - }); ok && queue.ControllerKey != nil { - f.FollowSigner(queue.ControllerKey) - } if h, ok := link.(HandOvers); ok { h.HearHandOvers(queue.HandOver) } diff --git a/internal/link/session.go b/internal/link/session.go index 832348e..882e03d 100644 --- a/internal/link/session.go +++ b/internal/link/session.go @@ -2,7 +2,6 @@ package link import ( "context" - "crypto/ed25519" "encoding/json" "errors" "fmt" @@ -54,9 +53,6 @@ type Session struct { // MoveSecret moves one value sealed to this machine into the vault, asked by the controller during the move of // the secrets (design 57's V3, issue 676). Nil answers every such ask with a refusal. MoveSecret MoveSecretFunc - // ControllerKey is the controller's key as the engine trusts it now: nil once its lease is handed away (novox/hq - // ADR 0563 §7). Each link verifies every signed ask with it, read per ask; nil ControllerKey keeps the membership's. - ControllerKey func() ed25519.PublicKey // Acts is the node seat node-engine this engine holds (novox/hq ADR 0563): served on every link, and its asks to the // operator asked and heard over it. Nil serves no seat. Acts *HandActs