|
| 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 | + |
0 commit comments