Skip to content

feat: add provider-neutral handshake attestation - #3162

Closed
Pawan Khandavilli (pkhandavilli) wants to merge 4 commits into
microsoft:mainfrom
pkhandavilli:akhandavilli-microsoft/implement-pr-4
Closed

Pawan Khandavilli (pkhandavilli) wants to merge 4 commits into
microsoft:mainfrom
pkhandavilli:akhandavilli-microsoft/implement-pr-4

Conversation

@pkhandavilli

Copy link
Copy Markdown
Contributor

Summary

Adds provider-neutral confidential-computing attestation to AgentMesh trust handshakes for ADR 0010 PR4. The change binds startup evidence to the agent DID and TEE public key, then requires fresh Layer 2 transcript signatures during handshake verification.

Problem

AgentMesh could perform Ed25519 trust handshakes, but it did not have a provider-neutral path to request, carry, bind, and verify confidential-computing attestation evidence during the handshake.

Changes

File What changed
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py Adds provider-neutral attestation request/binding models and helpers.
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation_collector.py Migrates collectors to the opaque attestation request model.
agent-governance-python/agent-mesh/src/agentmesh/trust/handshake.py Integrates optional/required attestation into challenge-response verification, TEE key signing, replay protection, and cache keys.
agent-governance-python/agent-mesh/src/agentmesh/identity/__init__.py Exports the new attestation request and binding helpers.
agent-governance-python/agent-mesh/tests/test_attestation*.py Adds provider-neutral binding and collector regression coverage.
agent-governance-python/agent-mesh/tests/test_handshake_attestation.py Adds handshake allow/deny coverage for attestation, key origin, tampering, replay, binding mismatch, and cache separation.
agent-governance-python/agent-mesh/tests/snapshots/handshake_response.json Updates the handshake response snapshot for optional attestation fields.
docs/security/audits/2026-05-29-trust-handshake-attestation.md Documents the PR4 security review and residual provider-integration scope.

Testing

  • ruff check src\agentmesh\identity\__init__.py src\agentmesh\identity\attestation.py src\agentmesh\identity\attestation_collector.py src\agentmesh\trust\handshake.py tests\test_attestation.py tests\test_attestation_collector.py tests\test_handshake_attestation.py
  • pytest tests\test_attestation.py tests\test_attestation_collector.py tests\test_handshake_attestation.py tests\test_handshake_timeout.py tests\test_handshake_security.py tests\test_handshake_e2e.py -q (73 passed, 5 warnings)
  • python scripts\docs\check_links.py
  • python scripts\docs\check_frontmatter.py

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests agent-mesh agent-mesh package security Security-related issues size/XL Extra large PR (500+ lines) labels Jun 23, 2026
@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `agent-mesh/src/agentmesh/identity/attestation.py`

AI-generated review output. Treat it as untrusted analysis and verify before acting.

agent-mesh/src/agentmesh/identity/attestation.py

  • test_attestation_request_validation -- Validate that AttestationRequest enforces constraints on binding, agent_did, and public_key_hash.
  • test_compute_startup_binding -- Verify correct computation of compute_startup_binding with valid and invalid inputs.
  • test_canonical_attestation_evidence_bytes -- Ensure canonical_attestation_evidence_bytes produces deterministic serialization for various AttestationEvidence inputs.
  • test_attestation_evidence_validation -- Test validation logic for AttestationEvidence, including binding_hash and legacy binding fields.

agent-mesh/src/agentmesh/trust/handshake.py

  • test_handshake_with_attestation -- Validate handshake flow with valid attestation evidence.
  • test_handshake_rejects_invalid_binding -- Ensure handshake rejects mismatched or tampered binding hashes.
  • test_handshake_replay_protection -- Test that replayed attestation evidence is correctly rejected.

@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

AI-generated review output. Treat it as untrusted analysis and verify before acting.

API Compatibility

Severity Change Impact
High AttestationEvidence class: agent_did, challenge_id, nonce, and public_key_hash fields are now optional instead of required. Existing code relying on these fields being mandatory may break if they are not provided.
High AttestationEvidence class: New validation logic for legacy binding requires agent_did, challenge_id, nonce, and public_key_hash to be present if challenge_id or nonce is provided. Existing code using legacy binding may fail if these fields are not provided.
High AttestationEvidence class: New binding_hash field added, which is required when challenge_id and nonce are absent. Existing code that does not provide binding_hash in such cases will break.
Medium Removed MockAttestationCollector and MockAttestationVerifier from exports. Code relying on these mocks will break.
Medium Removed MockSKRKeyStore from exports. Code relying on this mock will break.

