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:
@@ -7,24 +7,24 @@ import (
|
||||
"github.com/novox/mesh-control/internal/secrets"
|
||||
)
|
||||
|
||||
// SubmitRefresh is the Phase-C boundary: a refresh a manager NODE performed is published here, and
|
||||
// the control plane is given only the access token in the clear and an opaque re-sealed refresh
|
||||
// envelope — never the refresh token. These tests defend that the boundary keeps its shape.
|
||||
// SubmitRefresh is the boundary a manager NODE crosses to publish a refresh it performed: the control
|
||||
// plane is given only the access token in the clear and an opaque re-sealed refresh box — never the
|
||||
// refresh token. These tests defend that the boundary keeps its shape.
|
||||
|
||||
// A submitted refresh seals the access token to every holder, exactly as an in-process refresh does,
|
||||
// and delivers no refresh token to anybody.
|
||||
// A submitted refresh seals the access token to every CONSUMER holder, exactly as an in-process
|
||||
// refresh does, and delivers no refresh token to a consumer.
|
||||
func TestSubmitRefreshSealsTheAccessTokenAndNeverTheRefreshToken(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
// No in-process refresher registered: the anthropic production path uses SubmitRefresh, not
|
||||
// Refresh, precisely so nothing opens the envelope inside this process.
|
||||
_, _, holders, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
// Refresh, precisely so nothing opens the box inside this process.
|
||||
_, _, _, holders, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
|
||||
sealed, err := held.SubmitRefresh(ctx, "personal", "at-from-the-manager-node", nil, keys)
|
||||
sealed, err := held.SubmitRefresh(ctx, "personal", "at-from-the-manager-node", "", "", 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 {
|
||||
@@ -45,68 +45,62 @@ func TestSubmitRefreshSealsTheAccessTokenAndNeverTheRefreshToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The refresh token stored at rest is untouched by a submit that carried no rotation, and the manager
|
||||
// node — and only it — still opens it. The submit path never saw the refresh token in the clear.
|
||||
// The refresh token is untouched by a submit that carried no rotation, and the manager node — and
|
||||
// only it — still opens it. The submit path never saw the refresh token in the clear.
|
||||
func TestSubmitRefreshWithoutRotationLeavesTheGrantOpenableByTheManagerAlone(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
managerPub, managerPriv, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
managerPub, managerPriv, _, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
|
||||
before := grantRow(t, held, ctx)
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", "at-access", nil, keys); err != nil {
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", "at-access", "", "", keys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if grantRow(t, held, ctx) != before {
|
||||
t.Fatal("a submit with no rotation changed the stored refresh token")
|
||||
}
|
||||
|
||||
at, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
sealed, _, ok, err := held.RefreshGrant(ctx, "personal")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("the grant is not stored: ok=%v err=%v", ok, err)
|
||||
}
|
||||
got, err := secrets.OpenAtRest(at, managerPub, managerPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "rt-the-refresh-token" {
|
||||
got := openAnon(t, sealed, managerPub, managerPriv)
|
||||
if string(got) != "rt-the-refresh-token" {
|
||||
t.Fatalf("the manager read back %q", got)
|
||||
}
|
||||
// A node that is not the manager cannot: the whole of "the manager node only".
|
||||
otherPub, otherPriv := managerPair(t)
|
||||
if _, err := secrets.OpenAtRest(at, 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")
|
||||
}
|
||||
}
|
||||
|
||||
// A submit that carries a rotated envelope replaces the stored one — and the control plane stored it
|
||||
// A submit that carries a rotated box replaces the stored one — and the control plane stored it
|
||||
// without opening it: only the manager node reads the rotated token back.
|
||||
func TestSubmitRefreshWithRotationReplacesTheEnvelopeUnopened(t *testing.T) {
|
||||
func TestSubmitRefreshWithRotationReplacesTheBoxUnopened(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
managerPub, managerPriv, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
managerPub, managerPriv, _, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
|
||||
before := grantRow(t, held, ctx)
|
||||
|
||||
// The manager node re-sealed the rotated refresh token at rest; the control plane is handed only
|
||||
// this envelope.
|
||||
rotated, err := secrets.SealAtRest("rt-a-rotated-refresh-token", managerPub)
|
||||
// The manager node re-sealed the rotated refresh token to its own key; the control plane is handed
|
||||
// only this box.
|
||||
rotated, err := secrets.Seal(managerPub, []byte("rt-a-rotated-refresh-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", "at-access", &rotated, keys); err != nil {
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", "at-access", rotated, managerPub, keys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if grantRow(t, held, ctx) == before {
|
||||
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, _, 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 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)
|
||||
}
|
||||
}
|
||||
@@ -115,8 +109,8 @@ func TestSubmitRefreshWithRotationReplacesTheEnvelopeUnopened(t *testing.T) {
|
||||
// reporting success is the failure this whole design refuses.
|
||||
func TestSubmitRefreshRefusesAnEmptyAccessToken(t *testing.T) {
|
||||
held, ctx := fresh(t)
|
||||
_, _, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", " ", nil, keys); err == nil {
|
||||
_, _, _, _, keys := aRefreshableLicence(t, held, ctx, &fakeRefresher{})
|
||||
if _, err := held.SubmitRefresh(ctx, "personal", " ", "", "", keys); err == nil {
|
||||
t.Fatal("a refresh with no access token was published")
|
||||
}
|
||||
}
|
||||
@@ -131,7 +125,7 @@ func TestSubmitRefreshRefusesAStaticKeyLicence(t *testing.T) {
|
||||
if err := held.Use(ctx, "plain", "workstation", "assistant"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := held.SubmitRefresh(ctx, "plain", "at-access", nil,
|
||||
_, err := held.SubmitRefresh(ctx, "plain", "at-access", "", "",
|
||||
func(string) (string, error) { return ASealingKey(t), nil })
|
||||
if err == nil {
|
||||
t.Fatal("a refresh was submitted for a static-key licence")
|
||||
@@ -148,7 +142,7 @@ func TestSubmitRefreshRefusesWithoutAManager(t *testing.T) {
|
||||
if err := held.Add(ctx, "personal", "anthropic", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := held.SubmitRefresh(ctx, "personal", "at-access", nil,
|
||||
_, err := held.SubmitRefresh(ctx, "personal", "at-access", "", "",
|
||||
func(string) (string, error) { return "", nil })
|
||||
if err == nil {
|
||||
t.Fatal("a refresh was submitted for a licence with no manager")
|
||||
|
||||
Reference in New Issue
Block a user