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
This commit is contained in:
2026-11-02 11:24:05 +01:00
parent 0d1bbe967d
commit f5ccbb2700
7 changed files with 22 additions and 80 deletions
-3
View File
@@ -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
-4
View File
@@ -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()
+3 -12
View File
@@ -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"
+9 -29
View File
@@ -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
+10 -22
View File
@@ -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
-6
View File
@@ -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)
}
-4
View File
@@ -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