Skip to content

feat(scanner): add signed manifest checks#41

Merged
marmar9615-cloud merged 1 commit intomainfrom
feature/v050-scanner-signature-checks
Apr 29, 2026
Merged

feat(scanner): add signed manifest checks#41
marmar9615-cloud merged 1 commit intomainfrom
feature/v050-scanner-signature-checks

Conversation

@marmar9615-cloud
Copy link
Copy Markdown
Owner

Summary

Fourth implementation PR for v0.5.0 signed manifests. Wires
verifyManifestSignature() from PR #39 into the scanner, emitting
stable manifest.signature.* check IDs for every verifier outcome.
Local key-set input only — no remote
/.well-known/agentbridge-keys.json fetch in this PR. Default
scanner behavior is unchanged
— unsigned manifests still score the
same and signed manifests trigger no signature check unless callers
opt in via the new signature option.

Scanner signature surface

scoreManifest(manifest, options?) and scanUrl(url, options?)
both accept an optional signature block:

import { scanUrl } from "@marmarlabs/agentbridge-scanner";

const keySet = await loadYourKeySetSomehow(); // /.well-known/agentbridge-keys.json

const result = await scanUrl("https://orders.acme.example", {
  signature: {
    keySet,                                        // required to verify
    expectedIssuer: "https://orders.acme.example", // optional strict check
    requireSignature: false,                       // default: missing → info, no deduction
    now: new Date(),                               // optional override for testing
    clockSkewSeconds: 60,                          // forwarded to the verifier
  },
});

result.passed.find((c) => c.id === "manifest.signature.verified");
result.checks.find((c) => c.id === "manifest.signature.invalid");

Local-only (no remote fetch)

The scanner does not fetch the publisher's key set for you. Pass
the key set in via signature.keySet. A runtime helper for
remote fetch lands in a later v0.5.0 PR alongside MCP server
enforcement.

Check IDs added

All under category safety. Stable identifiers — once shipped,
renaming any of them is a major bump per docs/v1-readiness.md §13.

Check Severity (default) Severity (requireSignature) Deduction When
manifest.signature.verified (passed) info info 0 Signature verified
manifest.signature.missing info error 0 / 15 No signature block
manifest.signature.unverified-no-key-set info info 0 Signature present, no keySet supplied — verification skipped
manifest.signature.malformed error error 25 Schema fail (incl. inverted/zero-length time window)
manifest.signature.key-set-malformed warning warning 0 Operator-side issue, manifest readiness unaffected
manifest.signature.unsupported-algorithm error error 20 Outside EdDSA / ES256
manifest.signature.unknown-kid error error 25 kid not in keys[]
manifest.signature.revoked-kid error error 30 kid in revokedKids[]
manifest.signature.issuer-mismatch error error 25 iss vs keySet.issuer / baseUrl origin / expectedIssuer
manifest.signature.before-signed-at error error 20 now < signedAt − skew
manifest.signature.expired error error 20 now > expiresAt + skew
manifest.signature.canonicalization-failed error error 25 Manifest contains non-canonical values
manifest.signature.invalid error error 25 crypto.verify returned false
manifest.signature.key-type-mismatch error error 20 Key entry alg / JWK kty/crv mismatch sig alg

Tests added

packages/scanner/src/tests/signature-checks.test.ts24 tests:

  • Default behavior unchanged (regression).
  • requireSignature mode + default mode + signed-without-keySet path.
  • Verified Ed25519 + ES256 happy paths.
  • Every failure mode mapped (tampered, unknown kid, revoked, expired, issuer mismatch incl. baseUrl binding, malformed key set, malformed signature shape, inverted-window, key-type mismatch).
  • Hygiene: scanner output never echoes public-key x/y bytes.
  • scanUrl integration: default invocation introduces no signature checks; with keySet runs verifier; with requireSignature emits the missing-signature error.
  • Round-trip the eddsa-valid + es256-valid + tampered-manifest vectors from spec/signing/test-vectors.json through the scanner.