@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — Action Items:

AI-generated review output. Treat it as untrusted analysis and verify before acting.

TL;DR: 0 blockers, 2 warnings. The PR introduces a provider-neutral attestation mechanism for AgentMesh trust handshakes, with appropriate tests and documentation. However, there are some areas for improvement.

# Sev Issue Where
1 Warn Potential lack of validation for metadata and provider_context fields in AttestationRequest. attestation.py
2 Warn The MAX_EVIDENCE_AGE_SECONDS constant is defined but not used, which may indicate incomplete implementation or dead code. attestation.py

Action Items:

  1. Ensure that metadata and provider_context fields in AttestationRequest are validated for potential misuse or unexpected input.
  2. Review the purpose of MAX_EVIDENCE_AGE_SECONDS and either integrate it into the logic or remove it if unnecessary.

| Warnings | Fine as follow-up PRs. |

@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

AI-generated review output. Treat it as untrusted analysis and verify before acting.

Docs Sync

  • AttestationRequest in agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py -- missing docstring for new public API.
  • compute_binding_hash(binding: bytes) in agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py -- missing docstring for new public API.
  • compute_startup_binding(agent_did: str, public_key_hash: bytes | str) in agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py -- missing docstring for new public API.
  • compute_startup_binding_hash(agent_did: str, public_key_hash: bytes | str) in agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py -- missing docstring for new public API.
  • canonical_attestation_evidence_bytes(evidence: AttestationEvidence) in agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py -- missing docstring for new public API.
  • README.md -- no updates found for the new provider-neutral handshake attestation feature.
  • CHANGELOG.md -- missing entry for the addition of the provider-neutral handshake attestation feature.

@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured, security-conscious implementation of provider-neutral attestation for ADR 0010 PR4.

What's good:

  • The AttestationRequest abstraction cleanly decouples the collector interface from provider-specific fields. The single binding: bytes parameter is the right abstraction boundary.
  • compute_startup_binding uses a proper domain separator (agentmesh-attest-v1-startup) and length-prefixed UTF-8 encoding for the DID, which prevents prefix-collision attacks.
  • Cache key separation by (require_attestation, require_tee_bound_key, verifier_identity, reference_values_fingerprint) is correct: a cache hit from an unattestation-required path must not satisfy an attestation-required path.
  • _used_attestation_challenges replay protection is correctly scoped.
  • Security audit doc is included.

Minor notes:

  • In MockAttestationCollector.collect(), report_data_hash is set to binding_hash rather than an ADR 0010 canonical hash. This is fine for mocks since the MockAttestationCollector is test-only, but a comment would help clarify it is intentionally non-canonical.
  • The change from asyncio.TimeoutError to TimeoutError in initiate() is correct on Python 3.11+ (they are the same type), but worth a brief comment if the project still runs on 3.10 in any target environment.

No blocking issues. Approved.

@arian-gogani

Copy link
Copy Markdown
Contributor

the provider-neutral attestation shape is clean — binding startup evidence to the agent DID and TEE public key, then requiring fresh Layer 2 transcript signatures at handshake.

one thing worth naming explicitly for the audit trail downstream: this proves the session started with the right attestation evidence. it doesn't yet prove each action within that attested session stayed within authorized scope.

the per-action receipt layer sits below the handshake: after the attested session is established, each tool call within it produces a signed receipt with action_ref = SHA-256(JCS({agent_id, action_type, scope, timestamp_ms})). the receipt is signed by the same agent key that participated in the handshake — so a verifier can check: (1) the session was TEE-attested, (2) this specific action came from the attested agent, (3) the action stayed within the authorized scope.

the receipt chain is what lets a regulator or auditor reconstruct the full picture without the operator being present. the handshake attestation is the session-level anchor; the receipts are the action-level evidence. the demos repo issue #10 sketches how these compose in demo-03.

@github-actions

github-actions Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

No security issues found.

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Head adbe9532 is a merge of main only; no source change since the prior review. All seven prior findings stand and three more surfaced on a deeper read.

