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:
@@ -237,12 +237,18 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
found = &r.Needs[i]
|
||||
}
|
||||
}
|
||||
if found != nil && found.ByRecord && found.Sealed == "" {
|
||||
if found != nil && found.ByRecord && found.Sealed == "" && !found.Manager {
|
||||
// Answered by a record whose key has not been supplied since this consumer was
|
||||
// put on it. **Refused, not skipped.** The mesh discarded the plaintext when the
|
||||
// key was accepted and cannot seal another, so a machine that resolved cleanly
|
||||
// would receive no file at all and fail at whatever tried to read it — which is
|
||||
// the outcome ADR 0024 exists to avoid, arrived at politely.
|
||||
//
|
||||
// The manager holder is the one exception (novox/hq ADR 0050): an empty refresh token
|
||||
// is a licence whose manager has not adopted one yet, a real waiting state rather than
|
||||
// a lost key. It falls through to the skip below — its bound facts (carrying the
|
||||
// manager's public key) are still delivered, which is what adoption needs to seal the
|
||||
// first refresh token.
|
||||
return nil, fmt.Errorf(
|
||||
"%s on this machine uses the licence %q and no key has been sealed to it. "+
|
||||
"The mesh cannot make one; supply it again with `licence key %s`",
|
||||
|
||||
@@ -134,6 +134,12 @@ type Needed struct {
|
||||
Sealed string
|
||||
// For is the module that wanted it.
|
||||
For string
|
||||
// Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh
|
||||
// token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty
|
||||
// Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting
|
||||
// state, not a consumer missing its key. Set by the plan, which is the only layer that knows a
|
||||
// licence's manager; empty for every consumer.
|
||||
Manager bool
|
||||
}
|
||||
|
||||
// Refusal is why a set of assignments cannot become a declaration.
|
||||
|
||||
Reference in New Issue
Block a user