Skip to content

fix(security): bind cnf.jwk fingerprint to TEE report_data (CRYPTO-001) - #259

Merged
imran-siddique merged 1 commit into
mainfrom
fix/crypto-001-tee-bind-public-key
Jun 8, 2026
Merged

imran-siddique merged 1 commit into
mainfrom
fix/crypto-001-tee-bind-public-key

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Summary

  • Fixes CRYPTO-001: _verify_signature() accepted any Ed25519 keypair because the verifier used the public key from the claim being verified, with no check that the key was hardware-attested. An attacker could generate their own keypair, sign a fraudulent claim, embed their public key in cnf.jwk, and verification passed.
  • Gateway (startup.py): constructs the TEE nonce as SHA-256(public_key_bytes) || random_salt(32) so the hardware attestation report binds the specific keypair via report_data[:32].
  • Verifier (cmcp_verify/verify.py): new _verify_key_binding() extracts trace.runtime.nonce, takes first 32 bytes, and compares to SHA-256(cnf.jwk.x). Mismatch sets failure_reason = PUBLIC_KEY_NOT_BOUND unconditionally (higher-priority than signature failure). Software-only/Level-0 logs a warning and skips, consistent with other Level-0 behaviors.

Test plan

  • test_tee_key_binding_happy_path: valid key with matching report_data fingerprint passes
  • test_tee_key_binding_attack_path_mismatched_fingerprint: attacker re-signs with fresh key, nonce encodes gateway fingerprint, fails with PUBLIC_KEY_NOT_BOUND
  • test_tee_key_binding_absent_nonce_fails: hardware claim with no nonce fails with PUBLIC_KEY_NOT_BOUND
  • test_tee_key_binding_software_only_exempt: software-only provider exempt, no penalty
  • All 23 existing tests in test_verify.py pass
  • Full unit suite: 486 pass, 2 pre-existing benchmark failures unrelated to this change

Closes #140

🤖 Generated with Claude Code

…re verification (CRYPTO-001)

The verifier previously accepted any Ed25519 keypair because it extracted
the public key from the claim it was verifying with no check that the key
was hardware-attested. An attacker could generate a fresh keypair, sign a
fraudulent TRACE Claim, embed their own public key in cnf.jwk, and
_verify_signature() would return sig_ok=True.

Fix (gateway): startup.py now constructs the TEE attestation nonce as
SHA-256(public_key_bytes) || random_salt(32 bytes). The TEE hardware commits
this into the signed report_data field, binding the keypair to the attestation
report at instance startup.

Fix (verifier): new _verify_key_binding() in cmcp_verify/verify.py extracts
trace.runtime.nonce (base64url), decodes it, takes the first 32 bytes, and
compares to SHA-256(cnf.jwk.x public key bytes). A mismatch sets
failure_reason = PUBLIC_KEY_NOT_BOUND regardless of Ed25519 result, since a
substituted key means the signing key cannot be trusted. Software-only/Level-0
mode logs a warning and skips the check, consistent with other Level-0 behaviors.

The out-of-band trusted_public_key_hex cross-check is preserved as a separate
"trusted_public_key" verified field for callers that pin the key externally.

Closes #140

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@imran-siddique
imran-siddique merged commit 53f1987 into main Jun 8, 2026
1 of 8 checks passed
@imran-siddique
imran-siddique deleted the fix/crypto-001-tee-bind-public-key branch July 29, 2026 23:19
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.

CRITICAL: Signature verification uses self-embedded public key — no TEE binding (CRYPTO-001)

1 participant