Add the vault's enrolment verbs to its seat, so mesh-enrolment asks the vault to mint and sign and holds nothing

ADR 0587 §3 and §5 (changing ADR 0561 §5) have the vault make the token and its hash (enrolment-token), answer it
sealed to the operator key for tokens to relay (sealed, §8), drop both at every end (drop), and mint, answer again
and retire a joining node's account (node-account), with mesh-enrolment their only caller; sign names mesh-enrolment
for a joining node's certificate (with the serving key it presented, since the node has no record yet) and for a
redeem answer. Both verbs optional until the vault that runs serves them. Their answers' shapes are in go/delivery.
Tested as rows of TestTheVaultsSeatPromisesItsIntakeVerbs, no new test function.

(issue 774, 774/S37)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:41:03 +01:00
parent 2ee75954ad
commit d7a00e1f26
4 changed files with 63 additions and 9 deletions
+28 -6
View File
@@ -233,13 +233,35 @@ var defaultSeats = append([]Seat{
"controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.",
Input: schema(map[string]string{"user": "the bus user, e.g. ace.mesh-build-go"}, []string{"user"})},
{Name: "sign", Optional: true, Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): " +
"kind node-certificate signs the certificate of the calling machine's internal name for the serving key " +
"its record states, built from bus state alone and asked by that machine's node-engine only; kind " +
"enrolment signs an enrolment reply under its own context, for a caller the adoption record names. " +
"Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.",
"kind node-certificate signs the certificate of a machine's internal name, asked by that machine's " +
"node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for " +
"a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a " +
"redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature " +
"and the signer's fingerprint, and says signed; never a key.",
Input: schema(map[string]string{"kind": "node-certificate or enrolment",
"node": "the machine, for a node certificate: the caller's own",
"reply": "the enrolment reply's bytes in base64, for an enrolment"}, []string{"kind"})},
"node": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols",
"serving_key": "from mesh-enrolment: the serving key's public half the enrolling node presented",
"reply": "the redeem answer's bytes in base64, for an enrolment"}, []string{"kind"})},
// Enrolment (novox/hq ADR 0561 §5, ADR 0587 §3, §5; issue 774): mesh-enrolment is the only caller the vault's
// manifest names for these two.
{Name: "enrolment-token", Optional: true, Description: "Make a one-time token for a node mesh-enrolment " +
"issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash " +
"written as the bus user enrol.<node>. Answers only what the joining machine must trust (the bus's " +
"address, the bus certificate's fingerprint, the vault's public signing key), never the token; with " +
"sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash " +
"dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write.",
Input: schema(map[string]string{"node": "the node the token is for",
"expires": "when the token expires (RFC 3339), as the enrolment's key says",
"sealed": "\"true\": answer the token sealed to the operator key",
"drop": "\"true\": drop the node's token and its hash"}, []string{"node"}, "sealed", "drop")},
{Name: "node-account", Optional: true, Description: "Mint a joining node's bus account node.<node>, sealed " +
"to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the " +
"account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the " +
"node's node-logins key naming node.<node>. Asked by mesh-enrolment alone. Never the value in the clear.",
Input: schema(map[string]string{"node": "the joining node",
"sealing_key": "the sealing key's public half the node presented, which the account is sealed to",
"keys_digest": "the digest of every public key the node presented (delivery.KeysDigest)",
"retire": "\"true\": retire the account minted for this node"}, []string{"node"}, "retire")},
}},
// The owner of every config value (novox/hq ADR 0348 §9, design 57 K3): its holder alone writes the buckets
// mesh-config_values, _resolved, _predefined and _proposals, and serves the one verb `config` (§1, §10).
+5 -1
View File
@@ -65,6 +65,10 @@ func TestTheVaultsSeatPromisesItsIntakeVerbs(t *testing.T) {
"secret-ask": {"node", "module", "name", "desk", "asked_by", "outcome", "call", "sealed"},
"issued": {"provision", "provider", "module", "call", "sealed", "holders"},
"rotate": {"node", "module", "name", "provider", "provision", "by"},
// 774/S37/AC1: the enrolment verbs mesh-enrolment asks (ADR 0587 §3, §5).
"enrolment-token": {"node", "expires", "sealed", "drop"},
"node-account": {"node", "sealing_key", "keys_digest", "retire"},
"sign": {"kind", "node", "serving_key", "reply"},
}
for name, args := range want {
var verb *Verb
@@ -105,7 +109,7 @@ func TestTheVaultsSeatPromisesItsIntakeVerbs(t *testing.T) {
}
serving := Manifest{Module: "mesh-vault", Provides: []Offer{{Name: "secret", Scope: ScopeMesh}},
Claims: []Claim{{Name: "mesh-vault", Scope: ScopeMesh,
Serves: []string{"get", "accept", "secret-ask", "issued", "rotate"}}}}
Serves: []string{"get", "accept", "secret-ask", "issued", "rotate", "sign", "enrolment-token", "node-account"}}}}
if err := CanHold(serving, seat); err != nil {
t.Fatalf("the vault serving its verbs was refused: %v", err)
}
+28
View File
@@ -238,3 +238,31 @@ func (a RedeemAnswer) SignedBytes() []byte {
raw, _ := json.Marshal(a)
return raw
}
// **The vault's enrolment verbs** (ADR 0561 §5, ADR 0587 §3, §5), asked by mesh-enrolment alone.
const (
EnrolmentTokenVerb = "enrolment-token"
NodeAccountVerb = "node-account"
)
// EnrolmentTokenAnswer is what enrolment-token answers: what the joining machine must trust (the bus's address, its
// certificate's fingerprint, the vault's public signing key in base64), never the token; Sealed holds the token sealed
// to the operator key when asked with sealed, relayed by tokens as it is; Dropped says a drop left neither the token
// nor its hash.
type EnrolmentTokenAnswer struct {
Node string `json:"node"`
Broker string `json:"broker,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Signer string `json:"signer,omitempty"`
Sealed string `json:"sealed,omitempty"`
Dropped bool `json:"dropped,omitempty"`
}
// NodeAccountAnswer is what node-account answers: the login minted, the account sealed to the sealing key the node
// presented (base64), never in the clear; Retired says a retire left no account.
type NodeAccountAnswer struct {
Node string `json:"node"`
Login string `json:"login,omitempty"`
Account string `json:"account,omitempty"`
Retired bool `json:"retired,omitempty"`
}
+2 -2
View File
@@ -48,8 +48,8 @@ const (
// KindNodeCertificate is a node's certificate for its internal name, asked by that node's node-engine alone.
KindNodeCertificate = "node-certificate"
// KindEnrolment is an enrolment reply, signed under its own context; no caller is named until adoption's record
// (ADR 0561 §5).
// KindEnrolment is a redeem answer (RedeemAnswer), signed under its own context for mesh-enrolment alone (ADR 0587
// §3, §6).
KindEnrolment = "enrolment"
// KindAuthority is the vault's own authority certificate, which the vault signs for itself.
KindAuthority = "authority"