Skip to content

fix(attestation)!: bind caller offers to their role and require proof of possession, release 0.4.0 - #221

Merged
imran-siddique merged 3 commits into
mainfrom
fix/caller-attestation-relay
Oct 1, 2026
Merged

imran-siddique merged 3 commits into
mainfrom
fix/caller-attestation-relay

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Fixes GHSA-xxj2-p57x-x954.

A callee that sets require_caller_attestation: hardware can be made to record a caller with no TEE as caller_attestation="hardware".

The callee appraises the caller's caller_offer, an X25519 channel key plus a hardware report bound to the callee's challenge, but never asks the caller to prove it holds that channel key. The only link is caller_channel_key inside the holder proof, and the delegate signs that itself. Any ca2a node also attests its own channel key under whatever nonce reaches its public /.well-known/ca2a/channel?nonce= endpoint, and caller and callee offers used the same binding prefix.

The attack:

  1. The delegate gets challenge C from target T.
  2. It asks an honest hardware peer V for /channel?nonce=C.
  3. It presents V's offer as its own caller_offer.

T accepts, and the provenance record says hardware. Replaying an offer seen on the wire inside the challenge TTL works the same way. Reproduced on 8113abd with a simulated hardware provider and a correct verifier: accepted; caller_attestation = hardware.

Fix (0.4.0, breaking for callers that send caller_offer):

  • Role separation. Every offer carries attestation.role, which selects the prefix the hardware signs. Caller offers use ca2a-{tpm,snp,tdx}-caller-v2|; callee prefixes stay v1, so handshakes and archived captures still verify. /channel only mints callee offers, and a callee only accepts caller offers.
  • Proof of possession. With every caller_offer the caller sends caller_possession, {"version": "ca2a-caller-offer-v2", "mac"}: an HMAC-SHA256 over the challenge, both channel keys, the credential, capability, record, payload digest and holder proof signature, keyed by HKDF over X25519 between the attested caller key and the callee key. The callee recomputes it with its own channel key.

A relayed or replayed offer now fails on role or on possession. Pre-v2 callers get a clear error naming ca2a-caller-offer-v2. Callers that send no offer are unaffected.

Affected: ca2a-runtime 0.2.0 to 0.3.1. CWE-294 (authentication bypass by capture-replay).

Also releases 0.4.0: pyproject.toml, the release-artifact test, index.md and the CHANGELOG section.

Generated with Claude Code

imran-siddique and others added 2 commits September 30, 2026 13:40
… of possession

A delegate with no enclave could be recorded as caller_attestation="hardware"
by relaying an honest hardware peer's attestation. It fetched challenge C from
target T, asked peer V's public /.well-known/ca2a/channel endpoint to attest
under C, and presented V's offer as its caller_offer. T appraised the report but
never asked the presenter to prove it held the attested channel key, and callee
and caller offers were signed over the same binding.

Role separation: every offer carries a role (attestation.role on the wire) that
selects the binding prefix the hardware signs. Callee offers keep the v1
prefixes, so handshakes and archived captures still verify; caller offers use
ca2a-{tpm,snp,tdx}-caller-v2|. PeerNode.offer, and so /channel, only mints
callee offers. A callee accepts only caller offers, a caller only callee offers.

Proof of possession: with every caller_offer the caller sends
caller_possession {"version": "ca2a-caller-offer-v2", "mac"}, an HMAC-SHA256
over the JCS transcript (challenge, both channel keys, credential_id, subject,
requested_capability, record_id, payload digest, parent_record_hash, holder
proof signature) keyed by HKDF-SHA256 over X25519 between the caller's attested
channel key and the callee's. The callee recomputes it and compares in constant
time before recording any outcome. Missing or invalid proof is refused at every
rung whenever an offer is present.

BREAKING CHANGE: callers that send caller_offer must upgrade. A pre-v2 offer has
no role and no caller_possession and is refused with ATTESTATION_FAILED naming
ca2a-caller-offer-v2. Callers that send no offer and handshake responses are
unchanged on the wire. BaseProvider.attest and the binding helpers take a
keyword role (default callee); custom verifiers must derive the binding under
report.role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pip-audit flags CVE-2026-97687, CVE-2026-97688 and CVE-2026-97689 in
urllib3 2.7.0. The 2.8.0 hashes are the ones requirements/docs.txt already
pins and match PyPI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 92.56757% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ca2a_runtime/attestation.py 91.04% 6 Missing ⚠️
src/ca2a_runtime/peer.py 83.33% 2 Missing ⚠️
src/ca2a_runtime/tee/tpm.py 71.42% 2 Missing ⚠️
src/ca2a_runtime/hardware_acceptance.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@imran-siddique
imran-siddique merged commit 19407fb into main Oct 1, 2026
14 checks passed
@imran-siddique
imran-siddique deleted the fix/caller-attestation-relay branch October 1, 2026 04:15
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.

2 participants