Repository navigation
feat: add provider-neutral handshake attestation - #3162
Pawan Khandavilli (pkhandavilli) wants to merge 4 commits into
Conversation
🤖 AI Agent: test-generator — `agent-mesh/src/agentmesh/identity/attestation.py`
|
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: code-reviewer — Action Items:
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.
Action Items:
| Warnings | Fine as follow-up PRs. | |
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Well-structured, security-conscious implementation of provider-neutral attestation for ADR 0010 PR4.
What's good:
- The
AttestationRequestabstraction cleanly decouples the collector interface from provider-specific fields. The singlebinding: bytesparameter is the right abstraction boundary. compute_startup_bindinguses 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_challengesreplay protection is correctly scoped.- Security audit doc is included.
Minor notes:
- In
MockAttestationCollector.collect(),report_data_hashis set tobinding_hashrather than an ADR 0010 canonical hash. This is fine for mocks since theMockAttestationCollectoris test-only, but a comment would help clarify it is intentionally non-canonical. - The change from
asyncio.TimeoutErrortoTimeoutErrorininitiate()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.
|
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 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. |
🤖 AI Agent: security-scanner — View details
No security issues found. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
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
require_tee_bound_keyis bypassable:claims.key_originis copied from peer-suppliedevidence.key_origin(attestation_verifier.py:103); the gate athandshake.py:870-874trusts attacker-controlled data.- The Layer-2 signature covers only
evidence.evidencebytes (handshake.py:849-855,907-917).key_origin,runtime_measurements,secure_boot_verified, andexpires_atride unsigned and feed trust decisions. AttestationEvidence.expires_atis 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._used_attestation_challengesis unbounded and the read athandshake.py:818-819and write at :876 straddle theawaitat :863 with no lock (TOCTOU + memory growth).create_challenge()(handshake.py:886-888) writes_pending_challengeswithout_challenges_lockand without the_max_pending_challengescap.- 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). evidence.is_expired()is never checked at the handshake layer.
Should fix
MockAttestationVerifier/MockSKRKeyStoreare exported from public__all__(identity/__init__.py:109,121) with no production guard.respond()defaultsverifier_didto""(handshake.py:594-596) but verify usesself.agent_did(:851); silent verification failure for real responders.- Use
hmac.compare_digestfor digest equality (attestation.py:170,278;handshake.py:829,846). TrustBridge(bridge.py:101-105) constructsTrustHandshakewith 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.
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
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_originis copied from peer-supplied evidence intoAttestationClaimsand then used as a trust gate, creating a bypass forrequire_tee_bound_key- The Layer 2 signature covers only the raw
evidencebytes, leavingkey_origin,runtime_measurements,secure_boot_verified, andexpires_atunsigned but trust-decision-bearing _used_attestation_challengesis unbounded and the read-check/write sequence straddles anawaitwithout a lockcreate_challenge()writes_pending_challengesoutside_challenges_lockand without the_max_pending_challengescap- Per-call
require_attestation=False/require_tee_bound_key=Falseoverrides the constructor policy instead of enforcing a one-way ratchet evidence.is_expired()is not checked at the handshake layer
Suggested path forward
- Split style churn into a separate PR
- Add server-side max-age clamp to
expires_atat the handshake verification layer - Cover
key_origin,runtime_measurements,secure_boot_verified, andexpires_atwith the Layer 2 signature - Fix the TOCTOU and unbounded growth issues in the challenge replay set with a lock
- Make attestation policy settings (require_attestation, require_tee_bound_key) one-way ratchets taking the max of constructor and per-call values
- Call
evidence.is_expired()during handshake attestation verification - Fix
report_data_hashinMockAttestationCollectoror add a clear comment that the field is intentionally non-canonical for the provider-neutral path
adbe953 to
46a4087
Compare
|
Addressed the PR4 changes-requested feedback and force-updated the branch to a single clean commit on current Fixes included:
Validation:
|
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
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:
identity/__init__.py:35,38,75:MockAttestationCollector/MockAttestationVerifier/MockSKRKeyStoreare still imported but no longer re-exported. If nothing in the package referencesagentmesh.identity.<Mock*>, these are unused imports adjacent to this PR's own edit. Drop them (tests can import from the defining module directly).attestation_verifier.py:90,110: the max-age clamp anchors on peer-suppliedevidence.timestamp. It is integrity-protected by the Layer-2 signature but not TEE-bound, so on the startup-binding path a peer can settimestamp≈nowfor stale evidence. Either anchor on a verifier-sideverified_at, or document that startup-binding freshness rests on the Layer-2 nonce only.trust/handshake.py:978-984:_length_prefixed_utf8duplicates the helper inidentity/attestation.pywith65535hardcoded instead ofMAX_LENGTH_PREFIX_VALUE. Consolidate to one.trust/handshake.py:433-437:_trim_used_attestation_challenges_lockedis O(n) per insert at cap viamin()over the dict. Fine at cap=1000; note it or switch toOrderedDictif 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>
46a4087 to
6b28307
Compare
|
Addressed the latest four follow-up items and force-updated the branch to Changes:
Validation:
|
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
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_inputbindsagent_did,verifier_did,challenge_id,challenge.nonce, a SHA-256 of the full canonical evidence, a SHA-256 of the attestation public key, andkey_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()requiresverifier_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 clampsexpires_attoverified_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_originfrom its own config (self._key_origin), not from peer evidence (test_mock_verifier_does_not_copy_peer_supplied_key_origin), and rejects a response whoseattestation_key_origindisagrees with the verified claim. require_attestation/require_tee_bound_keyare OR-combined constructor+per-call, so a constructor requirement can't be weakened per call (both..._cannot_be_weakened_per_calltests). Missing evidence/signature/public-key/key-origin all fail closed in required mode.- Startup-binding path is protected by a model invariant (
binding_hashrequired whenchallenge_id/nonceabsent), and the binding is recomputed fromresponse.agent_did+ evidence public-key hash and compared withhmac.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.
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
AttestationRequestand 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. |
| 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}" |
| 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 |
| 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: |
There was a problem hiding this comment.
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_evidencebut omitsattestation_key_originwill 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 whenrequire_attestationorrequire_tee_bound_keyis 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 fromagentmesh.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>
|
Addressed the latest Copilot review in
Post-rebase validation:
|
There was a problem hiding this comment.
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 MockAttestationVerifierin 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 MockAttestationVerifierand 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:
|
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. |
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
agent-governance-python/agent-mesh/src/agentmesh/identity/attestation.pyagent-governance-python/agent-mesh/src/agentmesh/identity/attestation_collector.pyagent-governance-python/agent-mesh/src/agentmesh/trust/handshake.pyagent-governance-python/agent-mesh/src/agentmesh/identity/__init__.pyagent-governance-python/agent-mesh/tests/test_attestation*.pyagent-governance-python/agent-mesh/tests/test_handshake_attestation.pyagent-governance-python/agent-mesh/tests/snapshots/handshake_response.jsondocs/security/audits/2026-05-29-trust-handshake-attestation.mdTesting
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.pypytest 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.pypython scripts\docs\check_frontmatter.py