licences: deliver the refresh token by the ordinary sealed path, not a bespoke envelope
The refreshable-grant refresh token no longer rides a custom at-rest envelope that a
module opens with a node private key. A module is never given a node's private sealing
key, so that path could not exist -- the gap Phase C hit.
Instead the refresh token is a credential sealed to the MANAGER holder with the same
anonymous box (secrets.Seal / crypto_box_seal) every credential uses, stored as one
sealed blob, and delivered by the existing host-unseal-and-mount: the host opens it with
the node's real key and mounts the cleartext at the manager module's bound path, exactly
as a consumer's db password is delivered.
- refresh_grant now stores { sealed, manager_key }, dropping the AtRest token/wrapped_key
columns; internal/secrets/atrest.go is retired (nothing else used it).
- the licence records its manager as (node, module); KeyFor delivers the refresh token to
the manager holder and the access token to consumers, disambiguated by module so the two
can co-locate. Accept and the reseal skip the manager holder.
- the manager holder is delivered the node's PUBLIC sealing key in its bound facts, so the
module can re-seal a rotated refresh token with no private key of its own; the
declaration tolerates its empty pre-adoption secret rather than refusing.
- SubmitRefresh / set-grant take a sealed blob, never a refresh token in the clear.
The invariant holds unchanged: the control plane never reads the refresh token, and no node
but the manager holds it. A committed cross-language test proves the TypeScript module seal
opens under Go box.OpenAnonymous (the host's Unseal) -- both are NaCl crypto_box_seal.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -39,33 +39,56 @@ func nodeKeyPair(t *testing.T) (public string, open func(string) ([]byte, error)
|
||||
}
|
||||
}
|
||||
|
||||
// managerPair is like nodeKeyPair but also returns the private key string, because the manager needs
|
||||
// to OpenAtRest its own refresh token — the one node that reads it back.
|
||||
func managerPair(t *testing.T) (public, private string) {
|
||||
// managerPair is like nodeKeyPair but returns the private key string too, because the MANAGER opens
|
||||
// its own refresh token — the one node that reads it back — and the host on that node does so with
|
||||
// box.OpenAnonymous, exactly as it opens any sealed credential.
|
||||
func managerPair(t *testing.T) (public, private string, open func(string) ([]byte, error)) {
|
||||
t.Helper()
|
||||
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var pub, sk [32]byte
|
||||
copy(pub[:], priv.PublicKey().Bytes())
|
||||
copy(sk[:], priv.Bytes())
|
||||
return base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes()),
|
||||
base64.StdEncoding.EncodeToString(priv.Bytes())
|
||||
base64.StdEncoding.EncodeToString(priv.Bytes()),
|
||||
func(sealed string) ([]byte, error) {
|
||||
blob, err := base64.StdEncoding.DecodeString(sealed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, ok := box.OpenAnonymous(nil, blob, &pub, &sk)
|
||||
if !ok {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRefresher struct {
|
||||
access string
|
||||
newAtRest *secrets.AtRest
|
||||
got adapters.RefreshInput
|
||||
access string
|
||||
newSealed string
|
||||
newManagerKey string
|
||||
got adapters.RefreshInput
|
||||
}
|
||||
|
||||
func (f *fakeRefresher) Refresh(_ context.Context, in adapters.RefreshInput) (adapters.RefreshResult, error) {
|
||||
f.got = in
|
||||
return adapters.RefreshResult{AccessToken: f.access, NewAtRest: f.newAtRest}, nil
|
||||
return adapters.RefreshResult{
|
||||
AccessToken: f.access, NewSealed: f.newSealed, NewManagerKey: f.newManagerKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// A refreshable-grant licence set up end to end: a manager, a refresh token sealed at rest to it, two
|
||||
// holders each with a sealing key, and a fake vendor refresher plugged in.
|
||||
// A refreshable-grant licence set up end to end: a manager node running the manager module (a holder
|
||||
// delivered the refresh token), the refresh token sealed to it, two CONSUMER holders — one of them on
|
||||
// the manager node itself, to exercise co-location — and a fake vendor refresher plugged in.
|
||||
//
|
||||
// The manager node is "workstation" and its manager module is "manager"; the consuming module is
|
||||
// "assistant", present on both "workstation" and "laptop".
|
||||
func aRefreshableLicence(t *testing.T, held *Licences, ctx context.Context, fake *fakeRefresher) (
|
||||
managerPub, managerPriv string, holders map[string]func(string) ([]byte, error), keys SealingKeys,
|
||||
managerPub, managerPriv string, managerOpen func(string) ([]byte, error),
|
||||
holders map[string]func(string) ([]byte, error), keys SealingKeys,
|
||||
) {
|
||||
t.Helper()
|
||||
adapters.RegisterRefresher("anthropic", fake)
|
||||
@@ -74,45 +97,52 @@ func aRefreshableLicence(t *testing.T, held *Licences, ctx context.Context, fake
|
||||
if err := held.Add(ctx, "personal", "anthropic", map[string]any{"model": "a-model"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := held.SetManager(ctx, "personal", "workstation"); err != nil {
|
||||
if err := held.SetManager(ctx, "personal", "workstation", "manager"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
managerPub, managerPriv = managerPair(t)
|
||||
at, err := secrets.SealAtRest("rt-the-refresh-token", managerPub)
|
||||
managerPub, managerPriv, managerOpen = managerPair(t)
|
||||
sealedRefresh, err := secrets.Seal(managerPub, []byte("rt-the-refresh-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := held.SetRefreshGrant(ctx, "personal", at); err != nil {
|
||||
if err := held.SetRefreshGrant(ctx, "personal", sealedRefresh, managerPub); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The manager module is a holder too, on the manager node, so resealAndPublish has it to skip.
|
||||
if err := held.Use(ctx, "personal", "workstation", "manager"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
holders = map[string]func(string) ([]byte, error){}
|
||||
pub := map[string]string{}
|
||||
pub := map[string]string{"workstation": managerPub}
|
||||
_, holders["workstation"] = "", managerOpen // consumer on the manager node shares its key
|
||||
for _, node := range []string{"workstation", "laptop"} {
|
||||
p, open := nodeKeyPair(t)
|
||||
pub[node], holders[node] = p, open
|
||||
if node == "laptop" {
|
||||
p, open := nodeKeyPair(t)
|
||||
pub[node], holders[node] = p, open
|
||||
}
|
||||
if err := held.Use(ctx, "personal", node, "assistant"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
keys = func(node string) (string, error) { return pub[node], nil }
|
||||
return managerPub, managerPriv, holders, keys
|
||||
return managerPub, managerPriv, managerOpen, holders, keys
|
||||
}
|
||||
|
||||
// The point of the phase, in one test: a refresh seals the ACCESS token to every holder, and the
|
||||
// refresh token is nowhere a holder can reach it.
|
||||
// The point of the phase, in one test: a refresh seals the ACCESS token to every CONSUMER holder, the
|
||||
// manager holder is delivered the refresh token, and the refresh token is nowhere a consumer reaches.
|
||||
func TestARefreshDeliversTheAccessTokenAndNeverTheRefreshToken(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
fake := &fakeRefresher{access: "at-brand-new-access-token"}
|
||||
_, _, holders, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
_, _, managerOpen, holders, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
|
||||
sealed, err := held.Refresh(ctx, "personal", keys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sealed != 2 {
|
||||
t.Fatalf("%d holder(s) were resealed, expected 2", sealed)
|
||||
t.Fatalf("%d consumer holder(s) were resealed, expected 2", sealed)
|
||||
}
|
||||
|
||||
for node, open := range holders {
|
||||
@@ -130,26 +160,38 @@ func TestARefreshDeliversTheAccessTokenAndNeverTheRefreshToken(t *testing.T) {
|
||||
if string(got) != "at-brand-new-access-token" {
|
||||
t.Fatalf("%s was delivered %q, not the access token", node, got)
|
||||
}
|
||||
// The refresh token is not in the holder's delivery, opened or sealed.
|
||||
if string(got) == "rt-the-refresh-token" || strings.Contains(blob, "rt-the-refresh-token") {
|
||||
t.Fatalf("%s was delivered the refresh token", node)
|
||||
}
|
||||
}
|
||||
|
||||
// The manager holder is delivered the refresh token, and opens it with the node's own key.
|
||||
mgrBlob, err := held.KeyFor(ctx, "personal", "workstation", "manager")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := managerOpen(mgrBlob)
|
||||
if err != nil {
|
||||
t.Fatal("the manager cannot open the refresh token delivered to it")
|
||||
}
|
||||
if string(got) != "rt-the-refresh-token" {
|
||||
t.Fatalf("the manager was delivered %q, not the refresh token", got)
|
||||
}
|
||||
}
|
||||
|
||||
// KeyFor delivers the access token and cannot deliver the refresh token, because the refresh token
|
||||
// is not in licence_holder at all — the stripping is structural.
|
||||
func TestKeyForNeverCarriesTheRefreshToken(t *testing.T) {
|
||||
// A CONSUMER holder is never delivered the refresh token, because a consumer's row never holds it and
|
||||
// KeyFor for a consumer reads licence_holder — the separation is structural.
|
||||
func TestKeyForNeverCarriesTheRefreshTokenToAConsumer(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
fake := &fakeRefresher{access: "at-access"}
|
||||
_, _, _, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
_, _, _, _, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
if _, err := held.Refresh(ctx, "personal", keys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Every holder row, straight from the store: none of them holds the refresh token in any form.
|
||||
// Every CONSUMER holder row, straight from the store: none holds the refresh token in any form.
|
||||
rows, err := held.store.Pool().Query(ctx,
|
||||
`select coalesce(sealed, '') from licence_holder where licence = 'personal'`)
|
||||
`select coalesce(sealed, '') from licence_holder where licence = 'personal' and module = 'assistant'`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -160,57 +202,51 @@ func TestKeyForNeverCarriesTheRefreshToken(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(sealed, "rt-the-refresh-token") {
|
||||
t.Fatal("a holder row carries the refresh token")
|
||||
t.Fatal("a consumer holder row carries the refresh token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The refresh token at rest is not readable from the database alone: the row holds ciphertext and a
|
||||
// wrapped key, and only the manager node's private half opens it.
|
||||
func TestTheRefreshTokenAtRestNeedsTheManagersKey(t *testing.T) {
|
||||
// The refresh token is not readable from the database alone: the row holds a sealed box, and only the
|
||||
// manager node's private half opens it — the same guarantee every sealed credential here has.
|
||||
func TestTheRefreshTokenNeedsTheManagersKey(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
fake := &fakeRefresher{access: "at-access"}
|
||||
managerPub, managerPriv, _, _ := aRefreshableLicence(t, held, ctx, fake)
|
||||
managerPub, managerPriv, _, _, _ := aRefreshableLicence(t, held, ctx, fake)
|
||||
|
||||
// What the database holds, read straight from the row.
|
||||
var token, wrapped, managerKey string
|
||||
var sealed, managerKey string
|
||||
if err := held.store.Pool().QueryRow(ctx,
|
||||
`select token, wrapped_key, manager_key from refresh_grant where licence = 'personal'`).
|
||||
Scan(&token, &wrapped, &managerKey); err != nil {
|
||||
`select sealed, manager_key from refresh_grant where licence = 'personal'`).
|
||||
Scan(&sealed, &managerKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(token, "rt-the-refresh-token") || strings.Contains(wrapped, "rt-the-refresh-token") {
|
||||
if strings.Contains(sealed, "rt-the-refresh-token") {
|
||||
t.Fatal("the refresh token is in the row in the clear")
|
||||
}
|
||||
|
||||
// The manager, holding its private key, reads it back.
|
||||
got, err := secrets.OpenAtRest(
|
||||
secrets.AtRest{Token: token, WrappedKey: wrapped, ManagerKey: managerKey},
|
||||
managerPub, managerPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if managerKey != managerPub {
|
||||
t.Fatal("the stored manager key is not the manager's public key")
|
||||
}
|
||||
if got != "rt-the-refresh-token" {
|
||||
|
||||
// The manager, holding its private key, reads it back with box.OpenAnonymous (as the host does).
|
||||
got := openAnon(t, sealed, managerPub, managerPriv)
|
||||
if string(got) != "rt-the-refresh-token" {
|
||||
t.Fatalf("the manager read back %q", got)
|
||||
}
|
||||
|
||||
// Another node cannot, which is the whole of "the manager node only".
|
||||
otherPub, otherPriv := managerPair(t)
|
||||
if _, err := secrets.OpenAtRest(
|
||||
secrets.AtRest{Token: token, WrappedKey: wrapped, ManagerKey: managerKey},
|
||||
otherPub, otherPriv); err == nil {
|
||||
otherPub, otherPriv, _ := managerPair(t)
|
||||
if _, ok := tryOpenAnon(sealed, otherPub, otherPriv); ok {
|
||||
t.Fatal("a node that is not the manager opened the refresh token")
|
||||
}
|
||||
}
|
||||
|
||||
// After a refresh, holders hold a NEW access token, and the refresh token that was not rotated is
|
||||
// unchanged — never delivered either way.
|
||||
func TestAfterRefreshHoldersHoldANewAccessTokenAndTheGrantIsUnchanged(t *testing.T) {
|
||||
// After a refresh, consumers hold a NEW access token, and the refresh token that was not rotated is
|
||||
// unchanged — never delivered to a consumer either way.
|
||||
func TestAfterRefreshConsumersHoldANewAccessTokenAndTheGrantIsUnchanged(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
fake := &fakeRefresher{access: "at-first"}
|
||||
_, _, holders, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
_, _, _, holders, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
|
||||
// A prior access token, so we can see it change.
|
||||
if _, err := held.Refresh(ctx, "personal", keys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -224,7 +260,6 @@ func TestAfterRefreshHoldersHoldANewAccessTokenAndTheGrantIsUnchanged(t *testing
|
||||
}
|
||||
grantBefore := grantRow(t, held, ctx)
|
||||
|
||||
// A second refresh with a different access token and no rotation of the refresh token.
|
||||
fake.access = "at-second"
|
||||
if _, err := held.Refresh(ctx, "personal", keys); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -250,36 +285,36 @@ func TestAfterRefreshHoldersHoldANewAccessTokenAndTheGrantIsUnchanged(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
// When the vendor rotates the refresh token too, the stored envelope is replaced with the
|
||||
// re-encrypted one — and it is still not deliverable to a holder.
|
||||
func TestARotatedRefreshTokenReplacesTheStoredEnvelope(t *testing.T) {
|
||||
// When the vendor rotates the refresh token too, the stored sealed box is replaced with the re-sealed
|
||||
// one — and it is still delivered only to the manager, opening only with the manager's key.
|
||||
func TestARotatedRefreshTokenReplacesTheStoredBox(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
fake := &fakeRefresher{access: "at-access"}
|
||||
managerPub, managerPriv, _, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
managerPub, managerPriv, _, _, keys := aRefreshableLicence(t, held, ctx, fake)
|
||||
|
||||
grantBefore := grantRow(t, held, ctx)
|
||||
|
||||
rotated, err := secrets.SealAtRest("rt-a-rotated-refresh-token", managerPub)
|
||||
rotated, err := secrets.Seal(managerPub, []byte("rt-a-rotated-refresh-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.newAtRest = &rotated
|
||||
fake.newSealed, fake.newManagerKey = rotated, managerPub
|
||||
if _, err := held.Refresh(ctx, "personal", keys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if grantRow(t, held, ctx) == grantBefore {
|
||||
t.Fatal("the rotated refresh token did not replace the stored envelope")
|
||||
t.Fatal("the rotated refresh token did not replace the stored box")
|
||||
}
|
||||
at, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
sealed, key, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("the rotated grant is not stored: ok=%v err=%v", ok, err)
|
||||
}
|
||||
got, err := secrets.OpenAtRest(at, managerPub, managerPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if key != managerPub {
|
||||
t.Fatal("the rotated grant is not sealed to the manager's key")
|
||||
}
|
||||
if got != "rt-a-rotated-refresh-token" {
|
||||
got := openAnon(t, sealed, managerPub, managerPriv)
|
||||
if string(got) != "rt-a-rotated-refresh-token" {
|
||||
t.Fatalf("the stored grant opened to %q, not the rotated token", got)
|
||||
}
|
||||
}
|
||||
@@ -291,10 +326,10 @@ func TestAStaticKeyLicenceHasNoManagerAndNoRefresh(t *testing.T) {
|
||||
if err := held.Add(ctx, "plain", "anthropic-api-key", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := held.SetManager(ctx, "plain", "workstation"); err == nil {
|
||||
if err := held.SetManager(ctx, "plain", "workstation", "manager"); err == nil {
|
||||
t.Fatal("a static-key licence was given a manager")
|
||||
}
|
||||
if _, ok, err := held.RefreshGrant(ctx, "plain"); err != nil || ok {
|
||||
if _, _, ok, err := held.RefreshGrant(ctx, "plain"); err != nil || ok {
|
||||
t.Fatalf("a static-key licence has a refresh token stored: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if _, err := held.Refresh(ctx, "plain", func(string) (string, error) { return "", nil }); err == nil {
|
||||
@@ -317,15 +352,38 @@ func TestARefreshableLicenceWithoutAManagerIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// grantRow is the whole at-rest envelope as one string, for asserting it changed or did not.
|
||||
// grantRow is the whole sealed grant as one string, for asserting it changed or did not.
|
||||
func grantRow(t *testing.T, held *Licences, ctx context.Context) string {
|
||||
t.Helper()
|
||||
at, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
sealed, key, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return at.Token + "|" + at.WrappedKey + "|" + at.ManagerKey
|
||||
return sealed + "|" + key
|
||||
}
|
||||
|
||||
// openAnon opens an anonymous sealed box with a node's key pair — the host's Unseal, inlined for a test.
|
||||
func openAnon(t *testing.T, sealed, pubB64, privB64 string) []byte {
|
||||
t.Helper()
|
||||
out, ok := tryOpenAnon(sealed, pubB64, privB64)
|
||||
if !ok {
|
||||
t.Fatal("box.OpenAnonymous failed for a value that should open")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tryOpenAnon(sealed, pubB64, privB64 string) ([]byte, bool) {
|
||||
blob, err := base64.StdEncoding.DecodeString(sealed)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
pubRaw, _ := base64.StdEncoding.DecodeString(pubB64)
|
||||
privRaw, _ := base64.StdEncoding.DecodeString(privB64)
|
||||
var pub, priv [32]byte
|
||||
copy(pub[:], pubRaw)
|
||||
copy(priv[:], privRaw)
|
||||
return box.OpenAnonymous(nil, blob, &pub, &priv)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user