Skip to content

feat(cli): add signed manifest verification commands#43

Merged
marmar9615-cloud merged 2 commits intomainfrom
feature/v050-cli-signed-manifests
Apr 29, 2026
Merged

feat(cli): add signed manifest verification commands#43
marmar9615-cloud merged 2 commits intomainfrom
feature/v050-cli-signed-manifests

Conversation

@marmar9615-cloud
Copy link
Copy Markdown
Owner

Summary

Fifth implementation PR for v0.5.0 signed manifests. Wires the
core verifier from PR #39 into operator-facing CLI commands. No
MCP server enforcement
and no remote /.well-known/agentbridge-keys.json
fetch
in this PR. Default unsigned validation behavior is
unchanged
— adopters opt into signature verification via new
flags.

Validate command — new flags

agentbridge validate <manifest> [--keys <keyset.json>] [--require-signature] [--expected-issuer <origin>] [--now <iso>] [--clock-skew-seconds <n>] [--json]

Form Behavior
(default) v0.4.x behavior. Schema-validate only.
--keys <ks> Schema-validate + run verifyManifestSignature. Exit 0 on both green; exit 1 on either failure.
--require-signature Reject manifests with no signature block (exit 1).
--require-signature --keys <ks> Both: schema valid, signature present, signature verifies.
--require-signature without --keys on a signed manifest Refuses to silently report "verified" without verifying — exits 1 with verification skipped.
--json Adds signature: { ok, kid?, iss?, alg?, …, reason?, message? } next to the existing schema result.

New agentbridge verify command

agentbridge verify <manifest> --keys <keyset.json> [--expected-issuer <origin>] [--now <iso>] [--clock-skew-seconds <n>] [--json]

Always runs the verifier. Returns the structured outcome with the
stable VerifyManifestSignatureFailure enum from
@marmarlabs/agentbridge-core. --json mode emits a parseable
JSON object on stdout with no prose, suitable for CI.

$ agentbridge verify ./manifest.json --keys ./keys.json
✓ signature verified — alg=EdDSA kid=test-ed25519-2026-04 iss=https://projects.example.com
  · signedAt:  2026-04-28T12:00:00.000Z
  · expiresAt: 2026-04-29T12:00:00.000Z

$ agentbridge verify ./tampered.json --keys ./keys.json
✗ signature verification failed — signature-invalid
  · signature did not verify against the supplied public key
$ echo $?
1

$ agentbridge verify ./manifest.json --keys ./keys.json --json
{
  "ok": true,
  "kid": "test-ed25519-2026-04",
  "iss": "https://projects.example.com",
  "alg": "EdDSA",
  "signedAt": "2026-04-28T12:00:00.000Z",
  "expiresAt": "2026-04-29T12:00:00.000Z"
}

New agentbridge keys generate command (local dev only)

agentbridge keys generate --kid <id> --issuer <origin> --out-public <path> --out-private <path> [--alg EdDSA|ES256] [--not-before <iso>] [--not-after <iso>]

Local-dev helper for bootstrapping a publisher key set. Default
algorithm Ed25519. ES256 also accepted.

Private-key safety contract (pinned by tests):

  • --out-private is required — the CLI refuses to silently
    discard freshly-generated key material.
  • The private key file is created with mode 0o600 (owner-only)
    on POSIX.
  • The private d parameter is never echoed on stdout/stderr
    — only the file path and the kid/alg metadata.
  • The public key set written to --out-public is round-tripped
    through validateKeySet and contains no private d.
  • A sensitivity warning is printed to stderr after generation.

The generated keypair round-trips end-to-end: a manifest signed
with the new private key verifies cleanly via
agentbridge verify against the new public key set (covered by a
test).

Tests added

packages/cli/src/tests/signature-cli.test.ts25 tests:

  • Default unsigned validation behavior unchanged (regression).
  • validate --require-signature on unsigned manifest exits 1.
  • validate --require-signature on signed manifest without
    --keys refuses silent "verified" — exits 1.
  • validate --keys Ed25519 + ES256 happy paths via
    spec/signing/test-vectors.json.
  • validate --keys failure modes: tampered → signature-invalid,
    unknown kid → unknown-kid, expired (with --now +
    --clock-skew-seconds) → expired, missing key-set path,
    malformed key set.
  • verify happy path; verify no positional → exit 2; verify
    no --keys → exit 2; verify tampered →
    reason: signature-invalid.
  • verify --json emits parseable JSON for both success and
    failure with no prose on stdout.
  • verify --expected-issuer mismatch → issuer-mismatch.
  • verify malformed key set → exit 1, "failed to load".
  • Hygiene: scanner / verifier output never echoes public-key x/y
    bytes (success + failure paths).
  • keys generate happy path: schema-valid public set, no private
    d in public set.
  • keys generate POSIX mode 0o600 on private file.
  • keys generate private bytes never appear on stdout/stderr;
    sensitivity warning printed.
  • keys generate rejects when --out-private is omitted (exit 2).
  • keys generate rejects non-canonical issuer.
  • keys generate end-to-end: freshly-generated keypair signs +
    agentbridge verify succeeds against the new public set.

