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:
@@ -91,10 +91,12 @@ type callKey struct {
|
||||
// move is what `move-in` takes: the row of the controller's store (move.go).
|
||||
move *MoveOf
|
||||
// why is what `move-in` with replace_running was asked for, in the caller's words (owntake.go).
|
||||
why string
|
||||
public [32]byte
|
||||
private [32]byte
|
||||
until time.Time
|
||||
why string
|
||||
// generation is the licence's generation `licence-put` keeps its value at (licences.go).
|
||||
generation int
|
||||
public [32]byte
|
||||
private [32]byte
|
||||
until time.Time
|
||||
}
|
||||
|
||||
// Opened is a first step's answer: the call, and the key to seal its value to. Never a value.
|
||||
|
||||
@@ -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()
|
||||
}},
|
||||
{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",
|
||||
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, " +
|
||||
|
||||
@@ -150,6 +150,9 @@ type keepOptions struct {
|
||||
replaceOnly bool
|
||||
keepPrevious bool
|
||||
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
|
||||
@@ -234,6 +237,10 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
|
||||
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 {
|
||||
return Changed{}, &HeldAnother{Name: name, Generation: before.Generation, ChangedBy: before.ChangedBy,
|
||||
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() {
|
||||
rec.MadeAt = opt.madeAt.UTC()
|
||||
}
|
||||
if opt.generation > 0 {
|
||||
rec.Generation = opt.generation
|
||||
}
|
||||
if opt.movedFrom != nil {
|
||||
moved := *opt.movedFrom
|
||||
moved.At = now
|
||||
|
||||
Reference in New Issue
Block a user