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:
@@ -60,28 +60,32 @@ type Adapter interface {
|
||||
|
||||
// RefreshInput is what producing a new access token needs, and all a refresh is given.
|
||||
//
|
||||
// It carries the refresh token **only as its at-rest envelope** — the caller (the control plane)
|
||||
// never holds the refresh token in the clear, because it cannot open the envelope. Opening it, and
|
||||
// the vendor call that follows, happen where the manager node's private key is (ADR 0050, Phase C).
|
||||
// It carries the refresh token **only as its sealed blob** — the caller (the control plane) never
|
||||
// holds the refresh token in the clear, because it cannot open the box. Opening it, and the vendor
|
||||
// call that follows, happen where the manager node's private key is (ADR 0050, Phase C).
|
||||
type RefreshInput struct {
|
||||
Licence string
|
||||
// Manager is the node that holds the refresh token readably — the one place the envelope opens.
|
||||
// Manager is the node that holds the refresh token readably — the one place the box opens.
|
||||
Manager string
|
||||
// AtRest is the refresh token encrypted at rest under the manager's key. Opaque to the control
|
||||
// plane; meaningful only to the manager node that produced it.
|
||||
AtRest secrets.AtRest
|
||||
// Sealed is the refresh token as an anonymous sealed box to the manager's key. Opaque to the
|
||||
// control plane; openable only by the manager node's private half.
|
||||
Sealed string
|
||||
// ManagerKey is the manager's public sealing key the token was sealed to.
|
||||
ManagerKey string
|
||||
}
|
||||
|
||||
// RefreshResult is what a refresh produced: a new access token to seal per holder, and — only if the
|
||||
// vendor rotated it — the refresh token re-encrypted at rest, ready to replace the stored envelope.
|
||||
// vendor rotated it — the refresh token re-sealed to the manager, ready to replace the stored blob.
|
||||
type RefreshResult struct {
|
||||
// AccessToken is the new access token, in the clear. The mesh seals it per holder and discards
|
||||
// it, exactly as it does an accepted key. It is never the refresh token.
|
||||
AccessToken string
|
||||
// NewAtRest is the refresh token re-sealed at rest, present only when the vendor rotated the
|
||||
// refresh token too. Nil leaves the stored envelope untouched. Already encrypted, so the control
|
||||
// plane stores it without ever seeing the refresh token in the clear.
|
||||
NewAtRest *secrets.AtRest
|
||||
// NewSealed is the refresh token re-sealed to the manager node, present only when the vendor
|
||||
// rotated the refresh token too. Empty leaves the stored blob untouched. Already sealed, so the
|
||||
// control plane stores it without ever seeing the refresh token in the clear.
|
||||
NewSealed string
|
||||
// NewManagerKey is the key NewSealed was sealed to, carried with it.
|
||||
NewManagerKey string
|
||||
}
|
||||
|
||||
// Refresher is implemented only by a refreshable-grant adapter (ADR 0050): the vendor-neutral half
|
||||
@@ -229,11 +233,11 @@ func (s staticKey) Deliver(sealed string) string { return sealed }
|
||||
// vendor's actual OAuth call is the injected refresher.
|
||||
//
|
||||
// **What makes this the carve-out and not a second static key.** The refresh token never touches
|
||||
// this adapter and never touches a holder. It lives in the licences context's own at-rest store,
|
||||
// keyed by licence, encrypted to the manager node (secrets.AtRest). What Accept seals and Deliver
|
||||
// hands out is the ACCESS token, per holder, exactly as a static key's value is — so "the refresh
|
||||
// token is stripped on delivery" is structural here: there is nothing in a holder's row to strip,
|
||||
// because the refresh token was never put there.
|
||||
// this adapter. It lives in the licences context's own refresh_grant store, keyed by licence, sealed
|
||||
// to the manager node (secrets.Seal) and delivered to the manager holder alone. What Accept seals and
|
||||
// Deliver hands out to a CONSUMER is the ACCESS token, per holder, exactly as a static key's value is
|
||||
// — so "a consumer is never delivered the refresh token" is structural here: a consumer's row never
|
||||
// holds it, because the refresh token is a different holder's credential entirely.
|
||||
type refreshableGrant struct {
|
||||
vendor string
|
||||
refresher VendorRefresher
|
||||
|
||||
Reference in New Issue
Block a user