Live CLI command smoke (against built dist)

# default validate (signed manifest, no flags) — schema-valid only
$ node packages/cli/dist/bin.js validate /tmp/signed-basic.agentbridge.json
✓ valid manifest  Signed Manifest Basic v1.0.0
  baseUrl: https://projects.example.com
  actions: 2  resources: 1

# validate --keys
$ node packages/cli/dist/bin.js validate /tmp/signed-basic.agentbridge.json \
    --keys examples/signed-manifest-basic/agentbridge-keys.json --now 2026-04-28T18:00:00Z
✓ valid manifest  Signed Manifest Basic v1.0.0
✓ signature verified — alg=EdDSA kid=test-ed25519-2026-04 iss=https://projects.example.com

# verify happy + --json
$ node packages/cli/dist/bin.js verify /tmp/signed-basic.agentbridge.json \
    --keys examples/signed-manifest-basic/agentbridge-keys.json --now 2026-04-28T18:00:00Z --json
{ "ok": true, "kid": "test-ed25519-2026-04", "iss": "https://projects.example.com", "alg": "EdDSA", … }

# tampered → signature-invalid, exit 1
$ node packages/cli/dist/bin.js verify /tmp/tampered.json --keys … --now 2026-04-28T18:00:00Z
✗ signature verification failed — signature-invalid; exit=1

# keys generate
$ node packages/cli/dist/bin.js keys generate --kid live-smoke-key \
    --issuer https://example.com --out-public /tmp/keys.json --out-private /tmp/private.json
✓ generated EdDSA key  kid=live-smoke-key
warning: the private key file is sensitive material…
$ stat -f %Lp /tmp/private.json
600

All five live invocations green.

Browser / computer-use status

Not needed. The CLI is a Node binary; no UI / browser surface
changed. The Studio dashboard and demo-app weren't touched. The
live CLI command smoke against the built dist is the relevant
live test for this surface and is included above.