Blocking

  1. require_tee_bound_key is bypassable: claims.key_origin is copied from peer-supplied evidence.key_origin (attestation_verifier.py:103); the gate at handshake.py:870-874 trusts attacker-controlled data.
  2. The Layer-2 signature covers only evidence.evidence bytes (handshake.py:849-855,907-917). key_origin, runtime_measurements, secure_boot_verified, and expires_at ride unsigned and feed trust decisions.
  3. AttestationEvidence.expires_at is peer-supplied with no server-side max-age clamp (attestation.py:197-199,261-264). A peer can set 2099 and stale startup evidence never expires.
  4. _used_attestation_challenges is unbounded and the read at handshake.py:818-819 and write at :876 straddle the await at :863 with no lock (TOCTOU + memory growth).
  5. create_challenge() (handshake.py:886-888) writes _pending_challenges without _challenges_lock and without the _max_pending_challenges cap.
  6. Per-call require_attestation=False / require_tee_bound_key=False (handshake.py:420-425, :751-756) overrides the constructor policy. Make this a one-way ratchet (max of constructor and call).
  7. evidence.is_expired() is never checked at the handshake layer.

Should fix

  • MockAttestationVerifier / MockSKRKeyStore are exported from public __all__ (identity/__init__.py:109,121) with no production guard.
  • respond() defaults verifier_did to "" (handshake.py:594-596) but verify uses self.agent_did (:851); silent verification failure for real responders.
  • Use hmac.compare_digest for digest equality (attestation.py:170,278; handshake.py:829,846).
  • TrustBridge (bridge.py:101-105) constructs TrustHandshake with no attestation params, so the feature is unreachable from the public entrypoint.

Bloat
About 40% of the handshake.py diff is unrelated style churn (Optional[X] to X | None, timezone.utc to UTC, line reflow) plus an unrelated monotonic-timestamp shim (handshake.py:47,929-936). Please split into a separate cleanup PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AttestationRequest abstraction and startup-binding construction are well-designed, and the domain separator plus length-prefixed encoding in compute_startup_binding is correct. However, MohammadHaroonAbuomar's blocking findings are valid, and I can confirm several of them independently from the diff.

Blocking issues I can independently verify

1. expires_at has no server-side max-age clamp

In the updated AttestationEvidence, expires_at remains purely peer-supplied. The only constraint is expires_at > timestamp. A peer can set this to 2099, meaning stale startup evidence never expires. The verifier must clamp expires_at to timestamp + server_max_ttl rather than accepting whatever the peer supplies.

2. report_data_hash is repurposed in MockAttestationCollector

In attestation_collector.py, the new collect() sets:

binding_hash = compute_binding_hash(request.binding)
return AttestationEvidence(
    ...
    report_data_hash=binding_hash,   # ADR 0010 canonical hash field
    binding_hash=binding_hash,        # provider-neutral field
    ...
)

report_data_hash is defined as the "ADR 0010 canonical report-data hash". Assigning binding_hash (SHA-256 of raw binding bytes) to it silently corrupts tests that exercise the ADR 0010 hash path. Even if the non-legacy model-validator path does not check it, downstream code receiving AttestationEvidence from the mock will silently get wrong semantics from report_data_hash. The field should either carry a correct ADR 0010 hash or be documented to be intentionally non-canonical for the non-legacy path, with a comment explaining why.

3. Style churn mixed into functional PR

The diff mixes Optional[X] to X | None conversions, timezone.utc to UTC imports, and import block reordering throughout handshake.py alongside the functional attestation changes. This makes the functional diff harder to review for security properties and inflates the diff size. These should be in a separate cleanup PR.

Concurring with MohammadHaroonAbuomar's blocking list

His findings at handshake.py lines 870-874, 849-855, 818-819, 886-888, 420-425, and 751-756 reference the PR branch's version of handshake.py which I cannot read directly from main. Given the specificity of the line references and the internal consistency of the analysis, the following are credible blockers that should be addressed before merge:

  • key_origin is copied from peer-supplied evidence into AttestationClaims and then used as a trust gate, creating a bypass for require_tee_bound_key
  • The Layer 2 signature covers only the raw evidence bytes, leaving key_origin, runtime_measurements, secure_boot_verified, and expires_at unsigned but trust-decision-bearing
  • _used_attestation_challenges is unbounded and the read-check/write sequence straddles an await without a lock
  • create_challenge() writes _pending_challenges outside _challenges_lock and without the _max_pending_challenges cap
  • Per-call require_attestation=False / require_tee_bound_key=False overrides the constructor policy instead of enforcing a one-way ratchet
  • evidence.is_expired() is not checked at the handshake layer

