Skip to content

fix(verify): reject crypto_profile downgrade; show verification failing in getting-started - #237

Merged
imran-siddique merged 2 commits into
mainfrom
fix/crypto-profile-downgrade-check
Jul 27, 2026
Merged

fix(verify): reject crypto_profile downgrade; show verification failing in getting-started#237
imran-siddique merged 2 commits into
mainfrom
fix/crypto-profile-downgrade-check

Conversation

@imran-siddique

Copy link
Copy Markdown
Contributor

What

Two independent correctness fixes found while auditing the verifier against the spec.

1. Crypto profile downgrade (python/src/agent_manifest/_verify.py)

crypto_profile is inside the signing pre-image, but spec 3.6 excludes the entire signature block from it. So signature.algorithm can be rewritten without disturbing the signed bytes, and nothing anywhere in verify_manifest() read crypto_profile. A manifest declaring the post-quantum profile verified VALID on a classical-only Ed25519 signature.

Spec 4.2 already requires the verifier to reject rather than silently fall back ("this prevents downgrade attacks during the transition period"); this implements it.

  • Runs independently of trusted_keys. The downgrade is a property of the manifest, not of the verifier's key material.
  • One-directional: rejects a signature weaker than the declared profile requires, permits a stronger one. An issuer dual-signing ahead of the profile flip is not flagged.
  • Unrecognised algorithm identifiers are left to the existing signature-verification branch, which already reports them.
  • New conformance vector AM-VEC-020 so other-language SDKs inherit the check.

Not a forgery path: an attacker still needs a key in trusted_keys. It is a declared-guarantee gap, and it is the algorithm-agility bug class the DSSE authors cite as their reason for not profiling JWS.

2. Getting started shows verification failing (docs/getting-started.md)

The walkthrough ended on VALID and never showed a MISMATCH, so a reader never saw the check do anything. Adds a step covering both failure modes and the difference between them:

  • Edit the signed record: signature stops covering the bytes, signature_verified: false.
  • Runtime drift against an intact signature: declared-vs-actual binding fails, system_prompt: MISMATCH.

It also names the boot-time boundary explicitly and points at attest_runtime_state(), rather than leaving a reader to find the gap and read it as an omission.

Verification

Every value printed in the new docs section is copied from a real run, not written from memory.

  • pytest: 612 passed, 18 skipped; coverage 84.55% (gate 80%)
  • mypy src/agent_manifest: clean, 20 files
  • ruff check src/ tests/ --select E,F,W --ignore E501: clean
  • bandit -r src/agent_manifest: 0 findings
  • Existing vectors regenerated byte-identical; only index.json and the new AM-VEC-020.json changed

Note, not fixed here

The CLI examples in docs/getting-started.md, docs/index.md, and README.md do not run as printed. The console script maps to the cli group and every command hangs off a nested manifest group, so the real invocation is manifest manifest verify ..., not manifest verify .... tests/test_cli.py encodes the nested path, so the tests pass while the docs are wrong. Fixing it means either changing the shipped CLI surface or rewriting every documented command, which is a separate decision. The new docs section uses the Python API only, so it does not spread the bug.

imran-siddique and others added 2 commits July 26, 2026 17:05
crypto_profile is inside the signing pre-image, but spec 3.6 excludes the
whole signature block from it, so signature.algorithm could be rewritten
without disturbing the signed bytes. A manifest declaring the post-quantum
profile therefore verified VALID on a classical-only Ed25519 signature.

Cross-check the signed profile against the declared algorithm and fail
closed on a downgrade, per spec 4.2 ("prevents downgrade attacks during
the transition period"). The check runs independently of trusted_keys:
the downgrade is a property of the manifest, not of the verifier's key
material. It is one-directional, rejecting a signature weaker than the
declared profile requires and permitting a stronger one, so an issuer
dual-signing ahead of the profile flip is not flagged.

Adds conformance vector AM-VEC-020 so other-language SDKs inherit the
check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A VALID result only means something once you have seen the same manifest
come back MISMATCH. Adds a step that demonstrates the two independent
failure modes and how they differ: an edit to the signed record (the
signature stops covering the bytes) and runtime drift against an intact
signature (the declared-vs-actual binding fails).

Names the boot-time boundary rather than leaving a reader to discover it,
and points at attest_runtime_state() for freshness. Every printed value
is copied from a real run; prints .value so the comments match what the
snippets actually output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imran-siddique
imran-siddique merged commit a6fda80 into main Jul 27, 2026
14 checks passed
@imran-siddique
imran-siddique deleted the fix/crypto-profile-downgrade-check branch July 27, 2026 00:23
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