From f41a5a8df17e5284e0813915d336515fbc10ca5c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 01:10:39 +0100 Subject: [PATCH] Vault: keep a rotation's hand acts in mesh-sdk's NodeLogin.Acts, and test a tunnel without its key or range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-sdk now carries the hand acts that warranted a node's rotations as NodeLogin.Acts (ADR 0583 §2), so the vault's own `acts` field beside the codec is gone: a used act is refused from the key's Acts, and a key written without them keeps the ones recorded. The seat's tunnel takes all five fields; the wire table refuses an issue missing the machine's tunnel key or the range as well. Built against mesh-sdk 4224665; mesh-vault stays at 134 unit tests. (issue 774, 774/S43, 774/S46, review round 1b) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-vault/enrolment_test.go | 25 +----- .../mesh-vault/cmd/mesh-vault/noderotation.go | 82 ++++--------------- 2 files changed, 21 insertions(+), 86 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go index af6d1c15..1d33bde9 100644 --- a/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/enrolment_test.go @@ -25,26 +25,6 @@ import ( type loginsState struct { mu sync.Mutex keys map[string]NodeLogin - acts map[string][]string -} - -func (l *loginsState) Acts(node string) ([]string, error) { - l.mu.Lock() - defer l.mu.Unlock() - return slices.Clone(l.acts[node]), nil -} - -func (l *loginsState) RecordAct(n NodeLogin, act string) error { - if err := l.Put(n); err != nil { - return err - } - l.mu.Lock() - defer l.mu.Unlock() - if l.acts == nil { - l.acts = map[string][]string{} - } - l.acts[n.Node] = append(l.acts[n.Node], act) - return nil } func (l *loginsState) Get(node string) (*NodeLogin, error) { @@ -78,7 +58,6 @@ func (l *loginsState) Delete(node string) error { l.mu.Lock() defer l.mu.Unlock() delete(l.keys, node) - delete(l.acts, node) return nil } @@ -231,6 +210,8 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) { {"a tunnel without the hub's key", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("hub_key")}, nil, nil, "hub's tunnel key"}, {"a tunnel without the hub's endpoint", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("hub_endpoint")}, nil, nil, "where the hub's tunnel is dialled"}, {"a tunnel without the address", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("address")}, nil, nil, "this machine's address"}, + {"a tunnel without the machine's tunnel key", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("key")}, nil, nil, "the tunnel key it was issued for"}, + {"a tunnel without the range", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("range")}, nil, nil, "the private network's range"}, {"a tunnel with a field it has not", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": with(tunnel, map[string]any{"port": 1})}, nil, nil, "not a first tunnel"}, // Issue answers what the machine must trust; sealed and drop as the holder sends them, the switch as a word. {"issue", "enrolment-token", holder, map[string]any{"node": "new1", "expires": "2026-11-02T12:00:00Z", "tunnel": tunnel}, &issued, []string{"node", "broker", "fingerprint", "signer"}, ""}, @@ -452,7 +433,7 @@ func TestANodesLoginRotatesOneAtATimeAndEndsWithTheListWithoutTheOldOne(t *testi t.Error("a finished rotation was abandoned") } // Blocker 5 (ADR 0583 §2): the act is recorded in g14's key and warrants no second rotation. - if acts, _ := e.logins.Acts("g14"); !slices.Equal(acts, []string{"a1"}) { + if acts := e.logins.keys["g14"].Acts; !slices.Equal(acts, []string{"a1"}) { t.Errorf("g14's key records the acts %v", acts) } if _, err := ask("g14", "a1"); err == nil || !strings.Contains(err.Error(), "already") { diff --git a/modules/mesh-vault/cmd/mesh-vault/noderotation.go b/modules/mesh-vault/cmd/mesh-vault/noderotation.go index f732f9fd..9c074f3f 100644 --- a/modules/mesh-vault/cmd/mesh-vault/noderotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/noderotation.go @@ -52,14 +52,10 @@ const ( // and while a rotation runs the other login with its stage. Never a value or a hash. type NodeLogin = delivery.NodeLogin -// NodeLoginStore is the vault's state `node-logins`, by node. Beside mesh-sdk's NodeLogin each key keeps `acts`, the ids -// of the hand acts that warranted the node's rotations (ADR 0583 §2: every rotation is recorded), which Put keeps. +// NodeLoginStore is the vault's state `node-logins`, by node. type NodeLoginStore interface { Get(node string) (*NodeLogin, error) Put(n NodeLogin) error - // Acts are the hand acts recorded in node's key; RecordAct writes n with act added to them. - Acts(node string) ([]string, error) - RecordAct(n NodeLogin, act string) error Delete(node string) error Keys() ([]string, error) } @@ -79,32 +75,8 @@ func (busNodeLogins) Get(node string) (*NodeLogin, error) { return &n, nil } -func (b busNodeLogins) Put(n NodeLogin) error { - acts, err := b.Acts(n.Node) - if err != nil { - return err - } - return b.write(n, acts) -} - -func (busNodeLogins) Acts(node string) ([]string, error) { - e, err := stdio.State(NodeLoginsState).Get(node) - if err != nil || e == nil { - return nil, err - } - return actsOf(e.Value) -} - -func (b busNodeLogins) RecordAct(n NodeLogin, act string) error { - acts, err := b.Acts(n.Node) - if err != nil { - return err - } - return b.write(n, append(acts, act)) -} - -func (busNodeLogins) write(n NodeLogin, acts []string) error { - raw, err := encodeLoginKey(n, acts) +func (busNodeLogins) Put(n NodeLogin) error { + raw, err := delivery.EncodeNodeLogin(n) if err != nil { return err } @@ -112,31 +84,6 @@ func (busNodeLogins) write(n NodeLogin, acts []string) error { return err } -// encodeLoginKey is n through mesh-sdk's codec with the hand acts that warranted its rotations beside it. -func encodeLoginKey(n NodeLogin, acts []string) ([]byte, error) { - raw, err := delivery.EncodeNodeLogin(n) - if err != nil || len(acts) == 0 { - return raw, err - } - var m map[string]any - if err := json.Unmarshal(raw, &m); err != nil { - return nil, err - } - m["acts"] = acts - return json.Marshal(m) -} - -// actsOf are the hand acts a key of node-logins records. -func actsOf(raw []byte) ([]string, error) { - var k struct { - Acts []string `json:"acts"` - } - if err := json.Unmarshal(raw, &k); err != nil { - return nil, fmt.Errorf("a key of %s is not readable: %w", NodeLoginsState, err) - } - return k.Acts, nil -} - func (busNodeLogins) Delete(node string) error { return stdio.State(NodeLoginsState).Delete(node) } func (busNodeLogins) Keys() ([]string, error) { return stdio.State(NodeLoginsState).Keys() } @@ -180,10 +127,20 @@ func (v *Vault) getLogin(node string) (*NodeLogin, error) { return v.Logins.Get(node) } +// putLogin writes node's key; a key written without its hand acts keeps the ones recorded (ADR 0583 §2). func (v *Vault) putLogin(n NodeLogin) error { if v.Logins == nil { return errors.New("the vault has no state " + NodeLoginsState + " to write") } + if n.Acts == nil { + was, err := v.Logins.Get(n.Node) + if err != nil { + return err + } + if was != nil { + n.Acts = was.Acts + } + } return v.Logins.Put(n) } @@ -372,17 +329,13 @@ func (v *Vault) recordAct(node, act string) error { if v.Logins == nil { return errors.New("the vault has no state " + NodeLoginsState + " to record the rotation in") } - used, err := v.Logins.Acts(node) - if err != nil { - return err - } - if slices.Contains(used, act) { - return fmt.Errorf("the hand act %s warranted a rotation of %s already: record a new one", act, node) - } nl, err := v.getLogin(node) if err != nil { return err } + if nl != nil && slices.Contains(nl.Acts, act) { + return fmt.Errorf("the hand act %s warranted a rotation of %s already: record a new one", act, node) + } if nl == nil { cur, err := v.currentLogin(node) if err != nil { @@ -390,7 +343,8 @@ func (v *Vault) recordAct(node, act string) error { } nl = &NodeLogin{Node: node, Login: cur, Since: v.Now().UTC()} } - if err := v.Logins.RecordAct(*nl, act); err != nil { + nl.Acts = append(slices.Clone(nl.Acts), act) + if err := v.Logins.Put(*nl); err != nil { return fmt.Errorf("the hand act %s could not be recorded in %s %s: %w", act, NodeLoginsState, node, err) } return nil