Skip to content

fix(verify): return UNVERIFIABLE when an algorithm is unavailable; amend ADR-0005 - #245

Merged
imran-siddique merged 1 commit into
mainfrom
fix/unsupported-algorithm-unverifiable
Jul 27, 2026
Merged

fix(verify): return UNVERIFIABLE when an algorithm is unavailable; amend ADR-0005#245
imran-siddique merged 1 commit into
mainfrom
fix/unsupported-algorithm-unverifiable

Conversation

@imran-siddique

Copy link
Copy Markdown
Contributor

Started as the ADR-0005 reconciliation flagged on #243. Verifying the three divergences turned up a live bug behind the third one.

The bug

pyoqs is an optional extra (pip install "agent-manifest[pq]"), so on a default install the post-quantum verifiers are unavailable. Any manifest declaring ML-DSA-65 or hybrid-Ed25519-ML-DSA-65 reached _require_oqs(), which raised RuntimeError, and the engine caught only InvalidSignature and ValueError. Reproduced before fixing:

pyoqs available: False
UNHANDLED RuntimeError : ML-DSA-65 requires pyoqs. Install with: pip install "agent-manifest[pq]"

A manifest is untrusted input, so verify_manifest() owes the caller a verdict. Instead the verification endpoint would answer 500 to anything an attacker sent, in the default install, with no key material or valid signature required to trigger it.

The fix, and why UNVERIFIABLE

_require_oqs() now raises AlgorithmUnavailableError, a RuntimeError subclass so anything already catching RuntimeError is unaffected. The engine catches it, records the reason as a warning, and leaves signature_verified false so the existing fail-closed chain yields UNVERIFIABLE.

Not MISMATCH. The verifier could not appraise the signature at all, so it has established nothing about a manifest that may be entirely valid; MISMATCH would assert a defect it never observed. This is the same distinction §5.2 already draws for a verifier lacking key material. After the fix:

ML-DSA-65                 -> UNVERIFIABLE  verified=False  warn=True
hybrid-Ed25519-ML-DSA-65  -> UNVERIFIABLE  verified=False  warn=True

An algorithm identifier outside the registry stays a MISMATCH. Worth noting I had assumed that came from the verifier's unknown-algorithm branch; it does not. The schema enum rejects it first as schema:signature.algorithm, which is stricter than I expected, and the test asserts the real mechanism.

ADR-0005 amendment

Three statements in ADR-0005 did not match what shipped. Original text preserved per the repo's ADR immutability rule, corrections appended as an amendment following the ADR-0006 precedent:

  1. Two profile values, not three. ADR-0005 lists standard, post_quantum, hybrid. Spec §3.1/§4.2 and CryptoProfile define standard and post-quantum, hyphenated. Hybrid is a signature algorithm, not a profile, which is the better factoring: the profile states the posture claimed, the algorithm states how it was met, so one profile can admit both a pure and a hybrid signature mid-transition.
  2. Post-quantum is Level 3, not "Level 2 and above." §8.1 places ML-DSA-65, ML-KEM-768, and SHAKE-256 at Level 3.
  3. UNVERIFIABLE, not INCOMPATIBLE_VERSION. The instinct was right and the mechanism wrong twice over: INCOMPATIBLE_VERSION is reserved for unsupported specification versions (§2.4), so reusing it leaves a relying party unable to tell "I don't understand this format" from "I understand it but can't run this primitive"; and "raise" is not something a verifier should do to untrusted input. The one thing it got exactly right, that silent pass is not permitted, is preserved.

§4.2 now states the requirement normatively, including that INCOMPATIBLE_VERSION must not be used for an unsupported algorithm.

Verification

  • Bug reproduced before the fix and re-tested after, on both the ML-DSA and hybrid paths, the latter with a well-formed combined key so it reaches the capability check rather than failing key-length validation first.
  • 3 new tests: the ML-DSA gap, the hybrid gap, and the unavailable-versus-unknown distinction.
  • 621 passed, 18 skipped; coverage 84.63% (gate 80%); mypy, ruff, bandit clean.

No conformance vector for this one: the expected result depends on which extras the verifier has installed, so it is not portable across implementations the way the AM-VEC-* contract requires.

pyoqs is an optional extra, so on a default install any manifest
declaring ML-DSA-65 or hybrid reached _require_oqs() and crashed
verify_manifest() with an uncaught RuntimeError. A manifest is untrusted
input, so a verification endpoint answered 500 to anything an attacker
sent rather than returning a verdict.

_require_oqs() now raises AlgorithmUnavailableError, a RuntimeError
subclass so existing callers are unaffected. The engine catches it,
records the reason as a warning, and leaves signature_verified false so
the fail-closed chain yields UNVERIFIABLE.

UNVERIFIABLE rather than MISMATCH is the point: the verifier has
established nothing about a manifest that may be entirely valid, so
reporting a defect it never observed would be wrong. An algorithm outside
the registry stays a MISMATCH, rejected by the schema enum before
verification runs.

Spec 4.2 says this normatively, including that INCOMPATIBLE_VERSION is
reserved for unsupported specification versions and must not be used
here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imran-siddique
imran-siddique merged commit 90cb08b into main Jul 27, 2026
14 checks passed
@imran-siddique
imran-siddique deleted the fix/unsupported-algorithm-unverifiable branch July 27, 2026 16:45
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