Skip to content

Commit 6db9776

Browse files
committed
wss did wba test ok
1 parent f9f8f4c commit 6db9776

33 files changed

+6861
-20
lines changed

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ share/python-wheels/
2626
*.egg
2727
MANIFEST
2828

29+
# did docs
30+
docs/did_info/*
31+
*.pem
32+
2933
# PyInstaller
3034
# Usually these files are written by a python script from a template
3135
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -182,9 +186,9 @@ cython_debug/
182186
.abstra/
183187

184188
# Visual Studio Code
185-
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
189+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186190
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187-
# and can be added to the global gitignore or merged into this file. However, if you prefer,
191+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
188192
# you could uncomment the following to ignore the entire vscode folder
189193
# .vscode/
190194

anp_proxy/anp_sdk/__init__.py

Whitespace-only changes.

anp_proxy/anp_sdk/anp-did-spec.md

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
## ANP DID-WBA 身份认证规范与使用指南
2+
3+
本指南面向需要在工程中集成 ANP 的 DID-WBA(Web-Based Attestation for DID)身份认证的开发者,基于本仓库 `anp_sdk/anp_auth` 中现有实现,说明“请求端(Client)”与“验证端(Server)”如何使用 `agent_connect` 模块完成认证,并给出可直接复制的代码示例。
4+
5+
文档中的示例代码遵循如下约定:
6+
- 代码注释与日志使用英文;
7+
- 示例使用 FastAPI 作为服务端框架,`aiohttp` 作为客户端 HTTP 库;
8+
- JWT 使用 RS256,私钥签发、公网验签;
9+
- 依赖 `agent_connect.authentication` 中的核心能力:
10+
- `DIDWbaAuthHeader`
11+
- `verify_auth_header_signature`
12+
- `resolve_did_wba_document`
13+
- `extract_auth_header_parts`
14+
- `create_did_wba_document`
15+
16+
17+
### 1. 名词与流程概览
18+
19+
- DID-WBA Authorization Header:请求端基于 DID 文档与私钥生成的认证头,包含至少以下要素:`did``nonce``timestamp``verification_method``signature`(具体串行化由 `DIDWbaAuthHeader` 实现)。
20+
- Nonce:一次性随机数,服务端采用“使用即作废 + 过期清理”的策略防止重放。
21+
- Timestamp:请求时间戳,服务端校验时间窗口(默认 5 分钟)。
22+
- JWT Bearer:服务端在 DID 校验成功后签发的短期访问令牌,后续请求可直接使用 `Authorization: Bearer <token>`
23+
24+
认证流程(高层次):
25+
1) 客户端使用 `DIDWbaAuthHeader` 为目标 URL 生成 DID-WBA 认证头发起访问;
26+
2) 服务器检测到非 Bearer 的 Authorization,则进入 DID 校验:解析 header → 校验时间戳与 nonce → 解析 DID 文档并验签;
27+
3) 校验通过后,服务器生成 JWT(RS256)并随响应头返回(`authorization: bearer <token>`);
28+
4) 客户端保存该 token,后续请求可直接使用 Bearer 访问受保护资源;若 401 失效则清空并回退到 DID-WBA 重新获取。
29+
30+
31+
### 2. 依赖与配置
32+
33+
建议的依赖(以 pip 为例):
34+
```
35+
fastapi
36+
uvicorn
37+
aiohttp
38+
pyjwt
39+
pydantic-settings
40+
agent_connect # 提供 DID-WBA 的头部构造与验签、DID 文档解析等
41+
```
42+
43+
核心配置项(参考 `octopus/config/settings.py`):
44+
- DID 相关
45+
- `nonce_expiration_minutes`:服务端 nonce 有效期(默认 5)
46+
- `timestamp_expiration_minutes`:时间戳有效期(默认 5)
47+
- `did_documents_path`:客户端生成 DID 文档与密钥的目录
48+
- `did_document_filename`:DID 文档文件名(`did.json`
49+
- `local_port`:本地服务端口(用于生成示例 DID 文档中的回链)
50+
- JWT 相关
51+
- `jwt_algorithm`:JWT 算法(RS256)
52+
- `access_token_expire_minutes`:JWT 过期时间(建议 30~120 分钟)
53+
- `jwt_private_key_path`:服务端私钥 PEM 文件路径(签发)
54+
- `jwt_public_key_path`:服务端公钥 PEM 文件路径(验签)
55+
56+
57+
### 3. 验证端(Server)实现
58+
59+
本仓库提供了完整的 FastAPI 中间件实现,可直接复用:
60+
- `octopus/anp_sdk/anp_auth/auth_middleware.py`
61+
- `octopus/anp_sdk/anp_auth/did_auth.py`
62+
- `octopus/anp_sdk/anp_auth/token_auth.py`
63+
- `octopus/anp_sdk/anp_auth/jwt_keys.py`
64+
65+
要点:
66+
- 统一入口中间件 `auth_middleware`
67+
- 放行白名单路径(`EXEMPT_PATHS`)。
68+
- 非 Bearer 的 Authorization 头按 DID-WBA 流程处理;
69+
- Bearer 头使用公钥校验 JWT;
70+
- 通过后把 `authorization` 写回响应头,便于客户端提取。
71+
- DID 校验逻辑(`did_auth.handle_did_auth`):
72+
- 解析 header 得到 `did/nonce/timestamp/verification_method/signature`
73+
- 验证时间戳窗口(`verify_timestamp`);
74+
- 校验并登记 nonce(`is_valid_server_nonce`,一次性);
75+
- 解析 DID 文档(`resolve_did_wba_document`);
76+
- 使用文档与服务域名验签(`verify_auth_header_signature`);
77+
- 生成 JWT 并返回(`token_auth.create_access_token`)。
78+
- JWT 校验逻辑(`token_auth.handle_bearer_auth`):
79+
- 提取 Bearer token;
80+
- 用公钥验签并检查 `sub/iat/exp` 与 DID 前缀;
81+
- 返回携带 `did` 的身份信息。
82+
83+
示例:将认证中间件接入 FastAPI(其他工程可参考)
84+
85+
```python
86+
# app.py
87+
from fastapi import FastAPI
88+
from octopus.utils.log_base import setup_enhanced_logging
89+
from octopus.anp_sdk.anp_auth.auth_middleware import auth_middleware
90+
91+
92+
def create_app() -> FastAPI:
93+
# Initialize logging
94+
setup_enhanced_logging()
95+
96+
app = FastAPI()
97+
98+
# Register DID/JWT auth middleware
99+
app.middleware("http")(auth_middleware)
100+
101+
@app.get("/v1/status")
102+
async def status():
103+
return {"status": "ok"}
104+
105+
@app.get("/secure/me")
106+
async def me(request):
107+
# Example: read headers stored by middleware
108+
return {"headers": dict(request.state.headers)}
109+
110+
return app
111+
112+
113+
app = create_app()
114+
```
115+
116+
注意:
117+
- 确保 `settings.jwt_private_key_path``settings.jwt_public_key_path` 指向有效的 PEM 文件;
118+
- 放行路径应根据自身工程需求调整(例如 `/docs`, `/openapi.json` 等)。
119+
120+
121+
### 4. 请求端(Client)实现
122+
123+
客户端有两种常见方式:
124+
1) 直接使用 `DIDWbaAuthHeader` 生成 DID-WBA 头并发起请求;
125+
2) 复用本仓库提供的轻量封装 `ANPClient`,自动处理 token 缓存与 401 回退。
126+
127+
方式 A:直接使用 `DIDWbaAuthHeader`
128+
129+
```python
130+
# client_direct.py
131+
import asyncio
132+
import aiohttp
133+
from agent_connect.authentication import DIDWbaAuthHeader
134+
from octopus.utils.log_base import setup_enhanced_logging
135+
136+
137+
async def main():
138+
# Initialize logging
139+
setup_enhanced_logging()
140+
141+
# Paths to DID document and private key
142+
did_document_path = "./did.json"
143+
private_key_path = "./key-1_private.pem"
144+
145+
# Initialize DID-WBA client
146+
auth_client = DIDWbaAuthHeader(
147+
did_document_path=did_document_path,
148+
private_key_path=private_key_path,
149+
)
150+
151+
url = "https://your-api.example.com/secure/me"
152+
153+
# Build DID-WBA authorization header for the target URL
154+
headers = auth_client.get_auth_header(url)
155+
156+
async with aiohttp.ClientSession() as session:
157+
# First request with DID-WBA (expect server to return Bearer in response headers)
158+
async with session.get(url, headers=headers) as resp:
159+
# Update token from response headers for subsequent calls
160+
auth_client.update_token(url, dict(resp.headers))
161+
data = await resp.json()
162+
print("First call status:", resp.status, "payload:", data)
163+
164+
# Subsequent request can use Bearer automatically via get_auth_header
165+
headers2 = auth_client.get_auth_header(url)
166+
async with session.get(url, headers=headers2) as resp2:
167+
data2 = await resp2.json()
168+
print("Second call status:", resp2.status, "payload:", data2)
169+
170+
171+
if __name__ == "__main__":
172+
asyncio.run(main())
173+
```
174+
175+
方式 B:使用 `ANPClient`(本仓库封装)
176+
177+
```python
178+
# client_anp.py
179+
import asyncio
180+
from octopus.utils.log_base import setup_enhanced_logging
181+
from octopus.anp_sdk.anp_crawler.anp_client import ANPClient
182+
183+
184+
async def main():
185+
setup_enhanced_logging()
186+
187+
client = ANPClient(
188+
did_document_path="./did.json",
189+
private_key_path="./key-1_private.pem",
190+
)
191+
192+
# Auto add DID-WBA header; on 401, ANPClient will clear token and retry with fresh DID
193+
result = await client.fetch_url("https://your-api.example.com/secure/me")
194+
print(result)
195+
196+
197+
if __name__ == "__main__":
198+
asyncio.run(main())
199+
```
200+
201+
202+
### 5. DID 文档与私钥生成
203+
204+
可直接使用现成工具方法生成/加载 DID 文档与私钥(参考 `octopus/anp_sdk/anp_auth/did_auth.py` 中的 `generate_or_load_did`):
205+
206+
```python
207+
# did_bootstrap.py
208+
import asyncio
209+
from octopus.utils.log_base import setup_enhanced_logging
210+
from octopus.anp_sdk.anp_auth.did_auth import generate_or_load_did
211+
212+
213+
async def main():
214+
setup_enhanced_logging()
215+
216+
# unique_id 可选,用于区分不同用户的 DID 存储目录
217+
did_document, keys, did_dir = await generate_or_load_did(unique_id="user01")
218+
print("DID document saved under:", did_dir)
219+
print("DID:", did_document.get("id"))
220+
221+
222+
if __name__ == "__main__":
223+
asyncio.run(main())
224+
```
225+
226+
说明:
227+
- 函数会在配置的 `did_keys/user_<unique_id>/` 下生成 `did.json` 与对应的私钥 PEM;
228+
- 其中私钥 PEM 文件名遵循 `#fragment` 命名(如 `keys-1_private.pem`)。
229+
230+
231+
### 6. 服务端关键实现要点(参考实现)
232+
233+
以下逻辑已在仓库中提供,可直接复用或拷贝到其他工程:
234+
235+
- 路径豁免(`EXEMPT_PATHS`):放行静态与健康检查;
236+
- 鉴权入口(`verify_auth_header`):
237+
- `Authorization` 缺失 → 401;
238+
- 非 Bearer → `handle_did_auth`
239+
- Bearer → `handle_bearer_auth`
240+
- DID 校验:
241+
- `extract_auth_header_parts(authorization)` 解析头;
242+
- `verify_timestamp(ts)` 校验时间窗口;
243+
- `is_valid_server_nonce(nonce)` 防重放(一次性);
244+
- `resolve_did_wba_document(did)` 获取 DID 文档;
245+
- `verify_auth_header_signature(auth_header, did_document, domain)` 验签;
246+
- `create_access_token({"sub": did})` 生成 Bearer;
247+
- Bearer 校验:
248+
- 使用公钥 `jwt.decode(token, public_key, algorithms=["RS256"])`
249+
- 校验 `sub/iat/exp` 与 DID 前缀;
250+
- 拒绝未来时间签发或已过期;
251+
- 响应头透传:将 `authorization` 写回,便于客户端自动学习 token。
252+
253+
254+
### 7. 端到端调用示例(最小可运行片段)
255+
256+
服务端:
257+
258+
```python
259+
# server_minimal.py
260+
from fastapi import FastAPI
261+
from octopus.utils.log_base import setup_enhanced_logging
262+
from octopus.anp_sdk.anp_auth.auth_middleware import auth_middleware
263+
264+
265+
app = FastAPI()
266+
setup_enhanced_logging()
267+
app.middleware("http")(auth_middleware)
268+
269+
270+
@app.get("/secure/ping")
271+
async def secure_ping():
272+
return {"pong": True}
273+
```
274+
275+
客户端:
276+
277+
```python
278+
# client_minimal.py
279+
import asyncio
280+
import aiohttp
281+
from agent_connect.authentication import DIDWbaAuthHeader
282+
283+
284+
async def main():
285+
url = "http://127.0.0.1:8000/secure/ping"
286+
auth = DIDWbaAuthHeader(did_document_path="./did.json", private_key_path="./key-1_private.pem")
287+
288+
async with aiohttp.ClientSession() as s:
289+
# First call with DID-WBA header
290+
h = auth.get_auth_header(url)
291+
async with s.get(url, headers=h) as r1:
292+
auth.update_token(url, dict(r1.headers))
293+
print("first:", r1.status, await r1.json())
294+
295+
# Second call will use Bearer automatically
296+
h2 = auth.get_auth_header(url)
297+
async with s.get(url, headers=h2) as r2:
298+
print("second:", r2.status, await r2.json())
299+
300+
301+
if __name__ == "__main__":
302+
asyncio.run(main())
303+
```
304+
305+
306+
### 8. 常见问题(FAQ)
307+
308+
- 时间戳校验失败:确保客户端与服务端时间同步,或适当放宽窗口(默认 5 分钟)。
309+
- Nonce 重放被拒绝:每次请求都应由客户端生成新的 header;服务端会登记并拒绝重复。
310+
- 响应头未返回 authorization:确认服务端在中间件中成功签发并写入响应头;
311+
- JWT 验签失败:检查 `jwt_public_key_path` 与私钥配对是否正确,确认算法一致(RS256)。
312+
- 401 后自动恢复:客户端应在 401 时清空缓存 token 并重新获取 DID-WBA 头(`clear_token(url)`)。
313+
314+
315+
### 9. 安全与最佳实践
316+
317+
- 私钥仅存储在可信环境,避免写入公共仓库或镜像;
318+
- 全程使用 HTTPS 传输;
319+
- Bearer 的有效期要短、可续签;
320+
- Nonce 存储采用“使用即作废 + 定期过期清理”;
321+
- 服务端详细日志请避免记录完整签名内容,仅记录必要诊断信息。
322+
323+
324+
### 10. 参考 API 索引(来自本仓库)
325+
326+
- 请求端
327+
- `agent_connect.authentication.DIDWbaAuthHeader`
328+
- `get_auth_header(url: str) -> dict`
329+
- `update_token(url: str, headers: dict) -> Optional[str]`
330+
- `clear_token(url: str) -> None`
331+
- `octopus.anp_sdk.anp_crawler.anp_client.ANPClient.fetch_url(...)`
332+
333+
- 验证端
334+
- `octopus.anp_sdk.anp_auth.auth_middleware.auth_middleware`
335+
- `octopus.anp_sdk.anp_auth.did_auth.handle_did_auth(authorization: str, domain: str)`
336+
- `octopus.anp_sdk.anp_auth.did_auth.get_and_validate_domain(request)`
337+
- `octopus.anp_sdk.anp_auth.token_auth.handle_bearer_auth(token: str)`
338+
- `octopus.anp_sdk.anp_auth.token_auth.create_access_token(data: dict, expires_delta: Optional[timedelta] = None)`
339+
- `octopus.anp_sdk.anp_auth.jwt_keys.get_jwt_private_key(path)` / `get_jwt_public_key(path)`
340+
341+
342+
以上内容可作为其他工程接入 ANP DID-WBA 认证的模板。直接拷贝“服务端中间件 + 客户端最小示例”即可快速运行;如需更强的自动化(401 回退等),推荐使用 `ANPClient` 封装。
343+
344+
345+
346+
347+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .did_wba_verifier import DidWbaVerifier, DidWbaVerifierConfig, DidWbaVerifierError
2+
3+
__all__ = [
4+
"DidWbaVerifier",
5+
"DidWbaVerifierConfig",
6+
"DidWbaVerifierError",
7+
]

0 commit comments

Comments
 (0)