capiscio-sdk 0.3.0__tar.gz → 2.3.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.
- capiscio_sdk-2.3.0/.github/copilot-instructions.md +511 -0
- capiscio_sdk-2.3.0/.github/workflows/integration-tests.yml +238 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.github/workflows/pr-checks.yml +7 -3
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.github/workflows/publish.yml +4 -2
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.gitignore +6 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/CHANGELOG.md +31 -1
- capiscio_sdk-2.3.0/Dockerfile.test +23 -0
- capiscio_sdk-2.3.0/PKG-INFO +532 -0
- capiscio_sdk-2.3.0/README.md +486 -0
- capiscio_sdk-2.3.0/capiscio_sdk/__init__.py +110 -0
- capiscio_sdk-2.3.0/capiscio_sdk/_rpc/__init__.py +7 -0
- capiscio_sdk-2.3.0/capiscio_sdk/_rpc/client.py +1321 -0
- capiscio_sdk-2.3.0/capiscio_sdk/_rpc/gen/__init__.py +1 -0
- capiscio_sdk-2.3.0/capiscio_sdk/_rpc/process.py +232 -0
- capiscio_sdk-2.3.0/capiscio_sdk/badge.py +737 -0
- capiscio_sdk-2.3.0/capiscio_sdk/badge_keeper.py +304 -0
- capiscio_sdk-2.3.0/capiscio_sdk/dv.py +296 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/executor.py +5 -5
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/integrations/fastapi.py +3 -2
- capiscio_sdk-2.3.0/capiscio_sdk/scoring/__init__.py +112 -0
- capiscio_sdk-2.3.0/capiscio_sdk/simple_guard.py +346 -0
- capiscio_sdk-2.3.0/capiscio_sdk/validators/__init__.py +75 -0
- capiscio_sdk-2.3.0/capiscio_sdk/validators/_core.py +376 -0
- capiscio_sdk-2.3.0/docs/api-reference.md +79 -0
- capiscio_sdk-2.3.0/docs/guides/badge-verification.md +238 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/guides/scoring.md +4 -4
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/secure_ping_pong/client.py +4 -4
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/mkdocs.yml +2 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/pyproject.toml +40 -1
- capiscio_sdk-2.3.0/tests/integration/Dockerfile.test +33 -0
- capiscio_sdk-2.3.0/tests/integration/README.md +130 -0
- capiscio_sdk-2.3.0/tests/integration/docker-compose.yml +62 -0
- capiscio_sdk-2.3.0/tests/integration/requirements.txt +7 -0
- capiscio_sdk-2.3.0/tests/integration/test_badge_keeper.py +322 -0
- capiscio_sdk-2.3.0/tests/integration/test_dv_badge_flow.py +671 -0
- capiscio_sdk-2.3.0/tests/integration/test_dv_order_api.py +295 -0
- capiscio_sdk-2.3.0/tests/integration/test_dv_sdk.py +156 -0
- capiscio_sdk-2.3.0/tests/integration/test_grpc_scoring.py +102 -0
- capiscio_sdk-2.3.0/tests/integration/test_server_integration.py +413 -0
- capiscio_sdk-2.3.0/tests/integration/test_simple_guard.py +268 -0
- capiscio_sdk-2.3.0/tests/unit/test_badge.py +494 -0
- capiscio_sdk-2.3.0/tests/unit/test_badge_keeper.py +342 -0
- capiscio_sdk-2.3.0/tests/unit/test_core_validator.py +249 -0
- capiscio_sdk-2.3.0/tests/unit/test_fastapi_integration.py +106 -0
- capiscio_sdk-2.3.0/tests/unit/test_pop_badge.py +302 -0
- capiscio_sdk-2.3.0/tests/unit/test_simple_guard.py +204 -0
- capiscio_sdk-0.3.0/PKG-INFO +0 -126
- capiscio_sdk-0.3.0/README.md +0 -82
- capiscio_sdk-0.3.0/capiscio_sdk/__init__.py +0 -44
- capiscio_sdk-0.3.0/capiscio_sdk/scoring/__init__.py +0 -42
- capiscio_sdk-0.3.0/capiscio_sdk/simple_guard.py +0 -354
- capiscio_sdk-0.3.0/capiscio_sdk/validators/__init__.py +0 -18
- capiscio_sdk-0.3.0/tests/unit/test_fastapi_integration.py +0 -71
- capiscio_sdk-0.3.0/tests/unit/test_simple_guard.py +0 -154
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.github/markdown-link-check-config.json +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.github/workflows/docs.yml +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/.python-version +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/CONTRIBUTING.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/LICENSE +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/QUICK_REFERENCE.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/RELEASE_GUIDE.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/SECURITY.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/config.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/errors.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/infrastructure/__init__.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/infrastructure/cache.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/infrastructure/rate_limiter.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/py.typed +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/scoring/availability.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/scoring/compliance.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/scoring/trust.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/scoring/types.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/types.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/agent_card.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/certificate.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/message.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/protocol.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/semver.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/signature.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/capiscio_sdk/validators/url_security.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/assets/.!58931!favicon.ico +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/assets/favicon.ico +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/assets/logo.png +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/getting-started/concepts.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/getting-started/installation.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/getting-started/quickstart.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/guides/configuration.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/includes/abbreviations.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/index.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/javascripts/extra.js +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/stylesheets/extra.css +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/docs/stylesheets/unified.css +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/README.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/secure_ping_pong/README.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/secure_ping_pong/server.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/simple_agent/README.md +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/simple_agent/agent_executor.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/simple_agent/main.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/simple_agent/requirements.txt +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/examples/simple_agent/test_client.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/__init__.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/e2e/__init__.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/integration/__init__.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/integration/test_real_executor.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/__init__.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_agent_card.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_cache.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_certificate.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_config.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_errors.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_executor.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_message_validator.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_protocol_validator.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_rate_limiter.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_semver_validator.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_signature_validator.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_types.py +0 -0
- {capiscio_sdk-0.3.0 → capiscio_sdk-2.3.0}/tests/unit/test_url_security.py +0 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# capiscio-sdk-python - GitHub Copilot Instructions
|
|
2
|
+
|
|
3
|
+
## 🛑 ABSOLUTE RULES - NO EXCEPTIONS
|
|
4
|
+
|
|
5
|
+
These rules are non-negotiable. Violating them will cause production issues.
|
|
6
|
+
|
|
7
|
+
### 1. ALL WORK VIA PULL REQUESTS
|
|
8
|
+
- **NEVER commit directly to `main`.** All changes MUST go through PRs.
|
|
9
|
+
- Create feature branches: `feature/`, `fix/`, `chore/`
|
|
10
|
+
- PRs require CI to pass before merge consideration
|
|
11
|
+
|
|
12
|
+
### 2. LOCAL CI VALIDATION BEFORE PUSH
|
|
13
|
+
- **ALL tests MUST pass locally before pushing to a PR.**
|
|
14
|
+
- Run: `pytest -v`
|
|
15
|
+
- If tests fail locally, fix them before pushing. Never push failing code.
|
|
16
|
+
|
|
17
|
+
### 3. RFCs ARE READ-ONLY
|
|
18
|
+
- **DO NOT modify RFCs without explicit team authorization.**
|
|
19
|
+
- Implementation must conform to RFCs in `capiscio-rfcs/`
|
|
20
|
+
|
|
21
|
+
### 4. NO WATCH/BLOCKING COMMANDS
|
|
22
|
+
- **NEVER run blocking commands** without timeout
|
|
23
|
+
- Use `timeout` wrapper for long-running commands
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Repository Purpose
|
|
28
|
+
|
|
29
|
+
**capiscio-sdk-python** is the official Python SDK for CapiscIO, providing:
|
|
30
|
+
- SimpleGuard: Runtime badge verification middleware
|
|
31
|
+
- gRPC Client: Interface to capiscio-core gRPC services
|
|
32
|
+
- Badge verification utilities
|
|
33
|
+
- DID resolution helpers
|
|
34
|
+
|
|
35
|
+
**Technology Stack**: Python 3.9+, gRPC, cryptography, FastAPI/Flask integration
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
capiscio_sdk/
|
|
41
|
+
├── __init__.py
|
|
42
|
+
├── simple_guard.py # Middleware for FastAPI/Flask
|
|
43
|
+
├── grpc_client.py # gRPC client for capiscio-core
|
|
44
|
+
├── badge_verifier.py # Badge verification logic
|
|
45
|
+
├── did_resolver.py # DID resolution
|
|
46
|
+
├── models.py # Data models
|
|
47
|
+
└── exceptions.py # Custom exceptions
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Critical Development Rules
|
|
51
|
+
|
|
52
|
+
### 1. SimpleGuard Middleware
|
|
53
|
+
|
|
54
|
+
**FastAPI Integration:**
|
|
55
|
+
```python
|
|
56
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
57
|
+
from capiscio_sdk import SimpleGuard, BadgeVerificationError
|
|
58
|
+
|
|
59
|
+
app = FastAPI()
|
|
60
|
+
|
|
61
|
+
# Initialize SimpleGuard
|
|
62
|
+
guard = SimpleGuard(
|
|
63
|
+
issuer_url="https://registry.capisc.io",
|
|
64
|
+
min_trust_level=1, # Don't accept self-signed
|
|
65
|
+
cache_ttl=300, # 5 minutes
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@app.middleware("http")
|
|
69
|
+
async def verify_badge_middleware(request: Request, call_next):
|
|
70
|
+
# Skip health checks
|
|
71
|
+
if request.url.path == "/health":
|
|
72
|
+
return await call_next(request)
|
|
73
|
+
|
|
74
|
+
# Extract badge from header
|
|
75
|
+
badge_token = request.headers.get("X-CapiscIO-Badge")
|
|
76
|
+
if not badge_token:
|
|
77
|
+
raise HTTPException(status_code=401, detail="Missing badge")
|
|
78
|
+
|
|
79
|
+
# Verify badge
|
|
80
|
+
try:
|
|
81
|
+
badge = await guard.verify(badge_token)
|
|
82
|
+
request.state.badge = badge
|
|
83
|
+
request.state.agent_did = badge.subject
|
|
84
|
+
except BadgeVerificationError as e:
|
|
85
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
86
|
+
|
|
87
|
+
return await call_next(request)
|
|
88
|
+
|
|
89
|
+
@app.get("/protected")
|
|
90
|
+
async def protected_route(request: Request):
|
|
91
|
+
agent_did = request.state.agent_did
|
|
92
|
+
return {"message": f"Hello {agent_did}"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Flask Integration:**
|
|
96
|
+
```python
|
|
97
|
+
from flask import Flask, request, g, jsonify
|
|
98
|
+
from capiscio_sdk import SimpleGuard, BadgeVerificationError
|
|
99
|
+
|
|
100
|
+
app = Flask(__name__)
|
|
101
|
+
|
|
102
|
+
guard = SimpleGuard(
|
|
103
|
+
issuer_url="https://registry.capisc.io",
|
|
104
|
+
min_trust_level=1,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@app.before_request
|
|
108
|
+
def verify_badge():
|
|
109
|
+
# Skip health checks
|
|
110
|
+
if request.path == "/health":
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Extract badge
|
|
114
|
+
badge_token = request.headers.get("X-CapiscIO-Badge")
|
|
115
|
+
if not badge_token:
|
|
116
|
+
return jsonify({"error": "Missing badge"}), 401
|
|
117
|
+
|
|
118
|
+
# Verify badge
|
|
119
|
+
try:
|
|
120
|
+
badge = guard.verify_sync(badge_token)
|
|
121
|
+
g.badge = badge
|
|
122
|
+
g.agent_did = badge.subject
|
|
123
|
+
except BadgeVerificationError as e:
|
|
124
|
+
return jsonify({"error": str(e)}), 401
|
|
125
|
+
|
|
126
|
+
@app.route("/protected")
|
|
127
|
+
def protected_route():
|
|
128
|
+
return jsonify({"message": f"Hello {g.agent_did}"})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. gRPC Client Usage
|
|
132
|
+
|
|
133
|
+
**Initialize Client:**
|
|
134
|
+
```python
|
|
135
|
+
from capiscio_sdk import CapiscioGRPCClient
|
|
136
|
+
|
|
137
|
+
# Connect to capiscio-core gRPC server
|
|
138
|
+
client = CapiscioGRPCClient(
|
|
139
|
+
address="localhost:50051",
|
|
140
|
+
secure=False, # Use TLS in production
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Badge Verification:**
|
|
145
|
+
```python
|
|
146
|
+
from capiscio_sdk.grpc_client import VerifyBadgeRequest
|
|
147
|
+
|
|
148
|
+
# Verify badge via gRPC
|
|
149
|
+
request = VerifyBadgeRequest(
|
|
150
|
+
token="eyJhbGc...",
|
|
151
|
+
issuer_url="https://registry.capisc.io",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
response = client.verify_badge(request)
|
|
155
|
+
|
|
156
|
+
if response.valid:
|
|
157
|
+
print(f"Badge valid for {response.badge.subject}")
|
|
158
|
+
print(f"Trust level: {response.badge.trust_level}")
|
|
159
|
+
else:
|
|
160
|
+
print(f"Badge invalid: {response.error}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**DID Resolution:**
|
|
164
|
+
```python
|
|
165
|
+
from capiscio_sdk.grpc_client import ResolveDIDRequest
|
|
166
|
+
|
|
167
|
+
request = ResolveDIDRequest(
|
|
168
|
+
did="did:web:registry.capisc.io:agents:my-agent"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
response = client.resolve_did(request)
|
|
172
|
+
|
|
173
|
+
if response.success:
|
|
174
|
+
print(f"Public key: {response.did_document.verification_method[0].public_key_jwk}")
|
|
175
|
+
else:
|
|
176
|
+
print(f"Resolution failed: {response.error}")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Gateway Validation:**
|
|
180
|
+
```python
|
|
181
|
+
from capiscio_sdk.grpc_client import ValidateGatewayRequest
|
|
182
|
+
|
|
183
|
+
# Validate incoming request at gateway
|
|
184
|
+
request = ValidateGatewayRequest(
|
|
185
|
+
badge_token="eyJhbGc...",
|
|
186
|
+
target_url="https://agent.example.com/endpoint",
|
|
187
|
+
min_trust_level=1,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
response = client.validate_gateway(request)
|
|
191
|
+
|
|
192
|
+
if response.allowed:
|
|
193
|
+
print(f"Request allowed for agent: {response.agent_did}")
|
|
194
|
+
else:
|
|
195
|
+
print(f"Request denied: {response.reason}")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 3. Badge Verification Logic
|
|
199
|
+
|
|
200
|
+
**Core Verification Function:**
|
|
201
|
+
```python
|
|
202
|
+
import jwt
|
|
203
|
+
import requests
|
|
204
|
+
from typing import Dict, Any
|
|
205
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
206
|
+
|
|
207
|
+
class BadgeVerifier:
|
|
208
|
+
def __init__(self, issuer_url: str, min_trust_level: int = 1):
|
|
209
|
+
self.issuer_url = issuer_url
|
|
210
|
+
self.min_trust_level = min_trust_level
|
|
211
|
+
self._jwks_cache: Dict[str, Any] = {}
|
|
212
|
+
|
|
213
|
+
async def verify(self, token: str) -> Badge:
|
|
214
|
+
# Step 1: Parse token
|
|
215
|
+
unverified_claims = jwt.decode(
|
|
216
|
+
token,
|
|
217
|
+
options={"verify_signature": False}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Step 2: Fetch JWKS
|
|
221
|
+
jwks = await self._fetch_jwks(unverified_claims["iss"])
|
|
222
|
+
|
|
223
|
+
# Step 3: Verify signature
|
|
224
|
+
try:
|
|
225
|
+
claims = jwt.decode(
|
|
226
|
+
token,
|
|
227
|
+
key=jwks,
|
|
228
|
+
algorithms=["EdDSA"],
|
|
229
|
+
options={"verify_signature": True}
|
|
230
|
+
)
|
|
231
|
+
except jwt.InvalidSignatureError:
|
|
232
|
+
raise BadgeVerificationError("Invalid signature")
|
|
233
|
+
|
|
234
|
+
# Step 4: Validate claims
|
|
235
|
+
self._validate_claims(claims)
|
|
236
|
+
|
|
237
|
+
# Step 5: Create badge object
|
|
238
|
+
return Badge.from_claims(claims)
|
|
239
|
+
|
|
240
|
+
async def _fetch_jwks(self, issuer: str) -> Dict[str, Any]:
|
|
241
|
+
# Check cache
|
|
242
|
+
if issuer in self._jwks_cache:
|
|
243
|
+
return self._jwks_cache[issuer]
|
|
244
|
+
|
|
245
|
+
# Fetch from issuer
|
|
246
|
+
jwks_url = f"{issuer}/.well-known/jwks.json"
|
|
247
|
+
response = requests.get(jwks_url, timeout=5)
|
|
248
|
+
response.raise_for_status()
|
|
249
|
+
|
|
250
|
+
jwks = response.json()
|
|
251
|
+
self._jwks_cache[issuer] = jwks
|
|
252
|
+
|
|
253
|
+
return jwks
|
|
254
|
+
|
|
255
|
+
def _validate_claims(self, claims: Dict[str, Any]) -> None:
|
|
256
|
+
# Check required claims
|
|
257
|
+
required = ["iss", "sub", "jti", "exp", "iat", "trust_level"]
|
|
258
|
+
for claim in required:
|
|
259
|
+
if claim not in claims:
|
|
260
|
+
raise BadgeVerificationError(f"Missing claim: {claim}")
|
|
261
|
+
|
|
262
|
+
# Check expiration
|
|
263
|
+
if claims["exp"] < time.time():
|
|
264
|
+
raise BadgeVerificationError("Badge expired")
|
|
265
|
+
|
|
266
|
+
# Check trust level
|
|
267
|
+
if claims["trust_level"] < self.min_trust_level:
|
|
268
|
+
raise BadgeVerificationError(
|
|
269
|
+
f"Trust level {claims['trust_level']} below minimum {self.min_trust_level}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Check DID format
|
|
273
|
+
if not claims["sub"].startswith("did:"):
|
|
274
|
+
raise BadgeVerificationError("Invalid DID format")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 4. Data Models
|
|
278
|
+
|
|
279
|
+
**Badge Model:**
|
|
280
|
+
```python
|
|
281
|
+
from dataclasses import dataclass
|
|
282
|
+
from typing import Optional
|
|
283
|
+
from datetime import datetime
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class Badge:
|
|
287
|
+
issuer: str
|
|
288
|
+
subject: str # Agent DID
|
|
289
|
+
token_id: str
|
|
290
|
+
expires_at: datetime
|
|
291
|
+
issued_at: datetime
|
|
292
|
+
not_before: Optional[datetime]
|
|
293
|
+
trust_level: int
|
|
294
|
+
ial: Optional[int] = None
|
|
295
|
+
cnf: Optional[dict] = None
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def from_claims(cls, claims: dict) -> "Badge":
|
|
299
|
+
return cls(
|
|
300
|
+
issuer=claims["iss"],
|
|
301
|
+
subject=claims["sub"],
|
|
302
|
+
token_id=claims["jti"],
|
|
303
|
+
expires_at=datetime.fromtimestamp(claims["exp"]),
|
|
304
|
+
issued_at=datetime.fromtimestamp(claims["iat"]),
|
|
305
|
+
not_before=datetime.fromtimestamp(claims["nbf"]) if "nbf" in claims else None,
|
|
306
|
+
trust_level=claims["trust_level"],
|
|
307
|
+
ial=claims.get("ial"),
|
|
308
|
+
cnf=claims.get("cnf"),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def is_expired(self) -> bool:
|
|
313
|
+
return datetime.now() > self.expires_at
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def is_ial1(self) -> bool:
|
|
317
|
+
return self.ial == 1 and self.cnf is not None
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 5. Exception Handling
|
|
321
|
+
|
|
322
|
+
**Custom Exceptions:**
|
|
323
|
+
```python
|
|
324
|
+
class BadgeVerificationError(Exception):
|
|
325
|
+
"""Raised when badge verification fails"""
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
class DIDResolutionError(Exception):
|
|
329
|
+
"""Raised when DID resolution fails"""
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
class GRPCConnectionError(Exception):
|
|
333
|
+
"""Raised when gRPC connection fails"""
|
|
334
|
+
pass
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Usage:**
|
|
338
|
+
```python
|
|
339
|
+
try:
|
|
340
|
+
badge = await guard.verify(token)
|
|
341
|
+
except BadgeVerificationError as e:
|
|
342
|
+
logger.error(f"Badge verification failed: {e}")
|
|
343
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.exception("Unexpected error during verification")
|
|
346
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Testing
|
|
350
|
+
|
|
351
|
+
### Unit Tests
|
|
352
|
+
```python
|
|
353
|
+
import pytest
|
|
354
|
+
from capiscio_sdk import BadgeVerifier, BadgeVerificationError
|
|
355
|
+
|
|
356
|
+
@pytest.mark.asyncio
|
|
357
|
+
async def test_verify_valid_badge():
|
|
358
|
+
verifier = BadgeVerifier(
|
|
359
|
+
issuer_url="https://registry.capisc.io",
|
|
360
|
+
min_trust_level=1
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
token = generate_test_badge() # Helper function
|
|
364
|
+
|
|
365
|
+
badge = await verifier.verify(token)
|
|
366
|
+
|
|
367
|
+
assert badge.trust_level >= 1
|
|
368
|
+
assert badge.subject.startswith("did:")
|
|
369
|
+
|
|
370
|
+
@pytest.mark.asyncio
|
|
371
|
+
async def test_verify_expired_badge():
|
|
372
|
+
verifier = BadgeVerifier(
|
|
373
|
+
issuer_url="https://registry.capisc.io",
|
|
374
|
+
min_trust_level=1
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
token = generate_expired_badge()
|
|
378
|
+
|
|
379
|
+
with pytest.raises(BadgeVerificationError, match="expired"):
|
|
380
|
+
await verifier.verify(token)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Integration Tests
|
|
384
|
+
```python
|
|
385
|
+
@pytest.mark.integration
|
|
386
|
+
def test_grpc_client_verify_badge():
|
|
387
|
+
client = CapiscioGRPCClient(address="localhost:50051")
|
|
388
|
+
|
|
389
|
+
request = VerifyBadgeRequest(token=test_token)
|
|
390
|
+
response = client.verify_badge(request)
|
|
391
|
+
|
|
392
|
+
assert response.valid
|
|
393
|
+
assert response.badge.trust_level >= 1
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Common Development Tasks
|
|
397
|
+
|
|
398
|
+
### Installing
|
|
399
|
+
```bash
|
|
400
|
+
# Install in development mode
|
|
401
|
+
pip install -e .
|
|
402
|
+
|
|
403
|
+
# Install with all extras
|
|
404
|
+
pip install -e ".[dev,grpc]"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Running Tests
|
|
408
|
+
```bash
|
|
409
|
+
# Run all tests
|
|
410
|
+
pytest
|
|
411
|
+
|
|
412
|
+
# Run with coverage
|
|
413
|
+
pytest --cov=capiscio_sdk --cov-report=html
|
|
414
|
+
|
|
415
|
+
# Run specific test
|
|
416
|
+
pytest tests/test_simple_guard.py -v
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Type Checking
|
|
420
|
+
```bash
|
|
421
|
+
# Run mypy
|
|
422
|
+
mypy capiscio_sdk/
|
|
423
|
+
|
|
424
|
+
# Run with strict mode
|
|
425
|
+
mypy --strict capiscio_sdk/
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Linting
|
|
429
|
+
```bash
|
|
430
|
+
# Run ruff
|
|
431
|
+
ruff check capiscio_sdk/
|
|
432
|
+
|
|
433
|
+
# Auto-fix
|
|
434
|
+
ruff check --fix capiscio_sdk/
|
|
435
|
+
|
|
436
|
+
# Format
|
|
437
|
+
ruff format capiscio_sdk/
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Code Quality Standards
|
|
441
|
+
|
|
442
|
+
### 1. Type Hints
|
|
443
|
+
```python
|
|
444
|
+
# ✅ Use type hints
|
|
445
|
+
async def verify(self, token: str) -> Badge:
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
# ❌ No type hints
|
|
449
|
+
async def verify(self, token):
|
|
450
|
+
pass
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 2. Docstrings
|
|
454
|
+
```python
|
|
455
|
+
def verify_sync(self, token: str) -> Badge:
|
|
456
|
+
"""Verify a badge synchronously.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
token: The JWS badge token
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Badge: The verified badge object
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
BadgeVerificationError: If verification fails
|
|
466
|
+
"""
|
|
467
|
+
pass
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 3. Async/Await
|
|
471
|
+
```python
|
|
472
|
+
# ✅ Use async for I/O operations
|
|
473
|
+
async def fetch_jwks(self, issuer: str) -> dict:
|
|
474
|
+
async with aiohttp.ClientSession() as session:
|
|
475
|
+
async with session.get(f"{issuer}/.well-known/jwks.json") as response:
|
|
476
|
+
return await response.json()
|
|
477
|
+
|
|
478
|
+
# Also provide sync version for compatibility
|
|
479
|
+
def fetch_jwks_sync(self, issuer: str) -> dict:
|
|
480
|
+
response = requests.get(f"{issuer}/.well-known/jwks.json")
|
|
481
|
+
return response.json()
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Common Pitfalls
|
|
485
|
+
|
|
486
|
+
1. **Don't skip signature verification** - Always verify JWS
|
|
487
|
+
2. **Don't accept self-signed badges in production** - Set min_trust_level >= 1
|
|
488
|
+
3. **Don't cache badges forever** - Implement TTL
|
|
489
|
+
4. **Don't ignore exceptions** - Handle verification errors properly
|
|
490
|
+
5. **Don't log sensitive data** - Redact tokens in logs
|
|
491
|
+
|
|
492
|
+
## Environment Variables
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
# gRPC Client
|
|
496
|
+
CAPISCIO_GRPC_ADDRESS="localhost:50051"
|
|
497
|
+
CAPISCIO_GRPC_SECURE="false"
|
|
498
|
+
|
|
499
|
+
# Badge Verification
|
|
500
|
+
CAPISCIO_ISSUER_URL="https://registry.capisc.io"
|
|
501
|
+
CAPISCIO_MIN_TRUST_LEVEL="1"
|
|
502
|
+
CAPISCIO_CACHE_TTL="300"
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## References
|
|
506
|
+
|
|
507
|
+
- gRPC documentation: 225 lines in docs/grpc-integration.md
|
|
508
|
+
- Badge verification: docs/badge-verification.md
|
|
509
|
+
- DID resolution: docs/did-resolution.md
|
|
510
|
+
- RFC-002: https://github.com/capiscio/capiscio-rfcs/blob/main/docs/002-trust-badge.md
|
|
511
|
+
- RFC-003: https://github.com/capiscio/capiscio-rfcs/blob/main/docs/003-key-ownership-proof.md
|