mcps-secure 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ Business Source License 1.1
2
+
3
+ Licensor: CyberSecAI Ltd
4
+ Licensed Work: mcp-secure v1.0.0 and later
5
+ Change Date: 2030-03-31
6
+ Change License: Apache License, Version 2.0
7
+
8
+ Additional Use Grant:
9
+
10
+ You may use the Licensed Work for non-commercial purposes, including:
11
+ - Personal security research and learning
12
+ - Academic and educational use
13
+ - Internal security scanning within your organisation
14
+ - Contributing to the Licensed Work
15
+
16
+ For commercial use (including offering the Licensed Work as a
17
+ hosted service, incorporating it into a commercial product, or
18
+ reselling it), you must obtain a commercial license from the
19
+ Licensor. Contact: contact@agentsign.dev
20
+
21
+ Terms
22
+
23
+ The Licensor hereby grants you the right to copy, modify, create
24
+ derivative works, redistribute, and make non-production use of the
25
+ Licensed Work. The Licensor may make an Additional Use Grant, above,
26
+ permitting limited production use.
27
+
28
+ Effective on the Change Date, or the fourth anniversary of the first
29
+ publicly available distribution of a specific version of the Licensed
30
+ Work under this License, whichever comes first, the Licensor hereby
31
+ grants you rights under the terms of the Change License, and the
32
+ rights granted in the paragraph above terminate.
33
+
34
+ If your use of the Licensed Work does not comply with the requirements
35
+ currently in effect as described in this License, you must purchase a
36
+ commercial license from the Licensor, its affiliated entities, or
37
+ authorized resellers, or you must refrain from using the Licensed Work.
38
+
39
+ All copies of the original and modified Licensed Work, and derivative
40
+ works of the Licensed Work, are subject to this License. This License
41
+ applies separately for each version of the Licensed Work and the
42
+ Change Date may vary for each version of the Licensed Work released
43
+ by the Licensor.
44
+
45
+ You must conspicuously display this License on each original or
46
+ modified copy of the Licensed Work. If you receive the Licensed Work
47
+ in original or modified form from a third party, the terms and
48
+ conditions set forth in this License apply to your use of that work.
49
+
50
+ Any use of the Licensed Work in violation of this License will
51
+ automatically terminate your rights under this License for the
52
+ current and all other versions of the Licensed Work.
53
+
54
+ This License does not grant you any right in any trademark or logo of
55
+ the Licensor or its affiliates (provided that you may use a trademark
56
+ or logo of the Licensor as expressly required by this License).
57
+
58
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS
59
+ PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL
60
+ WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT
61
+ LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
62
+ PURPOSE, NON-INFRINGEMENT, AND TITLE.
63
+
64
+ CyberSecAI Ltd
65
+ cybersecai.co.uk
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcps-secure
3
+ Version: 1.0.0
4
+ Summary: MCPS -- MCP Secure. Cryptographic identity, message signing, and trust verification for the Model Context Protocol.
5
+ Author: CyberSecAI Ltd
6
+ License: BUSL-1.1
7
+ Project-URL: Homepage, https://mcp-secure.dev
8
+ Project-URL: Repository, https://github.com/razashariff/mcps
9
+ Project-URL: Documentation, https://mcp-secure.dev
10
+ Keywords: mcp,mcps,mcp-secure,model-context-protocol,agent-security,agent-identity,passport,message-signing,ecdsa,trust,revocation,owasp,agentsign,secure-mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: cryptography>=41.0.0
26
+ Dynamic: license-file
27
+
28
+ # MCPS -- MCP Secure
29
+
30
+ **Cryptographic identity, message signing, and trust verification for the Model Context Protocol.**
31
+
32
+ The HTTPS of the agent era. MCP becomes MCPS.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install mcp-secure
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from mcp_secure import generate_key_pair, create_passport, sign_passport, verify_passport_signature
44
+ from mcp_secure import sign_message, verify_message, sign_tool, verify_tool
45
+
46
+ # Generate keys
47
+ keys = generate_key_pair()
48
+
49
+ # Create and sign a passport
50
+ passport = create_passport(
51
+ name="my-agent",
52
+ version="1.0.0",
53
+ public_key=keys["public_key"],
54
+ capabilities=["read", "write"],
55
+ )
56
+
57
+ # Trust Authority signs the passport
58
+ ta_keys = generate_key_pair()
59
+ signed = sign_passport(passport, ta_keys["private_key"])
60
+ assert verify_passport_signature(signed, ta_keys["public_key"])
61
+
62
+ # Sign MCP messages
63
+ envelope = sign_message(
64
+ {"jsonrpc": "2.0", "method": "tools/list", "id": 1},
65
+ signed["passport_id"],
66
+ keys["private_key"],
67
+ )
68
+
69
+ # Verify
70
+ result = verify_message(envelope, keys["public_key"])
71
+ assert result["valid"]
72
+
73
+ # Tool integrity
74
+ tool = {"name": "read_file", "description": "Read a file", "inputSchema": {"type": "object"}}
75
+ sig = sign_tool(tool, keys["private_key"])
76
+ assert verify_tool(tool, sig, keys["public_key"])
77
+ ```
78
+
79
+ ## What MCPS Adds to MCP
80
+
81
+ | Feature | Description |
82
+ |---------|-------------|
83
+ | Agent Passports | ECDSA P-256 signed identity credentials |
84
+ | Message Signing | Every JSON-RPC message wrapped in signed envelope |
85
+ | Tool Integrity | Signed tool definitions prevent poisoning |
86
+ | Replay Protection | Nonce + 5-min timestamp window |
87
+ | Revocation | Real-time passport revocation via Trust Authority |
88
+ | Trust Levels | L0 (unsigned) to L4 (audited) |
89
+
90
+ ## OWASP MCP Top 10
91
+
92
+ Mitigates 8/10 OWASP MCP vulnerabilities: tool poisoning, supply chain attacks, auth bypass, shadow servers, and more.
93
+
94
+ ## Links
95
+
96
+ - **Website**: [mcp-secure.dev](https://mcp-secure.dev)
97
+ - **npm**: [mcp-secure](https://www.npmjs.com/package/mcp-secure)
98
+ - **Spec**: [SPEC.md](https://github.com/razashariff/mcps/blob/main/SPEC.md)
99
+
100
+ ## License
101
+
102
+ MIT -- CyberSecAI Ltd
@@ -0,0 +1,75 @@
1
+ # MCPS -- MCP Secure
2
+
3
+ **Cryptographic identity, message signing, and trust verification for the Model Context Protocol.**
4
+
5
+ The HTTPS of the agent era. MCP becomes MCPS.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install mcp-secure
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from mcp_secure import generate_key_pair, create_passport, sign_passport, verify_passport_signature
17
+ from mcp_secure import sign_message, verify_message, sign_tool, verify_tool
18
+
19
+ # Generate keys
20
+ keys = generate_key_pair()
21
+
22
+ # Create and sign a passport
23
+ passport = create_passport(
24
+ name="my-agent",
25
+ version="1.0.0",
26
+ public_key=keys["public_key"],
27
+ capabilities=["read", "write"],
28
+ )
29
+
30
+ # Trust Authority signs the passport
31
+ ta_keys = generate_key_pair()
32
+ signed = sign_passport(passport, ta_keys["private_key"])
33
+ assert verify_passport_signature(signed, ta_keys["public_key"])
34
+
35
+ # Sign MCP messages
36
+ envelope = sign_message(
37
+ {"jsonrpc": "2.0", "method": "tools/list", "id": 1},
38
+ signed["passport_id"],
39
+ keys["private_key"],
40
+ )
41
+
42
+ # Verify
43
+ result = verify_message(envelope, keys["public_key"])
44
+ assert result["valid"]
45
+
46
+ # Tool integrity
47
+ tool = {"name": "read_file", "description": "Read a file", "inputSchema": {"type": "object"}}
48
+ sig = sign_tool(tool, keys["private_key"])
49
+ assert verify_tool(tool, sig, keys["public_key"])
50
+ ```
51
+
52
+ ## What MCPS Adds to MCP
53
+
54
+ | Feature | Description |
55
+ |---------|-------------|
56
+ | Agent Passports | ECDSA P-256 signed identity credentials |
57
+ | Message Signing | Every JSON-RPC message wrapped in signed envelope |
58
+ | Tool Integrity | Signed tool definitions prevent poisoning |
59
+ | Replay Protection | Nonce + 5-min timestamp window |
60
+ | Revocation | Real-time passport revocation via Trust Authority |
61
+ | Trust Levels | L0 (unsigned) to L4 (audited) |
62
+
63
+ ## OWASP MCP Top 10
64
+
65
+ Mitigates 8/10 OWASP MCP vulnerabilities: tool poisoning, supply chain attacks, auth bypass, shadow servers, and more.
66
+
67
+ ## Links
68
+
69
+ - **Website**: [mcp-secure.dev](https://mcp-secure.dev)
70
+ - **npm**: [mcp-secure](https://www.npmjs.com/package/mcp-secure)
71
+ - **Spec**: [SPEC.md](https://github.com/razashariff/mcps/blob/main/SPEC.md)
72
+
73
+ ## License
74
+
75
+ MIT -- CyberSecAI Ltd
@@ -0,0 +1,76 @@
1
+ """
2
+ MCPS -- MCP Secure
3
+ Cryptographic identity, message signing, and trust verification for MCP.
4
+
5
+ Copyright (c) 2026 CyberSecAI Ltd. All rights reserved.
6
+ License: MIT
7
+ """
8
+
9
+ from .core import (
10
+ MCPS_VERSION,
11
+ SUPPORTED_VERSIONS,
12
+ TRUST_LEVELS,
13
+ ERROR_CODES,
14
+ MAX_ISSUER_CHAIN_DEPTH,
15
+ MAX_PASSPORT_BYTES,
16
+ MAX_CAPABILITIES,
17
+ generate_key_pair,
18
+ public_key_to_jwk,
19
+ create_passport,
20
+ sign_passport,
21
+ verify_passport_signature,
22
+ is_passport_expired,
23
+ validate_passport_format,
24
+ validate_origin,
25
+ get_effective_trust_level,
26
+ verify_issuer_chain,
27
+ sign_message,
28
+ verify_message,
29
+ sign_tool,
30
+ verify_tool,
31
+ create_transcript_binding,
32
+ verify_transcript_binding,
33
+ create_transcript_mac,
34
+ verify_transcript_mac,
35
+ check_revocation,
36
+ negotiate_version,
37
+ secure_mcp,
38
+ secure_mcp_client,
39
+ NonceStore,
40
+ canonical_json,
41
+ )
42
+
43
+ __version__ = "1.0.0"
44
+ __all__ = [
45
+ "MCPS_VERSION",
46
+ "SUPPORTED_VERSIONS",
47
+ "TRUST_LEVELS",
48
+ "ERROR_CODES",
49
+ "MAX_ISSUER_CHAIN_DEPTH",
50
+ "MAX_PASSPORT_BYTES",
51
+ "MAX_CAPABILITIES",
52
+ "generate_key_pair",
53
+ "public_key_to_jwk",
54
+ "create_passport",
55
+ "sign_passport",
56
+ "verify_passport_signature",
57
+ "is_passport_expired",
58
+ "validate_passport_format",
59
+ "validate_origin",
60
+ "get_effective_trust_level",
61
+ "verify_issuer_chain",
62
+ "sign_message",
63
+ "verify_message",
64
+ "sign_tool",
65
+ "verify_tool",
66
+ "create_transcript_binding",
67
+ "verify_transcript_binding",
68
+ "create_transcript_mac",
69
+ "verify_transcript_mac",
70
+ "check_revocation",
71
+ "negotiate_version",
72
+ "secure_mcp",
73
+ "secure_mcp_client",
74
+ "NonceStore",
75
+ "canonical_json",
76
+ ]
@@ -0,0 +1,847 @@
1
+ """
2
+ MCPS -- MCP Secure (Core)
3
+ Cryptographic security layer for the Model Context Protocol.
4
+
5
+ Specification: SEP-2395 (Cryptographic Security Layer for MCP)
6
+ Canonicalization: RFC 8785 (JSON Canonicalization Scheme)
7
+ Signing: ECDSA P-256 (NIST FIPS 186-5, RFC 6979)
8
+ Signature Format: IEEE P1363 r||s fixed-length (RFC 7518 Section 3.4)
9
+
10
+ Copyright (c) 2026 CyberSecAI Ltd. All rights reserved.
11
+ Patent: GB2604808.2
12
+ License: MIT
13
+ """
14
+
15
+ import json
16
+ import hashlib
17
+ import os
18
+ import time
19
+ import threading
20
+ import urllib.request
21
+ import urllib.error
22
+ from datetime import datetime, timezone, timedelta
23
+ from cryptography.hazmat.primitives.asymmetric import ec
24
+ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
25
+ from cryptography.hazmat.primitives import hashes, serialization
26
+ from cryptography.exceptions import InvalidSignature
27
+ import base64
28
+ import math
29
+ from urllib.parse import urlparse
30
+
31
+ MCPS_VERSION = "1.0"
32
+ SUPPORTED_VERSIONS = ["1.0"]
33
+ NONCE_BYTES = 16
34
+ TIMESTAMP_WINDOW_S = 5 * 60 # 5 minutes
35
+ DEFAULT_TRUST_AUTHORITY = "https://agentsign.dev"
36
+ PASSPORT_PREFIX = "asp_"
37
+
38
+ # P-256 curve order (FIPS 186-5) for low-S normalization
39
+ P256_ORDER = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
40
+ P256_HALF_ORDER = P256_ORDER >> 1
41
+ P1363_SIG_LENGTH = 64 # 32 bytes r + 32 bytes s for P-256
42
+
43
+ # Passport size limits (DoS prevention)
44
+ MAX_ISSUER_CHAIN_DEPTH = 5
45
+ MAX_PASSPORT_BYTES = 8192
46
+ MAX_CAPABILITIES = 64
47
+
48
+ TRUST_LEVELS = {
49
+ "UNSIGNED": 0, # Plain MCP, no MCPS -- self-signed passports capped here
50
+ "IDENTIFIED": 1, # Passport signed by any Trust Authority
51
+ "VERIFIED": 2, # Passport signed by recognized TA (in verifier's trust store)
52
+ "SCANNED": 3, # Verified + TA verified origin ownership
53
+ "AUDITED": 4, # Scanned + TA performed security audit + revocation required
54
+ }
55
+
56
+ # Error codes: string code + JSON-RPC numeric code
57
+ # Numeric codes use -33xxx to avoid collision with JSON-RPC reserved range (-32000 to -32099)
58
+ ERROR_CODES = {
59
+ "INVALID_PASSPORT": {"code": "MCPS-001", "jsonrpc_code": -33001, "message": "Invalid passport format"},
60
+ "PASSPORT_EXPIRED": {"code": "MCPS-002", "jsonrpc_code": -33002, "message": "Passport expired"},
61
+ "PASSPORT_REVOKED": {"code": "MCPS-003", "jsonrpc_code": -33003, "message": "Passport revoked"},
62
+ "INVALID_SIGNATURE": {"code": "MCPS-004", "jsonrpc_code": -33004, "message": "Invalid message signature"},
63
+ "REPLAY_ATTACK": {"code": "MCPS-005", "jsonrpc_code": -33005, "message": "Replay attack detected"},
64
+ "TIMESTAMP_OUT_OF_WINDOW": {"code": "MCPS-006", "jsonrpc_code": -33006, "message": "Timestamp out of window"},
65
+ "AUTHORITY_UNREACHABLE": {"code": "MCPS-007", "jsonrpc_code": -33007, "message": "Trust authority unreachable"},
66
+ "TOOL_INTEGRITY_FAILED": {"code": "MCPS-008", "jsonrpc_code": -33008, "message": "Tool definition signature invalid or hash changed"},
67
+ "INSUFFICIENT_TRUST": {"code": "MCPS-009", "jsonrpc_code": -33009, "message": "Insufficient trust level"},
68
+ "RATE_LIMITED": {"code": "MCPS-010", "jsonrpc_code": -33010, "message": "Rate limit exceeded"},
69
+ "ORIGIN_MISMATCH": {"code": "MCPS-011", "jsonrpc_code": -33011, "message": "Passport origin does not match server URI"},
70
+ "CAPABILITY_MISMATCH": {"code": "MCPS-012", "jsonrpc_code": -33012, "message": "Transcript binding verification failed (downgrade detected)"},
71
+ "PASSPORT_TOO_LARGE": {"code": "MCPS-013", "jsonrpc_code": -33013, "message": "Passport exceeds maximum size"},
72
+ "CHAIN_TOO_DEEP": {"code": "MCPS-014", "jsonrpc_code": -33014, "message": "Issuer chain exceeds maximum depth"},
73
+ "VERSION_MISMATCH": {"code": "MCPS-015", "jsonrpc_code": -33015, "message": "No mutually supported MCPS version"},
74
+ }
75
+
76
+
77
+ # ── RFC 8785: JSON Canonicalization Scheme ──
78
+
79
+
80
+ def canonical_json(obj):
81
+ """Deterministic JSON serialization per RFC 8785 (JCS).
82
+
83
+ Guarantees identical bytes across Python and Node.js for the same logical value.
84
+
85
+ Key properties:
86
+ - Object keys sorted lexicographically (Unicode code point order)
87
+ - No whitespace between tokens
88
+ - Numbers: ES2024 Number.toString() semantics (1.0 -> "1", no trailing zeros)
89
+ - Strings: minimal JSON escaping
90
+ - Recursive for nested objects
91
+ """
92
+ if obj is None:
93
+ return "null"
94
+ if isinstance(obj, bool):
95
+ return "true" if obj else "false"
96
+ if isinstance(obj, int):
97
+ return str(obj)
98
+ if isinstance(obj, float):
99
+ if math.isinf(obj) or math.isnan(obj):
100
+ raise ValueError("Infinity/NaN not allowed in JCS")
101
+ if obj == 0.0:
102
+ return "0" # handles -0.0
103
+ # ES2024 Number.toString(): integers as integers, shortest float representation
104
+ if obj == int(obj) and abs(obj) < 2**53:
105
+ return str(int(obj))
106
+ return repr(obj)
107
+ if isinstance(obj, str):
108
+ return json.dumps(obj, ensure_ascii=False)
109
+ if isinstance(obj, list):
110
+ return "[" + ",".join(canonical_json(item) for item in obj) + "]"
111
+ if isinstance(obj, dict):
112
+ keys = sorted(obj.keys())
113
+ return "{" + ",".join(
114
+ json.dumps(k, ensure_ascii=False) + ":" + canonical_json(obj[k]) for k in keys
115
+ ) + "}"
116
+ return json.dumps(obj)
117
+
118
+
119
+ # ── ECDSA P1363 Signing with Low-S ──
120
+
121
+
122
+ def _sign_bytes(data_bytes, private_key_pem):
123
+ """Sign bytes with ECDSA P-256 SHA-256, IEEE P1363 format with low-S normalization.
124
+
125
+ Output: base64-encoded r||s (exactly 64 bytes for P-256).
126
+ Low-S normalization prevents signature malleability (BIP-0062).
127
+ Format per RFC 7518 Section 3.4.
128
+ """
129
+ private_key = serialization.load_pem_private_key(
130
+ private_key_pem.encode(), password=None
131
+ )
132
+ der_sig = private_key.sign(data_bytes, ec.ECDSA(hashes.SHA256()))
133
+ r, s = decode_dss_signature(der_sig)
134
+
135
+ # Low-S normalization: if s > n/2, replace with n - s
136
+ if s > P256_HALF_ORDER:
137
+ s = P256_ORDER - s
138
+
139
+ # IEEE P1363: r||s, each 32 bytes for P-256
140
+ raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")
141
+ return base64.b64encode(raw_sig).decode()
142
+
143
+
144
+ def _verify_bytes(data_bytes, signature_b64, public_key_pem):
145
+ """Verify ECDSA P1363 signature with low-S normalization.
146
+
147
+ Accepts and normalizes high-S signatures for interoperability.
148
+ """
149
+ public_key = serialization.load_pem_public_key(public_key_pem.encode())
150
+ try:
151
+ raw_sig = base64.b64decode(signature_b64)
152
+ if len(raw_sig) != P1363_SIG_LENGTH:
153
+ return False
154
+
155
+ # Parse IEEE P1363 format
156
+ r = int.from_bytes(raw_sig[:32], "big")
157
+ s = int.from_bytes(raw_sig[32:], "big")
158
+
159
+ # Low-S normalization for verification
160
+ if s > P256_HALF_ORDER:
161
+ s = P256_ORDER - s
162
+
163
+ # Convert to DER for cryptography library
164
+ der_sig = encode_dss_signature(r, s)
165
+ public_key.verify(der_sig, data_bytes, ec.ECDSA(hashes.SHA256()))
166
+ return True
167
+ except (InvalidSignature, Exception):
168
+ return False
169
+
170
+
171
+ # ── Key Generation ──
172
+
173
+
174
+ def generate_key_pair():
175
+ """Generate an ECDSA P-256 key pair for MCPS signing."""
176
+ private_key = ec.generate_private_key(ec.SECP256R1())
177
+ private_pem = private_key.private_bytes(
178
+ encoding=serialization.Encoding.PEM,
179
+ format=serialization.PrivateFormat.PKCS8,
180
+ encryption_algorithm=serialization.NoEncryption(),
181
+ ).decode()
182
+ public_pem = private_key.public_key().public_bytes(
183
+ encoding=serialization.Encoding.PEM,
184
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
185
+ ).decode()
186
+ return {"public_key": public_pem, "private_key": private_pem}
187
+
188
+
189
+ def _int_to_base64url(n, length):
190
+ """Convert integer to base64url-encoded bytes of given length."""
191
+ b = n.to_bytes(length, byteorder="big")
192
+ return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
193
+
194
+
195
+ def public_key_to_jwk(public_key_pem):
196
+ """Export public key as compact JWK for passport embedding."""
197
+ pub_key = serialization.load_pem_public_key(public_key_pem.encode())
198
+ numbers = pub_key.public_numbers()
199
+ return {
200
+ "kty": "EC",
201
+ "crv": "P-256",
202
+ "x": _int_to_base64url(numbers.x, 32),
203
+ "y": _int_to_base64url(numbers.y, 32),
204
+ }
205
+
206
+
207
+ # ── Passport ──
208
+
209
+
210
+ def create_passport(name, version="1.0.0", public_key=None, origin=None,
211
+ capabilities=None, scan_results=None, ttl_days=365,
212
+ issuer="self", issuer_chain=None, previous_key_hash=None):
213
+ """Create an agent passport with origin binding.
214
+
215
+ Args:
216
+ name: Agent name
217
+ version: Agent version
218
+ public_key: PEM public key string
219
+ origin: URI of the authorized server (e.g. https://api.example.com)
220
+ capabilities: List of agent capabilities (max 64)
221
+ scan_results: SDLC scan results dict
222
+ ttl_days: Days until expiration
223
+ issuer: Issuer identifier
224
+ issuer_chain: Chain of base64url-encoded intermediate TA passports (max depth 5)
225
+ previous_key_hash: SHA-256 hash of previous public key (for key rotation)
226
+ """
227
+ passport_id = PASSPORT_PREFIX + os.urandom(16).hex()
228
+ now = datetime.now(timezone.utc)
229
+ expires = now + timedelta(days=ttl_days)
230
+
231
+ # Self-signed passports are ALWAYS L0 per SEP-2395 Section 6.1
232
+ is_self_signed = not issuer or issuer == "self"
233
+ if is_self_signed:
234
+ trust_level = TRUST_LEVELS["UNSIGNED"]
235
+ elif scan_results:
236
+ trust_level = TRUST_LEVELS["SCANNED"]
237
+ else:
238
+ trust_level = TRUST_LEVELS["IDENTIFIED"]
239
+
240
+ passport = {
241
+ "mcps_version": MCPS_VERSION,
242
+ "passport_id": passport_id,
243
+ "agent": {
244
+ "name": name,
245
+ "version": version,
246
+ "capabilities": (capabilities or [])[:MAX_CAPABILITIES],
247
+ },
248
+ "public_key": public_key_to_jwk(public_key) if public_key else None,
249
+ "origin": origin,
250
+ "scan": scan_results,
251
+ "trust_level": trust_level,
252
+ "issued_at": now.isoformat().replace("+00:00", "Z"),
253
+ "expires_at": expires.isoformat().replace("+00:00", "Z"),
254
+ "issuer": issuer,
255
+ "issuer_chain": (issuer_chain or [])[:MAX_ISSUER_CHAIN_DEPTH],
256
+ }
257
+
258
+ # Key rotation: link to previous key for compromise recovery
259
+ if previous_key_hash:
260
+ passport["key_rotation"] = {
261
+ "previous_key_hash": previous_key_hash,
262
+ "rotated_at": now.isoformat().replace("+00:00", "Z"),
263
+ }
264
+
265
+ return passport
266
+
267
+
268
+ def sign_passport(passport, authority_private_key):
269
+ """Sign a passport with the Trust Authority's private key.
270
+ Uses RFC 8785 (JCS) for canonical serialization before signing.
271
+ Signature: IEEE P1363 r||s with low-S normalization."""
272
+ payload = canonical_json(passport).encode()
273
+ signature = _sign_bytes(payload, authority_private_key)
274
+ return {**passport, "signature": signature}
275
+
276
+
277
+ def verify_passport_signature(passport, authority_public_key):
278
+ """Verify a passport's signature against the Trust Authority's public key."""
279
+ sig = passport.get("signature")
280
+ if not sig:
281
+ return False
282
+ rest = {k: v for k, v in passport.items() if k != "signature"}
283
+ payload = canonical_json(rest).encode()
284
+ return _verify_bytes(payload, sig, authority_public_key)
285
+
286
+
287
+ def is_passport_expired(passport):
288
+ """Check if a passport has expired."""
289
+ expires = passport.get("expires_at", "")
290
+ if not expires:
291
+ return True
292
+ exp_dt = datetime.fromisoformat(expires.replace("Z", "+00:00"))
293
+ return exp_dt < datetime.now(timezone.utc)
294
+
295
+
296
+ def validate_passport_format(passport):
297
+ """Validate passport format including size limits."""
298
+ if not passport:
299
+ return {"valid": False, "error": ERROR_CODES["INVALID_PASSPORT"]}
300
+ pid = passport.get("passport_id", "")
301
+ if not pid or not pid.startswith(PASSPORT_PREFIX):
302
+ return {"valid": False, "error": ERROR_CODES["INVALID_PASSPORT"]}
303
+ if not passport.get("public_key"):
304
+ return {"valid": False, "error": ERROR_CODES["INVALID_PASSPORT"]}
305
+ if not passport.get("issued_at") or not passport.get("expires_at"):
306
+ return {"valid": False, "error": ERROR_CODES["INVALID_PASSPORT"]}
307
+ if is_passport_expired(passport):
308
+ return {"valid": False, "error": ERROR_CODES["PASSPORT_EXPIRED"]}
309
+
310
+ # Size limits (DoS prevention)
311
+ chain = passport.get("issuer_chain", [])
312
+ if len(chain) > MAX_ISSUER_CHAIN_DEPTH:
313
+ return {"valid": False, "error": ERROR_CODES["CHAIN_TOO_DEEP"]}
314
+ serialized = canonical_json(passport)
315
+ if len(serialized.encode("utf-8")) > MAX_PASSPORT_BYTES:
316
+ return {"valid": False, "error": ERROR_CODES["PASSPORT_TOO_LARGE"]}
317
+
318
+ return {"valid": True, "error": None}
319
+
320
+
321
+ def validate_origin(passport, expected_origin):
322
+ """Validate that a passport's origin matches the expected server URI.
323
+ Origin comparison uses scheme + authority (host + port) per RFC 6454."""
324
+ passport_origin = passport.get("origin")
325
+ if not passport_origin:
326
+ return {"valid": False, "error": ERROR_CODES["ORIGIN_MISMATCH"]}
327
+ if not expected_origin:
328
+ return {"valid": True, "error": None}
329
+
330
+ try:
331
+ p = urlparse(passport_origin)
332
+ e = urlparse(expected_origin)
333
+ match = (p.scheme == e.scheme and p.hostname == e.hostname
334
+ and (p.port or "") == (e.port or ""))
335
+ if match:
336
+ return {"valid": True, "error": None}
337
+ return {"valid": False, "error": ERROR_CODES["ORIGIN_MISMATCH"]}
338
+ except Exception:
339
+ return {"valid": False, "error": ERROR_CODES["ORIGIN_MISMATCH"]}
340
+
341
+
342
+ def get_effective_trust_level(passport, trusted_issuers=None):
343
+ """Get the effective trust level for a passport.
344
+ Self-signed passports are ALWAYS capped at L0.
345
+ Unknown issuers are treated as L0."""
346
+ issuer = passport.get("issuer", "self")
347
+ if not issuer or issuer == "self":
348
+ return TRUST_LEVELS["UNSIGNED"]
349
+ if trusted_issuers is not None and issuer not in trusted_issuers:
350
+ return TRUST_LEVELS["UNSIGNED"]
351
+ return passport.get("trust_level", TRUST_LEVELS["UNSIGNED"])
352
+
353
+
354
+ def verify_issuer_chain(passport, trust_store):
355
+ """Verify an issuer chain from agent passport up to a trusted root.
356
+
357
+ Each issuer_chain entry is a base64-encoded JSON intermediate TA passport
358
+ containing its own signature, public_key, issuer, and other standard fields.
359
+
360
+ Chain verification walks: agent -> intermediate(s) -> root TA.
361
+ Max chain depth: MAX_ISSUER_CHAIN_DEPTH (5).
362
+ """
363
+ issuer = passport.get("issuer")
364
+ if not issuer:
365
+ return {"valid": False, "chain_length": 0, "root_issuer": None}
366
+
367
+ chain = passport.get("issuer_chain", [])
368
+ if len(chain) > MAX_ISSUER_CHAIN_DEPTH:
369
+ return {"valid": False, "chain_length": len(chain), "root_issuer": None, "error": "chain_too_deep"}
370
+
371
+ # Direct trust: issuer is in trust store
372
+ if issuer in trust_store:
373
+ is_valid = verify_passport_signature(passport, trust_store[issuer])
374
+ return {"valid": is_valid, "chain_length": 1,
375
+ "root_issuer": issuer if is_valid else None}
376
+
377
+ if not chain:
378
+ return {"valid": False, "chain_length": 0, "root_issuer": None}
379
+
380
+ # Decode chain entries: each is base64-encoded JSON intermediate TA passport
381
+ intermediates = []
382
+ for entry in chain:
383
+ try:
384
+ decoded = base64.b64decode(entry).decode("utf-8")
385
+ intermediates.append(json.loads(decoded))
386
+ except Exception:
387
+ return {"valid": False, "chain_length": len(chain),
388
+ "root_issuer": None, "error": "invalid_chain_entry"}
389
+
390
+ # Walk chain: verify each link from agent passport up to root
391
+ current = passport
392
+ for i, intermediate in enumerate(intermediates):
393
+ pub_key_jwk = intermediate.get("public_key")
394
+ if not pub_key_jwk:
395
+ return {"valid": False, "chain_length": i,
396
+ "root_issuer": None, "error": "intermediate_missing_key"}
397
+
398
+ # Convert intermediate's JWK to PEM
399
+ try:
400
+ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1
401
+ x = int.from_bytes(base64.urlsafe_b64decode(pub_key_jwk["x"] + "=="), "big")
402
+ y = int.from_bytes(base64.urlsafe_b64decode(pub_key_jwk["y"] + "=="), "big")
403
+ pub_numbers = EllipticCurvePublicNumbers(x, y, SECP256R1())
404
+ pub_obj = pub_numbers.public_key()
405
+ intermediate_pem = pub_obj.public_bytes(
406
+ serialization.Encoding.PEM,
407
+ serialization.PublicFormat.SubjectPublicKeyInfo
408
+ ).decode()
409
+ except Exception:
410
+ return {"valid": False, "chain_length": i,
411
+ "root_issuer": None, "error": "invalid_intermediate_key"}
412
+
413
+ # Verify current passport was signed by this intermediate
414
+ if not verify_passport_signature(current, intermediate_pem):
415
+ return {"valid": False, "chain_length": i,
416
+ "root_issuer": None, "error": "signature_verification_failed"}
417
+
418
+ # Check if this intermediate's issuer is a trusted root
419
+ int_issuer = intermediate.get("issuer")
420
+ if int_issuer and int_issuer in trust_store:
421
+ if verify_passport_signature(intermediate, trust_store[int_issuer]):
422
+ return {"valid": True, "chain_length": i + 2,
423
+ "root_issuer": int_issuer}
424
+
425
+ current = intermediate
426
+
427
+ return {"valid": False, "chain_length": len(intermediates),
428
+ "root_issuer": None, "error": "no_trusted_root"}
429
+
430
+
431
+ # ── Message Signing ──
432
+
433
+
434
+ def sign_message(mcp_message, passport_id, private_key, channel_binding=None):
435
+ """Wrap an MCP JSON-RPC message in an MCPS signed envelope.
436
+
437
+ Uses SHA-256 hash of JCS(message) to avoid double-canonicalization
438
+ fragility (embedding one JCS string inside another JCS object).
439
+
440
+ Args:
441
+ mcp_message: The original MCP JSON-RPC message dict
442
+ passport_id: The agent's passport ID
443
+ private_key: PEM private key string
444
+ channel_binding: Optional TLS channel binding token (RFC 9266 tls-exporter)
445
+ """
446
+ nonce = os.urandom(NONCE_BYTES).hex()
447
+ timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
448
+
449
+ # Hash message instead of nesting JCS strings (prevents double-canonicalization fragility)
450
+ message_hash = hashlib.sha256(canonical_json(mcp_message).encode()).hexdigest()
451
+
452
+ sig_payload_obj = {
453
+ "message_hash": message_hash,
454
+ "nonce": nonce,
455
+ "passport_id": passport_id,
456
+ "timestamp": timestamp,
457
+ }
458
+
459
+ # Optional TLS channel binding (RFC 9266 tls-exporter)
460
+ if channel_binding:
461
+ sig_payload_obj["channel_binding"] = channel_binding
462
+
463
+ sig_payload = canonical_json(sig_payload_obj).encode()
464
+ signature = _sign_bytes(sig_payload, private_key)
465
+
466
+ return {
467
+ "mcps": {
468
+ "version": MCPS_VERSION,
469
+ "passport_id": passport_id,
470
+ "timestamp": timestamp,
471
+ "nonce": nonce,
472
+ "signature": signature,
473
+ },
474
+ **mcp_message,
475
+ }
476
+
477
+
478
+ def verify_message(envelope, public_key_pem, channel_binding=None):
479
+ """Verify an MCPS envelope's signature."""
480
+ if not envelope or not isinstance(envelope.get("mcps"), dict):
481
+ return {"valid": False, "error": ERROR_CODES["INVALID_SIGNATURE"]}
482
+
483
+ mcps_field = envelope["mcps"]
484
+ if not mcps_field.get("signature"):
485
+ return {"valid": False, "error": ERROR_CODES["INVALID_SIGNATURE"]}
486
+
487
+ jsonrpc_message = {k: v for k, v in envelope.items() if k != "mcps"}
488
+
489
+ # Check timestamp window
490
+ try:
491
+ msg_time = datetime.fromisoformat(
492
+ mcps_field["timestamp"].replace("Z", "+00:00")
493
+ ).timestamp()
494
+ except (KeyError, ValueError):
495
+ return {"valid": False, "error": ERROR_CODES["TIMESTAMP_OUT_OF_WINDOW"]}
496
+
497
+ now = time.time()
498
+ if abs(now - msg_time) > TIMESTAMP_WINDOW_S:
499
+ return {"valid": False, "error": ERROR_CODES["TIMESTAMP_OUT_OF_WINDOW"]}
500
+
501
+ # Reconstruct signing payload (message hash, not nested JCS)
502
+ message_hash = hashlib.sha256(canonical_json(jsonrpc_message).encode()).hexdigest()
503
+
504
+ sig_payload_obj = {
505
+ "message_hash": message_hash,
506
+ "nonce": mcps_field["nonce"],
507
+ "passport_id": mcps_field["passport_id"],
508
+ "timestamp": mcps_field["timestamp"],
509
+ }
510
+
511
+ if channel_binding:
512
+ sig_payload_obj["channel_binding"] = channel_binding
513
+
514
+ sig_payload = canonical_json(sig_payload_obj).encode()
515
+
516
+ if _verify_bytes(sig_payload, mcps_field["signature"], public_key_pem):
517
+ return {"valid": True, "error": None}
518
+ return {"valid": False, "error": ERROR_CODES["INVALID_SIGNATURE"]}
519
+
520
+
521
+ # ── Nonce Store (Replay Protection) ──
522
+
523
+
524
+ class NonceStore:
525
+ """Thread-safe nonce store for replay attack detection."""
526
+
527
+ def __init__(self, window_s=TIMESTAMP_WINDOW_S):
528
+ self._nonces = {}
529
+ self._window_s = window_s
530
+ self._lock = threading.Lock()
531
+ self._timer = threading.Timer(window_s, self._gc)
532
+ self._timer.daemon = True
533
+ self._timer.start()
534
+
535
+ def check(self, nonce):
536
+ """Return True if nonce is fresh (not a replay)."""
537
+ with self._lock:
538
+ if nonce in self._nonces:
539
+ return False
540
+ self._nonces[nonce] = time.time()
541
+ return True
542
+
543
+ def _gc(self):
544
+ cutoff = time.time() - self._window_s
545
+ with self._lock:
546
+ self._nonces = {n: t for n, t in self._nonces.items() if t >= cutoff}
547
+ self._timer = threading.Timer(self._window_s, self._gc)
548
+ self._timer.daemon = True
549
+ self._timer.start()
550
+
551
+ def destroy(self):
552
+ self._timer.cancel()
553
+ with self._lock:
554
+ self._nonces.clear()
555
+
556
+
557
+ # ── Tool Integrity ──
558
+
559
+
560
+ def sign_tool(tool, private_key, author_origin=None):
561
+ """Sign an MCP tool definition.
562
+ Hashes the ENTIRE tool object (name + description + inputSchema) using JCS.
563
+ Optionally binds to author_origin to prevent tool relaying attacks.
564
+
565
+ Args:
566
+ tool: MCP tool dict with name, description, inputSchema
567
+ private_key: PEM private key string
568
+ author_origin: Author's server origin (binds tool to serving origin)
569
+ """
570
+ canonical = canonical_json({
571
+ "author_origin": author_origin,
572
+ "description": tool["description"],
573
+ "inputSchema": tool.get("inputSchema", {}),
574
+ "name": tool["name"],
575
+ })
576
+
577
+ tool_hash = hashlib.sha256(canonical.encode()).hexdigest()
578
+ signature = _sign_bytes(canonical.encode(), private_key)
579
+
580
+ return {"signature": signature, "tool_hash": tool_hash}
581
+
582
+
583
+ def verify_tool(tool, signature, public_key_pem, pinned_hash=None, author_origin=None):
584
+ """Verify an MCP tool definition's signature.
585
+
586
+ Args:
587
+ tool: MCP tool dict
588
+ signature: Base64 P1363 signature string
589
+ public_key_pem: PEM public key string
590
+ pinned_hash: Previously pinned tool_hash for change detection
591
+ author_origin: Expected author origin
592
+ """
593
+ canonical = canonical_json({
594
+ "author_origin": author_origin,
595
+ "description": tool["description"],
596
+ "inputSchema": tool.get("inputSchema", {}),
597
+ "name": tool["name"],
598
+ })
599
+
600
+ tool_hash = hashlib.sha256(canonical.encode()).hexdigest()
601
+ hash_changed = (pinned_hash is not None) and (tool_hash != pinned_hash)
602
+
603
+ valid = _verify_bytes(canonical.encode(), signature, public_key_pem)
604
+ return {"valid": valid, "tool_hash": tool_hash, "hash_changed": hash_changed}
605
+
606
+
607
+ # ── Transcript Binding (Anti-Downgrade) ──
608
+
609
+
610
+ def create_transcript_binding(client_init_params, server_init_result, private_key):
611
+ """Create a transcript binding (formerly "transcript MAC").
612
+ Uses ECDSA signatures (asymmetric), NOT MAC (symmetric) -- hence the rename.
613
+
614
+ Both parties sign the agreed-upon security parameters to prevent an active
615
+ attacker from stripping MCPS capability during handshake.
616
+
617
+ IMPORTANT: transcript covers initialize `params` (client) and `result` (server),
618
+ NOT the full JSON-RPC envelope. This is the security-relevant boundary.
619
+
620
+ Args:
621
+ client_init_params: Client's initialize params (capabilities, clientInfo)
622
+ server_init_result: Server's initialize result (capabilities, serverInfo)
623
+ private_key: Signer's PEM private key
624
+ """
625
+ client_jcs = canonical_json(client_init_params)
626
+ server_jcs = canonical_json(server_init_result)
627
+ transcript_hash = hashlib.sha256((client_jcs + server_jcs).encode()).hexdigest()
628
+
629
+ signature = _sign_bytes(transcript_hash.encode(), private_key)
630
+
631
+ return {"transcript_hash": transcript_hash, "transcript_signature": signature}
632
+
633
+
634
+ def verify_transcript_binding(transcript_hash, transcript_signature, public_key_pem,
635
+ client_init_params, server_init_result):
636
+ """Verify a transcript binding from the other party."""
637
+ client_jcs = canonical_json(client_init_params)
638
+ server_jcs = canonical_json(server_init_result)
639
+ expected_hash = hashlib.sha256((client_jcs + server_jcs).encode()).hexdigest()
640
+
641
+ if transcript_hash != expected_hash:
642
+ return {"valid": False, "error": ERROR_CODES["CAPABILITY_MISMATCH"]}
643
+
644
+ if _verify_bytes(transcript_hash.encode(), transcript_signature, public_key_pem):
645
+ return {"valid": True, "error": None}
646
+ return {"valid": False, "error": ERROR_CODES["CAPABILITY_MISMATCH"]}
647
+
648
+
649
+ # Backwards-compatible aliases (renamed from MAC to Binding)
650
+ create_transcript_mac = create_transcript_binding
651
+ verify_transcript_mac = verify_transcript_binding
652
+
653
+
654
+ # ── Version Negotiation ──
655
+
656
+
657
+ def negotiate_version(client_versions, server_versions=None):
658
+ """Negotiate the highest mutually supported MCPS version.
659
+
660
+ Args:
661
+ client_versions: Client's supported version(s) -- string or list
662
+ server_versions: Server's supported versions (defaults to SUPPORTED_VERSIONS)
663
+
664
+ Returns:
665
+ str or None: Highest mutual version, or None if none
666
+ """
667
+ if isinstance(client_versions, str):
668
+ client_versions = [client_versions]
669
+ if server_versions is None:
670
+ server_versions = SUPPORTED_VERSIONS
671
+ if isinstance(server_versions, str):
672
+ server_versions = [server_versions]
673
+
674
+ mutual = [v for v in client_versions if v in server_versions]
675
+ if not mutual:
676
+ return None
677
+
678
+ # Sort by version descending (highest first)
679
+ def version_key(v):
680
+ parts = v.split(".")
681
+ return tuple(int(p) for p in parts)
682
+
683
+ mutual.sort(key=version_key, reverse=True)
684
+ return mutual[0]
685
+
686
+
687
+ # ── Revocation Client ──
688
+
689
+
690
+ def check_revocation(passport_id, trust_authority=DEFAULT_TRUST_AUTHORITY, ta_public_key=None):
691
+ """Check passport revocation status against the Trust Authority.
692
+ Revocation endpoint is discovered from TA metadata, NOT from the verified party."""
693
+ url = f"{trust_authority.rstrip('/')}/api/verify/{passport_id}"
694
+ try:
695
+ req = urllib.request.Request(url, headers={"User-Agent": "mcps-python/1.0"})
696
+ with urllib.request.urlopen(req, timeout=5) as resp:
697
+ data = json.loads(resp.read().decode())
698
+
699
+ verified = False
700
+ if ta_public_key and data.get("signature"):
701
+ sig = data["signature"]
702
+ rest = {k: v for k, v in data.items() if k != "signature"}
703
+ payload = canonical_json(rest).encode()
704
+ verified = _verify_bytes(payload, sig, ta_public_key)
705
+
706
+ return {
707
+ "status": data.get("status", "UNKNOWN"),
708
+ "revoked": data.get("status") == "REVOKED",
709
+ "reason": data.get("reason"),
710
+ "revoked_at": data.get("revoked_at"),
711
+ "verified": verified,
712
+ }
713
+ except Exception:
714
+ return {"status": "UNREACHABLE", "revoked": False, "reason": "network_error", "verified": False}
715
+
716
+
717
+ # ── MCPS Middleware ──
718
+
719
+
720
+ def secure_mcp(mcp_server, passport=None, private_key=None,
721
+ trust_authority=DEFAULT_TRUST_AUTHORITY, min_trust_level=2,
722
+ origin=None, trusted_issuers=None, trust_authorities=None,
723
+ on_revoked=None, on_audit=None):
724
+ """Create an MCPS-secured wrapper around an MCP server.
725
+ Implements fail-closed by default when Trust Authority is unreachable.
726
+ Supports multiple Trust Authorities via trust_authorities list."""
727
+ return MCPSServer(mcp_server, passport, private_key, trust_authority,
728
+ min_trust_level, origin, trusted_issuers, trust_authorities,
729
+ on_revoked, on_audit)
730
+
731
+
732
+ class MCPSServer:
733
+ def __init__(self, mcp_server, passport, private_key, trust_authority,
734
+ min_trust_level, origin, trusted_issuers, trust_authorities,
735
+ on_revoked, on_audit):
736
+ self._mcp_server = mcp_server
737
+ self._nonce_store = NonceStore()
738
+ self._passport_cache = {}
739
+ self._passport = passport
740
+ self._private_key = private_key
741
+ self._trust_authorities = trust_authorities or [trust_authority]
742
+ self._min_trust_level = min_trust_level
743
+ self._origin = origin
744
+ self._trusted_issuers = trusted_issuers
745
+ self._on_revoked = on_revoked
746
+ self._on_audit = on_audit
747
+ self.mcps_version = MCPS_VERSION
748
+
749
+ def _audit(self, event, data):
750
+ if self._on_audit:
751
+ entry = {"timestamp": datetime.now(timezone.utc).isoformat(), "event": event, **data}
752
+ self._on_audit(entry)
753
+
754
+ def handle_message(self, envelope):
755
+ if not envelope or not isinstance(envelope.get("mcps"), dict):
756
+ self._audit("rejected", {"reason": "invalid_format"})
757
+ return {"error": ERROR_CODES["INVALID_SIGNATURE"]}
758
+
759
+ mcps_field = envelope["mcps"]
760
+ passport_id = mcps_field.get("passport_id", "")
761
+
762
+ if not self._nonce_store.check(mcps_field.get("nonce", "")):
763
+ self._audit("rejected", {"reason": "replay_attack", "passport_id": passport_id})
764
+ return {"error": ERROR_CODES["REPLAY_ATTACK"]}
765
+
766
+ passport = self._passport_cache.get(passport_id)
767
+ if not passport:
768
+ # Try each trust authority (multi-TA support)
769
+ rev = None
770
+ for ta in self._trust_authorities:
771
+ rev = check_revocation(passport_id, ta)
772
+ if rev["status"] != "UNREACHABLE":
773
+ break
774
+
775
+ if rev["revoked"]:
776
+ self._audit("rejected", {"reason": "revoked", "passport_id": passport_id})
777
+ if self._on_revoked:
778
+ self._on_revoked(passport_id, rev["reason"])
779
+ return {"error": ERROR_CODES["PASSPORT_REVOKED"]}
780
+ if rev["status"] == "UNREACHABLE":
781
+ self._audit("rejected", {"reason": "authority_unreachable_fail_closed", "passport_id": passport_id})
782
+ return {"error": ERROR_CODES["AUTHORITY_UNREACHABLE"]}
783
+ passport = {"passport_id": passport_id, "trust_level": TRUST_LEVELS["VERIFIED"]}
784
+ self._passport_cache[passport_id] = passport
785
+
786
+ effective_level = get_effective_trust_level(passport, self._trusted_issuers)
787
+ if effective_level < self._min_trust_level:
788
+ self._audit("rejected", {"reason": "insufficient_trust", "passport_id": passport_id})
789
+ return {"error": ERROR_CODES["INSUFFICIENT_TRUST"]}
790
+
791
+ if self._origin and passport.get("origin"):
792
+ origin_result = validate_origin(passport, self._origin)
793
+ if not origin_result["valid"]:
794
+ self._audit("rejected", {"reason": "origin_mismatch", "passport_id": passport_id})
795
+ return {"error": ERROR_CODES["ORIGIN_MISMATCH"]}
796
+
797
+ jsonrpc_message = {k: v for k, v in envelope.items() if k != "mcps"}
798
+
799
+ self._audit("accepted", {"passport_id": passport_id, "method": jsonrpc_message.get("method")})
800
+
801
+ if self._mcp_server and hasattr(self._mcp_server, "handle_message"):
802
+ return self._mcp_server.handle_message(jsonrpc_message)
803
+ return {"result": "ok"}
804
+
805
+ def sign(self, mcp_message):
806
+ return sign_message(mcp_message, self._passport, self._private_key)
807
+
808
+ def destroy(self):
809
+ self._nonce_store.destroy()
810
+ self._passport_cache.clear()
811
+
812
+
813
+ def secure_mcp_client(mcp_client, trust_authority=DEFAULT_TRUST_AUTHORITY, on_revoked=None):
814
+ """Create an MCPS client wrapper for verifying server responses."""
815
+ return MCPSClient(mcp_client, trust_authority, on_revoked)
816
+
817
+
818
+ class MCPSClient:
819
+ def __init__(self, mcp_client, trust_authority, on_revoked):
820
+ self._mcp_client = mcp_client
821
+ self._nonce_store = NonceStore()
822
+ self._trust_authority = trust_authority
823
+ self._on_revoked = on_revoked
824
+ self.mcps_version = MCPS_VERSION
825
+
826
+ def send(self, mcp_message, passport_id, private_key):
827
+ envelope = sign_message(mcp_message, passport_id, private_key)
828
+ if self._mcp_client and hasattr(self._mcp_client, "send"):
829
+ return self._mcp_client.send(envelope)
830
+ return envelope
831
+
832
+ def verify(self, envelope):
833
+ if not envelope or not isinstance(envelope.get("mcps"), dict):
834
+ return {"valid": False, "error": ERROR_CODES["INVALID_SIGNATURE"]}
835
+ mcps_field = envelope["mcps"]
836
+ passport_id = mcps_field.get("passport_id", "")
837
+ rev = check_revocation(passport_id, self._trust_authority)
838
+ if rev["revoked"]:
839
+ if self._on_revoked:
840
+ self._on_revoked(passport_id, rev["reason"])
841
+ return {"valid": False, "error": ERROR_CODES["PASSPORT_REVOKED"]}
842
+ if not self._nonce_store.check(mcps_field.get("nonce", "")):
843
+ return {"valid": False, "error": ERROR_CODES["REPLAY_ATTACK"]}
844
+ return {"valid": True, "passport_id": passport_id}
845
+
846
+ def destroy(self):
847
+ self._nonce_store.destroy()
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcps-secure
3
+ Version: 1.0.0
4
+ Summary: MCPS -- MCP Secure. Cryptographic identity, message signing, and trust verification for the Model Context Protocol.
5
+ Author: CyberSecAI Ltd
6
+ License: BUSL-1.1
7
+ Project-URL: Homepage, https://mcp-secure.dev
8
+ Project-URL: Repository, https://github.com/razashariff/mcps
9
+ Project-URL: Documentation, https://mcp-secure.dev
10
+ Keywords: mcp,mcps,mcp-secure,model-context-protocol,agent-security,agent-identity,passport,message-signing,ecdsa,trust,revocation,owasp,agentsign,secure-mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security :: Cryptography
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: cryptography>=41.0.0
26
+ Dynamic: license-file
27
+
28
+ # MCPS -- MCP Secure
29
+
30
+ **Cryptographic identity, message signing, and trust verification for the Model Context Protocol.**
31
+
32
+ The HTTPS of the agent era. MCP becomes MCPS.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install mcp-secure
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from mcp_secure import generate_key_pair, create_passport, sign_passport, verify_passport_signature
44
+ from mcp_secure import sign_message, verify_message, sign_tool, verify_tool
45
+
46
+ # Generate keys
47
+ keys = generate_key_pair()
48
+
49
+ # Create and sign a passport
50
+ passport = create_passport(
51
+ name="my-agent",
52
+ version="1.0.0",
53
+ public_key=keys["public_key"],
54
+ capabilities=["read", "write"],
55
+ )
56
+
57
+ # Trust Authority signs the passport
58
+ ta_keys = generate_key_pair()
59
+ signed = sign_passport(passport, ta_keys["private_key"])
60
+ assert verify_passport_signature(signed, ta_keys["public_key"])
61
+
62
+ # Sign MCP messages
63
+ envelope = sign_message(
64
+ {"jsonrpc": "2.0", "method": "tools/list", "id": 1},
65
+ signed["passport_id"],
66
+ keys["private_key"],
67
+ )
68
+
69
+ # Verify
70
+ result = verify_message(envelope, keys["public_key"])
71
+ assert result["valid"]
72
+
73
+ # Tool integrity
74
+ tool = {"name": "read_file", "description": "Read a file", "inputSchema": {"type": "object"}}
75
+ sig = sign_tool(tool, keys["private_key"])
76
+ assert verify_tool(tool, sig, keys["public_key"])
77
+ ```
78
+
79
+ ## What MCPS Adds to MCP
80
+
81
+ | Feature | Description |
82
+ |---------|-------------|
83
+ | Agent Passports | ECDSA P-256 signed identity credentials |
84
+ | Message Signing | Every JSON-RPC message wrapped in signed envelope |
85
+ | Tool Integrity | Signed tool definitions prevent poisoning |
86
+ | Replay Protection | Nonce + 5-min timestamp window |
87
+ | Revocation | Real-time passport revocation via Trust Authority |
88
+ | Trust Levels | L0 (unsigned) to L4 (audited) |
89
+
90
+ ## OWASP MCP Top 10
91
+
92
+ Mitigates 8/10 OWASP MCP vulnerabilities: tool poisoning, supply chain attacks, auth bypass, shadow servers, and more.
93
+
94
+ ## Links
95
+
96
+ - **Website**: [mcp-secure.dev](https://mcp-secure.dev)
97
+ - **npm**: [mcp-secure](https://www.npmjs.com/package/mcp-secure)
98
+ - **Spec**: [SPEC.md](https://github.com/razashariff/mcps/blob/main/SPEC.md)
99
+
100
+ ## License
101
+
102
+ MIT -- CyberSecAI Ltd
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ mcp_secure/__init__.py
5
+ mcp_secure/core.py
6
+ mcps_secure.egg-info/PKG-INFO
7
+ mcps_secure.egg-info/SOURCES.txt
8
+ mcps_secure.egg-info/dependency_links.txt
9
+ mcps_secure.egg-info/requires.txt
10
+ mcps_secure.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ cryptography>=41.0.0
@@ -0,0 +1 @@
1
+ mcp_secure
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mcps-secure"
7
+ version = "1.0.0"
8
+ description = "MCPS -- MCP Secure. Cryptographic identity, message signing, and trust verification for the Model Context Protocol."
9
+ readme = "README.md"
10
+ license = {text = "BUSL-1.1"}
11
+ requires-python = ">=3.9"
12
+ authors = [{name = "CyberSecAI Ltd"}]
13
+ keywords = [
14
+ "mcp", "mcps", "mcp-secure", "model-context-protocol",
15
+ "agent-security", "agent-identity", "passport",
16
+ "message-signing", "ecdsa", "trust", "revocation",
17
+ "owasp", "agentsign", "secure-mcp",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: Other/Proprietary License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Security :: Cryptography",
30
+ "Topic :: Software Development :: Libraries",
31
+ ]
32
+ dependencies = [
33
+ "cryptography>=41.0.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://mcp-secure.dev"
38
+ Repository = "https://github.com/razashariff/mcps"
39
+ Documentation = "https://mcp-secure.dev"
40
+
41
+ [tool.setuptools.packages.find]
42
+ include = ["mcp_secure*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+