V-1: the vault serves licence-put, keeping the caller's newest generation

A licence's grant and access token move into the vault (ADR 0550 §3), handed in two steps
with a key made for the call, as move-in does; the caller's generation is the record's,
an older one is refused, and secret-changed names the secret and its generation.

Builds: design 39 bus-and-switch §15, B3, B4, B5, B6

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
2026-11-11 23:07:44 +01:00
parent 1c03299721
commit e85cf002a4
6 changed files with 178 additions and 5 deletions
@@ -92,6 +92,8 @@ type callKey struct {
move *MoveOf move *MoveOf
// why is what `move-in` with replace_running was asked for, in the caller's words (owntake.go). // why is what `move-in` with replace_running was asked for, in the caller's words (owntake.go).
why string why string
// generation is the licence's generation `licence-put` keeps its value at (licences.go).
generation int
public [32]byte public [32]byte
private [32]byte private [32]byte
until time.Time until time.Time
@@ -0,0 +1,80 @@
package main
// A licence's two secrets (design 39 bus-and-switch §15, B3 to B6): `licence.<licence key>.grant` and
// `licence.<licence key>.access`, handed to the vault by the licence manager through `licence-put` in two steps — name
// the licence, the part and the generation and get a call and a key made for it, then send the call and the value sealed
// to that key. The caller's generation is the record's; the vault keeps the newest and refuses an older one, and says
// `secret-changed` with the secret's name and generation, never the value.
import (
"errors"
"fmt"
"regexp"
)
// LicencePutVerb is the verb that hands a licence's secret to the vault.
const LicencePutVerb = "licence-put"
// The parts of a licence the vault holds (§15).
const (
LicenceGrant = "grant"
LicenceAccess = "access"
)
// licenceKey is a licence's key form (§10, B6): lower case, letters, digits, `-`, `.` and `_`.
var licenceKey = regexp.MustCompile(`^[a-z0-9_.-]{1,120}$`)
// LicenceSecret is the name of one part of a licence in the vault.
func LicenceSecret(key, part string) (string, error) {
if !licenceKey.MatchString(key) {
return "", fmt.Errorf("%q is not a licence's key: lower-case letters, digits, -, . and _", key)
}
if part != LicenceGrant && part != LicenceAccess {
return "", fmt.Errorf("the part %q is not a part of a licence: grant or access", part)
}
return "licence." + key + "." + part, nil
}
// LicencePutOpen opens a call to take one part of a licence at a generation.
func (v *Vault) LicencePutOpen(caller, licence, part string, generation int) (Opened, error) {
if err := fromAGrantedCaller(caller, LicencePutVerb); err != nil {
return Opened{}, err
}
secret, err := LicenceSecret(licence, part)
if err != nil {
return Opened{}, err
}
if generation < 1 {
return Opened{}, errors.New("licence-put takes the licence's generation, one or more; nothing is taken")
}
id, key, err := v.openCall(LicencePutVerb, secret, caller, nil)
if err != nil {
return Opened{}, err
}
v.callsMu.Lock()
if c := v.pending[id]; c != nil {
c.generation = generation
}
v.callsMu.Unlock()
return Opened{Call: id, SealTo: key, Secret: secret}, nil
}
// LicencePutTake opens the value sealed to the call's key and keeps it at the call's generation.
func (v *Vault) LicencePutTake(caller, call, sealed string) (Changed, error) {
c, err := v.takeCall(LicencePutVerb, call)
if err != nil {
return Changed{}, err
}
if c.by != caller {
return Changed{}, fmt.Errorf("the licence-put call %q was opened by another caller; nothing was taken", call)
}
value, err := c.open(sealed)
if err != nil {
return Changed{}, err
}
defer zero(value)
if len(value) == 0 {
return Changed{}, errors.New("the value handed in is empty; nothing was taken")
}
return v.keepWith(c.secret, string(value), caller, "", OriginAccepted, keepOptions{generation: c.generation})
}
@@ -0,0 +1,58 @@
package main
import (
"strings"
"testing"
)
// putLicence hands one part of a licence to the vault in licence-put's two steps.
func putLicence(t *testing.T, v testVault, caller, key, part string, generation int, value string) (Changed, error) {
t.Helper()
o, err := v.LicencePutOpen(caller, key, part, generation)
if err != nil {
return Changed{}, err
}
return v.LicencePutTake(caller, o.Call, sealToCall(t, o.SealTo, value))
}
// design 39 bus-and-switch §15, B3, B5: the caller's generation is the record's; an older one is refused; the
// event names the secret and its generation, never the value.
func TestLicencePutKeepsTheNewestGenerationAndRefusesAnOlder(t *testing.T) {
v := newVault(t, nil)
const manager = "claude-licence-manager"
if _, err := putLicence(t, v, manager, "jane_at_example.org", LicenceGrant, 3, `{"refreshToken":"rt-3"}`); err != nil {
t.Fatal(err)
}
rec, held, err := v.Store.Read("licence.jane_at_example.org.grant")
if err != nil || !held || rec.Generation != 3 {
t.Fatalf("held %v at generation %d (%v), want generation 3", held, rec.Generation, err)
}
if _, err := putLicence(t, v, manager, "jane_at_example.org", LicenceGrant, 2, `{"refreshToken":"rt-2"}`); err == nil ||
!strings.Contains(err.Error(), "not newer") {
t.Fatalf("an older generation was taken: %v", err)
}
if c, err := putLicence(t, v, manager, "jane_at_example.org", LicenceGrant, 4, `{"refreshToken":"rt-4"}`); err != nil ||
c.Generation != 4 {
t.Fatalf("generation 4: %+v %v", c, err)
}
said := 0
for _, raw := range v.bus.raw {
if strings.Contains(raw, "rt-") {
t.Fatalf("an event carries the value: %s", raw)
}
if strings.Contains(raw, `"secret-changed"`) && strings.Contains(raw, "licence.jane_at_example.org.grant") {
said++
}
}
if said != 2 {
t.Fatalf("secret-changed said %d times, want 2: %v", said, v.bus.raw)
}
for _, bad := range []struct{ key, part string }{{"Jane@x", LicenceGrant}, {"jane", "refresh"}} {
if _, err := v.LicencePutOpen(manager, bad.key, bad.part, 1); err == nil {
t.Errorf("licence-put took %s/%s", bad.key, bad.part)
}
}
if _, err := v.LicencePutOpen("", "jane", LicenceAccess, 1); err == nil {
t.Error("licence-put took an unproven caller")
}
}
@@ -208,6 +208,28 @@ func definitions(env *Env) []toolDef {
} }
return v.OwnDrift() return v.OwnDrift()
}}, }},
{Name: Seat + "." + LicencePutVerb,
Description: "Hand the vault one of a licence's two secrets (design 39 bus-and-switch §15), in two steps: name " +
"the licence by its key, the part (grant or access) and its generation, and get a call and a key made for " +
"it; then send the call and the value sealed to that key. Keeps the newest generation and refuses an older " +
"one; says secret-changed with the secret's name and generation, never the value.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"licence": str("the first step: the licence's key"),
"part": str("the first step: grant or access"),
"generation": map[string]any{"type": "number", "description": "the first step: the licence's generation"},
"call": str("the second step: the call the first step answered"),
"sealed": str("the second step: the value sealed to the call's key (NaCl anonymous box, base64) — never the value")}},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
if call := arg(a, "call"); call != "" {
return v.LicencePutTake(caller, call, arg(a, "sealed"))
}
g, _ := a["generation"].(float64)
return v.LicencePutOpen(caller, arg(a, "licence"), arg(a, "part"), int(g))
}},
{Name: Seat + ".move-in", {Name: Seat + ".move-in",
Description: "Take one value of the controller's store into the vault, as it is (design 57's V3): from the " + Description: "Take one value of the controller's store into the vault, as it is (design 57's V3): from the " +
"node-engine of a machine it was sealed to, and no other caller, in two steps — name the row (node, " + "node-engine of a machine it was sealed to, and no other caller, in two steps — name the row (node, " +
@@ -150,6 +150,9 @@ type keepOptions struct {
replaceOnly bool replaceOnly bool
keepPrevious bool keepPrevious bool
refuse func(before Record) string refuse func(before Record) string
// generation, when one or more, is the record's generation as the caller numbers it (licence-put, licences.go): a
// value at a generation no newer than the one held is refused.
generation int
} }
// keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept // keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept
@@ -234,6 +237,10 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
return Changed{}, errors.New(why) return Changed{}, errors.New(why)
} }
} }
if opt.generation > 0 && opt.generation <= before.Generation {
return Changed{}, fmt.Errorf("%s is held at generation %d, and generation %d is not newer: nothing was taken",
name, before.Generation, opt.generation)
}
if opt.neverReplace { if opt.neverReplace {
return Changed{}, &HeldAnother{Name: name, Generation: before.Generation, ChangedBy: before.ChangedBy, return Changed{}, &HeldAnother{Name: name, Generation: before.Generation, ChangedBy: before.ChangedBy,
ChangedAt: before.ChangedAt, Origin: before.Origin} ChangedAt: before.ChangedAt, Origin: before.Origin}
@@ -266,6 +273,9 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
} else if !opt.madeAt.IsZero() { } else if !opt.madeAt.IsZero() {
rec.MadeAt = opt.madeAt.UTC() rec.MadeAt = opt.madeAt.UTC()
} }
if opt.generation > 0 {
rec.Generation = opt.generation
}
if opt.movedFrom != nil { if opt.movedFrom != nil {
moved := *opt.movedFrom moved := *opt.movedFrom
moved.At = now moved.At = now
+2 -1
View File
@@ -238,7 +238,8 @@
"rotate-node", "rotate-node",
"node-account-confirm", "node-account-confirm",
"abandon", "abandon",
"retire" "retire",
"licence-put"
] ]
} }
], ],