Confirmations

  • Default unsigned scanner behavior unchanged. Verified by the regression test, by validate:examples, and by the existing packages/scanner/src/tests/scanner.test.ts + scanner-fixtures.test.ts continuing to pass.
  • No MCP / CLI enforcement. Scanner emits check IDs; downstream layers branch on them in subsequent PRs.
  • No remote key-set fetch. Operator passes the key set in.
  • No package versions changed. pack:dry-run confirms all six @marmarlabs/agentbridge-* still at 0.4.0.
  • No new dependencies. Pure Node crypto + existing @marmarlabs/agentbridge-core import. package-lock.json untouched.
  • No npm publish, no git tag, no GitHub release.
  • Dependabot PRs untouched.
  • Codex examples / CLI / scripts paths avoided — no edits under packages/cli/*, examples/* (incl. examples/signed-manifest-basic/), scripts/*, 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.

Browser / computer-use status

Not needed. The scanner is a Node library; no UI / browser
surface changed. The Studio dashboard and demo-app weren't
touched. CLI smoke (validate:examples,
validate:mcp-config-examples) plus the test / build /
pack:dry-run pipeline are the appropriate smoke for this
surface.

Commands run

npm run typecheck:clean                                          # clean
npx vitest run packages/scanner/src/tests/signature-checks.test.ts  # 24/24
npx vitest run packages/scanner/src/tests                        # 44/44 (20 existing + 24 new)
npm test                                                         # 409/409 across 25 files (+24 vs 385 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

Test plan

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

🤖 Generated with Claude Code

Fourth implementation PR for v0.5.0 signed manifests. Wires the
core verifyManifestSignature() helper from PR #39 into the scanner,
emitting stable manifest.signature.* check IDs the CLI / MCP /
Studio layers will branch on next. Local key-set input only — no
remote /.well-known/agentbridge-keys.json fetch in this PR.

Adds:
- packages/scanner/src/tests/signature-checks.test.ts — 24 tests
  covering:
    * Default unsigned scanner output unchanged (regression).
    * scoreManifest with no signature options or empty signature
      options is observably identical to v0.4.x.
    * requireSignature mode emits manifest.signature.missing as
      error (deduction 15) on unsigned manifests.
    * Default mode emits manifest.signature.missing as info
      (deduction 0) when signature options are present but
      keySet is omitted — informational only.
    * Signed manifest with no keySet emits
      manifest.signature.unverified-no-key-set (info, no
      deduction) instead of `missing`.
    * Verified Ed25519 + ES256 manifests emit
      manifest.signature.verified.
    * Failure-mode mappings: tampered → invalid (25), unknown
      kid → unknown-kid (25), revoked kid → revoked-kid (30),
      expired → expired (20), issuer mismatch (incl. baseUrl
      origin binding) → issuer-mismatch (25), inverted-window or
      shape-bad signature → malformed (25), malformed key set →
      key-set-malformed (warning, deduction 0 — operator-side
      issue, manifest readiness unaffected), key-type mismatch
      (alg vs JWK) → key-type-mismatch (20).
    * Hygiene: scanner output never echoes public-key x/y bytes
      (pinned regression).
    * scanUrl integration: default invocation introduces no
      signature checks; with signature.keySet routes to verifier;
      with requireSignature=true emits the missing-signature
      error.
    * Round-trip the eddsa-valid + es256-valid + tampered-manifest
      vectors from spec/signing/test-vectors.json through
      scoreManifest.

Modifies:
- packages/scanner/src/score.ts — adds SignatureScoringOptions
  + ScoringOptions; scoreManifest accepts an optional second arg;
  scoreSignature helper runs verifyManifestSignature and maps
  every VerifyManifestSignatureFailure to a stable scanner
  check ID with severity + deduction per the design's §13.5
  matrix. Defensive `never` exhaustiveness check on the failure
  mapping so a future verifier addition surfaces explicitly
  rather than silently dropping. Default behavior — no second
  arg — produces bit-for-bit identical output to v0.4.x for both
  unsigned and signed manifests.
- packages/scanner/src/scanner.ts — adds optional
  `signature: SignatureScoringOptions` to ScanOptions; threads
  it into scoreManifest. No remote key-set fetch and no change
  to the existing manifest-fetch network path.
- packages/scanner/README.md — adds a concise "Signed-manifest
  checks (v0.5.0, optional)" section with example, full check
  ID table (severity, deduction, when), and explicit notes on
  what is deferred (remote key fetch, MCP/CLI enforcement) and
  the additive-verification reminder.

Does NOT (deliberately):
- add MCP server enforcement
- add CLI --require-signature
- 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 scanner is a Node library; no UI/browser
  surface changed. No Next.js builds touched. The CLI smoke
  validations (validate:examples, validate:mcp-config-examples)
  and the npm test / build / pack:dry-run are the appropriate
  smoke for this surface.

Verified locally:
- npm run typecheck:clean (clean)
- npm test (409/409 across 25 files; was 385/24 on main = +24)
- npm run build (all packages built)
- npm run pack:dry-run (all six @marmarlabs/agentbridge-* OK at
  0.4.0; scanner packed 16.3 → 24.1KB for the new module)
- npm run validate:examples (✅ all examples validate)
- npm run validate:mcp-config-examples (✅ all client-config
  examples validate)
- npx vitest run packages/scanner/src/tests (44/44 — the existing
  20 + 24 new)

Tracking: #31

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@marmar9615-cloud marmar9615-cloud force-pushed the feature/v050-scanner-signature-checks branch from 2bd376b to 0a3120a Compare April 29, 2026 01:56
@marmar9615-cloud marmar9615-cloud merged commit 314c023 into main Apr 29, 2026
2 checks passed
marmar9615-cloud added a commit that referenced this pull request Apr 29, 2026
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>
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