Suggested path forward

  1. Split style churn into a separate PR
  2. Add server-side max-age clamp to expires_at at the handshake verification layer
  3. Cover key_origin, runtime_measurements, secure_boot_verified, and expires_at with the Layer 2 signature
  4. Fix the TOCTOU and unbounded growth issues in the challenge replay set with a lock
  5. Make attestation policy settings (require_attestation, require_tee_bound_key) one-way ratchets taking the max of constructor and per-call values
  6. Call evidence.is_expired() during handshake attestation verification
  7. Fix report_data_hash in MockAttestationCollector or add a clear comment that the field is intentionally non-canonical for the provider-neutral path

@pkhandavilli
Pawan Khandavilli (pkhandavilli) force-pushed the akhandavilli-microsoft/implement-pr-4 branch from adbe953 to 46a4087 Compare July 1, 2026 14:32
@pkhandavilli

Copy link
Copy Markdown
Contributor Author

Addressed the PR4 changes-requested feedback and force-updated the branch to a single clean commit on current upstream/main (46a40874).

Fixes included:

  • Signed canonical attestation evidence plus response public-key hash and key-origin metadata, so trust-affecting evidence fields cannot be tampered after signature.
  • Made mock verifier claims provider/server-controlled instead of copying peer-supplied key_origin, and added verifier-side max evidence age with expiry clamping.
  • Made handshake attestation fail closed for expired evidence, missing verifier DID, weakened per-call policy attempts, and missing/signed key-origin metadata.
  • Reserved replay keys before verifier awaits and bounded replay/challenge state under locks.
  • Wired attestation options through TrustBridge, removed test mocks from public __all__, and documented mock collector binding semantics.
  • Reduced the unrelated handshake style churn called out in review.

Validation:

  • pytest tests\test_attestation.py tests\test_attestation_collector.py tests\test_attestation_verifier.py tests\test_handshake_attestation.py tests\test_trust_bridge_attestation.py -q => 65 passed.
  • pytest tests\test_handshake_timeout.py tests\test_handshake_security.py tests\test_handshake_e2e.py tests\test_coverage_boost.py::TestTrustBridge tests\test_coverage_boost.py::TestProtocolBridge -q => 49 passed.
  • ruff check --select E,F,W,I --ignore E501 <changed PR4 files> => all checks passed.

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8/9 prior asks addressed: key_origin now verifier-derived, Layer-2 transcript covers the full canonical evidence, max_evidence_age_seconds clamp, replay set locked and bounded, create_challenge locked and capped, one-way ratchet on require_*, Mock* out of __all__, is_expired() checked, style churn dropped.

Remaining before this is approvable:

  1. identity/__init__.py:35,38,75: MockAttestationCollector / MockAttestationVerifier / MockSKRKeyStore are still imported but no longer re-exported. If nothing in the package references agentmesh.identity.<Mock*>, these are unused imports adjacent to this PR's own edit. Drop them (tests can import from the defining module directly).
  2. attestation_verifier.py:90,110: the max-age clamp anchors on peer-supplied evidence.timestamp. It is integrity-protected by the Layer-2 signature but not TEE-bound, so on the startup-binding path a peer can set timestamp≈now for stale evidence. Either anchor on a verifier-side verified_at, or document that startup-binding freshness rests on the Layer-2 nonce only.
  3. trust/handshake.py:978-984: _length_prefixed_utf8 duplicates the helper in identity/attestation.py with 65535 hardcoded instead of MAX_LENGTH_PREFIX_VALUE. Consolidate to one.
  4. trust/handshake.py:433-437: _trim_used_attestation_challenges_locked is O(n) per insert at cap via min() over the dict. Fine at cap=1000; note it or switch to OrderedDict if the cap grows.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Pawan Khandavilli <107432976+pkhandavilli@users.noreply.github.com>
@pkhandavilli

Copy link
Copy Markdown
Contributor Author

