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:
+130
-62
@@ -206,8 +206,31 @@ func (l *Licences) Chosen(ctx context.Context, node, module string) (string, err
|
||||
// for today's vendors. An unregistered vendor is not consulted: a static-key blob delivers as it is,
|
||||
// and a licence whose key was accepted at all necessarily had a registered adapter.
|
||||
func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (string, error) {
|
||||
// The manager holder is delivered the REFRESH token, not an access token: it is the one holder
|
||||
// that refreshes rather than consumes (novox/hq ADR 0050). It is sealed to this same node's key
|
||||
// with the very same anonymous box a consumer's credential is, so it rides the identical
|
||||
// host-unseal-and-mount path — the host opens it, the manager module reads cleartext, and the
|
||||
// module is never handed a private key. Nothing to strip on the consumer side and nothing bespoke
|
||||
// on this one: the refresh token is simply the credential this particular holder receives.
|
||||
managerNode, managerModule, err := l.ManagerOf(ctx, licence)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if managerNode != "" && node == managerNode && module == managerModule {
|
||||
sealed, _, ok, err := l.RefreshGrant(ctx, licence)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
// No refresh token adopted yet — empty, exactly as a consumer with no key. The
|
||||
// declaration refuses that by name, where the module and path are both in view.
|
||||
return "", nil
|
||||
}
|
||||
return sealed, nil
|
||||
}
|
||||
|
||||
var sealed *string
|
||||
err := l.store.Pool().QueryRow(ctx,
|
||||
err = l.store.Pool().QueryRow(ctx,
|
||||
`select sealed from licence_holder where licence = $1 and node = $2 and module = $3`,
|
||||
licence, node, module).Scan(&sealed)
|
||||
if errors.Is(err, pgx.ErrNoRows) || sealed == nil {
|
||||
@@ -279,8 +302,18 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
||||
"Put a consumer on it first, then supply the key", licence)
|
||||
}
|
||||
|
||||
// The manager holder is delivered the refresh token, not an operator-supplied access key — its
|
||||
// row is fed by adoption and refresh, not by this. Skipped so an accepted value never clobbers it.
|
||||
managerNode, managerModule, err := l.ManagerOf(ctx, licence)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sealed := 0
|
||||
for _, h := range holders {
|
||||
if managerNode != "" && h.Node == managerNode && h.Module == managerModule {
|
||||
continue
|
||||
}
|
||||
key, err := keys(h.Node)
|
||||
if err != nil {
|
||||
return sealed, err
|
||||
@@ -305,35 +338,47 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
||||
return sealed, nil
|
||||
}
|
||||
|
||||
// ManagerOf is the node that holds a licence's refresh token readably, empty if none is named.
|
||||
// ManagerOf is the node and module that hold a licence's refresh token readably, both empty if none
|
||||
// is named.
|
||||
//
|
||||
// Empty for every static-key licence, which has nothing to refresh, and for a refreshable-grant one
|
||||
// before its manager is set (novox/hq ADR 0050).
|
||||
func (l *Licences) ManagerOf(ctx context.Context, licence string) (string, error) {
|
||||
var manager *string
|
||||
err := l.store.Pool().QueryRow(ctx,
|
||||
`select manager from licence where name = $1`, licence).Scan(&manager)
|
||||
// before its manager is set (novox/hq ADR 0050). The module is returned alongside the node because a
|
||||
// node may run the manager module and a consuming module of the same licence at once, and which
|
||||
// holder is delivered the refresh token turns on the module, not the node alone.
|
||||
func (l *Licences) ManagerOf(ctx context.Context, licence string) (node, module string, err error) {
|
||||
var mgr, mod *string
|
||||
err = l.store.Pool().QueryRow(ctx,
|
||||
`select manager, manager_module from licence where name = $1`, licence).Scan(&mgr, &mod)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", fmt.Errorf("this mesh has no licence called %q", licence)
|
||||
return "", "", fmt.Errorf("this mesh has no licence called %q", licence)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
if manager == nil {
|
||||
return "", nil
|
||||
if mgr == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
return *manager, nil
|
||||
if mod == nil {
|
||||
return *mgr, "", nil
|
||||
}
|
||||
return *mgr, *mod, nil
|
||||
}
|
||||
|
||||
// SetManager names the one node that holds a licence's refresh token and refreshes it centrally.
|
||||
// SetManager names the one node, and the module on it, that hold a licence's refresh token and
|
||||
// refresh it centrally.
|
||||
//
|
||||
// **Only a refreshable-grant licence has one.** A static-key licence has no refresh token, so naming
|
||||
// a manager for it is refused rather than kept — the absent manager is part of what keeps a static
|
||||
// key from ever growing a value something holds readably at rest (novox/hq ADR 0050). The bound
|
||||
// "the manager node only" starts here, at the one place a manager is written.
|
||||
func (l *Licences) SetManager(ctx context.Context, licence, node string) error {
|
||||
if strings.TrimSpace(node) == "" {
|
||||
return errors.New("a manager needs a node")
|
||||
// "the manager module only" starts here, at the one place a manager is written.
|
||||
//
|
||||
// **The module is named too, and it is the holder that is delivered the refresh token.** The manager
|
||||
// module must also be put on the licence as a holder (`Use`), so the plan resolves its model-access
|
||||
// requirement; naming it here is what tells delivery to hand THAT holder the refresh token rather than
|
||||
// an access token.
|
||||
func (l *Licences) SetManager(ctx context.Context, licence, node, module string) error {
|
||||
if strings.TrimSpace(node) == "" || strings.TrimSpace(module) == "" {
|
||||
return errors.New("a manager needs a node and the module on it that refreshes")
|
||||
}
|
||||
vendor, err := l.vendorOf(ctx, licence)
|
||||
if err != nil {
|
||||
@@ -349,7 +394,7 @@ func (l *Licences) SetManager(ctx context.Context, licence, node string) error {
|
||||
"has a refresh token to hold", licence, adapter.Shape())
|
||||
}
|
||||
tag, err := l.store.Pool().Exec(ctx,
|
||||
`update licence set manager = $2 where name = $1`, licence, node)
|
||||
`update licence set manager = $2, manager_module = $3 where name = $1`, licence, node, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -359,42 +404,42 @@ func (l *Licences) SetManager(ctx context.Context, licence, node string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRefreshGrant stores, or replaces, a licence's refresh token as its at-rest envelope.
|
||||
// SetRefreshGrant stores, or replaces, a licence's refresh token as one sealed blob.
|
||||
//
|
||||
// **The envelope is opaque here.** It was produced by the manager node — the only place the refresh
|
||||
// token is ever in the clear (novox/hq ADR 0050, Phase C) — and this context keeps it and forwards
|
||||
// it to a refresh without opening it. The control plane holds no key that could, which is the whole
|
||||
// point of where the carve-out draws the line.
|
||||
func (l *Licences) SetRefreshGrant(ctx context.Context, licence string, at secrets.AtRest) error {
|
||||
if at.Token == "" || at.WrappedKey == "" || at.ManagerKey == "" {
|
||||
return errors.New("an incomplete refresh-token envelope is not one to keep")
|
||||
// **The blob is opaque here, and it is an ordinary sealed box.** It was produced where the refresh
|
||||
// token was in the clear — the manager node, at adoption or after a rotation — sealed to that node's
|
||||
// public sealing key with the same `crypto_box_seal` every credential uses (novox/hq ADR 0050). This
|
||||
// context keeps it and delivers it without opening it: the control plane holds no private key that
|
||||
// could, which is the whole point of where the carve-out draws the line.
|
||||
func (l *Licences) SetRefreshGrant(ctx context.Context, licence, sealed, managerKey string) error {
|
||||
if strings.TrimSpace(sealed) == "" || strings.TrimSpace(managerKey) == "" {
|
||||
return errors.New("an incomplete refresh-token grant is not one to keep")
|
||||
}
|
||||
_, err := l.store.Pool().Exec(ctx,
|
||||
`insert into refresh_grant (licence, token, wrapped_key, manager_key)
|
||||
values ($1, $2, $3, $4)
|
||||
`insert into refresh_grant (licence, sealed, manager_key)
|
||||
values ($1, $2, $3)
|
||||
on conflict (licence) do update set
|
||||
token = excluded.token, wrapped_key = excluded.wrapped_key,
|
||||
manager_key = excluded.manager_key, updated_at = now()`,
|
||||
licence, at.Token, at.WrappedKey, at.ManagerKey)
|
||||
sealed = excluded.sealed, manager_key = excluded.manager_key, updated_at = now()`,
|
||||
licence, sealed, managerKey)
|
||||
if err != nil && strings.Contains(err.Error(), "refresh_grant_licence_fkey") {
|
||||
return fmt.Errorf("this mesh has no licence called %q", licence)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshGrant is a licence's refresh token as its at-rest envelope, and whether one is stored.
|
||||
func (l *Licences) RefreshGrant(ctx context.Context, licence string) (secrets.AtRest, bool, error) {
|
||||
var at secrets.AtRest
|
||||
err := l.store.Pool().QueryRow(ctx,
|
||||
`select token, wrapped_key, manager_key from refresh_grant where licence = $1`, licence).
|
||||
Scan(&at.Token, &at.WrappedKey, &at.ManagerKey)
|
||||
// RefreshGrant is a licence's sealed refresh token, the key it was sealed to, and whether one is
|
||||
// stored.
|
||||
func (l *Licences) RefreshGrant(ctx context.Context, licence string) (sealed, managerKey string, ok bool, err error) {
|
||||
err = l.store.Pool().QueryRow(ctx,
|
||||
`select sealed, manager_key from refresh_grant where licence = $1`, licence).
|
||||
Scan(&sealed, &managerKey)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return secrets.AtRest{}, false, nil
|
||||
return "", "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return secrets.AtRest{}, false, err
|
||||
return "", "", false, err
|
||||
}
|
||||
return at, true, nil
|
||||
return sealed, managerKey, true, nil
|
||||
}
|
||||
|
||||
// Refresh mints a new access token for a refreshable-grant licence, seals it to every holder, and
|
||||
@@ -459,10 +504,10 @@ func (l *Licences) Refresh(ctx context.Context, licence string, keys SealingKeys
|
||||
" licence manager %s <node>", licence, licence)
|
||||
}
|
||||
|
||||
var at secrets.AtRest
|
||||
var sealedRefresh, managerKey string
|
||||
err = tx.QueryRow(ctx,
|
||||
`select token, wrapped_key, manager_key from refresh_grant where licence = $1`, licence).
|
||||
Scan(&at.Token, &at.WrappedKey, &at.ManagerKey)
|
||||
`select sealed, manager_key from refresh_grant where licence = $1`, licence).
|
||||
Scan(&sealedRefresh, &managerKey)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, fmt.Errorf(
|
||||
"%q has no refresh token stored yet; its manager %s adopts one first "+
|
||||
@@ -473,7 +518,9 @@ func (l *Licences) Refresh(ctx context.Context, licence string, keys SealingKeys
|
||||
}
|
||||
|
||||
result, err := refresher.Refresh(ctx,
|
||||
adapters.RefreshInput{Licence: licence, Manager: *manager, AtRest: at})
|
||||
adapters.RefreshInput{
|
||||
Licence: licence, Manager: *manager, Sealed: sealedRefresh, ManagerKey: managerKey,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -483,9 +530,10 @@ func (l *Licences) Refresh(ctx context.Context, licence string, keys SealingKeys
|
||||
}
|
||||
|
||||
// The reseal-and-publish half, shared with SubmitRefresh: the new access token is sealed to every
|
||||
// holder that exists now, and a rotated refresh token replaces the stored envelope — never seen
|
||||
// in the clear either way.
|
||||
sealed, err := resealAndPublish(ctx, tx, licence, result.AccessToken, result.NewAtRest, keys)
|
||||
// consumer holder that exists now, and a rotated refresh token replaces the stored sealed blob —
|
||||
// never seen in the clear either way.
|
||||
sealed, err := resealAndPublish(
|
||||
ctx, tx, licence, result.AccessToken, result.NewSealed, result.NewManagerKey, keys)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -513,7 +561,7 @@ func (l *Licences) Refresh(ctx context.Context, licence string, keys SealingKeys
|
||||
// `Refresh`; the only difference is where the access token came from — a module on the manager node
|
||||
// rather than a plug-in in this process.
|
||||
func (l *Licences) SubmitRefresh(
|
||||
ctx context.Context, licence, accessToken string, newAtRest *secrets.AtRest, keys SealingKeys,
|
||||
ctx context.Context, licence, accessToken, newSealed, newManagerKey string, keys SealingKeys,
|
||||
) (int, error) {
|
||||
if strings.TrimSpace(accessToken) == "" {
|
||||
return 0, fmt.Errorf(
|
||||
@@ -561,7 +609,7 @@ func (l *Licences) SubmitRefresh(
|
||||
" licence manager %s <node>", licence, licence)
|
||||
}
|
||||
|
||||
sealed, err := resealAndPublish(ctx, tx, licence, accessToken, newAtRest, keys)
|
||||
sealed, err := resealAndPublish(ctx, tx, licence, accessToken, newSealed, newManagerKey, keys)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -572,22 +620,41 @@ func (l *Licences) SubmitRefresh(
|
||||
return sealed, nil
|
||||
}
|
||||
|
||||
// resealAndPublish seals a new access token to every current holder and, if one is given, replaces
|
||||
// the stored refresh envelope with a rotated one. It is the half `Refresh` and `SubmitRefresh` share:
|
||||
// the value's source differs, what is done with it does not.
|
||||
// resealAndPublish seals a new access token to every CONSUMER holder and, if one is given, replaces
|
||||
// the stored sealed refresh token with a rotated one. It is the half `Refresh` and `SubmitRefresh`
|
||||
// share: the value's source differs, what is done with it does not.
|
||||
//
|
||||
// The refresh token is never touched here — a rotated one arrives already re-sealed at rest, and is
|
||||
// stored as the opaque envelope it is. `KeyFor` can therefore only ever deliver the access token.
|
||||
// **The manager holder is skipped.** It is delivered the refresh token, not an access token (`KeyFor`);
|
||||
// sealing an access token into its row would be a value nothing reads, and — worse — would overwrite
|
||||
// the delivery bookkeeping for the one holder whose credential is the refresh token. So the reseal
|
||||
// walks consumer holders only, and the count it returns is the number of consumers a push will carry
|
||||
// the new access token to.
|
||||
//
|
||||
// The refresh token is never in the clear here — a rotated one arrives already sealed to the manager
|
||||
// node, and is stored as the opaque blob it is. A consumer's `KeyFor` reads licence_holder, so it can
|
||||
// only ever deliver an access token.
|
||||
func resealAndPublish(
|
||||
ctx context.Context, tx pgx.Tx, licence, accessToken string, newAtRest *secrets.AtRest,
|
||||
ctx context.Context, tx pgx.Tx, licence, accessToken, newSealed, newManagerKey string,
|
||||
keys SealingKeys,
|
||||
) (int, error) {
|
||||
var managerNode, managerModule *string
|
||||
if err := tx.QueryRow(ctx,
|
||||
`select manager, manager_module from licence where name = $1`, licence).
|
||||
Scan(&managerNode, &managerModule); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
holders, err := holdersTx(ctx, tx, licence)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sealed := 0
|
||||
for _, h := range holders {
|
||||
if managerNode != nil && managerModule != nil &&
|
||||
h.Node == *managerNode && h.Module == *managerModule {
|
||||
// The manager holder receives the refresh token, not this access token. Left untouched.
|
||||
continue
|
||||
}
|
||||
key, err := keys(h.Node)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -609,17 +676,18 @@ func resealAndPublish(
|
||||
sealed++
|
||||
}
|
||||
|
||||
// The refresh token stays put unless the vendor rotated it, in which case it arrived already
|
||||
// re-encrypted at rest — replaced here without ever being seen in the clear.
|
||||
if newAtRest != nil {
|
||||
if newAtRest.Token == "" || newAtRest.WrappedKey == "" || newAtRest.ManagerKey == "" {
|
||||
// The refresh token stays put unless the vendor rotated it, in which case the manager sealed the
|
||||
// new one to its own node key before submitting — replaced here without ever being seen in the
|
||||
// clear.
|
||||
if newSealed != "" {
|
||||
if newManagerKey == "" {
|
||||
return 0, fmt.Errorf(
|
||||
"the refresh returned an incomplete re-sealed refresh token for %q", licence)
|
||||
"the refresh returned a re-sealed refresh token for %q with no manager key", licence)
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`update refresh_grant set token = $2, wrapped_key = $3, manager_key = $4, updated_at = now()
|
||||
`update refresh_grant set sealed = $2, manager_key = $3, updated_at = now()
|
||||
where licence = $1`,
|
||||
licence, newAtRest.Token, newAtRest.WrappedKey, newAtRest.ManagerKey); err != nil {
|
||||
licence, newSealed, newManagerKey); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user