The caller must be on the vault's trusted list retire-callers (the holder of mesh-config alone) and carry a hand act it recorded for exactly this node, module and name. A waiter on rotated or own-replaced does not wake on own-retired. Reviewer note of issue 1966: HandedOf returns a placement read error instead of dropping the modules placed there. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
202 lines
8.7 KiB
Go
202 lines
8.7 KiB
Go
package main
|
|
|
|
// The tools of enrolment and node rotation (novox/hq ADR 0587, ADR 0583): the seat's verbs the vault serves, and the
|
|
// tools that read what it holds of them. Arguments are strings and booleans the runtime hands; the caller is the one the
|
|
// runtime proved.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// EnrolmentHeld is one enrolment token the vault holds, as secret_holders lists it: never the token or its hash.
|
|
type EnrolmentHeld struct {
|
|
Node string `json:"node"`
|
|
User string `json:"user"`
|
|
Generation int `json:"generation"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
At time.Time `json:"at"`
|
|
// Account says the node's own account is made too (node-account answered).
|
|
Account bool `json:"account_made"`
|
|
}
|
|
|
|
// EnrolmentsHeld lists the enrolment tokens held, by node.
|
|
func (v *Vault) EnrolmentsHeld() ([]EnrolmentHeld, error) {
|
|
out := []EnrolmentHeld{}
|
|
if v.Accounts == nil {
|
|
return out, nil
|
|
}
|
|
users, err := v.Accounts.Users()
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
for _, u := range users {
|
|
a, err := v.Accounts.Get(u)
|
|
if err != nil || a == nil || a.Kept != KeptEnrolment {
|
|
continue
|
|
}
|
|
node := u[len(EnrolUserPrefix):]
|
|
n, _ := v.Accounts.Get(NodeEnginePrefix + node)
|
|
out = append(out, EnrolmentHeld{Node: node, User: u, Generation: a.Generation, Fingerprint: a.Fingerprint, At: a.At,
|
|
Account: n != nil && n.Kept == KeptNode})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Node < out[j].Node })
|
|
return out, nil
|
|
}
|
|
|
|
func enrolmentTools(env *Env) []toolDef {
|
|
node := str("the node's name")
|
|
return []toolDef{
|
|
{Name: Seat + ".enrolment-token",
|
|
Description: "The enrolment token of a joining machine, for the seat mesh-enrolment alone (novox/hq ADR 0587 §3, §5, " +
|
|
"§8). Issue (the default): takes the machine's first tunnel, makes the one-time secret, keeps both with the operator " +
|
|
"copy, writes the secret's hash as the bus user enrol.<node>, and answers the bus's address, its certificate's " +
|
|
"fingerprint and the vault's signing key, never the token. sealed: the whole token sealed to the operator key, " +
|
|
"for the operator to open at their terminal. drop: the token and its hash gone, at every end of an enrolment.",
|
|
Input: map[string]any{"node": node, "expires": str("when the token ends, as the seat states it"),
|
|
"tunnel": map[string]any{"type": "object", "description": "the machine's first tunnel at issue: hub_key, " +
|
|
"hub_endpoint, address (and key, range)"},
|
|
"sealed": map[string]any{"type": "boolean", "description": "answer the token sealed to the operator key"},
|
|
"drop": map[string]any{"type": "boolean", "description": "drop the token and its hash"}},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tunnel, err := tunnelArg(a["tunnel"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.EnrolmentToken(caller, EnrolmentTokenArgs{Node: arg(a, "node"), Expires: arg(a, "expires"),
|
|
Tunnel: tunnel, Sealed: isTrue(a["sealed"]), Drop: isTrue(a["drop"])})
|
|
}},
|
|
{Name: Seat + ".node-account",
|
|
Description: "A joining node's account node.<node>, for the seat mesh-enrolment alone at the redeem (novox/hq ADR 0587 " +
|
|
"§3): made once for a node and the keys it presented, answered sealed to the sealing key presented, with the " +
|
|
"node's key in node-logins naming the login in use. Asked again for the same keys it answers the same account. " +
|
|
"retire: the account, its value and its key taken away at a revoke.",
|
|
Input: map[string]any{"node": node, "sealing_key": str("the machine's X25519 sealing key, raw in base64 or SPKI PEM"),
|
|
"keys_digest": str("the digest of the keys the machine presented"),
|
|
"retire": map[string]any{"type": "boolean", "description": "take the account away"}},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.NodeAccount(caller, NodeAccountArgs{Node: arg(a, "node"), SealingKey: arg(a, "sealing_key"),
|
|
KeysDigest: arg(a, "keys_digest"), Retire: isTrue(a["retire"])})
|
|
}},
|
|
{Name: Seat + ".rotate-node",
|
|
Description: "Begin the rotation of a node's login (novox/hq ADR 0583 §2, §7): answered only for a call that carries a hand " +
|
|
"act recorded through mesh-hand-acts, read back by its id. One rotation at a time mesh-wide: a call asked while " +
|
|
"another runs is queued and begun when it is done, the bus node after every other. Makes the other login " +
|
|
"(node.<n>--b or node.<n>), keeps its value for the node's engine, and says rotation-begun.",
|
|
Input: map[string]any{"node": node, "why": str("why the login rotates"),
|
|
"act": str("the id of the hand act recorded through mesh-hand-acts that warrants it")},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.RotateNode(caller, RotateNodeArgs{Node: arg(a, "node"), Why: arg(a, "why"), Act: arg(a, "act")})
|
|
}},
|
|
{Name: Seat + ".node-account-confirm",
|
|
Description: "A node-engine, dialled as its new login, says it connects with it (novox/hq ADR 0583 §3, §4): the runtime " +
|
|
"proves the login, the old login's hash is removed, and rotation-done follows when the loaded list lacks it.",
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.NodeAccountConfirm(caller)
|
|
}},
|
|
{Name: Seat + ".abandon",
|
|
Description: "Abandon a node's rotation before its confirmation (novox/hq ADR 0583 §5): the new login and its value are " +
|
|
"dropped, the old hash kept, the rotating part of the node's key cleared; a rotation only asked leaves the queue.",
|
|
Input: map[string]any{"node": node},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.AbandonRotation(caller, arg(a, "node"))
|
|
}},
|
|
{Name: Seat + "." + RetireVerb,
|
|
Description: "Retire one module's own secret on a node because no manifest reads it any more (design 32): removed " +
|
|
"whole, and own-retired said (node, module, name, the ask, the caller; never a value). Answered to the holder of " +
|
|
"the seat mesh-config alone, as the vault's list retire-callers names it, and only for a hand act it recorded " +
|
|
"through mesh-hand-acts for this verb naming exactly this node, module and name (`act`), read back by its id.",
|
|
Input: map[string]any{"node": node, "module": str("the module whose secret it is"), "name": str("the secret's name"),
|
|
"act": str("the id of the hand act recorded through mesh-hand-acts that warrants it"),
|
|
"ask": str("the id of the operator's ask whose Approve this answers, kept in what is said")},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.RetireOwn(caller, RetireArgs{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
|
|
Act: arg(a, "act"), Ask: arg(a, "ask")})
|
|
}},
|
|
{Name: "node_logins",
|
|
Description: "Every node's key of the vault's state node-logins: the login its node-engine dials as and, while a rotation " +
|
|
"runs, the other login and its stage; with the rotations queued and the enrolment tokens held. Never a value or a hash.",
|
|
run: func(string, map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if v.Logins == nil {
|
|
return nil, errors.New("the vault has no state node-logins")
|
|
}
|
|
keys, err := v.Logins.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(keys)
|
|
rows := []NodeLogin{}
|
|
for _, k := range keys {
|
|
if nl, err := v.Logins.Get(k); err == nil && nl != nil {
|
|
rows = append(rows, *nl)
|
|
}
|
|
}
|
|
held, _ := v.EnrolmentsHeld()
|
|
return map[string]any{"logins": rows, "rotation": v.RotationState(), "enrolments": held}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
// tunnelArg reads enrolment-token's `tunnel`, an object or its JSON as a string, as mesh-sdk's TokenTunnel; nil when
|
|
// absent.
|
|
func tunnelArg(v any) (*delivery.TokenTunnel, error) {
|
|
var raw []byte
|
|
switch t := v.(type) {
|
|
case nil:
|
|
return nil, nil
|
|
case string:
|
|
if strings.TrimSpace(t) == "" {
|
|
return nil, nil
|
|
}
|
|
raw = []byte(t)
|
|
default:
|
|
b, err := json.Marshal(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw = b
|
|
}
|
|
var tt delivery.TokenTunnel
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&tt); err != nil {
|
|
return nil, fmt.Errorf("`tunnel` is not a first tunnel (hub_key, hub_endpoint, address, key, range): %w", err)
|
|
}
|
|
return &tt, nil
|
|
}
|