Addressed the latest four follow-up items and force-updated the branch to 6b28307c on current upstream/main (58de5fee).

Changes:

  • Dropped the remaining MockAttestationCollector, MockAttestationVerifier, and MockSKRKeyStore imports from agentmesh.identity.__init__; tests still import mocks from their defining modules.
  • Changed MockAttestationVerifier claim expiry to clamp from verifier-side verified_at instead of peer-supplied evidence.timestamp, with regression assertions updated to lock that behavior.
  • Removed the duplicate trust.handshake._length_prefixed_utf8 helper and reused the identity attestation helper so the length limit stays centralized.
  • Added an inline note that replay-trim's O(n) oldest-entry scan is bounded by _max_used_attestation_challenges, with guidance to switch to OrderedDict if the cap grows substantially.

Validation:

  • pytest tests\test_attestation.py tests\test_attestation_collector.py tests\test_attestation_verifier.py tests\test_handshake_attestation.py tests\test_trust_bridge_attestation.py -q => 65 passed.
  • pytest tests\test_handshake_timeout.py tests\test_handshake_security.py tests\test_handshake_e2e.py tests\test_coverage_boost.py::TestTrustBridge tests\test_coverage_boost.py::TestProtocolBridge -q => 49 passed.
  • ruff check --select E,F,W,I --ignore E501 <changed PR4 files> => all checks passed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the provider-neutral handshake attestation (ADR 0010 PR4) with a focus on soundness, freshness, and downgrade resistance. This is careful work and the security properties hold.

Transcript / binding soundness

  • compute_layer2_signature_input binds agent_did, verifier_did, challenge_id, challenge.nonce, a SHA-256 of the full canonical evidence, a SHA-256 of the attestation public key, and key_origin — all length-prefixed. Token-swapping is prevented (evidence hash is in the signed transcript) and evidence-field tampering breaks the signature (test_attestation_trust_field_tampering_is_rejected).
  • The signature is over the verifier's fresh challenge nonce, and respond() requires verifier_did, so a signature is bound to a specific verifier and challenge — no cross-verifier or cross-challenge replay.

Freshness / replay

  • is_expired() is checked at the handshake layer, and the verifier clamps expires_at to verified_at + min(max_evidence_age, ttl) so a peer-supplied far-future expiry can't widen the window (test_peer_supplied_far_future_expiry_is_clamped_by_verifier_policy).
  • Replay is reserved atomically on (agent_did, challenge_id, nonce); failed verification forgets the reservation, successful reuse is rejected, and the set is bounded — covered by the sequential, concurrent, and bounded-memory tests.

Downgrade resistance

  • The verifier derives key_origin from its own config (self._key_origin), not from peer evidence (test_mock_verifier_does_not_copy_peer_supplied_key_origin), and rejects a response whose attestation_key_origin disagrees with the verified claim.
  • require_attestation / require_tee_bound_key are OR-combined constructor+per-call, so a constructor requirement can't be weakened per call (both ..._cannot_be_weakened_per_call tests). Missing evidence/signature/public-key/key-origin all fail closed in required mode.
  • Startup-binding path is protected by a model invariant (binding_hash required when challenge_id/nonce absent), and the binding is recomputed from response.agent_did + evidence public-key hash and compared with hmac.compare_digest. Public-key-hash and binding comparisons are all constant-time.

Cache keys correctly separate attestation requirement tiers so a non-attested cache hit can't satisfy a required-attestation request. Full CI matrix is green (agent-mesh 3.11–3.13, integrations, security). Threat-model doc is a welcome addition. LGTM.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blocking security issues are resolved at head: key_origin is verifier-derived, the full evidence is under the Layer-2 signature, expiry is clamped, replay is locked and bounded, and the require_* flags ratchet one-way. Before merge, resolve the four residual items, in particular anchoring the max-age clamp on a verifier-side timestamp (or documenting that startup-binding freshness rests solely on the nonce), and drop the unused Mock imports.

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor:

  • tests: no assertions for report_data_hash validation, replay-after-eviction, key_origin mismatch, future timestamps, or empty-signer policy enforcement.

