Vault: take the node-logins key and the enrolment token from mesh-sdk's contract

The vault wrote its own copies of the NodeLogin key and the token; the node-engine and the grants part read them through
mesh-sdk's codec (delivery.EncodeNodeLogin refuses a key without its version, a login that is not the node's, a stage
without a rotation). It now writes and reads through that codec and builds the token with delivery.Token, so the shapes
are one (ADR 0583 §1 insight, ADR 0587 §3). Built against the group's mesh-sdk with a go.work outside the worktree; go.mod
still pins the earlier mesh-sdk, for the lead to re-pin.

(issue 774, 774/S43, 774/S46, 774/S49)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:42:19 +01:00
parent 9249ca7b57
commit 272eff8b39
3 changed files with 33 additions and 64 deletions
+9 -40
View File
@@ -24,6 +24,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal"
"golang.org/x/crypto/bcrypt"
)
@@ -31,9 +32,9 @@ import (
// The names enrolment gives: the module that holds the seat mesh-enrolment, the prefix of a token's bus user, and the
// vault's state of each node's logins.
const (
EnrolmentModule = "mesh-enrolment"
EnrolUserPrefix = "enrol."
NodeLoginsState = "node-logins"
EnrolmentModule = delivery.EnrolmentModule
EnrolUserPrefix = delivery.EnrolmentUserPrefix
NodeLoginsState = delivery.NodeLoginsState
// MeshHostModule is the module a node's own login secret is kept under (`own/<node>/mesh-host/<login>`, ADR 0583 §3).
MeshHostModule = "mesh-host"
)
@@ -130,41 +131,9 @@ func (v *Vault) fromEnrolment(caller, verb string) error {
// TokenBase is what a token states beside its secret: where the bus is and the certificate to expect there, and the
// machine's first tunnel when the token carries one.
type TokenBase struct {
Broker string `json:"broker"`
Fingerprint string `json:"fingerprint"`
Tunnel *TokenTunnel `json:"tunnel,omitempty"`
}
// TokenTunnel is the joining machine's side of its first tunnel, in the wire format the node-engine parses.
type TokenTunnel struct {
Key string `json:"key"`
Address string `json:"address"`
Range string `json:"range"`
HubKey string `json:"hub_key"`
HubEndpoint string `json:"hub_endpoint"`
}
// EnrolToken is the token a person carries to a joining machine, in the wire format of the node-engine's parser
// (mesh-host internal/identity.Token): version, node, the bus's address and certificate fingerprint, the vault's public
// signing key, the one-time secret, and the tunnel when there is one.
type EnrolToken struct {
Version int `json:"v"`
Node string `json:"node,omitempty"`
Broker string `json:"broker,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Signer []byte `json:"signer,omitempty"`
Secret string `json:"secret"`
Adopted bool `json:"adopted,omitempty"`
Tunnel *TokenTunnel `json:"tunnel,omitempty"`
}
// Text is the token as a person types it: base64url of its JSON.
func (t EnrolToken) Text() (string, error) {
raw, err := json.Marshal(t)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(raw), nil
Broker string `json:"broker"`
Fingerprint string `json:"fingerprint"`
Tunnel *delivery.TokenTunnel `json:"tunnel,omitempty"`
}
// EnrolmentTokenArgs are `enrolment-token`'s arguments: the node, when it expires (the seat's word, echoed), and which
@@ -268,8 +237,8 @@ func (v *Vault) showEnrolmentToken(caller, node string) (EnrolmentTokenAnswer, e
if err != nil {
return EnrolmentTokenAnswer{}, fmt.Errorf("the token carries the vault's signing key, which is not held yet: %w", err)
}
text, err := EnrolToken{Version: 1, Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint,
Signer: []byte(keys.key.Public().(ed25519.PublicKey)), Secret: secret, Tunnel: base.Tunnel}.Text()
text, err := delivery.Token{Version: 1, Node: node, Broker: base.Broker, Fingerprint: base.Fingerprint,
Signer: []byte(keys.key.Public().(ed25519.PublicKey)), Secret: secret, Tunnel: base.Tunnel}.Encode()
if err != nil {
return EnrolmentTokenAnswer{}, err
}
@@ -35,7 +35,10 @@ func (l *loginsState) Get(node string) (*NodeLogin, error) {
func (l *loginsState) Put(n NodeLogin) error {
l.mu.Lock()
defer l.mu.Unlock()
raw, _ := json.Marshal(n)
raw, err := delivery.EncodeNodeLogin(n)
if err != nil {
return err
}
if field := credentialField(raw); field != "" {
return fmt.Errorf("%s carries a field %q, which names a credential", n.Node, field)
}
@@ -152,7 +155,7 @@ func TestTheVaultMakesAnEnrolmentsTokenAccountAndSignature(t *testing.T) {
t.Fatalf("show answered %+v, %v", shown, err)
}
text := openOperator(t, e.operatorPublic, e.operatorPrivate, shown.Sealed)
var tok EnrolToken
var tok delivery.Token
body, _ := base64.RawURLEncoding.DecodeString(text)
signer, _ := e.keys()
if err := json.Unmarshal(body, &tok); err != nil || tok.Version != 1 || tok.Secret != secret || tok.Node != "new1" ||
@@ -29,35 +29,28 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"golang.org/x/crypto/bcrypt"
)
// The events a rotation says, and the stages of a node's key.
// The events a rotation says and the stages of a node's key are mesh-sdk's (delivery/node_logins.go); a rotation warranted
// by a hand act is warranted for a day.
const (
RotationBegunEvent = "rotation-begun"
NodeAccountReadyEvent = "node-account-ready"
RotationDoneEvent = "rotation-done"
RotationBegunEvent = delivery.RotationBegunEvent
NodeAccountReadyEvent = delivery.NodeAccountReadyEvent
RotationDoneEvent = delivery.RotationDoneEvent
StageBegun = "begun"
StageReady = "ready"
StageConfirmed = "confirmed"
StageBegun = delivery.RotationBegun
StageReady = delivery.RotationReady
StageConfirmed = delivery.RotationConfirmed
// HandActWithin is how recent a hand act is that warrants a rotation.
HandActWithin = 24 * time.Hour
)
// NodeLogin is one key of the vault's state `node-logins`: the login the node's node-engine dials as, and, while a
// rotation runs, the other login and how far it has come. Never a value or a hash.
type NodeLogin struct {
Node string `json:"node"`
Login string `json:"login"`
// Rotating is the other login while a rotation runs; empty reads as none.
Rotating string `json:"rotating,omitempty"`
Stage string `json:"stage,omitempty"`
Since time.Time `json:"since,omitzero"`
// ListRevision is the revision of the loaded user list the rotation began at.
ListRevision uint64 `json:"list-revision,omitempty"`
}
// NodeLogin is one key of the vault's state node-logins (mesh-sdk's contract): the login the node's node-engine dials as,
// 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.
type NodeLoginStore interface {
@@ -75,15 +68,19 @@ func (busNodeLogins) Get(node string) (*NodeLogin, error) {
if err != nil || e == nil {
return nil, err
}
var n NodeLogin
if err := json.Unmarshal(e.Value, &n); err != nil {
n, err := delivery.DecodeNodeLogin(e.Value)
if err != nil {
return nil, fmt.Errorf("%s %s is not a node's login: %w", NodeLoginsState, node, err)
}
return &n, nil
}
func (busNodeLogins) Put(n NodeLogin) error {
_, err := stdio.State(NodeLoginsState).Put(n.Node, n)
raw, err := delivery.EncodeNodeLogin(n)
if err != nil {
return err
}
_, err = stdio.State(NodeLoginsState).Put(n.Node, json.RawMessage(raw))
return err
}