57/F1: The sealed box moves into the SDK, in Go and TypeScript (issue 650) #26

Merged
mesh-admin merged 3 commits from pkg/57-f1-seal-box into feat/57-config-and-provisioning 2026-10-13 23:52:12 +00:00
Contributor

Work package F1 of design 57 (building config and provisioning; ADR 0349). Issue 650.

What changed

  • go/seal: the ADR 0206 sealed box (X25519, HKDF-SHA256, AES-256-GCM, envelope {v, eph, iv, tag, ct}, PEM keys), moved from mesh-catalog modules/claude-licence-manager/cmd/claude-licence-manager/seal.go. API: seal.Box, seal.KeyPair, seal.GenerateKeyPair(), seal.Seal(plaintext, recipientPEM), seal.Open(box, privatePEM), seal.Version.
  • @novox/mesh-sdk/seal (src/seal): the TypeScript twin, seal, open, generateKeyPair, SealedBox, KeyPair, VERSION; also re-exported as seal from the package root.
  • Standard library only, as go/merge-check.sh promises. The licence manager used crypto/hkdf, which needs Go 1.24 while this module says go 1.22; HKDF-SHA256 for one 32-byte block is written out with crypto/hmac instead of raising the module's Go version. The byte-for-byte vectors prove it equals hkdf.Key.
  • Two small hardenings over the original, neither changing a valid box: Open refuses an iv or tag of the wrong length (node would accept a truncated tag), and checks that eph is an X25519 key.

Why

The vault (issue 674), the provisioner client and providers (issue 675 and later) all need this box. Code shared between modules belongs in the SDK.

How it is proven

Fixtures in go/seal/testdata/:

  • sealed-by-licence-manager.json: made by the licence manager's own seal.go (mesh-catalog feat/57-config-and-provisioning at 8aa37e8) to a fixed test recipient key. sealed holds boxes from its unchanged Seal; vectors holds boxes from the same code with only its two randomness sources (one-time key, iv) fixed, so a seal can be compared byte for byte. The key is derived from a fixed label and guards nothing.
  • sealed-by-typescript.json: the agent module's TypeScript box the licence manager already keeps.

Go tests (go/seal/seal_test.go): TestABoxTheLicenceManagerSealedOpens, TestSealIsByteForByteTheLicenceManagersBox, TestABoxSealedInTypeScriptOpens, TestABoxOpensOnlyForItsRecipient, TestATamperedBoxDoesNotOpen, TestAKeyOfAnotherKindIsRefused.
TypeScript tests (test/seal.test.ts), the same six over the same fixture files.
Both failed before (package missing) and pass now.

The reverse direction (the licence manager opens what the SDK seals): the vectors show the SDK seals exactly the licence manager's bytes. Also, with the licence manager switched to this package (prepared, see below), all its existing tests pass unchanged, including TestABoxSealedInTypeScriptOpensInGo.

Deliberately left

  • The licence manager's switch to this package is a second pull request in mesh-catalog onto feat/57-config-and-provisioning. It can only be opened after this merges, because its require git.novox.be/novox/mesh-sdk/go must name the merged commit's pseudo-version. The change is ready: seal.go becomes aliases (type SealedBox = seal.Box, ...), newGCM moves into crypt.go; tested here against this branch.
  • modules/claude-code/cmd/claude-code/seal.go in mesh-catalog is a second, identical copy of the box. It is not in F1's scope; it should switch the same way.

Reviewer's checklist

  1. Does what issue 650's Expected and design 57's F1 say: the box lives in mesh-sdk/go/seal with a TypeScript twin. Nothing outside it (the README line names the new package).
  2. Done-when proof is tests that failed before (no package) and pass now. No existing test touched.
  3. Composition: not applicable. This is an SDK-only addition; no module composes differently until it bumps its require.
  4. No secret anywhere: fixture keys are test keys derived from fixed labels; plaintexts are made up. No database, no migration.
  5. Strict parsers: no manifest or declaration field added.
  6. sh merge-check.sh (npm ci, tsc --noEmit, npm test: 53 of 53 pass; gofmt -l empty, go vet ./..., go test -count=1 ./...: all ok) and sh go/merge-check.sh: pass.
  7. Words: sealed box, licence manager, vault; ADR and issue numbers named with what they are.

The branch has three commits: the first two carried a placeholder README and package.json by my mistake, which the following commits replace. I checked that the branch's final tree equals my tested local tree (git diff empty). Please squash on merge.

Work package F1 of design 57 (building config and provisioning; ADR 0349). Issue 650. ## What changed - `go/seal`: the ADR 0206 sealed box (X25519, HKDF-SHA256, AES-256-GCM, envelope `{v, eph, iv, tag, ct}`, PEM keys), moved from mesh-catalog `modules/claude-licence-manager/cmd/claude-licence-manager/seal.go`. API: `seal.Box`, `seal.KeyPair`, `seal.GenerateKeyPair()`, `seal.Seal(plaintext, recipientPEM)`, `seal.Open(box, privatePEM)`, `seal.Version`. - `@novox/mesh-sdk/seal` (src/seal): the TypeScript twin, `seal`, `open`, `generateKeyPair`, `SealedBox`, `KeyPair`, `VERSION`; also re-exported as `seal` from the package root. - Standard library only, as go/merge-check.sh promises. The licence manager used `crypto/hkdf`, which needs Go 1.24 while this module says `go 1.22`; HKDF-SHA256 for one 32-byte block is written out with `crypto/hmac` instead of raising the module's Go version. The byte-for-byte vectors prove it equals `hkdf.Key`. - Two small hardenings over the original, neither changing a valid box: `Open` refuses an iv or tag of the wrong length (node would accept a truncated tag), and checks that `eph` is an X25519 key. ## Why The vault (issue 674), the provisioner client and providers (issue 675 and later) all need this box. Code shared between modules belongs in the SDK. ## How it is proven Fixtures in `go/seal/testdata/`: - `sealed-by-licence-manager.json`: made by the licence manager's own seal.go (mesh-catalog feat/57-config-and-provisioning at 8aa37e8) to a fixed test recipient key. `sealed` holds boxes from its unchanged `Seal`; `vectors` holds boxes from the same code with only its two randomness sources (one-time key, iv) fixed, so a seal can be compared byte for byte. The key is derived from a fixed label and guards nothing. - `sealed-by-typescript.json`: the agent module's TypeScript box the licence manager already keeps. Go tests (go/seal/seal_test.go): `TestABoxTheLicenceManagerSealedOpens`, `TestSealIsByteForByteTheLicenceManagersBox`, `TestABoxSealedInTypeScriptOpens`, `TestABoxOpensOnlyForItsRecipient`, `TestATamperedBoxDoesNotOpen`, `TestAKeyOfAnotherKindIsRefused`. TypeScript tests (test/seal.test.ts), the same six over the same fixture files. Both failed before (package missing) and pass now. The reverse direction (the licence manager opens what the SDK seals): the vectors show the SDK seals exactly the licence manager's bytes. Also, with the licence manager switched to this package (prepared, see below), all its existing tests pass unchanged, including `TestABoxSealedInTypeScriptOpensInGo`. ## Deliberately left - The licence manager's switch to this package is a second pull request in mesh-catalog onto feat/57-config-and-provisioning. It can only be opened after this merges, because its `require git.novox.be/novox/mesh-sdk/go` must name the merged commit's pseudo-version. The change is ready: `seal.go` becomes aliases (`type SealedBox = seal.Box`, ...), `newGCM` moves into crypt.go; tested here against this branch. - `modules/claude-code/cmd/claude-code/seal.go` in mesh-catalog is a second, identical copy of the box. It is not in F1's scope; it should switch the same way. ## Reviewer's checklist 1. [x] Does what issue 650's Expected and design 57's F1 say: the box lives in mesh-sdk/go/seal with a TypeScript twin. Nothing outside it (the README line names the new package). 2. [x] Done-when proof is tests that failed before (no package) and pass now. No existing test touched. 3. [x] Composition: not applicable. This is an SDK-only addition; no module composes differently until it bumps its require. 4. [x] No secret anywhere: fixture keys are test keys derived from fixed labels; plaintexts are made up. No database, no migration. 5. [x] Strict parsers: no manifest or declaration field added. 6. [x] `sh merge-check.sh` (npm ci, tsc --noEmit, npm test: 53 of 53 pass; gofmt -l empty, go vet ./..., go test -count=1 ./...: all ok) and `sh go/merge-check.sh`: pass. 7. [x] Words: sealed box, licence manager, vault; ADR and issue numbers named with what they are. The branch has three commits: the first two carried a placeholder README and package.json by my mistake, which the following commits replace. I checked that the branch's final tree equals my tested local tree (`git diff` empty). Please squash on merge.
mesh-admin added 3 commits 2026-10-13 23:49:03 +00:00
go/seal and its TypeScript twin @novox/mesh-sdk/seal are the ADR 0206 box
the licence manager kept to itself. Fixtures made by the licence manager's
own seal.go prove both byte for byte, in both directions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go/seal and its TypeScript twin @novox/mesh-sdk/seal are the ADR 0206 box
the licence manager kept to itself. Fixtures made by the licence manager's
own seal.go prove both byte for byte, in both directions. (Replaces the
placeholder README the branch's first commit carried by mistake.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the TypeScript twin of the sealed box, proven by the Go package's fixtures (issue 650)
mesh/merge-gate head a7997fc857ae merged into trunk c579f4de7511 — pass: the change touches no module of the mesh's graph
mesh/repo-check head a7997fc857ae merged into trunk c579f4de7511 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
a7997fc857
(Also replaces the placeholder package.json the previous commit carried by mistake.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Contributor

Delivery novox/mesh-sdk@a7997fc857ae — delivered since 2026-10-13T23:52:19Z

Delivery plan — builds nothing: the change touches no module of the mesh's graph

Transitions

  • 2026-10-13 23:49 (new) → proposed (announced): novox/mesh-sdk#26's head announced
  • 2026-10-13 23:50 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
  • 2026-10-13 23:50 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
  • 2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
  • 2026-10-13 23:52 published → delivered (done): nothing for a walk to move

The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.

<!-- mesh-delivery:view --> **Delivery** `novox/mesh-sdk@a7997fc857ae` — **delivered** since 2026-10-13T23:52:19Z **Delivery plan** — builds nothing: the change touches no module of the mesh's graph **Transitions** - 2026-10-13 23:49 (new) → proposed (announced): novox/mesh-sdk#26's head announced - 2026-10-13 23:50 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit - 2026-10-13 23:50 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail - 2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move - 2026-10-13 23:52 published → delivered (done): nothing for a walk to move The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Author
Contributor

Verdict: APPROVE at a7997fc857

Independent review of package F1 (issue 650). What I checked and ran, in my own worktree of the head:

  • sh merge-check.sh: exit 0 (npm ci, tsc --noEmit, 53 of 53 TypeScript tests pass including the six seal tests; gofmt, go vet and go test all pass). sh go/merge-check.sh: exit 0, go/seal ok.
  • HKDF: boxKey (go/seal/seal.go:186-195) follows RFC 5869 for L=32. Extract computes PRK = HMAC-SHA256(salt, IKM). Expand computes T(1) = HMAC-SHA256(PRK, info || 0x01). The salt is never empty, so the RFC's zero-salt default does not arise. I compared it with the original hkdf.Key from the licence manager's seal.go (mesh-catalog feat/57 at 8aa37e8) on 2000 random inputs: all equal. The TypeScript side uses hkdfSync with the same salt (eph SPKI DER followed by the recipient's raw key) and the same info string, "novox-mesh sealed box v1".
  • Fixtures: the licence manager's original code opens all 5 sealed boxes and reproduces all 5 vectors byte for byte. sealed-by-typescript.json is identical to the licence manager's copy.
  • Proof that the tests bite: with one byte of the HKDF expand changed in Go, 3 seal tests fail. With the info string changed in TypeScript, 3 fail.
  • Nonce: Seal and seal draw a fresh 12-byte iv from crypto/rand (randomBytes in TypeScript) and a fresh one-time key on every call. The fixed-randomness sealWith is unexported in Go and is not in the package's ./seal export in TypeScript. Open refuses an iv that is not 12 bytes or a tag that is not 16. No manual secret comparisons: the GCM tag check is the libraries' own. Open also no longer aliases ct when appending the tag, as the original did.
  • X25519: keys are checked for curve on both sides, and an all-zero shared secret is refused by Go's ecdh and by OpenSSL. Only test keys are in the fixtures.

Findings (none blocks the merge):

  1. Must, at merge time: squash. Commit 1b36bcd replaces go/README.md with "PLACEHOLDER". Commit adfb9c8 replaces package.json with "PKGJSON", so the TypeScript build fails at that commit. The two commits also share one title. The final tree is correct; merge with method: squash.
  2. Should, can follow: go/seal/testdata/sealed-by-licence-manager.json:29 and :85 use a plaintext shaped like an Anthropic API key (sk-ant-api03-test-...). It is fake, but secret scanners and the redactor may flag it. A neutral string would avoid that.
  3. Note: src/seal/box.ts:280 and :285 let node's own error escape for a malformed eph, instead of "the box does not open with this key". This is harmless.
  4. Note: issue 650's Expected also says the licence manager uses the box. That part comes in the follow-up mesh-catalog pull request, so issue 650 stays in fixing until it lands. The second copy in modules/claude-code needs the same switch.
Verdict: APPROVE at a7997fc857aea43266bc56cf096de8a4031b2c9f Independent review of package F1 (issue 650). What I checked and ran, in my own worktree of the head: - `sh merge-check.sh`: exit 0 (npm ci, tsc --noEmit, 53 of 53 TypeScript tests pass including the six seal tests; gofmt, go vet and go test all pass). `sh go/merge-check.sh`: exit 0, `go/seal` ok. - HKDF: `boxKey` (go/seal/seal.go:186-195) follows RFC 5869 for L=32. Extract computes PRK = HMAC-SHA256(salt, IKM). Expand computes T(1) = HMAC-SHA256(PRK, info || 0x01). The salt is never empty, so the RFC's zero-salt default does not arise. I compared it with the original `hkdf.Key` from the licence manager's seal.go (mesh-catalog feat/57 at 8aa37e8) on 2000 random inputs: all equal. The TypeScript side uses `hkdfSync` with the same salt (eph SPKI DER followed by the recipient's raw key) and the same info string, "novox-mesh sealed box v1". - Fixtures: the licence manager's original code opens all 5 `sealed` boxes and reproduces all 5 `vectors` byte for byte. sealed-by-typescript.json is identical to the licence manager's copy. - Proof that the tests bite: with one byte of the HKDF expand changed in Go, 3 seal tests fail. With the info string changed in TypeScript, 3 fail. - Nonce: `Seal` and `seal` draw a fresh 12-byte iv from crypto/rand (randomBytes in TypeScript) and a fresh one-time key on every call. The fixed-randomness `sealWith` is unexported in Go and is not in the package's `./seal` export in TypeScript. Open refuses an iv that is not 12 bytes or a tag that is not 16. No manual secret comparisons: the GCM tag check is the libraries' own. Open also no longer aliases `ct` when appending the tag, as the original did. - X25519: keys are checked for curve on both sides, and an all-zero shared secret is refused by Go's ecdh and by OpenSSL. Only test keys are in the fixtures. Findings (none blocks the merge): 1. Must, at merge time: squash. Commit 1b36bcd replaces go/README.md with "PLACEHOLDER". Commit adfb9c8 replaces package.json with "PKGJSON", so the TypeScript build fails at that commit. The two commits also share one title. The final tree is correct; merge with `method: squash`. 2. Should, can follow: go/seal/testdata/sealed-by-licence-manager.json:29 and :85 use a plaintext shaped like an Anthropic API key (`sk-ant-api03-test-...`). It is fake, but secret scanners and the redactor may flag it. A neutral string would avoid that. 3. Note: src/seal/box.ts:280 and :285 let node's own error escape for a malformed eph, instead of "the box does not open with this key". This is harmless. 4. Note: issue 650's Expected also says the licence manager uses the box. That part comes in the follow-up mesh-catalog pull request, so issue 650 stays in fixing until it lands. The second copy in modules/claude-code needs the same switch.
mesh-admin merged commit c8b07242a0 into feat/57-config-and-provisioning 2026-10-13 23:52:12 +00:00
mesh-admin deleted branch pkg/57-f1-seal-box 2026-10-13 23:52:13 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-sdk#26