public_key_hash=evidence.public_key_hash,
):
return "Attestation evidence binding mismatch"
elif evidence.binding_hash is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the startup-binding path never validates report_data_hash: only the self-declared binding_hash is recomputed, nothing links the two, and AttestationVerifier.verify receives no expected-binding input. Empirically: evidence with garbage report_data_hash passes require_attestation=True + require_tee_bound_key=True. Latent evidence-swap forgery (same canonical-binding class as #3508) that materializes with the first real provider verifier. Enforce report_data_hash == binding_hash in the startup path or add expected-binding to the verifier interface.

async with self._attestation_replay_lock:
self._used_attestation_challenges.pop(replay_key, None)

def _trim_used_attestation_challenges_locked(self) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handshake.py:433 _trim_used_attestation_challenges_locked: the replay cache evicts oldest-first when full (cap 1000) instead of failing closed, so a replayed challenge is accepted after flooding (verified at cap=3). The audit doc advertises single-use attested challenges; reject reservation when full or expire by challenge TTL.

await self._forget_attestation_challenge(replay_key)
return f"Attestation verification failed: {exc}"

if response.attestation_key_origin != claims.key_origin:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: evidence.key_origin is never cross-checked against claims/response key_origin (handshake.py:926); future-dated evidence bypasses the max-age check (attestation_verifier.py:90); ReferenceValues defaults are permissive with silent policy no-ops (SIGNING_IDENTITY with empty signers passes; STABLE_CLAIMS unimplemented). Fast-follows.

@MohammadHaroonAbuomar MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor:

  • tests: no assertions for report_data_hash validation, replay-after-eviction, key_origin mismatch, future timestamps, or empty-signer policy enforcement.

public_key_hash=evidence.public_key_hash,
):
return "Attestation evidence binding mismatch"
elif evidence.binding_hash is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the startup-binding path never validates report_data_hash: only the self-declared binding_hash is recomputed, nothing links the two, and AttestationVerifier.verify receives no expected-binding input. Empirically: evidence with garbage report_data_hash passes require_attestation=True + require_tee_bound_key=True. Latent evidence-swap forgery (same canonical-binding class as #3508) that materializes with the first real provider verifier. Enforce report_data_hash == binding_hash in the startup path or add expected-binding to the verifier interface.

async with self._attestation_replay_lock:
self._used_attestation_challenges.pop(replay_key, None)

def _trim_used_attestation_challenges_locked(self) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handshake.py:433 _trim_used_attestation_challenges_locked: the replay cache evicts oldest-first when full (cap 1000) instead of failing closed, so a replayed challenge is accepted after flooding (verified at cap=3). The audit doc advertises single-use attested challenges; reject reservation when full or expire by challenge TTL.

await self._forget_attestation_challenge(replay_key)
return f"Attestation verification failed: {exc}"

if response.attestation_key_origin != claims.key_origin:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: evidence.key_origin is never cross-checked against claims/response key_origin (handshake.py:926); future-dated evidence bypasses the max-age check (attestation_verifier.py:90); ReferenceValues defaults are permissive with silent policy no-ops (SIGNING_IDENTITY with empty signers passes; STABLE_CLAIMS unimplemented). Fast-follows.

@prayagupa Prayag (prayagupa) left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provider-neutral shape is useful and CI is green, but two security blockers remain: startup verification trusts binding_hash without proving provider report_data_hash carries the expected DID/key binding, and replay-cache eviction makes an old signed challenge valid again after saturation.

Please validate the expected binding at the provider-verifier boundary, retain replay protection for the challenge lifetime (or fail closed at capacity), and add regressions for mismatched report_data_hash and replay after saturation.

Enforce provider-bound report data, fail-closed replay retention, evidence freshness, key-origin agreement, and reference-policy validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3a43e738-2fe6-4700-aa4f-752ad7b7abfd
Signed-off-by: Pawan Khandavilli <107432976+pkhandavilli@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds provider-neutral confidential-computing attestation support to AgentMesh’s Ed25519 trust handshake flow (Python), including evidence binding, Layer 2 transcript signing, replay protection, cache key separation, and expanded test coverage.

TL;DR: 1 blocker, 2 warnings. Fix #1 and this ships.

# Sev Issue Where
1 Block Optional mode accepts malformed/partial attestation fields when attestation_evidence is present (silent downgrade) agentmesh/trust/handshake.py
2 Warn InvalidSignature errors often stringify to empty text → unhelpful rejection reason agentmesh/trust/handshake.py
3 Warn Cache lookup fallback is unreachable dead code (peer DID key never stored) agentmesh/trust/handshake.py

Changes:

  • Introduces provider-neutral AttestationRequest and startup binding helpers, updates evidence canonicalization and reference-value validation.
  • Extends trust handshake to optionally/strictly verify attestation evidence + Layer 2 transcript signatures with replay protection.
  • Adds/updates tests and snapshots to cover attestation allow/deny, tampering, replay, and cache separation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/security/audits/2026-05-29-trust-handshake-attestation.md Adds a security audit note documenting threat model and mitigations for attested handshakes.
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py Adds provider-neutral request/binding helpers and updates attestation evidence model/validation.
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation_collector.py Migrates collectors to accept AttestationRequest(binding=...) and produces binding-hash-based mock evidence.
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation_verifier.py Extends verifier API with expected_report_data_hash and tightens mock verifier freshness/binding checks.
agent-governance-python/agent-mesh/src/agentmesh/identity/init.py Exports new attestation helpers/types via the identity package surface.
agent-governance-python/agent-mesh/src/agentmesh/trust/handshake.py Implements attestation-aware handshake response fields, Layer 2 transcript signing, verification, replay protection, and cache separation.
agent-governance-python/agent-mesh/src/agentmesh/trust/bridge.py Threads attestation requirements/config through TrustBridge → TrustHandshake.
agent-governance-python/agent-mesh/tests/test_attestation.py Updates evidence/binding tests and adds coverage for new request + deterministic canonicalization.
agent-governance-python/agent-mesh/tests/test_attestation_collector.py Updates collector tests to use AttestationRequest and adds opaque binding coverage.
agent-governance-python/agent-mesh/tests/test_attestation_verifier.py Adds tests for expected report-data mismatch and future-dated evidence rejection.
agent-governance-python/agent-mesh/tests/test_handshake_attestation.py New end-to-end handshake tests for required/optional attestation, tampering, replay, and cache separation.
agent-governance-python/agent-mesh/tests/test_trust_bridge_attestation.py Adds TrustBridge coverage validating requirements are enforced through handshake.
agent-governance-python/agent-mesh/tests/snapshots/handshake_response.json Updates snapshot to include new optional attestation fields.

Comment on lines +926 to +930
try:
signature = base64.b64decode(response.attestation_signature)
Ed25519PublicKey.from_public_bytes(public_key_bytes).verify(signature, transcript)
except (InvalidSignature, ValueError, TypeError) as exc:
return f"Attestation signature verification failed: {exc}"
Comment on lines +866 to +877
if self.attestation_verifier is None:
if require_attestation or require_tee_bound_key:
return "Attestation verifier required but not configured"
return None
if not response.attestation_signature:
if require_attestation or require_tee_bound_key:
return "Attestation signature required but missing"
return None
if not response.attestation_public_key:
if require_attestation or require_tee_bound_key:
return "Attestation public key required but missing"
return None
Comment on lines +358 to +361
lookup_keys: list[Any] = [cache_key]
if not require_attestation and not require_tee_bound_key:
lookup_keys.append(peer_did)
for lookup_key in lookup_keys:
Copilot AI review requested due to automatic review settings August 1, 2026 00:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

agent-governance-python/agent-mesh/src/agentmesh/trust/handshake.py:879

  • In optional-attestation mode, a peer that includes attestation_evidence but omits attestation_key_origin will currently fail the entire handshake. For consistency with the other optional attestation fields (attestation_signature / attestation_public_key), this should only be a hard failure when require_attestation or require_tee_bound_key is enabled; otherwise the verifier should ignore the incomplete attestation block and proceed with the legacy handshake result.
        if not response.attestation_signature:
            if require_attestation or require_tee_bound_key:
                return "Attestation signature required but missing"
            return None
        if not response.attestation_public_key:
            if require_attestation or require_tee_bound_key:
                return "Attestation public key required but missing"
            return None
        if response.attestation_key_origin is None:
            return "Attestation key origin required but missing"

agent-governance-python/agent-mesh/src/agentmesh/identity/init.py:37

  • agentmesh.identity.__init__ no longer re-exports the test/mocking helpers (MockAttestationCollector, MockAttestationVerifier, MockSKRKeyStore) that previously appeared to be part of the public identity surface. If external callers import these from agentmesh.identity, this becomes a breaking change that isn’t called out in the PR description. Consider re-exporting them (or keeping deprecated aliases) to avoid unexpected import breaks.
from .attestation_collector import (
    AttestationCollector,
    NoopAttestationCollector,
)
from .attestation_verifier import AttestationVerifier
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3a43e738-2fe6-4700-aa4f-752ad7b7abfd
Signed-off-by: Pawan Khandavilli <107432976+pkhandavilli@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 01:46
@pkhandavilli

Copy link
Copy Markdown
Contributor Author

Addressed the latest Copilot review in a3423e04, rebased onto the maintainer-updated branch:

  • Presented attestation evidence now fails closed when the verifier, Layer 2 signature, or attestation public key is missing, while optional mode still accepts responses with no evidence.
  • InvalidSignature now returns a stable, useful rejection reason.
  • Removed the unreachable peer-DID cache fallback and aligned the legacy cache-expiry regression with the production tuple cache key.
  • Updated the security audit to document the optional-attestation bundle invariant.

Post-rebase validation:

  • Focused attestation, handshake, TrustBridge, and coverage suite: 382 passed.
  • Scoped Ruff: passed.
  • Docs links: 0 new broken links.
  • Diff integrity: passed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (7)

agent-governance-python/agent-mesh/src/agentmesh/trust/handshake.py:507

  • Catching bare TimeoutError here can incorrectly classify unrelated TimeoutError exceptions thrown inside the handshake (e.g., from dependencies) as a handshake timeout. Since this code uses asyncio.wait_for(), it should catch asyncio.TimeoutError specifically.
        except TimeoutError:

agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.py:193

  • The AttestationEvidence.public_key_hash description still refers to the agent Ed25519 identity key, but this field is compared against the Layer 2 attestation public key in handshake verification. This mismatch can confuse provider integrations and reviewers.
    public_key_hash: str | None = Field(
        None,
        description="Lowercase hex SHA-256 hash of the agent Ed25519 public key",
    )

agent-governance-python/agent-mesh/src/agentmesh/identity/init.py:37

  • This module no longer re-exports MockAttestationCollector/MockAttestationVerifier, which can be a breaking change for users relying on from agentmesh.identity import MockAttestationVerifier in tests/examples. If the intent is not to break imports, re-export the mocks here (even if deprecated).
from .attestation_collector import (
    AttestationCollector,
    NoopAttestationCollector,
)
from .attestation_verifier import AttestationVerifier

agent-governance-python/agent-mesh/src/agentmesh/identity/init.py:76

  • MockSKRKeyStore is no longer re-exported from agentmesh.identity, which can break existing imports used in tests/examples. If this is unintended, re-export it alongside the other TEE keystore helpers.
from .tee_keystore import (
    LocalTEEKeyStore,
    SoftwareKeyHandle,
    TEEKeyHandle,
    TEEKeyStore,

agent-governance-python/agent-mesh/src/agentmesh/identity/init.py:112

  • If MockSKRKeyStore is intended to remain part of the public identity surface, it should also be restored in all to keep star-imports and tooling behavior stable.
    "TEEKeyStore",
    "TEEKeyHandle",
    "SoftwareKeyHandle",
    "LocalTEEKeyStore",
    "require_tee_bound_key",

agent-governance-python/agent-mesh/src/agentmesh/identity/init.py:122

  • If the mock attestation collector/verifier are meant to remain part of the public identity surface, they should be included in all to avoid breaking from agentmesh.identity import MockAttestationVerifier and similar imports.
    "ReferenceValues",
    "AttestationCollector",
    "NoopAttestationCollector",
    "AttestationVerifier",

agent-governance-python/agent-mesh/tests/test_attestation.py:87

  • This test name no longer matches what it asserts (it now checks missing binding_hash, not a mismatched report_data_hash), which makes failures harder to interpret.
    def test_rejects_mismatched_report_data_hash(self) -> None:

@MohammadHaroonAbuomar

Copy link
Copy Markdown
Collaborator

Closing as stale: this has carried open review requests since June without a resolution, and the attestation design has moved on since. Feel free to reopen with a rebase onto current main if you want to pick it back up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-mesh agent-mesh package documentation Improvements or additions to documentation security Security-related issues size/XL Extra large PR (500+ lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants