@agentunion/fastaun-browser 0.2.18 → 0.2.20
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.
- package/CHANGELOG.md +26 -0
- package/_packed_docs/CHANGELOG.md +26 -0
- package/_packed_docs/agent.md/SCHEMA.md +173 -0
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +43 -0
- package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +205 -0
- package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +197 -0
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +549 -0
- package/_packed_docs/protocol/02-/350/257/201/344/271/246/344/270/216/344/277/241/344/273/273/344/275/223/347/263/273.md +810 -0
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +262 -0
- package/_packed_docs/protocol/04-Peer-/345/255/220/345/215/217/350/256/256.md +180 -0
- package/_packed_docs/protocol/05-Relay-/345/255/220/345/215/217/350/256/256.md +164 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1135 -0
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +234 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +900 -0
- package/_packed_docs/protocol/08-AUN-E2EE.md +413 -0
- package/_packed_docs/protocol/09-/345/256/211/345/205/250/350/200/203/350/231/221.md +316 -0
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +804 -0
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +271 -0
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +329 -0
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +141 -0
- package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -0
- package/_packed_docs/protocol/README.md +71 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +118 -0
- package/_packed_docs/protocol/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/protocol/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/protocol/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/protocol/aun-docs-guide.md +49 -0
- package/_packed_docs/protocol/index.md +114 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-agent.md/347/255/276/345/220/215/345/215/217/350/256/256.md +205 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +249 -0
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +337 -0
- package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +80 -0
- package/_packed_docs/protocol//351/231/204/345/275/225C-/347/247/201/351/222/245/347/256/241/347/220/206/344/270/216/350/272/253/344/273/275/346/201/242/345/244/215.md +704 -0
- package/_packed_docs/protocol//351/231/204/345/275/225D-Root_CA_/346/262/273/347/220/206/346/234/272/345/210/266.md +620 -0
- package/_packed_docs/protocol//351/231/204/345/275/225E-Root_CA_/345/207/206/345/205/245/346/265/201/347/250/213.md +605 -0
- package/_packed_docs/protocol//351/231/204/345/275/225F-Issuer_CA_/347/224/263/350/257/267/346/265/201/347/250/213.md +548 -0
- package/_packed_docs/protocol//351/231/204/345/275/225G-AID_/345/255/244/345/204/277/351/242/204/351/230/262/344/270/216/346/225/221/346/217/264/346/234/272/345/210/266.md +513 -0
- package/_packed_docs/protocol//351/231/204/345/275/225H-Identity/346/234/215/345/212/241/345/256/236/347/216/260/346/214/207/345/215/227.md +619 -0
- package/_packed_docs/protocol//351/231/204/345/275/225I-/350/267/250/345/237/237/346/266/210/346/201/257/350/267/257/347/224/261/345/256/236/347/216/260/346/214/207/345/215/227.md +492 -0
- package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +402 -0
- package/_packed_docs/protocol//351/231/204/345/275/225K-Agent_Web/345/217/221/347/216/260/345/215/217/350/256/256.md +130 -0
- package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +267 -0
- package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +367 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +223 -0
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +354 -0
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +172 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +373 -0
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +611 -0
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1152 -0
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +150 -0
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +89 -0
- package/_packed_docs/sdk/09-custody-api-manual.md +445 -0
- package/_packed_docs/sdk/09-group-rpc-manual.md +1895 -0
- package/_packed_docs/sdk/09-message-rpc-manual.md +597 -0
- package/_packed_docs/sdk/09-meta-rpc-manual.md +142 -0
- package/_packed_docs/sdk/09-payload-reference.md +702 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +408 -0
- package/_packed_docs/sdk/09-stream-rpc-manual.md +275 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +72 -0
- package/_packed_docs/sdk/INDEX.md +131 -0
- package/_packed_docs/sdk/README.md +307 -0
- package/dist/auth.d.ts +11 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +92 -17
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +51 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +394 -122
- package/dist/client.js.map +1 -1
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +20 -0
- package/dist/e2ee.js.map +1 -1
- package/dist/keystore/index.d.ts +11 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +35 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +91 -0
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/namespaces/auth.d.ts +10 -3
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +94 -15
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +24 -0
- package/dist/transport.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
# 附录 H:Auth 服务实现指南(非规范性)
|
|
2
|
+
|
|
3
|
+
> **本文档为非规范性内容**:提供 Auth 服务的实现建议、验证流程、安全约束和代码示例,不是协议强制要求。
|
|
4
|
+
|
|
5
|
+
## H.1 证书链验证实现
|
|
6
|
+
|
|
7
|
+
### H.1.1 Auth 服务验证客户端证书链
|
|
8
|
+
|
|
9
|
+
在 `auth.aid_login1` 阶段,Auth 服务需要验证客户端提交的证书链:
|
|
10
|
+
|
|
11
|
+
**推荐验证步骤**:
|
|
12
|
+
```
|
|
13
|
+
1. 解析客户端提交的 Agent 证书(如 alice.aid.pub)
|
|
14
|
+
2. 提取证书中的 AIA 扩展,获取 Issuer CA URL
|
|
15
|
+
3. 下载 Issuer CA 证书(aid.pub)并缓存
|
|
16
|
+
4. 验证证书链签名:
|
|
17
|
+
- 用 aid.pub 公钥验证 alice.aid.pub 签名 ✓
|
|
18
|
+
- 用 Root CA 公钥验证 aid.pub 签名 ✓
|
|
19
|
+
5. 检查证书有效期(notBefore/notAfter)
|
|
20
|
+
6. 检查证书吊销状态(推荐,CRL/OCSP)
|
|
21
|
+
7. 验证证书 CN 与 AID 一致
|
|
22
|
+
8. 验证通过,继续处理
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**实现示例(Node.js)**:
|
|
26
|
+
```javascript
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const x509 = require('@peculiar/x509');
|
|
29
|
+
|
|
30
|
+
async function verifyCertChain(certPem, aid) {
|
|
31
|
+
// 1. 解析证书
|
|
32
|
+
const cert = new x509.X509Certificate(certPem);
|
|
33
|
+
|
|
34
|
+
// 2. 验证 CN 与 AID 一致
|
|
35
|
+
const cn = cert.subject.split(',').find(s => s.startsWith('CN=')).split('=')[1];
|
|
36
|
+
if (cn !== aid) {
|
|
37
|
+
throw new Error('Certificate CN does not match AID');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. 检查有效期
|
|
41
|
+
const now = new Date();
|
|
42
|
+
if (now < cert.notBefore || now > cert.notAfter) {
|
|
43
|
+
throw new Error('Certificate expired or not yet valid');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. 提取 AIA 扩展,下载 Issuer CA
|
|
47
|
+
const aiaExt = cert.getExtension('1.3.6.1.5.5.7.1.1'); // AIA OID
|
|
48
|
+
const issuerCaUrl = extractIssuerCaUrl(aiaExt);
|
|
49
|
+
const issuerCaCert = await downloadAndCacheCert(issuerCaUrl);
|
|
50
|
+
|
|
51
|
+
// 5. 验证签名链
|
|
52
|
+
const issuerPublicKey = issuerCaCert.publicKey;
|
|
53
|
+
const isValid = cert.verify(issuerPublicKey);
|
|
54
|
+
if (!isValid) {
|
|
55
|
+
throw new Error('Certificate signature verification failed');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 6. 验证 Issuer CA 到 Root CA
|
|
59
|
+
const rootCaCert = await getRootCaCert();
|
|
60
|
+
const issuerValid = issuerCaCert.verify(rootCaCert.publicKey);
|
|
61
|
+
if (!issuerValid) {
|
|
62
|
+
throw new Error('Issuer CA signature verification failed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 7. 检查吊销状态(可选但推荐)
|
|
66
|
+
await checkRevocationStatus(cert);
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### H.1.2 客户端验证 Auth 服务证书链
|
|
73
|
+
|
|
74
|
+
在 `auth.aid_login1` 响应阶段,客户端需要验证 Auth 服务的证书链:
|
|
75
|
+
|
|
76
|
+
**推荐验证步骤**:
|
|
77
|
+
```
|
|
78
|
+
1. 解析 Auth 服务返回的 auth_cert(如 auth.aid.pub)
|
|
79
|
+
2. 提取 AIA 扩展,下载 Issuer CA(aid.pub)
|
|
80
|
+
3. 验证证书链签名到 Root CA
|
|
81
|
+
4. 检查证书有效期
|
|
82
|
+
5. 用 auth_cert 公钥验证 client_nonce_signature
|
|
83
|
+
6. 验证通过,确认 Auth 服务身份真实
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**实现示例(浏览器 JavaScript)**:
|
|
87
|
+
```javascript
|
|
88
|
+
async function verifyAuthService(authCert, clientNonce, clientNonceSignature) {
|
|
89
|
+
// 1. 解析证书
|
|
90
|
+
const certDer = pemToDer(authCert);
|
|
91
|
+
const cert = await parseCertificate(certDer);
|
|
92
|
+
|
|
93
|
+
// 2. 检查有效期
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (now < cert.notBefore || now > cert.notAfter) {
|
|
96
|
+
throw new Error('Auth service certificate expired');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. 下载并验证 Issuer CA
|
|
100
|
+
const issuerCaUrl = extractIssuerCaUrl(cert);
|
|
101
|
+
const issuerCaCert = await fetch(issuerCaUrl).then(r => r.text());
|
|
102
|
+
|
|
103
|
+
// 4. 验证证书链到 Root CA
|
|
104
|
+
await verifyCertChainToRoot(cert, issuerCaCert);
|
|
105
|
+
|
|
106
|
+
// 5. 验证 client_nonce 签名
|
|
107
|
+
const publicKey = await crypto.subtle.importKey(
|
|
108
|
+
'spki',
|
|
109
|
+
cert.publicKey,
|
|
110
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
111
|
+
false,
|
|
112
|
+
['verify']
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const signatureValid = await crypto.subtle.verify(
|
|
116
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
117
|
+
publicKey,
|
|
118
|
+
base64ToArrayBuffer(clientNonceSignature),
|
|
119
|
+
new TextEncoder().encode(clientNonce)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!signatureValid) {
|
|
123
|
+
throw new Error('Auth service signature verification failed');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## H.2 签名验证实现
|
|
131
|
+
|
|
132
|
+
### H.2.1 login_aid2 服务端验证流程
|
|
133
|
+
|
|
134
|
+
**推荐验证步骤**:
|
|
135
|
+
```
|
|
136
|
+
1. 验证 request_id 与 login_aid1 中的一致
|
|
137
|
+
2. 验证 nonce 与 login_aid1 中返回的一致且未过期(推荐有效期 30 秒)
|
|
138
|
+
3. 从 cert 中提取公钥
|
|
139
|
+
4. 验证签名:ECDSA_verify(public_key, SHA256(nonce + ":" + client_time), signature)
|
|
140
|
+
5. (可选)检查 client_time 与 server_time 的偏差,仅用于审计日志,不影响认证
|
|
141
|
+
6. 签名验证通过后,生成 JWT token
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**实现示例(Node.js)**:
|
|
145
|
+
```javascript
|
|
146
|
+
async function verifyLoginAid2(params, sessionData) {
|
|
147
|
+
// 1. 验证 request_id
|
|
148
|
+
if (params.request_id !== sessionData.request_id) {
|
|
149
|
+
throw new Error('Request ID mismatch');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. 验证 nonce
|
|
153
|
+
if (params.nonce !== sessionData.nonce) {
|
|
154
|
+
throw new Error('Nonce mismatch');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nonceAge = Date.now() - sessionData.nonceCreatedAt;
|
|
158
|
+
if (nonceAge > 30000) { // 30 秒
|
|
159
|
+
throw new Error('Nonce expired');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3. 提取公钥
|
|
163
|
+
const cert = new x509.X509Certificate(params.cert);
|
|
164
|
+
const publicKey = await crypto.subtle.importKey(
|
|
165
|
+
'spki',
|
|
166
|
+
cert.publicKey.rawData,
|
|
167
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
168
|
+
false,
|
|
169
|
+
['verify']
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 4. 验证签名
|
|
173
|
+
const message = `${params.nonce}:${params.client_time}`;
|
|
174
|
+
const messageBuffer = new TextEncoder().encode(message);
|
|
175
|
+
const signatureBuffer = Buffer.from(params.signature, 'base64');
|
|
176
|
+
|
|
177
|
+
const isValid = await crypto.subtle.verify(
|
|
178
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
179
|
+
publicKey,
|
|
180
|
+
signatureBuffer,
|
|
181
|
+
messageBuffer
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!isValid) {
|
|
185
|
+
throw new Error('Signature verification failed');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 5. 时钟偏移检查(仅审计)
|
|
189
|
+
const serverTime = Math.floor(Date.now() / 1000);
|
|
190
|
+
const clockSkew = Math.abs(serverTime - params.client_time);
|
|
191
|
+
if (clockSkew > 300) { // 5 分钟
|
|
192
|
+
console.warn(`Large clock skew detected: ${clockSkew}s for ${params.aid}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 6. 生成 JWT token
|
|
196
|
+
const token = await generateJwtToken(params.aid);
|
|
197
|
+
|
|
198
|
+
return { token, expires_in: 3600 };
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### H.2.2 客户端签名实现
|
|
203
|
+
|
|
204
|
+
**浏览器实现示例**:
|
|
205
|
+
```javascript
|
|
206
|
+
// 客户端签名 nonce
|
|
207
|
+
async function signNonce(privateKey, nonce, clientTime) {
|
|
208
|
+
const message = `${nonce}:${clientTime}`;
|
|
209
|
+
const messageBuffer = new TextEncoder().encode(message);
|
|
210
|
+
|
|
211
|
+
const signature = await crypto.subtle.sign(
|
|
212
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
213
|
+
privateKey,
|
|
214
|
+
messageBuffer
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return arrayBufferToBase64(signature);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 使用示例
|
|
221
|
+
const nonce = "server_challenge_nonce";
|
|
222
|
+
const clientTime = Math.floor(Date.now() / 1000);
|
|
223
|
+
const signature = await signNonce(privateKey, nonce, clientTime);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## H.3 Nonce 管理实现
|
|
227
|
+
|
|
228
|
+
### H.3.1 Nonce 生命周期管理
|
|
229
|
+
|
|
230
|
+
**推荐实现**:
|
|
231
|
+
```javascript
|
|
232
|
+
class NonceManager {
|
|
233
|
+
constructor() {
|
|
234
|
+
this.nonces = new Map(); // key: aid+request_id, value: {nonce, createdAt, consumed}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 生成 nonce
|
|
238
|
+
create(aid, requestId) {
|
|
239
|
+
const nonce = crypto.randomUUID();
|
|
240
|
+
const key = `${aid}:${requestId}`;
|
|
241
|
+
|
|
242
|
+
this.nonces.set(key, {
|
|
243
|
+
nonce,
|
|
244
|
+
createdAt: Date.now(),
|
|
245
|
+
consumed: false
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 60 秒后自动清理
|
|
249
|
+
setTimeout(() => this.nonces.delete(key), 60000);
|
|
250
|
+
|
|
251
|
+
return nonce;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 验证并消费 nonce
|
|
255
|
+
consume(aid, requestId, nonce) {
|
|
256
|
+
const key = `${aid}:${requestId}`;
|
|
257
|
+
const entry = this.nonces.get(key);
|
|
258
|
+
|
|
259
|
+
if (!entry) {
|
|
260
|
+
throw new Error('Nonce not found or expired');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (entry.consumed) {
|
|
264
|
+
throw new Error('Nonce already consumed');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (entry.nonce !== nonce) {
|
|
268
|
+
throw new Error('Nonce mismatch');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const age = Date.now() - entry.createdAt;
|
|
272
|
+
if (age > 60000) { // 60 秒
|
|
273
|
+
this.nonces.delete(key);
|
|
274
|
+
throw new Error('Nonce expired');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 标记为已消费
|
|
278
|
+
entry.consumed = true;
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**推荐配置**:
|
|
286
|
+
- Nonce 有效期:30-60 秒
|
|
287
|
+
- Nonce 格式:UUID v4
|
|
288
|
+
- 存储:内存(Redis)或数据库
|
|
289
|
+
- 清理策略:过期自动删除
|
|
290
|
+
|
|
291
|
+
## H.4 Token 刷新限制实现
|
|
292
|
+
|
|
293
|
+
### H.4.1 刷新链管理
|
|
294
|
+
|
|
295
|
+
**推荐限制**:
|
|
296
|
+
- 刷新链总时长:不超过 30 天
|
|
297
|
+
- 最大刷新次数:720 次(每小时刷新一次 × 30 天)
|
|
298
|
+
- 达到限制后:返回错误,客户端必须重新执行完整认证
|
|
299
|
+
|
|
300
|
+
**实现示例**:
|
|
301
|
+
```javascript
|
|
302
|
+
class TokenRefreshManager {
|
|
303
|
+
constructor() {
|
|
304
|
+
this.refreshChains = new Map(); // key: aid, value: {firstIssued, refreshCount}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async refreshToken(aid, currentToken) {
|
|
308
|
+
// 验证当前 token
|
|
309
|
+
const payload = await verifyJwtToken(currentToken);
|
|
310
|
+
|
|
311
|
+
// 获取刷新链信息
|
|
312
|
+
let chain = this.refreshChains.get(aid);
|
|
313
|
+
if (!chain) {
|
|
314
|
+
chain = {
|
|
315
|
+
firstIssued: payload.iat * 1000,
|
|
316
|
+
refreshCount: 0
|
|
317
|
+
};
|
|
318
|
+
this.refreshChains.set(aid, chain);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 检查时长限制(30 天)
|
|
322
|
+
const chainAge = Date.now() - chain.firstIssued;
|
|
323
|
+
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 天
|
|
324
|
+
if (chainAge > maxAge) {
|
|
325
|
+
this.refreshChains.delete(aid);
|
|
326
|
+
throw new Error('Refresh chain expired, please re-authenticate');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 检查次数限制(720 次)
|
|
330
|
+
if (chain.refreshCount >= 720) {
|
|
331
|
+
this.refreshChains.delete(aid);
|
|
332
|
+
throw new Error('Refresh limit exceeded, please re-authenticate');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 生成新 token
|
|
336
|
+
const newToken = await generateJwtToken(aid);
|
|
337
|
+
chain.refreshCount++;
|
|
338
|
+
|
|
339
|
+
return { token: newToken, expires_in: 3600 };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**说明**:
|
|
345
|
+
- 这些限制值是推荐值,具体由服务端实现决定
|
|
346
|
+
- 可以根据安全需求调整限制值
|
|
347
|
+
- 建议在 token payload 中包含刷新链信息
|
|
348
|
+
|
|
349
|
+
## H.5 证书续期实现
|
|
350
|
+
|
|
351
|
+
### H.5.1 renew_cert 验证流程
|
|
352
|
+
|
|
353
|
+
**推荐验证步骤**:
|
|
354
|
+
```
|
|
355
|
+
1. 客户端调用 auth.aid_login1 获取 nonce
|
|
356
|
+
2. 用旧私钥签名 nonce
|
|
357
|
+
3. 调用 auth.renew_cert,提交旧证书 + 签名
|
|
358
|
+
4. Auth 服务验证:
|
|
359
|
+
- nonce 未被消费且未过期
|
|
360
|
+
- 旧证书中的公钥与签名匹配
|
|
361
|
+
- 旧证书的 Subject 与 AID 一致
|
|
362
|
+
- 旧证书在宽限期内(推荐 ≤ 90 天)
|
|
363
|
+
5. Auth 服务用同一公钥生成新 CSR,提交 CA 签发新证书
|
|
364
|
+
6. nonce 标记为已消费,不可复用
|
|
365
|
+
7. 返回新证书,客户端替换本地证书
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**安全约束(推荐值)**:
|
|
369
|
+
- 宽限期:≤ 90 天(证书过期后 90 天内可续期)
|
|
370
|
+
- 超过宽限期:必须重新走 `auth.create_aid`
|
|
371
|
+
- 旧证书必须未被吊销
|
|
372
|
+
- 新证书复用原公钥
|
|
373
|
+
|
|
374
|
+
**实现示例**:
|
|
375
|
+
```javascript
|
|
376
|
+
async function renewCert(params) {
|
|
377
|
+
// 1. 验证 nonce
|
|
378
|
+
await nonceManager.consume(params.aid, params.request_id, params.nonce);
|
|
379
|
+
|
|
380
|
+
// 2. 解析旧证书
|
|
381
|
+
const oldCert = new x509.X509Certificate(params.old_cert);
|
|
382
|
+
|
|
383
|
+
// 3. 检查证书 Subject 与 AID 一致
|
|
384
|
+
const cn = extractCN(oldCert.subject);
|
|
385
|
+
if (cn !== params.aid) {
|
|
386
|
+
throw new Error('Certificate CN does not match AID');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 4. 检查宽限期(90 天)
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
const expiredAt = oldCert.notAfter.getTime();
|
|
392
|
+
const gracePeriod = 90 * 24 * 60 * 60 * 1000; // 90 天
|
|
393
|
+
|
|
394
|
+
if (now - expiredAt > gracePeriod) {
|
|
395
|
+
throw new Error('Certificate expired beyond grace period');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 5. 检查吊销状态
|
|
399
|
+
const isRevoked = await checkRevocationStatus(oldCert);
|
|
400
|
+
if (isRevoked) {
|
|
401
|
+
throw new Error('Certificate has been revoked');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 6. 验证签名(证明持有私钥)
|
|
405
|
+
const publicKey = oldCert.publicKey;
|
|
406
|
+
const isValid = await crypto.subtle.verify(
|
|
407
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
408
|
+
publicKey,
|
|
409
|
+
Buffer.from(params.signature, 'base64'),
|
|
410
|
+
new TextEncoder().encode(params.nonce)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (!isValid) {
|
|
414
|
+
throw new Error('Signature verification failed');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 7. 用同一公钥生成新证书
|
|
418
|
+
const newCert = await caService.signCert({
|
|
419
|
+
aid: params.aid,
|
|
420
|
+
publicKey: publicKey,
|
|
421
|
+
validityDays: 365
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
status: 'renewed',
|
|
426
|
+
cert: newCert.certPem,
|
|
427
|
+
ca_cert: newCert.caCertPem
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## H.6 密钥轮转实现
|
|
433
|
+
|
|
434
|
+
### H.6.1 rekey 验证流程
|
|
435
|
+
|
|
436
|
+
**推荐验证步骤**:
|
|
437
|
+
```
|
|
438
|
+
1. 客户端生成新密钥对
|
|
439
|
+
2. 调用 auth.aid_login1 获取 nonce
|
|
440
|
+
3. 用旧私钥签名 nonce + new_public_key(防止公钥替换攻击)
|
|
441
|
+
4. 调用 auth.rekey,提交旧证书 + 新公钥 + 签名
|
|
442
|
+
5. Auth 服务验证:
|
|
443
|
+
- nonce 未被消费且未过期
|
|
444
|
+
- 旧证书公钥与签名匹配
|
|
445
|
+
- Subject 与 AID 一致
|
|
446
|
+
- 旧证书在有效期内或宽限期内
|
|
447
|
+
6. Auth 服务用新公钥生成 CSR,提交 CA 签发新证书
|
|
448
|
+
7. nonce 标记为已消费
|
|
449
|
+
8. 返回新证书,客户端保存新证书和新私钥
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**签名内容**:
|
|
453
|
+
```
|
|
454
|
+
message = nonce + new_public_key
|
|
455
|
+
signature = ECDSA_sign(old_private_key, SHA256(message))
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**实现示例**:
|
|
459
|
+
```javascript
|
|
460
|
+
async function rekey(params) {
|
|
461
|
+
// 1. 验证 nonce
|
|
462
|
+
await nonceManager.consume(params.aid, params.request_id, params.nonce);
|
|
463
|
+
|
|
464
|
+
// 2. 解析旧证书
|
|
465
|
+
const oldCert = new x509.X509Certificate(params.old_cert);
|
|
466
|
+
|
|
467
|
+
// 3. 检查证书 Subject 与 AID 一致
|
|
468
|
+
const cn = extractCN(oldCert.subject);
|
|
469
|
+
if (cn !== params.aid) {
|
|
470
|
+
throw new Error('Certificate CN does not match AID');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 4. 检查证书有效期或宽限期
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
const expiredAt = oldCert.notAfter.getTime();
|
|
476
|
+
const gracePeriod = 90 * 24 * 60 * 60 * 1000;
|
|
477
|
+
|
|
478
|
+
if (now > expiredAt + gracePeriod) {
|
|
479
|
+
throw new Error('Certificate expired beyond grace period');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 5. 验证签名(nonce + new_public_key)
|
|
483
|
+
const message = params.nonce + params.new_public_key;
|
|
484
|
+
const publicKey = oldCert.publicKey;
|
|
485
|
+
|
|
486
|
+
const isValid = await crypto.subtle.verify(
|
|
487
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
488
|
+
publicKey,
|
|
489
|
+
Buffer.from(params.signature, 'base64'),
|
|
490
|
+
new TextEncoder().encode(message)
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
if (!isValid) {
|
|
494
|
+
throw new Error('Signature verification failed');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 6. 解析新公钥
|
|
498
|
+
const newPublicKey = Buffer.from(params.new_public_key, 'base64');
|
|
499
|
+
|
|
500
|
+
// 7. 用新公钥生成新证书
|
|
501
|
+
const newCert = await caService.signCert({
|
|
502
|
+
aid: params.aid,
|
|
503
|
+
publicKey: newPublicKey,
|
|
504
|
+
validityDays: 365
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
status: 'rekeyed',
|
|
509
|
+
cert: newCert.certPem,
|
|
510
|
+
ca_cert: newCert.caCertPem
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## H.7 证书自动续期实现
|
|
516
|
+
|
|
517
|
+
### H.7.1 login_aid2 自动续期
|
|
518
|
+
|
|
519
|
+
在 `login_aid2` 响应中,当证书有效期过半时自动返回新证书:
|
|
520
|
+
|
|
521
|
+
**实现示例**:
|
|
522
|
+
```javascript
|
|
523
|
+
async function checkAndRenewCert(cert) {
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
const notBefore = cert.notBefore.getTime();
|
|
526
|
+
const notAfter = cert.notAfter.getTime();
|
|
527
|
+
const totalValidity = notAfter - notBefore;
|
|
528
|
+
const remaining = notAfter - now;
|
|
529
|
+
|
|
530
|
+
// 有效期过半时自动续期
|
|
531
|
+
if (remaining < totalValidity / 2) {
|
|
532
|
+
const newCert = await caService.signCert({
|
|
533
|
+
aid: extractCN(cert.subject),
|
|
534
|
+
publicKey: cert.publicKey,
|
|
535
|
+
validityDays: 365
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
new_cert: newCert.certPem,
|
|
540
|
+
ca_cert: newCert.caCertPem
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 在 login_aid2 响应中使用
|
|
548
|
+
async function loginAid2(params) {
|
|
549
|
+
// ... 验证签名等 ...
|
|
550
|
+
|
|
551
|
+
const token = await generateJwtToken(params.aid);
|
|
552
|
+
const result = {
|
|
553
|
+
status: 'ok',
|
|
554
|
+
aid: params.aid,
|
|
555
|
+
token,
|
|
556
|
+
expires_in: 3600
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// 检查是否需要续期
|
|
560
|
+
const cert = new x509.X509Certificate(params.cert);
|
|
561
|
+
const renewResult = await checkAndRenewCert(cert);
|
|
562
|
+
|
|
563
|
+
if (renewResult) {
|
|
564
|
+
result.new_cert = renewResult.new_cert;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**说明**:
|
|
572
|
+
- 自动续期是可选功能,由服务端实现决定
|
|
573
|
+
- 推荐在证书有效期过半时触发
|
|
574
|
+
- 客户端应检查 `new_cert` 字段并更新本地证书
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## H.8 Token 生命周期与证书轮换实现
|
|
579
|
+
|
|
580
|
+
### H.8.1 Token 生命周期管理
|
|
581
|
+
|
|
582
|
+
**推荐配置**:
|
|
583
|
+
- **有效期**:1-24 小时(推荐 1 小时)
|
|
584
|
+
- **过期处理**:客户端需要重新认证
|
|
585
|
+
- **刷新机制**:通过 `auth.refresh_token` 刷新
|
|
586
|
+
- **刷新限制**:刷新链总时长不超过 30 天,或最多刷新 720 次
|
|
587
|
+
|
|
588
|
+
**双 Token 模式**(推荐):
|
|
589
|
+
```
|
|
590
|
+
短期 access_token(1 小时)+ 长期 refresh_token(7 天)
|
|
591
|
+
access_token 过期后,用 refresh_token 换取新的 access_token
|
|
592
|
+
达到刷新限制后,必须重新用证书签名登录
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### H.8.2 证书轮换参考数值
|
|
596
|
+
|
|
597
|
+
各层级证书轮换的推荐数值:
|
|
598
|
+
|
|
599
|
+
| 层级 | 典型有效期 | 过渡期 | 影响范围 |
|
|
600
|
+
|------|-----------|--------|---------|
|
|
601
|
+
| Root CA | 20-30 年 | 数年 | 全局信任锚,所有客户端证书库需更新 |
|
|
602
|
+
| Issuer CA | 10-15 年 | 1-2 年 | 该 Issuer 下所有 Agent |
|
|
603
|
+
| Auth 服务证书 | 1-2 年 | ≤ 旧 token 最大剩余有效期 | 已签发的 JWT |
|
|
604
|
+
|
|
605
|
+
**说明**:以上数值为推荐值,具体由各 Issuer 的安全策略决定。
|
|
606
|
+
|
|
607
|
+
### H.8.3 审计与监控
|
|
608
|
+
|
|
609
|
+
**推荐审计日志**:
|
|
610
|
+
- Auth 服务记录所有 token 签发日志
|
|
611
|
+
- 服务记录 token 验证失败日志
|
|
612
|
+
- 可以追踪异常 token 使用模式(如短时间内大量刷新、跨地域使用等)
|
|
613
|
+
|
|
614
|
+
**Token 撤销**(推荐实现):
|
|
615
|
+
- 维护 token 黑名单(内存或 Redis)
|
|
616
|
+
- 检测到异常时可主动撤销 token
|
|
617
|
+
- 黑名单条目在 token 自然过期后自动清理
|
|
618
|
+
|
|
619
|
+
---
|