Files
mesh-controller/internal/secrets/sealedbox_xcheck_test.go
T
jschoubben 33fd28ffa6 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
2026-09-07 01:55:08 +02:00

80 lines
3.1 KiB
Go

package secrets
import (
"encoding/base64"
"encoding/json"
"os"
"testing"
"golang.org/x/crypto/nacl/box"
)
// The manager module's TypeScript seal and this package's Go seal are the SAME anonymous sealed box,
// byte for byte — the property the refreshable-grant carve-out rests on (novox/hq ADR 0050).
//
// **Why it must hold.** The refresh token is sealed to the manager node — at adoption and after each
// rotation — by the manager MODULE, in TypeScript (mesh-catalog anthropic-manager/sealedbox.ts). The
// HOST then unseals it with Go's box.OpenAnonymous (mesh-host identity.SealingKey.Unseal) to mount the
// cleartext, and mesh-control seals every other credential with box.SealAnonymous (secrets.Seal). If
// the TS seal and the Go box disagreed by a byte, the host would refuse the refresh token as a value
// it cannot open — silently, as a manager that never gets its credential. So this is load-bearing, and
// it is pinned here rather than trusted.
//
// The fixture is produced by the module's own compiled seal() over a fresh node key pair; this test
// opens it with box.OpenAnonymous — exactly what the host runs — and with secrets.Open, and recovers
// the plaintext. Regenerate it with the module's seal() if the construction ever changes; a drift
// shows up here as a fixture Go cannot open, which is the whole point.
func TestModuleSealedBoxOpensInGo(t *testing.T) {
raw, err := os.ReadFile("testdata/module-sealedbox-fixture.json")
if err != nil {
t.Fatal(err)
}
var f struct {
ManagerPublicKey string `json:"managerPublicKey"`
ManagerPrivateKey string `json:"managerPrivateKey"`
Plaintext string `json:"plaintext"`
Sealed string `json:"sealed"`
}
if err := json.Unmarshal(raw, &f); err != nil {
t.Fatal(err)
}
pub, err := base64.StdEncoding.DecodeString(f.ManagerPublicKey)
if err != nil || len(pub) != 32 {
t.Fatalf("the fixture public key is not a 32-byte X25519 key")
}
priv, err := base64.StdEncoding.DecodeString(f.ManagerPrivateKey)
if err != nil || len(priv) != 32 {
t.Fatalf("the fixture private key is not 32 bytes")
}
blob, err := base64.StdEncoding.DecodeString(f.Sealed)
if err != nil {
t.Fatalf("the sealed value is not base64: %v", err)
}
// The host's path: box.OpenAnonymous with the node's key pair.
var pubA, privA [32]byte
copy(pubA[:], pub)
copy(privA[:], priv)
out, ok := box.OpenAnonymous(nil, blob, &pubA, &privA)
if !ok {
t.Fatal("box.OpenAnonymous (the host's Unseal) FAILED to open the module's TS seal — " +
"the TypeScript crypto_box_seal has drifted from Go's box")
}
if string(out) != f.Plaintext {
t.Fatalf("opened to %q, expected %q", out, f.Plaintext)
}
// And it is exactly what secrets.Seal produces: a value this package can round-trip is one the TS
// module could equally have produced, so the two are interchangeable at the seam.
roundTrip, err := Seal(f.ManagerPublicKey, []byte(f.Plaintext))
if err != nil {
t.Fatal(err)
}
rtBlob, _ := base64.StdEncoding.DecodeString(roundTrip)
back, ok := box.OpenAnonymous(nil, rtBlob, &pubA, &privA)
if !ok || string(back) != f.Plaintext {
t.Fatal("secrets.Seal did not round-trip under box.OpenAnonymous")
}
}