Confirmations

  • Default unsigned validation behavior unchanged. Verified by the regression test, by validate:examples, and by the existing cli.test.ts (17 tests) continuing to pass without modification.
  • No MCP enforcement. CLI only.
  • No remote key fetch. Operator passes the key set as a local file.
  • No package versions changed. pack:dry-run confirms all six @marmarlabs/agentbridge-* still at 0.4.0.
  • No new dependencies. Pure Node crypto + the existing @marmarlabs/agentbridge-core import. package-lock.json untouched.
  • No npm publish, no git tag, no GitHub release.
  • Dependabot PRs untouched.
  • Codex scanner-reporting paths avoided — no edits under packages/scanner/*, examples/scanner-signature-reporting/*, examples/README.md, apps/mcp-server/*, docs/releases/*, the root README, or CHANGELOG.md.
  • Safety invariants intact — confirmation gate, origin pinning, target-origin allowlist, audit redaction, stdio stdout hygiene, HTTP transport auth/origin checks all unchanged.
  • Private key material never leaks to stdout/stderr — covered by tests for both verify (no public-key bytes echoed) and keys generate (no private d echoed).

Commands run

npm run typecheck:clean                                        # clean
npx vitest run packages/cli/src/tests/signature-cli.test.ts    # 25/25
npx vitest run packages/cli/src/tests                          # 69/69
npm test                                                       # 435/435 across 26 files (+26 vs 409 on main)
npm run build                                                  # all packages built
npm run pack:dry-run                                           # all six @marmarlabs/agentbridge-* OK at 0.4.0
npm run validate:examples                                      # ✅ all examples validate
npm run validate:mcp-config-examples                           # ✅ all client-config examples validate
# live CLI smoke (above)

Test plan

  • CI green on Node 20.x and 22.x.
  • All package versions remain 0.4.0.
  • +26 net new tests.

🤖 Generated with Claude Code

Fifth implementation PR for v0.5.0 signed manifests. Wires the
core verifier from PR #39 + the scanner check IDs from PR #41
into operator-facing CLI commands. No MCP server enforcement, no
remote /.well-known/agentbridge-keys.json fetch in this PR.

Adds:
- packages/cli/src/commands/verify.ts — `agentbridge verify
  <manifest> --keys <keyset.json>` with optional flags
  --expected-issuer, --now, --clock-skew-seconds, --json. Exit 0
  on verified, exit 1 on any failure (carries the verifier's
  stable reason enum), exit 2 on usage error. --json mode emits
  a parseable structured outcome on stdout with no prose.
- packages/cli/src/commands/keys.ts — `agentbridge keys generate
  --kid <id> --issuer <origin> --out-public <path> --out-private
  <path> [--alg EdDSA|ES256]`. Local-dev helper for bootstrapping
  a key set. Defaults to Ed25519. Refuses to silently discard
  freshly-generated material (--out-private is required). Writes
  the private key with mode 0o600 (owner-only) on POSIX. Never
  echoes the private `d` parameter to stdout/stderr; prints a
  sensitivity warning routed to stderr.
- packages/cli/src/commands/key-loader.ts — small shared helper
  that loads a key set from disk and runs validateKeySet().
- packages/cli/src/tests/signature-cli.test.ts — 25 tests:
    * Default unsigned validation behavior unchanged.
    * --require-signature on an unsigned manifest exits 1.
    * --require-signature without --keys on a signed manifest
      exits 1 (refuses to silently report "verified" without
      verifying).
    * Ed25519 + ES256 happy paths via the spec/signing/test-vectors.json
      vectors.
    * Tampered, unknown-kid, expired, malformed-key-set, missing
      key-set path.
    * verify happy path, --json structure (success + failure),
      --expected-issuer mismatch, missing --keys (exit 2), missing
      manifest (exit 2).
    * Output never echoes the public-key x/y bytes (pinned for
      both success and failure paths).
    * keys generate: Ed25519 keypair, schema-valid public key set
      (no private `d`), POSIX mode 0o600 on the private file,
      no private bytes on stdout/stderr, sensitivity warning on
      stderr, --out-private required (exit 2 if omitted),
      non-canonical issuer rejected, freshly-generated key
      round-trips through Node crypto sign + agentbridge verify.

Modifies:
- packages/cli/src/commands/validate.ts — accepts new flags
  --keys, --require-signature, --expected-issuer, --now,
  --clock-skew-seconds. Default (no signature flags) behavior
  bit-identical to v0.4.x. With --keys, runs verifier and prints
  outcome. With --require-signature without --keys, refuses
  silent "verified" outcomes — surfaces missing-signature
  (unsigned) or no-key-set-supplied (signed but unverifiable)
  with exit 1. JSON mode includes a `signature` field next to
  the existing schema-validation result. Exports
  summarizeVerifyResult so the verify command shares the same
  output shape.
- packages/cli/src/index.ts — dispatches `verify` and `keys`
  subcommands; threads new flags into runValidate / runVerify;
  expands --help with the new commands and a parseSkew helper.
- packages/cli/README.md — adds concise sections for
  validate --keys / --require-signature, the verify command,
  the keys generate command (with explicit local-dev /
  KMS-in-production warnings), updated regression-tested-examples
  blurb, no-MCP-enforcement-yet reminder.

Does NOT (deliberately):
- add MCP server signature enforcement
- fetch remote key sets
- require signatures by default
- bump any package version
- add any runtime dependency
- publish, tag, or release

Browser / computer-use status:
- Not needed. The CLI is a Node binary; no UI/browser surface
  changed. The Studio dashboard and demo-app weren't touched.
  Live CLI command smoke against the built dist (validate
  default, validate --keys, verify happy, verify --json, verify
  tampered → exit 1 + signature-invalid, keys generate with
  POSIX 0o600 + private bytes never on stdout) is the relevant
  live test for this surface and is documented in the PR body.

Verified locally:
- npm run typecheck:clean (clean)
- npx vitest run packages/cli/src/tests/signature-cli.test.ts (25/25)
- npx vitest run packages/cli/src/tests (17 + 11 + 11 + 5 + 25 = 69/69)
- npm test (435/435 across 26 files; was 409/25 on main = +26)
- npm run build (all packages built)
- npm run pack:dry-run (all six @marmarlabs/agentbridge-* OK at
  0.4.0; CLI packed 29.4 → 57.4KB for the new modules)
- npm run validate:examples (✅ all examples validate)
- npm run validate:mcp-config-examples (✅ all client-config
  examples validate)
- live: validate (default + --keys), verify (happy + --json +
  tampered), keys generate (mode 600, no private bytes on
  stdout) — all pass against `node packages/cli/dist/bin.js`.

Tracking: #31

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@marmar9615-cloud marmar9615-cloud force-pushed the feature/v050-cli-signed-manifests branch from 13776cd to 1f078fa Compare April 29, 2026 02:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13776cd251

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/commands/keys.ts
Codex review of PR #43 caught a real safety bug: `fs.writeFile(..., { mode: 0o600 })`
only applies the mode when Node *creates* the file. On a rewrite —
the realistic operator scenario where someone re-runs `agentbridge
keys generate` against an existing private-key path — Node truncates
and writes but preserves the existing permissions. An older 0o644
file would silently keep world-read after fresh private bytes were
written into it, breaking the documented owner-only contract on
shared systems.

Fix: explicit `fs.chmod(outPrivate, 0o600)` after the write on
POSIX. Windows skipped — chmod there only toggles the read-only
bit, not real permissions, and our existing test only checks mode
on POSIX.

Test added: pre-create the private file with 0o644, run `keys
generate` against it, assert the resulting file mode is 0o600.
Without the fix, the test fails with mode 0o644.

Verified locally:
- npx vitest run packages/cli/src/tests/signature-cli.test.ts (26/26)

Refs: PR #43, issue #31

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@marmar9615-cloud marmar9615-cloud merged commit 3a4eaca into main Apr 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant