Skip to content

Commit e77f0fa

Browse files
feat(e2e): EncryptedTrustBridge — handshake-gated encrypted channels (microsoft#1238)
* docs: refresh proposals index — close stale, add fresh submissions - Replaced 14 closed pre-release proposals with 6 fresh submissions (v3.2.0) - New active: CoSAI WS4 microsoft#86, OWASP ASI #12, CrewAI #5562, AutoGen #7613, Google ADK #5418, MCP Servers #3988 - Removed stale awesome-list PRs and misc integration issues - Cleaned up summary table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(e2e): EncryptedTrustBridge — handshake-gated encrypted channels (microsoft#1226) Integrates TrustHandshake with SecureChannel so agents must pass trust verification before an encrypted channel is established. New files: - agentmesh/encryption/bridge.py — EncryptedTrustBridge - open_secure_channel(): verify peer then X3DH + Double Ratchet - accept_secure_channel(): responder side - publish_prekey_bundle(): distribute pre-keys - Session management: get/close/close_all - Agent DIDs bound as associated data - tests/test_encrypted_bridge.py — 10 tests covering: - Full open/accept/exchange flow - 5-turn bidirectional conversation - Session close with key cleanup - Multi-session management (close all) - Pre-key bundle publishing with/without OTK - Associated data binding Closes microsoft#1226 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b0b5b73 commit e77f0fa

2 files changed

Lines changed: 394 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Encrypted trust bridge — TrustHandshake + SecureChannel integration.
4+
5+
Extends TrustBridge.verify_peer() to optionally establish an E2E
6+
encrypted SecureChannel after successful authentication. The trust
7+
handshake authenticates the peer (Ed25519 challenge-response + trust
8+
score check), then X3DH + Double Ratchet provide forward-secret
9+
encrypted messaging.
10+
11+
Usage:
12+
bridge = EncryptedTrustBridge(
13+
agent_did="did:mesh:alice",
14+
key_manager=alice_key_manager,
15+
)
16+
channel = await bridge.open_secure_channel("did:mesh:bob", bob_bundle)
17+
ciphertext = channel.send(b"governed action")
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import logging
23+
from dataclasses import dataclass, field
24+
from typing import Optional
25+
26+
from agentmesh.encryption.channel import ChannelEstablishment, SecureChannel
27+
from agentmesh.encryption.x3dh import InMemoryPreKeyStore, PreKeyBundle, PreKeyStore, X3DHKeyManager
28+
from agentmesh.trust.bridge import TrustBridge
29+
from agentmesh.trust.handshake import HandshakeResult
30+
31+
logger = logging.getLogger(__name__)
32+
33+
34+
@dataclass
35+
class EncryptedPeerSession:
36+
"""An active encrypted session with a verified peer."""
37+
38+
peer_did: str
39+
channel: SecureChannel
40+
handshake_result: HandshakeResult
41+
establishment: ChannelEstablishment | None = None
42+
43+
44+
class EncryptedTrustBridge:
45+
"""Trust bridge that gates encrypted channels on successful handshake.
46+
47+
Agents must pass the trust handshake (identity verification + trust
48+
score threshold) before an encrypted channel is established. Peers
49+
that fail the handshake never reach the key exchange step.
50+
"""
51+
52+
def __init__(
53+
self,
54+
agent_did: str,
55+
key_manager: X3DHKeyManager,
56+
trust_bridge: TrustBridge | None = None,
57+
prekey_store: PreKeyStore | None = None,
58+
min_trust_score: int = 700,
59+
) -> None:
60+
"""Initialize the encrypted trust bridge.
61+
62+
Args:
63+
agent_did: This agent's DID.
64+
key_manager: X3DH key manager with this agent's identity keys.
65+
trust_bridge: Optional existing TrustBridge instance. If None,
66+
a new one is created.
67+
prekey_store: Pre-key storage backend. Defaults to in-memory.
68+
min_trust_score: Minimum trust score required to open a channel.
69+
"""
70+
self._agent_did = agent_did
71+
self._key_manager = key_manager
72+
self._bridge = trust_bridge or TrustBridge(agent_did=agent_did)
73+
self._prekey_store = prekey_store or InMemoryPreKeyStore()
74+
self._min_trust_score = min_trust_score
75+
self._sessions: dict[str, EncryptedPeerSession] = {}
76+
77+
# Ensure we have a signed pre-key for receiving channels
78+
if key_manager.signed_pre_key is None:
79+
key_manager.generate_signed_pre_key()
80+
81+
@property
82+
def agent_did(self) -> str:
83+
return self._agent_did
84+
85+
@property
86+
def active_sessions(self) -> dict[str, EncryptedPeerSession]:
87+
"""Return all active encrypted sessions, keyed by peer DID."""
88+
return dict(self._sessions)
89+
90+
def publish_prekey_bundle(self, include_otk: bool = True) -> PreKeyBundle:
91+
"""Generate and publish this agent's pre-key bundle.
92+
93+
Args:
94+
include_otk: Whether to include a one-time pre-key.
95+
96+
Returns:
97+
The public PreKeyBundle for distribution to peers.
98+
"""
99+
otk_id = None
100+
if include_otk:
101+
otks = self._key_manager.generate_one_time_pre_keys(1)
102+
otk_id = otks[0].key_id
103+
104+
bundle = self._key_manager.get_public_bundle(otk_id=otk_id)
105+
self._prekey_store.store_bundle(self._agent_did, bundle)
106+
return bundle
107+
108+
async def open_secure_channel(
109+
self,
110+
peer_did: str,
111+
peer_bundle: PreKeyBundle,
112+
required_trust_score: int | None = None,
113+
skip_handshake: bool = False,
114+
) -> SecureChannel:
115+
"""Open an E2E encrypted channel to a peer after trust verification.
116+
117+
The flow is:
118+
1. Run TrustHandshake (Ed25519 challenge-response + trust check)
119+
2. If verified, run X3DH key agreement
120+
3. Initialize Double Ratchet from shared secret
121+
4. Return SecureChannel ready for send/receive
122+
123+
Args:
124+
peer_did: The peer agent's DID.
125+
peer_bundle: The peer's published pre-key bundle.
126+
required_trust_score: Override the default trust threshold.
127+
skip_handshake: If True, skip trust verification (for testing
128+
or pre-verified peers only).
129+
130+
Returns:
131+
A SecureChannel for encrypted communication.
132+
133+
Raises:
134+
PermissionError: If the peer fails trust verification.
135+
"""
136+
threshold = required_trust_score or self._min_trust_score
137+
138+
# Step 1: Trust verification
139+
if not skip_handshake:
140+
result = await self._bridge.verify_peer(
141+
peer_did=peer_did,
142+
required_trust_score=threshold,
143+
)
144+
if not result.verified:
145+
raise PermissionError(
146+
f"Peer {peer_did} failed trust verification: {result.rejection_reason}"
147+
)
148+
handshake_result = result
149+
else:
150+
handshake_result = HandshakeResult(
151+
verified=True,
152+
peer_did=peer_did,
153+
trust_score=threshold,
154+
trust_level="trusted",
155+
)
156+
157+
# Step 2: X3DH + Double Ratchet via SecureChannel
158+
ad = f"{self._agent_did}|{peer_did}".encode()
159+
channel, establishment = SecureChannel.create_sender(
160+
self._key_manager, peer_bundle, associated_data=ad
161+
)
162+
163+
session = EncryptedPeerSession(
164+
peer_did=peer_did,
165+
channel=channel,
166+
handshake_result=handshake_result,
167+
establishment=establishment,
168+
)
169+
self._sessions[peer_did] = session
170+
171+
logger.info(
172+
"Opened encrypted channel to %s (trust=%d, level=%s)",
173+
peer_did,
174+
handshake_result.trust_score,
175+
handshake_result.trust_level,
176+
)
177+
return channel
178+
179+
def accept_secure_channel(
180+
self,
181+
peer_did: str,
182+
establishment: ChannelEstablishment,
183+
handshake_result: HandshakeResult | None = None,
184+
) -> SecureChannel:
185+
"""Accept an incoming encrypted channel from a verified peer.
186+
187+
Args:
188+
peer_did: The initiating peer's DID.
189+
establishment: The ChannelEstablishment from the initiator.
190+
handshake_result: Optional handshake result if already verified.
191+
192+
Returns:
193+
A SecureChannel for encrypted communication.
194+
"""
195+
ad = f"{peer_did}|{self._agent_did}".encode()
196+
channel = SecureChannel.create_receiver(
197+
self._key_manager, establishment, associated_data=ad
198+
)
199+
200+
session = EncryptedPeerSession(
201+
peer_did=peer_did,
202+
channel=channel,
203+
handshake_result=handshake_result or HandshakeResult(
204+
verified=True, peer_did=peer_did,
205+
),
206+
)
207+
self._sessions[peer_did] = session
208+
209+
logger.info("Accepted encrypted channel from %s", peer_did)
210+
return channel
211+
212+
def get_session(self, peer_did: str) -> EncryptedPeerSession | None:
213+
"""Get an active session with a peer."""
214+
return self._sessions.get(peer_did)
215+
216+
def close_session(self, peer_did: str) -> bool:
217+
"""Close an encrypted session and clear key material.
218+
219+
Returns:
220+
True if a session existed and was closed.
221+
"""
222+
session = self._sessions.pop(peer_did, None)
223+
if session is None:
224+
return False
225+
session.channel.close()
226+
logger.info("Closed encrypted session with %s", peer_did)
227+
return True
228+
229+
def close_all_sessions(self) -> int:
230+
"""Close all active sessions. Returns the number closed."""
231+
count = len(self._sessions)
232+
for peer_did in list(self._sessions.keys()):
233+
self.close_session(peer_did)
234+
return count
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Tests for EncryptedTrustBridge — handshake + encrypted channel integration."""
4+
5+
import pytest
6+
from nacl.signing import SigningKey
7+
8+
from agentmesh.encryption.bridge import EncryptedPeerSession, EncryptedTrustBridge
9+
from agentmesh.encryption.channel import SecureChannel
10+
from agentmesh.encryption.x3dh import X3DHKeyManager
11+
12+
13+
def _make_manager() -> X3DHKeyManager:
14+
sk = SigningKey.generate()
15+
return X3DHKeyManager.from_ed25519_keys(
16+
bytes(sk) + bytes(sk.verify_key), bytes(sk.verify_key)
17+
)
18+
19+
20+
def _make_bridge(did: str) -> tuple[EncryptedTrustBridge, X3DHKeyManager]:
21+
mgr = _make_manager()
22+
bridge = EncryptedTrustBridge(agent_did=did, key_manager=mgr, min_trust_score=500)
23+
return bridge, mgr
24+
25+
26+
class TestEncryptedTrustBridge:
27+
@pytest.mark.asyncio
28+
async def test_open_and_accept_channel(self):
29+
"""Full flow: Alice opens channel, Bob accepts, they exchange messages."""
30+
alice_bridge, alice_mgr = _make_bridge("did:mesh:alice")
31+
bob_bridge, bob_mgr = _make_bridge("did:mesh:bob")
32+
33+
bob_bundle = bob_bridge.publish_prekey_bundle()
34+
35+
alice_ch = await alice_bridge.open_secure_channel(
36+
"did:mesh:bob", bob_bundle, skip_handshake=True
37+
)
38+
alice_session = alice_bridge.get_session("did:mesh:bob")
39+
assert alice_session is not None
40+
41+
bob_ch = bob_bridge.accept_secure_channel(
42+
"did:mesh:alice", alice_session.establishment
43+
)
44+
45+
enc = alice_ch.send(b"hello bob")
46+
assert bob_ch.receive(enc) == b"hello bob"
47+
48+
enc2 = bob_ch.send(b"hello alice")
49+
assert alice_ch.receive(enc2) == b"hello alice"
50+
51+
@pytest.mark.asyncio
52+
async def test_bidirectional_conversation(self):
53+
"""Multi-turn conversation through encrypted bridge."""
54+
alice_bridge, _ = _make_bridge("did:mesh:alice")
55+
bob_bridge, _ = _make_bridge("did:mesh:bob")
56+
57+
bob_bundle = bob_bridge.publish_prekey_bundle()
58+
alice_ch = await alice_bridge.open_secure_channel(
59+
"did:mesh:bob", bob_bundle, skip_handshake=True
60+
)
61+
bob_ch = bob_bridge.accept_secure_channel(
62+
"did:mesh:alice", alice_bridge.get_session("did:mesh:bob").establishment
63+
)
64+
65+
for i in range(5):
66+
enc = alice_ch.send(f"alice-{i}".encode())
67+
assert bob_ch.receive(enc) == f"alice-{i}".encode()
68+
69+
enc = bob_ch.send(f"bob-{i}".encode())
70+
assert alice_ch.receive(enc) == f"bob-{i}".encode()
71+
72+
@pytest.mark.asyncio
73+
async def test_close_session(self):
74+
alice_bridge, _ = _make_bridge("did:mesh:alice")
75+
bob_bridge, _ = _make_bridge("did:mesh:bob")
76+
77+
bob_bundle = bob_bridge.publish_prekey_bundle()
78+
alice_ch = await alice_bridge.open_secure_channel(
79+
"did:mesh:bob", bob_bundle, skip_handshake=True
80+
)
81+
82+
assert alice_bridge.close_session("did:mesh:bob") is True
83+
assert alice_bridge.get_session("did:mesh:bob") is None
84+
assert alice_ch.is_closed is True
85+
86+
@pytest.mark.asyncio
87+
async def test_close_nonexistent_session(self):
88+
bridge, _ = _make_bridge("did:mesh:alice")
89+
assert bridge.close_session("did:mesh:unknown") is False
90+
91+
@pytest.mark.asyncio
92+
async def test_close_all_sessions(self):
93+
alice_bridge, _ = _make_bridge("did:mesh:alice")
94+
bob_bridge, _ = _make_bridge("did:mesh:bob")
95+
carol_bridge, _ = _make_bridge("did:mesh:carol")
96+
97+
bob_bundle = bob_bridge.publish_prekey_bundle()
98+
carol_bundle = carol_bridge.publish_prekey_bundle()
99+
100+
await alice_bridge.open_secure_channel(
101+
"did:mesh:bob", bob_bundle, skip_handshake=True
102+
)
103+
await alice_bridge.open_secure_channel(
104+
"did:mesh:carol", carol_bundle, skip_handshake=True
105+
)
106+
107+
assert len(alice_bridge.active_sessions) == 2
108+
closed = alice_bridge.close_all_sessions()
109+
assert closed == 2
110+
assert len(alice_bridge.active_sessions) == 0
111+
112+
@pytest.mark.asyncio
113+
async def test_publish_prekey_bundle(self):
114+
bridge, mgr = _make_bridge("did:mesh:alice")
115+
bundle = bridge.publish_prekey_bundle(include_otk=True)
116+
assert len(bundle.identity_key) == 32
117+
assert len(bundle.signed_pre_key) == 32
118+
assert bundle.one_time_pre_key is not None
119+
120+
@pytest.mark.asyncio
121+
async def test_publish_prekey_bundle_no_otk(self):
122+
bridge, _ = _make_bridge("did:mesh:alice")
123+
bundle = bridge.publish_prekey_bundle(include_otk=False)
124+
assert bundle.one_time_pre_key is None
125+
126+
@pytest.mark.asyncio
127+
async def test_active_sessions_property(self):
128+
bridge, _ = _make_bridge("did:mesh:alice")
129+
assert len(bridge.active_sessions) == 0
130+
131+
bob_bridge, _ = _make_bridge("did:mesh:bob")
132+
bob_bundle = bob_bridge.publish_prekey_bundle()
133+
await bridge.open_secure_channel("did:mesh:bob", bob_bundle, skip_handshake=True)
134+
135+
sessions = bridge.active_sessions
136+
assert "did:mesh:bob" in sessions
137+
assert isinstance(sessions["did:mesh:bob"], EncryptedPeerSession)
138+
139+
@pytest.mark.asyncio
140+
async def test_agent_did_property(self):
141+
bridge, _ = _make_bridge("did:mesh:test-agent")
142+
assert bridge.agent_did == "did:mesh:test-agent"
143+
144+
@pytest.mark.asyncio
145+
async def test_channel_with_associated_data(self):
146+
"""Channels bind agent DIDs as associated data."""
147+
alice_bridge, _ = _make_bridge("did:mesh:alice")
148+
bob_bridge, _ = _make_bridge("did:mesh:bob")
149+
150+
bob_bundle = bob_bridge.publish_prekey_bundle()
151+
alice_ch = await alice_bridge.open_secure_channel(
152+
"did:mesh:bob", bob_bundle, skip_handshake=True
153+
)
154+
bob_ch = bob_bridge.accept_secure_channel(
155+
"did:mesh:alice", alice_bridge.get_session("did:mesh:bob").establishment
156+
)
157+
158+
# Messages work because AD matches
159+
enc = alice_ch.send(b"test")
160+
assert bob_ch.receive(enc) == b"test"

0 commit comments

Comments
 (0)