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
482 lines
16 KiB
Go
482 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// licenceCommand is everything about model access the mesh holds.
|
|
//
|
|
// **A licence is a named thing and the name is the operator's** (novox/hq ADR 0024). *The personal
|
|
// account*, *the organisation's account* — those are names a person uses, and the mesh has to use
|
|
// them too, because the whole point is saying which one a given consumer uses.
|
|
func licenceCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New(
|
|
"licence add|list|use|release|key|manager|set-grant|refresh|submit-refresh|forget")
|
|
}
|
|
switch args[0] {
|
|
case "add":
|
|
return licenceAdd(ctx, args[1:])
|
|
case "list":
|
|
return licenceList(ctx)
|
|
case "use":
|
|
return licenceUse(ctx, args[1:], true)
|
|
case "release":
|
|
return licenceUse(ctx, args[1:], false)
|
|
case "key":
|
|
return licenceKey(ctx, args[1:])
|
|
case "manager":
|
|
return licenceManager(ctx, args[1:])
|
|
case "set-grant":
|
|
return licenceSetGrant(ctx, args[1:])
|
|
case "refresh":
|
|
return licenceRefresh(ctx, args[1:])
|
|
case "submit-refresh":
|
|
return licenceSubmitRefresh(ctx, args[1:])
|
|
case "forget":
|
|
return licenceForget(ctx, args[1:])
|
|
}
|
|
return fmt.Errorf(
|
|
"licence %q; it is add, list, use, release, key, manager, set-grant, refresh, "+
|
|
"submit-refresh or forget", args[0])
|
|
}
|
|
|
|
func licenceAdd(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("licence add", flag.ContinueOnError)
|
|
// What a consumer must know that is not secret — a base URL, a model name. Never the key.
|
|
serves := set.String("serves", "",
|
|
"JSON a consumer must know that is not secret, such as a base URL or a model")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 2 {
|
|
return errors.New(`licence add <vendor> <name> [--serves '{"model":"..."}']`)
|
|
}
|
|
vendor, name := positionals[0], positionals[1]
|
|
|
|
values := map[string]any{}
|
|
if strings.TrimSpace(*serves) != "" {
|
|
if err := json.Unmarshal([]byte(*serves), &values); err != nil {
|
|
return fmt.Errorf("--serves is not JSON: %w", err)
|
|
}
|
|
}
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
if err := held.Add(ctx, name, vendor, values); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s (%s) recorded. Nothing uses it yet, and it has no key:\n"+
|
|
" licence use %s <node> <module>\n licence key %s\n", name, vendor, name, name)
|
|
return nil
|
|
}
|
|
|
|
func licenceList(ctx context.Context) error {
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
all, err := held.All(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(all) == 0 {
|
|
// Said, not printed as nothing: an empty list and a failed read must never look the same.
|
|
fmt.Println("this mesh holds no licences")
|
|
return nil
|
|
}
|
|
for _, one := range all {
|
|
holders, err := held.HoldersOf(ctx, one.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s (%s)\n", one.Name, one.Vendor)
|
|
if len(holders) == 0 {
|
|
fmt.Printf(" nobody uses it\n")
|
|
}
|
|
for _, h := range holders {
|
|
// Whether it has a key is the question somebody is actually asking, so it is said
|
|
// per holder rather than per licence: the key was sealed to the holders that existed
|
|
// when it was supplied, and one recorded afterwards has none.
|
|
state := "has no key — supply it again with `licence key " + one.Name + "`"
|
|
if h.Sealed != "" {
|
|
state = "has a key"
|
|
}
|
|
fmt.Printf(" %s on %s: %s\n", h.Module, h.Node, state)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func licenceUse(ctx context.Context, args []string, using bool) error {
|
|
verb := "use"
|
|
if !using {
|
|
verb = "release"
|
|
}
|
|
if len(args) != 3 {
|
|
return fmt.Errorf("licence %s <name> <node> <module>", verb)
|
|
}
|
|
name, node, module := args[0], args[1], args[2]
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
if !using {
|
|
if err := held.StopUsing(ctx, name, node, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s on %s no longer uses %s. Its copy of the key goes on the next push\n",
|
|
module, node, name)
|
|
return nil
|
|
}
|
|
if err := held.Use(ctx, name, node, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s on %s uses %s.\n", module, node, name)
|
|
// The consequence, said now rather than discovered as a machine that resolves and receives
|
|
// nothing: the mesh discarded the plaintext, so a holder added after the key was supplied has
|
|
// no key and the mesh cannot make one.
|
|
sealed, err := held.KeyFor(ctx, name, node, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sealed == "" {
|
|
fmt.Printf(" It has no key yet — the mesh discarded the plaintext when it was supplied "+
|
|
"and cannot seal another. Supply it again:\n licence key %s\n", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// licenceKey is the *accept* verb novox/hq ADR 0024 names as missing.
|
|
//
|
|
// Take a value, seal it to each holder, and discard the plaintext. Every other credential the
|
|
// mesh handles it generated itself; an API key arrives from a person, and a mesh that kept
|
|
// operator-supplied keys readably is the arrangement this project measured and rejected.
|
|
func licenceKey(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("licence key", flag.ContinueOnError)
|
|
// A file rather than an argument, by default. A key on a command line is a key in shell
|
|
// history and in every process listing taken while it ran.
|
|
from := set.String("file", "", "read the key from a file instead of standard input")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("licence key <name> [--file <path>]")
|
|
}
|
|
name := positionals[0]
|
|
|
|
var value string
|
|
if *from != "" {
|
|
raw, err := os.ReadFile(*from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = strings.TrimSpace(string(raw))
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "reading the key from standard input; it is not echoed anywhere")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return fmt.Errorf("nothing was given on standard input: %w", err)
|
|
}
|
|
value = strings.TrimSpace(line)
|
|
}
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
sealed, err := held.Accept(ctx, name, value, func(node string) (string, error) {
|
|
return inv.SealingKeyOf(ctx, node)
|
|
})
|
|
if err != nil {
|
|
if sealed > 0 {
|
|
// Some holders got it and some did not, and the person holding the key is the only
|
|
// one who can finish the job. Saying how far it got is the difference between running
|
|
// this again knowing what it will do and running it hoping.
|
|
return fmt.Errorf(
|
|
"%w\n\n%d holder(s) were sealed before this. Running `licence key %s` again "+
|
|
"with the same key seals the rest and changes nothing for those already done",
|
|
err, sealed, name)
|
|
}
|
|
return err
|
|
}
|
|
// Not echoed back, ever. What is stored is unreadable by whoever holds it, the mesh included,
|
|
// and printing the value here would put the one copy that matters on a terminal.
|
|
fmt.Printf("sealed to %d holder(s). The mesh has discarded the key and cannot read it back\n",
|
|
sealed)
|
|
fmt.Printf(" run `push` to deliver it\n")
|
|
return nil
|
|
}
|
|
|
|
// licenceManager names the one node that holds a refreshable-grant licence's refresh token readably
|
|
// and refreshes it centrally (novox/hq ADR 0050).
|
|
//
|
|
// **Only a refreshable-grant licence has one.** A static-key licence has no refresh token to hold, so
|
|
// naming a manager for it is refused where the mistake is made rather than kept as a field that means
|
|
// 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) != 3 {
|
|
return errors.New("licence manager <name> <node> <module>")
|
|
}
|
|
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, module); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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.** `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"`
|
|
}
|
|
|
|
// 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 != "" {
|
|
raw, err = os.ReadFile(from)
|
|
} else {
|
|
raw, err = readAllStdin()
|
|
}
|
|
if err != nil {
|
|
return sealedGrantJSON{}, 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 g.Sealed == "" || g.ManagerKey == "" {
|
|
return sealedGrantJSON{}, errors.New(
|
|
"a sealed refresh token is {sealed, manager_key}, and one part is missing")
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func readAllStdin() ([]byte, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
return io.ReadAll(reader)
|
|
}
|
|
|
|
// 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).
|
|
//
|
|
// **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 sealed refresh token from a file instead of standard input")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("licence set-grant <name> [--file <path>]")
|
|
}
|
|
name := positionals[0]
|
|
|
|
grant, err := readSealedGrant(*from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
if err := held.SetRefreshGrant(ctx, name, grant.Sealed, grant.ManagerKey); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
// licenceSubmitRefresh publishes a refresh a MANAGER NODE already performed: the new access token is
|
|
// sealed to every holder, and a rotated refresh token replaces the stored envelope (novox/hq ADR
|
|
// 0050, Phase C).
|
|
//
|
|
// **This is the boundary the invariant rests on.** The manager runtime, on the manager node, opened
|
|
// the at-rest envelope with that node's key, called the vendor's OAuth endpoint, and produced this:
|
|
// the new access token in the clear, and — only if the vendor rotated it — the refresh token already
|
|
// re-sealed at rest. This reads exactly those two things and no refresh token in the clear ever
|
|
// reaches it, because it is never given one. The access token is sealed per holder and discarded,
|
|
// as any accepted key is; the rotated envelope is stored opaque.
|
|
func licenceSubmitRefresh(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("licence submit-refresh", flag.ContinueOnError)
|
|
accessFrom := set.String("access-file", "",
|
|
"read the new access token from a file instead of standard input")
|
|
grantFrom := set.String("grant-file", "",
|
|
"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
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New(
|
|
"licence submit-refresh <name> [--access-file <path>] [--grant-file <path>]")
|
|
}
|
|
name := positionals[0]
|
|
|
|
var accessToken string
|
|
if *accessFrom != "" {
|
|
raw, err := os.ReadFile(*accessFrom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accessToken = strings.TrimSpace(string(raw))
|
|
} else {
|
|
raw, err := readAllStdin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accessToken = strings.TrimSpace(string(raw))
|
|
}
|
|
if accessToken == "" {
|
|
return errors.New("no access token was given, so there is nothing to seal")
|
|
}
|
|
|
|
// The rotated sealed token is optional: absent, the stored refresh token is left exactly as it was.
|
|
var newSealed, newManagerKey string
|
|
if *grantFrom != "" {
|
|
grant, err := readSealedGrant(*grantFrom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newSealed, newManagerKey = grant.Sealed, grant.ManagerKey
|
|
}
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
held, err := open.Licences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inv := open.inventory
|
|
|
|
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 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"+
|
|
" run `push` to deliver it\n", name, sealed, rotatedNote)
|
|
return nil
|
|
}
|
|
|
|
// licenceRefresh mints a new access token for a refreshable-grant licence and seals it to every
|
|
// holder (novox/hq ADR 0050). The refresh token stays with the manager and is never delivered.
|
|
//
|
|
// The vendor's actual refresh is a plug-in this build does not ship (Phase C), so here this reports
|
|
// that plainly rather than pretending to have refreshed.
|
|
func licenceRefresh(ctx context.Context, args []string) error {
|
|
if len(args) != 1 {
|
|
return errors.New("licence refresh <name>")
|
|
}
|
|
name := args[0]
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
held, err := open.Licences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inv := open.inventory
|
|
|
|
sealed, err := held.Refresh(ctx, name, func(node string) (string, error) {
|
|
return inv.SealingKeyOf(ctx, node)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("refreshed %s: a new access token sealed to %d holder(s), and the refresh token left "+
|
|
"with its manager.\n run `push` to deliver it\n", name, sealed)
|
|
return nil
|
|
}
|
|
|
|
func licenceForget(ctx context.Context, args []string) error {
|
|
if len(args) != 1 {
|
|
return errors.New("licence forget <name>")
|
|
}
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
if err := held.Forget(ctx, args[0]); err != nil {
|
|
return err
|
|
}
|
|
// Said plainly, because the mesh cannot do it and pretending otherwise is worse than useless:
|
|
// a licence outliving its holder is a live credential nobody is watching.
|
|
fmt.Printf("%s is forgotten, and every record of who held it with it.\n"+
|
|
" The key itself is not the mesh's to revoke — do that where the licence was bought\n",
|
|
args[0])
|
|
return nil
|
|
}
|