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:
2026-09-07 01:55:08 +02:00
parent 8e0c22fc2e
commit 33fd28ffa6
15 changed files with 563 additions and 593 deletions
+53 -103
View File
@@ -10,8 +10,6 @@ import (
"io"
"os"
"strings"
"github.com/novox/mesh-control/internal/secrets"
)
// licenceCommand is everything about model access the mesh holds.
@@ -22,7 +20,7 @@ import (
func licenceCommand(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New(
"licence add|list|use|release|key|manager|grant|set-grant|refresh|submit-refresh|forget")
"licence add|list|use|release|key|manager|set-grant|refresh|submit-refresh|forget")
}
switch args[0] {
case "add":
@@ -37,8 +35,6 @@ func licenceCommand(ctx context.Context, args []string) error {
return licenceKey(ctx, args[1:])
case "manager":
return licenceManager(ctx, args[1:])
case "grant":
return licenceGrant(ctx, args[1:])
case "set-grant":
return licenceSetGrant(ctx, args[1:])
case "refresh":
@@ -49,7 +45,7 @@ func licenceCommand(ctx context.Context, args []string) error {
return licenceForget(ctx, args[1:])
}
return fmt.Errorf(
"licence %q; it is add, list, use, release, key, manager, grant, set-grant, refresh, "+
"licence %q; it is add, list, use, release, key, manager, set-grant, refresh, "+
"submit-refresh or forget", args[0])
}
@@ -248,50 +244,42 @@ func licenceKey(ctx context.Context, args []string) error {
// nothing — the absent manager is part of what keeps a static key from ever holding a value readably
// at rest.
func licenceManager(ctx context.Context, args []string) error {
if len(args) != 2 {
return errors.New("licence manager <name> <node>")
if len(args) != 3 {
return errors.New("licence manager <name> <node> <module>")
}
name, node := args[0], args[1]
name, node, module := args[0], args[1], args[2]
held, err := openLicences(ctx)
if err != nil {
return err
}
defer held.Close()
if err := held.SetManager(ctx, name, node); err != nil {
if err := held.SetManager(ctx, name, node, module); err != nil {
return err
}
fmt.Printf("%s holds and refreshes %s.\n"+
" Its refresh token is kept encrypted at rest, readable by %s alone — no other node, and "+
"not this database on its own.\n", node, name, node)
fmt.Printf("%s on %s holds and refreshes %s.\n"+
" Its refresh token is sealed to %s's key — readable by that node alone, not by any other "+
"node and not by this database. Put %s on the licence too so it is delivered the token:\n"+
" licence use %s %s %s\n", module, node, name, node, module, name, node, module)
return nil
}
// envelopeJSON is the wire shape of a refresh-token at-rest envelope on this command surface: the
// three opaque parts of secrets.AtRest and nothing else.
// sealedGrantJSON is the wire shape of a sealed refresh token on this command surface: an anonymous
// sealed box and the public key it was sealed to, and nothing else.
//
// **Every field of it is ciphertext or a public key.** `token` is the refresh token under a data
// key, `wrapped_key` is that data key sealed to the manager node, `manager_key` is the manager's
// public sealing key. None of them is the refresh token in the clear — which is why this surface may
// print one out (`grant`) and read one in (`set-grant`, `submit-refresh`) without the control plane
// ever holding a refresh token it could read. The manager runtime, on the manager node, is the only
// place these open (novox/hq ADR 0050, Phase C).
type envelopeJSON struct {
Token string `json:"token"`
WrappedKey string `json:"wrapped_key"`
// **Every field of it is ciphertext or a public key.** `sealed` is the refresh token as a
// `crypto_box_seal` to the manager node's public key; `manager_key` is that public key. Neither is
// the refresh token in the clear — which is why this surface may read one in (`set-grant`,
// `submit-refresh`) without the control plane ever holding a refresh token it could read. The manager
// module, on the manager node, seals it; the HOST, on that node, unseals it to deliver cleartext. This
// database, and this surface, only ever forward the box (novox/hq ADR 0050).
type sealedGrantJSON struct {
Sealed string `json:"sealed"`
ManagerKey string `json:"manager_key"`
}
func (e envelopeJSON) atRest() secrets.AtRest {
return secrets.AtRest{Token: e.Token, WrappedKey: e.WrappedKey, ManagerKey: e.ManagerKey}
}
func envelopeOf(a secrets.AtRest) envelopeJSON {
return envelopeJSON{Token: a.Token, WrappedKey: a.WrappedKey, ManagerKey: a.ManagerKey}
}
// readEnvelope reads an at-rest envelope from a file or standard input as JSON.
func readEnvelope(from string) (envelopeJSON, error) {
// readSealedGrant reads a sealed refresh token from a file or standard input as JSON.
func readSealedGrant(from string) (sealedGrantJSON, error) {
var raw []byte
var err error
if from != "" {
@@ -300,17 +288,17 @@ func readEnvelope(from string) (envelopeJSON, error) {
raw, err = readAllStdin()
}
if err != nil {
return envelopeJSON{}, err
return sealedGrantJSON{}, err
}
var env envelopeJSON
if err := json.Unmarshal(raw, &env); err != nil {
return envelopeJSON{}, fmt.Errorf("the refresh-token envelope is not JSON: %w", err)
var g sealedGrantJSON
if err := json.Unmarshal(raw, &g); err != nil {
return sealedGrantJSON{}, fmt.Errorf("the sealed refresh token is not JSON: %w", err)
}
if env.Token == "" || env.WrappedKey == "" || env.ManagerKey == "" {
return envelopeJSON{}, errors.New(
"an at-rest envelope is {token, wrapped_key, manager_key}, and one part is missing")
if g.Sealed == "" || g.ManagerKey == "" {
return sealedGrantJSON{}, errors.New(
"a sealed refresh token is {sealed, manager_key}, and one part is missing")
}
return env, nil
return g, nil
}
func readAllStdin() ([]byte, error) {
@@ -318,55 +306,17 @@ func readAllStdin() ([]byte, error) {
return io.ReadAll(reader)
}
// licenceGrant prints a licence's refresh-token envelope, so the manager runtime can fetch the
// opaque thing it will open on the manager node (novox/hq ADR 0050, Phase C).
// licenceSetGrant stores a sealed refresh token the manager module produced — adoption, and the
// re-seal after a rotation done outside this process (novox/hq ADR 0050).
//
// **What is printed is ciphertext.** The envelope is the refresh token sealed at rest to the manager
// node's key; it opens nowhere but that node. Printing it here is how the manager runtime — which
// does not read this database directly — is handed the envelope to open, and it discloses nothing a
// copy of the store did not already hold.
func licenceGrant(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("licence grant <name>")
}
name := args[0]
held, err := openLicences(ctx)
if err != nil {
return err
}
defer held.Close()
at, ok, err := held.RefreshGrant(ctx, name)
if err != nil {
return err
}
if !ok {
// Said, not printed as an empty object: a licence with no grant and a failed read must not
// look the same to whatever parses this.
return fmt.Errorf(
"%q has no refresh token stored; its manager adopts one first with `licence set-grant %s`",
name, name)
}
out, err := json.Marshal(envelopeOf(at))
if err != nil {
return err
}
fmt.Println(string(out))
return nil
}
// licenceSetGrant stores a refresh-token envelope the manager runtime produced — adoption, and the
// re-seal after a rotation done outside this process (novox/hq ADR 0050, Phase C).
//
// **It takes an envelope, never a refresh token.** The manager node reads the operator's refresh
// token, seals it at rest to its own key, and hands the sealed envelope here. So the one moment a
// refresh token is in the clear is on the manager node, never in the control plane — the same bound
// the whole carve-out keeps. This surface refuses anything that is not a complete envelope rather
// than storing half of one.
// **It takes a sealed box, never a refresh token.** The manager module, on the manager node, reads
// the operator's refresh token, seals it to that node's own public key, and hands the box here. So the
// one moment a refresh token is in the clear is on the manager node, never in the control plane — the
// same bound the whole carve-out keeps. This surface refuses anything that is not a complete sealed
// grant rather than storing half of one.
func licenceSetGrant(ctx context.Context, args []string) error {
set := flag.NewFlagSet("licence set-grant", flag.ContinueOnError)
from := set.String("file", "", "read the envelope from a file instead of standard input")
from := set.String("file", "", "read the sealed refresh token from a file instead of standard input")
positionals, err := parseAround(set, args)
if err != nil {
return err
@@ -376,7 +326,7 @@ func licenceSetGrant(ctx context.Context, args []string) error {
}
name := positionals[0]
env, err := readEnvelope(*from)
grant, err := readSealedGrant(*from)
if err != nil {
return err
}
@@ -386,11 +336,11 @@ func licenceSetGrant(ctx context.Context, args []string) error {
return err
}
defer held.Close()
if err := held.SetRefreshGrant(ctx, name, env.atRest()); err != nil {
if err := held.SetRefreshGrant(ctx, name, grant.Sealed, grant.ManagerKey); err != nil {
return err
}
fmt.Printf("%s now holds a refresh token for %s, encrypted at rest and readable by that node "+
"alone.\n the control plane stored the envelope without opening it\n",
fmt.Printf("%s now holds a refresh token for %s, sealed to that node's key and readable by it "+
"alone.\n the control plane stored the box without opening it; run `push` to deliver it\n",
"the manager", name)
return nil
}
@@ -410,7 +360,7 @@ func licenceSubmitRefresh(ctx context.Context, args []string) error {
accessFrom := set.String("access-file", "",
"read the new access token from a file instead of standard input")
grantFrom := set.String("grant-file", "",
"the rotated refresh-token envelope, if the vendor rotated it; omit if it did not")
"the rotated sealed refresh token, if the vendor rotated it; omit if it did not")
positionals, err := parseAround(set, args)
if err != nil {
return err
@@ -439,15 +389,14 @@ func licenceSubmitRefresh(ctx context.Context, args []string) error {
return errors.New("no access token was given, so there is nothing to seal")
}
// The rotated envelope is optional: absent, the stored refresh token is left exactly as it was.
var rotated *secrets.AtRest
// The rotated sealed token is optional: absent, the stored refresh token is left exactly as it was.
var newSealed, newManagerKey string
if *grantFrom != "" {
env, err := readEnvelope(*grantFrom)
grant, err := readSealedGrant(*grantFrom)
if err != nil {
return err
}
at := env.atRest()
rotated = &at
newSealed, newManagerKey = grant.Sealed, grant.ManagerKey
}
open, err := openStores(ctx)
@@ -461,15 +410,16 @@ func licenceSubmitRefresh(ctx context.Context, args []string) error {
}
inv := open.inventory
sealed, err := held.SubmitRefresh(ctx, name, accessToken, rotated, func(node string) (string, error) {
return inv.SealingKeyOf(ctx, node)
})
sealed, err := held.SubmitRefresh(ctx, name, accessToken, newSealed, newManagerKey,
func(node string) (string, error) {
return inv.SealingKeyOf(ctx, node)
})
if err != nil {
return err
}
rotatedNote := "the refresh token was left with its manager unchanged"
if rotated != nil {
rotatedNote = "the rotated refresh token replaced the stored envelope, still readable by the " +
if newSealed != "" {
rotatedNote = "the rotated refresh token replaced the stored box, still readable by the " +
"manager node alone"
}
fmt.Printf("submitted a refresh for %s: a new access token sealed to %d holder(s), and %s.\n"+
+43
View File
@@ -89,6 +89,25 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
return catalogue.Resolution{}, nil, err
}
resolved.Needs[i].Sealed = sealed
// If this holder is the licence's manager, hand it the manager node's PUBLIC sealing key
// in its bound facts (novox/hq ADR 0050). It is safe to disclose — a public key — and it
// is what the manager module needs to re-seal a rotated refresh token to this same node,
// having been given no private key of its own. A consumer holder gets none.
pub, err := managerPublicKeyFor(ctx, open, inv, n.From, nodeName, n.For)
if err != nil {
return catalogue.Resolution{}, nil, err
}
if pub != "" {
serves := map[string]any{}
for k, v := range resolved.Needs[i].Serves {
serves[k] = v
}
serves["manager_public_key"] = pub
resolved.Needs[i].Serves = serves
// The manager holder: its empty pre-adoption refresh token is a waiting state, not a
// missing consumer key, so the declaration tolerates it rather than refusing.
resolved.Needs[i].Manager = true
}
continue
}
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From)
@@ -685,6 +704,30 @@ func keyFor(ctx context.Context, open *stores, licence, node, module string) (st
return held.KeyFor(ctx, licence, node, module)
}
// managerPublicKeyFor is the manager node's public sealing key, but only when (node, module) is the
// licence's manager holder — empty otherwise.
//
// It is delivered to the manager module in its bound facts so it can re-seal a rotated refresh token
// to this node (novox/hq ADR 0050). Public, so it travels in the clear like any other bound fact; and
// scoped to the manager holder alone, so a consumer never receives it and nothing invites a consumer
// to seal anything.
func managerPublicKeyFor(
ctx context.Context, open *stores, inv *inventory.Inventory, licence, node, module string,
) (string, error) {
held, err := open.Licences(ctx)
if err != nil {
return "", err
}
managerNode, managerModule, err := held.ManagerOf(ctx, licence)
if err != nil {
return "", err
}
if managerNode == "" || node != managerNode || module != managerModule {
return "", nil
}
return inv.SealingKeyOf(ctx, node)
}
// portsOn is one module's assignments on one machine, by the port the software uses.
func portsOn(
ctx context.Context, inv *inventory.Inventory, node, module string,