README.md
| 1 | # PQC MCP Transport |
| 2 | |
| 3 |  |
| 4 |  |
| 5 |  |
| 6 | |
| 7 | Post-quantum secured transport layer for the **Model Context Protocol (MCP)**. Every JSON-RPC message is signed with **ML-DSA** (FIPS 204) digital signatures, providing cryptographic authentication, integrity verification, and replay protection that is resistant to both classical and quantum attacks. |
| 8 | |
| 9 | ## Installation |
| 10 | |
| 11 | ```bash |
| 12 | pip install pqc-mcp-transport |
| 13 | ``` |
| 14 | |
| 15 | For development: |
| 16 | |
| 17 | ```bash |
| 18 | pip install pqc-mcp-transport[dev] |
| 19 | ``` |
| 20 | |
| 21 | ## Quick Start |
| 22 | |
| 23 | ### Server |
| 24 | |
| 25 | ```python |
| 26 | import asyncio |
| 27 | from quantumshield.identity.agent import AgentIdentity |
| 28 | from pqc_mcp_transport import PQCMCPServer |
| 29 | |
| 30 | server_identity = AgentIdentity.create("my-server") |
| 31 | server = PQCMCPServer(identity=server_identity) |
| 32 | |
| 33 | @server.tool("greet", description="Greet someone") |
| 34 | async def greet(name: str) -> str: |
| 35 | return f"Hello, {name}!" |
| 36 | |
| 37 | asyncio.run(server.run(port=8080)) |
| 38 | ``` |
| 39 | |
| 40 | ### Client |
| 41 | |
| 42 | ```python |
| 43 | import asyncio |
| 44 | from quantumshield.identity.agent import AgentIdentity |
| 45 | from pqc_mcp_transport import PQCMCPClient |
| 46 | |
| 47 | async def main(): |
| 48 | agent = AgentIdentity.create("my-client") |
| 49 | client = PQCMCPClient(identity=agent, server_url="http://localhost:8080") |
| 50 | |
| 51 | session = await client.connect() # PQC handshake |
| 52 | result = await client.call_tool("greet", {"name": "World"}) |
| 53 | print(result) # Verified response |
| 54 | await client.close() |
| 55 | |
| 56 | asyncio.run(main()) |
| 57 | ``` |
| 58 | |
| 59 | ## Architecture |
| 60 | |
| 61 | ``` |
| 62 | Client Server |
| 63 | ------ ------ |
| 64 | | | |
| 65 | | 1. HandshakeRequest (signed with ML-DSA) | |
| 66 | |---------------------------------------------->| |
| 67 | | | verify client sig |
| 68 | | 2. HandshakeResponse (signed with ML-DSA) | |
| 69 | |<----------------------------------------------| |
| 70 | | verify server sig | |
| 71 | | | |
| 72 | | === Session Established (mutual auth) === | |
| 73 | | | |
| 74 | | 3. JSON-RPC Request + _pqc envelope | |
| 75 | |---------------------------------------------->| |
| 76 | | verify sig, | check nonce, |
| 77 | | execute tool | sign response |
| 78 | | 4. JSON-RPC Response + _pqc envelope | |
| 79 | |<----------------------------------------------| |
| 80 | | verify response sig | |
| 81 | ``` |
| 82 | |
| 83 | ## Protocol Specification |
| 84 | |
| 85 | ### Handshake (Mutual Authentication) |
| 86 | |
| 87 | 1. **Client** generates a nonce, signs `{did}:{nonce}:{timestamp}` with its ML-DSA private key, and sends a `HandshakeRequest`. |
| 88 | 2. **Server** verifies the client's signature, generates its own nonce and a session ID, signs `{did}:{client_nonce}:{server_nonce}:{session_id}`, and returns a `HandshakeResponse`. |
| 89 | 3. **Client** verifies the server's signature and the echoed nonce. A `PQCSession` is created on both sides. |
| 90 | |
| 91 | ### Message Format |
| 92 | |
| 93 | Every MCP JSON-RPC message carries a `_pqc` envelope: |
| 94 | |
| 95 | ```json |
| 96 | { |
| 97 | "jsonrpc": "2.0", |
| 98 | "method": "tools/call", |
| 99 | "id": "abc123", |
| 100 | "params": { "name": "greet", "arguments": { "name": "World" } }, |
| 101 | "_pqc": { |
| 102 | "signer_did": "did:pqaid:abcdef...", |
| 103 | "algorithm": "ML-DSA-65", |
| 104 | "timestamp": "2025-01-15T10:30:00+00:00", |
| 105 | "nonce": "a1b2c3d4e5f6...", |
| 106 | "signature": "3045022100...", |
| 107 | "public_key": "302a300506..." |
| 108 | } |
| 109 | } |
| 110 | ``` |
| 111 | |
| 112 | The `_pqc` field is stripped before signing (canonical form) and before passing the message to standard MCP handlers. |
| 113 | |
| 114 | ### Signing Process |
| 115 | |
| 116 | 1. Remove `_pqc` from the message. |
| 117 | 2. Serialize with `json.dumps(msg, sort_keys=True, separators=(',', ':'))`. |
| 118 | 3. Compute `SHA3-256` hash of the canonical bytes. |
| 119 | 4. Sign the hash with ML-DSA. |
| 120 | 5. Attach the `_pqc` envelope with signature, public key, nonce, and timestamp. |
| 121 | |
| 122 | ## Security Properties |
| 123 | |
| 124 | | Property | Mechanism | |
| 125 | |---|---| |
| 126 | | **Message Authentication** | Every message is ML-DSA signed | |
| 127 | | **Mutual Authentication** | Both client and server verify each other during handshake | |
| 128 | | **Integrity** | Canonical JSON + SHA3-256 hash prevents tampering | |
| 129 | | **Replay Protection** | Per-session nonce tracking rejects duplicates | |
| 130 | | **Session Expiry** | Sessions have a configurable TTL (default: 1 hour) | |
| 131 | | **Quantum Resistance** | ML-DSA (FIPS 204) is resistant to Shor's algorithm | |
| 132 | | **Audit Trail** | Every operation is logged with signature metadata | |
| 133 | |
| 134 | ## API Reference |
| 135 | |
| 136 | ### `MessageSigner` |
| 137 | |
| 138 | | Method | Description | |
| 139 | |---|---| |
| 140 | | `canonicalize(message)` | Deterministic JSON serialization (static) | |
| 141 | | `sign_message(message)` | Add `_pqc` envelope with ML-DSA signature | |
| 142 | | `verify_message(message)` | Verify `_pqc` envelope, returns `VerificationResult` (static) | |
| 143 | | `strip_pqc(message)` | Remove `_pqc` for standard MCP processing (static) | |
| 144 | |
| 145 | ### `PQCHandshake` |
| 146 | |
| 147 | | Method | Description | |
| 148 | |---|---| |
| 149 | | `initiate(identity)` | Create a signed handshake request | |
| 150 | | `respond(request, server_identity)` | Verify client, create signed response | |
| 151 | | `complete(response, client_identity, nonce)` | Verify server, create session | |
| 152 | |
| 153 | ### `PQCSession` |
| 154 | |
| 155 | | Method | Description | |
| 156 | |---|---| |
| 157 | | `is_valid()` | Check if session has not expired | |
| 158 | | `check_nonce(nonce)` | Register nonce, raise `ReplayAttackError` on reuse | |
| 159 | | `log_operation(...)` | Record operation in audit trail | |
| 160 | | `get_audit_log()` | Return list of `AuditEntry` records | |
| 161 | |
| 162 | ### `PQCMCPClient` |
| 163 | |
| 164 | | Method | Description | |
| 165 | |---|---| |
| 166 | | `connect()` | Perform PQC handshake, return `PQCSession` | |
| 167 | | `call_tool(name, arguments)` | Send signed tool call, verify response | |
| 168 | | `list_tools()` | List available tools (signed request) | |
| 169 | | `close()` | Close connection | |
| 170 | |
| 171 | ### `PQCMCPServer` |
| 172 | |
| 173 | | Method | Description | |
| 174 | |---|---| |
| 175 | | `tool(name, description)` | Decorator to register a tool handler | |
| 176 | | `handle_request(raw_message)` | Process incoming request with PQC verification | |
| 177 | | `handle_handshake(request)` | Handle handshake initiation | |
| 178 | | `get_tool_list()` | Return registered tools | |
| 179 | | `run(host, port)` | Start HTTP server | |
| 180 | |
| 181 | ### `PQCMiddleware` |
| 182 | |
| 183 | ASGI middleware for adding PQC to existing frameworks (Starlette, FastAPI): |
| 184 | |
| 185 | ```python |
| 186 | from pqc_mcp_transport.middleware import PQCMiddleware |
| 187 | app = PQCMiddleware(app, server_identity=identity) |
| 188 | ``` |
| 189 | |
| 190 | ### Exceptions |
| 191 | |
| 192 | | Exception | When | |
| 193 | |---|---| |
| 194 | | `PQCTransportError` | Base exception | |
| 195 | | `SignatureVerificationError` | Signature did not verify | |
| 196 | | `HandshakeError` | Handshake failed | |
| 197 | | `SessionExpiredError` | Session timed out | |
| 198 | | `ReplayAttackError` | Nonce reused | |
| 199 | | `PeerNotAuthenticatedError` | No handshake completed | |
| 200 | |
| 201 | ## Examples |
| 202 | |
| 203 | See the `examples/` directory: |
| 204 | |
| 205 | - **`simple_server.py`** -- Run a PQC MCP server with signed responses |
| 206 | - **`simple_client.py`** -- Connect to a server with PQC handshake |
| 207 | - **`mutual_auth.py`** -- In-memory mutual authentication demo |
| 208 | |
| 209 | ## Development |
| 210 | |
| 211 | ```bash |
| 212 | # Install dev dependencies |
| 213 | pip install -e ".[dev]" |
| 214 | |
| 215 | # Run tests |
| 216 | pytest |
| 217 | |
| 218 | # Lint |
| 219 | ruff check src/ tests/ |
| 220 | ``` |
| 221 | |
| 222 | ## Contributing |
| 223 | |
| 224 | 1. Fork the repository |
| 225 | 2. Create a feature branch (`git checkout -b feature/my-feature`) |
| 226 | 3. Write tests for your changes |
| 227 | 4. Ensure all tests pass (`pytest`) |
| 228 | 5. Submit a pull request |
| 229 | |
| 230 | ## License |
| 231 | |
| 232 | Apache License 2.0. See [LICENSE](LICENSE) for details. |
| 233 | |