@agentunion/fastaun 0.2.13

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.
Files changed (69) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +78 -0
  3. package/dist/auth.d.ts +287 -0
  4. package/dist/auth.js +1668 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/client.d.ts +359 -0
  7. package/dist/client.js +3918 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/config.d.ts +43 -0
  10. package/dist/config.js +119 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/crypto.d.ts +41 -0
  13. package/dist/crypto.js +85 -0
  14. package/dist/crypto.js.map +1 -0
  15. package/dist/discovery.d.ts +22 -0
  16. package/dist/discovery.js +110 -0
  17. package/dist/discovery.js.map +1 -0
  18. package/dist/e2ee-group.d.ts +192 -0
  19. package/dist/e2ee-group.js +1134 -0
  20. package/dist/e2ee-group.js.map +1 -0
  21. package/dist/e2ee.d.ts +120 -0
  22. package/dist/e2ee.js +890 -0
  23. package/dist/e2ee.js.map +1 -0
  24. package/dist/errors.d.ts +115 -0
  25. package/dist/errors.js +253 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/events.d.ts +39 -0
  28. package/dist/events.js +82 -0
  29. package/dist/events.js.map +1 -0
  30. package/dist/index.d.ts +23 -0
  31. package/dist/index.js +32 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keystore/aid-db.d.ts +79 -0
  34. package/dist/keystore/aid-db.js +621 -0
  35. package/dist/keystore/aid-db.js.map +1 -0
  36. package/dist/keystore/file.d.ts +82 -0
  37. package/dist/keystore/file.js +395 -0
  38. package/dist/keystore/file.js.map +1 -0
  39. package/dist/keystore/index.d.ts +88 -0
  40. package/dist/keystore/index.js +7 -0
  41. package/dist/keystore/index.js.map +1 -0
  42. package/dist/keystore/sqlite-backup.d.ts +40 -0
  43. package/dist/keystore/sqlite-backup.js +379 -0
  44. package/dist/keystore/sqlite-backup.js.map +1 -0
  45. package/dist/logger.d.ts +6 -0
  46. package/dist/logger.js +53 -0
  47. package/dist/logger.js.map +1 -0
  48. package/dist/namespaces/auth.d.ts +49 -0
  49. package/dist/namespaces/auth.js +248 -0
  50. package/dist/namespaces/auth.js.map +1 -0
  51. package/dist/namespaces/custody.d.ts +47 -0
  52. package/dist/namespaces/custody.js +231 -0
  53. package/dist/namespaces/custody.js.map +1 -0
  54. package/dist/secret-store/file-store.d.ts +25 -0
  55. package/dist/secret-store/file-store.js +124 -0
  56. package/dist/secret-store/file-store.js.map +1 -0
  57. package/dist/secret-store/index.d.ts +28 -0
  58. package/dist/secret-store/index.js +19 -0
  59. package/dist/secret-store/index.js.map +1 -0
  60. package/dist/seq-tracker.d.ts +29 -0
  61. package/dist/seq-tracker.js +221 -0
  62. package/dist/seq-tracker.js.map +1 -0
  63. package/dist/transport.d.ts +60 -0
  64. package/dist/transport.js +355 -0
  65. package/dist/transport.js.map +1 -0
  66. package/dist/types.d.ts +170 -0
  67. package/dist/types.js +12 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +42 -0
package/dist/auth.js ADDED
@@ -0,0 +1,1668 @@
1
+ /**
2
+ * AuthFlow — 认证流程管理
3
+ *
4
+ * Node.js 完整实现,与 Python SDK 的 AuthFlow 接口对齐。
5
+ * 功能:
6
+ * - AID 创建(在 Gateway 注册身份)
7
+ * - 两阶段挑战-应答认证(login1 + login2)
8
+ * - 证书链验证(chain + CRL + OCSP)
9
+ * - Token 刷新
10
+ * - 会话初始化
11
+ * - 证书恢复与自动续期
12
+ */
13
+ import * as crypto from 'node:crypto';
14
+ import * as fs from 'node:fs';
15
+ import * as http from 'node:http';
16
+ import * as https from 'node:https';
17
+ import * as path from 'node:path';
18
+ import { URL, fileURLToPath } from 'node:url';
19
+ import WebSocket from 'ws';
20
+ import { AuthError, StateError, ValidationError, AUNError, mapRemoteError } from './errors.js';
21
+ import { isJsonObject, } from './types.js';
22
+ // ── 日志辅助 ──────────────────────────────────────────────────
23
+ /** 简易日志:前缀 [aun_core.auth] */
24
+ function _authLog(level, msg, ...args) {
25
+ const ts = new Date().toISOString();
26
+ const formatted = args.reduce((s, a) => s.replace('%s', String(a)), msg);
27
+ // eslint-disable-next-line no-console
28
+ console.log(`[${ts}] [aun_core.auth] ${level}: ${formatted}`);
29
+ }
30
+ // ── 签名验证辅助 ──────────────────────────────────────────────
31
+ /**
32
+ * 验证签名:支持 ECDSA P-256 (SHA256)、ECDSA P-384 (SHA384)、Ed25519。
33
+ * 与 Python _verify_signature 完全对齐。
34
+ */
35
+ function _verifySignature(pubKey, signature, data) {
36
+ const asymKeyDetails = pubKey.asymmetricKeyType;
37
+ if (asymKeyDetails === 'ed25519') {
38
+ const ok = crypto.verify(null, data, pubKey, signature);
39
+ if (!ok)
40
+ throw new AuthError('ed25519 signature verification failed');
41
+ return;
42
+ }
43
+ if (asymKeyDetails === 'ec') {
44
+ // 通过导出公钥的 JWK 判断曲线
45
+ const jwk = pubKey.export({ format: 'jwk' });
46
+ const crv = jwk.crv;
47
+ const alg = crv === 'P-384' ? 'SHA384' : 'SHA256';
48
+ const ok = crypto.verify(alg, data, pubKey, signature);
49
+ if (!ok)
50
+ throw new AuthError(`ecdsa ${alg} signature verification failed`);
51
+ return;
52
+ }
53
+ throw new AuthError(`unsupported public key type: ${asymKeyDetails}`);
54
+ }
55
+ // ── X.509 辅助函数 ────────────────────────────────────────────
56
+ /** 将 PEM bundle 拆分为独立的 PEM 字符串数组 */
57
+ function _splitPemBundle(bundleText) {
58
+ const marker = '-----END CERTIFICATE-----';
59
+ const certs = [];
60
+ for (const part of bundleText.split(marker)) {
61
+ const trimmed = part.trim();
62
+ if (!trimmed)
63
+ continue;
64
+ certs.push(`${trimmed}\n${marker}\n`);
65
+ }
66
+ return certs;
67
+ }
68
+ /** 加载 PEM 字符串为 X509Certificate 对象 */
69
+ function _loadX509(pem) {
70
+ return new crypto.X509Certificate(pem);
71
+ }
72
+ /** 从 X509Certificate 提取公钥 KeyObject */
73
+ function _extractPublicKey(cert) {
74
+ // Node.js 18+: cert.publicKey 已经是 KeyObject 类型
75
+ const pk = cert.publicKey;
76
+ if (pk && typeof pk === 'object' && 'type' in pk) {
77
+ return pk;
78
+ }
79
+ // fallback: 从 PEM 格式创建
80
+ return crypto.createPublicKey(cert.toString());
81
+ }
82
+ const CERT_CLOCK_SKEW_MS = 300_000;
83
+ /** 检查证书时间有效性,允许测试环境和容器之间存在短暂时钟偏差 */
84
+ function _ensureCertTimeValid(cert, label, nowMs) {
85
+ const notBefore = new Date(cert.validFrom).getTime();
86
+ const notAfter = new Date(cert.validTo).getTime();
87
+ if (nowMs + CERT_CLOCK_SKEW_MS < notBefore) {
88
+ throw new AuthError(`${label} is not yet valid`);
89
+ }
90
+ if (nowMs - CERT_CLOCK_SKEW_MS > notAfter) {
91
+ throw new AuthError(`${label} has expired`);
92
+ }
93
+ }
94
+ /**
95
+ * 检查 BasicConstraints 扩展中 CA=true。
96
+ * Node.js crypto.X509Certificate 不直接暴露 BasicConstraints,
97
+ * 但 cert.ca 属性(Node 20+)可用,或者检查 keyUsage。
98
+ * 这里通过 cert 的 infoAccess / 文本表示解析。
99
+ */
100
+ function _isCaCert(cert) {
101
+ // Node.js X509Certificate 在 v20+ 提供 .ca 属性
102
+ const compatCert = cert;
103
+ if (typeof compatCert.ca === 'boolean') {
104
+ return compatCert.ca;
105
+ }
106
+ // 降级:解析 cert 文本表示中的 BasicConstraints
107
+ const text = cert.toString();
108
+ // 检查是否包含 CA:TRUE
109
+ if (/CA:TRUE/i.test(text))
110
+ return true;
111
+ // 自签证书(issuer == subject)也视为 CA
112
+ if (cert.issuer === cert.subject)
113
+ return true;
114
+ return false;
115
+ }
116
+ /** 检查证书是否为自签(issuer == subject) */
117
+ function _isSelfSigned(cert) {
118
+ return cert.issuer === cert.subject;
119
+ }
120
+ /**
121
+ * 验证 cert 是否由 issuerCert 签发。
122
+ * 使用 X509Certificate.checkIssued (Node 18+)。
123
+ */
124
+ function _checkIssuedBy(cert, issuerCert) {
125
+ if (typeof cert.checkIssued === 'function') {
126
+ return cert.checkIssued(issuerCert);
127
+ }
128
+ // 降级比较 issuer/subject
129
+ return cert.issuer === issuerCert.subject;
130
+ }
131
+ /**
132
+ * 验证 cert 的签名是否由 issuerCert 的公钥签署。
133
+ * 使用 X509Certificate.verify (Node 18+)。
134
+ */
135
+ function _verifyCertSignedBy(cert, issuerCert) {
136
+ if (typeof cert.verify === 'function') {
137
+ return cert.verify(_extractPublicKey(issuerCert));
138
+ }
139
+ // 如果 verify 不可用,只能信赖 checkIssued
140
+ return _checkIssuedBy(cert, issuerCert);
141
+ }
142
+ /** 获取证书序列号的十六进制字符串(小写) */
143
+ function _certSerialHex(cert) {
144
+ return cert.serialNumber.toLowerCase();
145
+ }
146
+ /** 获取证书的 Subject CN */
147
+ function _certSubjectCN(cert) {
148
+ // cert.subject 格式: "CN=xxx\nO=yyy\n..."
149
+ const match = cert.subject.match(/CN=([^\n]+)/);
150
+ return match ? match[1].trim() : null;
151
+ }
152
+ /** 获取证书 DER 编码(用于去重比较) */
153
+ function _certDer(cert) {
154
+ return Buffer.from(cert.raw);
155
+ }
156
+ // ── HTTP 辅助 ─────────────────────────────────────────────────
157
+ /** 将 WebSocket URL 转换为 HTTP URL + 路径 */
158
+ function _gatewayHttpUrl(gatewayUrl, urlPath) {
159
+ const parsed = new URL(gatewayUrl);
160
+ const scheme = parsed.protocol === 'wss:' ? 'https:' : 'http:';
161
+ return `${scheme}//${parsed.host}${urlPath}`;
162
+ }
163
+ /** 发起 HTTP GET 请求,返回文本内容 */
164
+ async function _fetchText(url, verifySsl) {
165
+ try {
166
+ return await _httpGet(url, verifySsl);
167
+ }
168
+ catch (err) {
169
+ throw new AuthError(`failed to fetch ${url}: ${err instanceof Error ? err.message : String(err)}`);
170
+ }
171
+ }
172
+ /** 发起 HTTP GET 请求,返回 JSON 对象 */
173
+ async function _fetchJson(url, verifySsl) {
174
+ const text = await _fetchText(url, verifySsl);
175
+ try {
176
+ const payload = JSON.parse(text);
177
+ if (!isJsonObject(payload)) {
178
+ throw new AuthError(`invalid JSON payload from ${url}`);
179
+ }
180
+ return payload;
181
+ }
182
+ catch (err) {
183
+ if (err instanceof AuthError)
184
+ throw err;
185
+ throw new AuthError(`invalid JSON from ${url}: ${err instanceof Error ? err.message : String(err)}`);
186
+ }
187
+ }
188
+ /** 底层 HTTP GET 实现,兼容 http/https + SSL 控制 */
189
+ function _httpGet(url, verifySsl) {
190
+ return new Promise((resolve, reject) => {
191
+ const parsed = new URL(url);
192
+ const mod = parsed.protocol === 'https:' ? https : http;
193
+ const options = {
194
+ timeout: 5000,
195
+ };
196
+ if (!verifySsl) {
197
+ options.rejectUnauthorized = false;
198
+ }
199
+ const req = mod.get(url, options, (res) => {
200
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
201
+ reject(new Error(`HTTP ${res.statusCode} from ${url}`));
202
+ res.resume();
203
+ return;
204
+ }
205
+ const chunks = [];
206
+ res.on('data', (chunk) => chunks.push(chunk));
207
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
208
+ res.on('error', reject);
209
+ });
210
+ req.on('error', reject);
211
+ req.on('timeout', () => {
212
+ req.destroy();
213
+ reject(new Error(`timeout fetching ${url}`));
214
+ });
215
+ });
216
+ }
217
+ function _defaultConnectionFactory(url) {
218
+ return new Promise((resolve, reject) => {
219
+ const ws = new WebSocket(url, {
220
+ handshakeTimeout: 5000,
221
+ rejectUnauthorized: false,
222
+ });
223
+ ws.on('open', () => resolve(ws));
224
+ ws.on('error', (err) => reject(new AuthError(`websocket connect failed: ${err.message}`)));
225
+ });
226
+ }
227
+ // ── AuthFlow ──────────────────────────────────────────────────
228
+ export class AuthFlow {
229
+ static _INSTANCE_STATE_FIELDS = [
230
+ 'access_token',
231
+ 'refresh_token',
232
+ 'kite_token',
233
+ 'access_token_expires_at',
234
+ ];
235
+ _keystore;
236
+ _crypto;
237
+ _aid;
238
+ _deviceId;
239
+ _slotId;
240
+ _verifySsl;
241
+ _connectionFactory;
242
+ _rootCaPath;
243
+ _chainCacheTtl;
244
+ // 根证书(启动时加载)
245
+ _rootCerts;
246
+ // Gateway CA 链缓存:gateway_url -> PEM 字符串数组
247
+ _gatewayChainCache = new Map();
248
+ // Gateway CRL 缓存:gateway_url -> CRL 缓存条目
249
+ _gatewayCrlCache = new Map();
250
+ // Gateway OCSP 缓存:gateway_url -> (serial_hex -> OCSP 缓存条目)
251
+ _gatewayOcspCache = new Map();
252
+ // 证书链验证结果缓存:cert_serial -> verified_at (ms)
253
+ _chainVerifiedCache = new Map();
254
+ // Gateway CA 链预验证标记:cache_key -> verified
255
+ _gatewayCaVerified = new Map();
256
+ constructor(opts) {
257
+ this._keystore = opts.keystore;
258
+ this._crypto = opts.crypto;
259
+ this._aid = opts.aid ?? null;
260
+ this._deviceId = String(opts.deviceId ?? '').trim();
261
+ this._slotId = String(opts.slotId ?? '').trim();
262
+ this._connectionFactory = opts.connectionFactory ?? _defaultConnectionFactory;
263
+ this._rootCaPath = opts.rootCaPath ?? null;
264
+ this._verifySsl = opts.verifySsl ?? false;
265
+ this._chainCacheTtl = (opts.chainCacheTtl ?? 86400) * 1000; // 转为毫秒
266
+ this._rootCerts = this._loadRootCerts(this._rootCaPath);
267
+ }
268
+ // ── 公开方法 ────────────────────────────────────────────────
269
+ /** 加载身份信息(密钥对 + 证书 + 元数据) */
270
+ loadIdentity(aid) {
271
+ const identity = this._loadIdentityOrRaise(aid);
272
+ const cert = this._keystore.loadCert(String(identity.aid));
273
+ if (cert)
274
+ identity.cert = cert;
275
+ const instanceState = this._loadInstanceState(String(identity.aid));
276
+ if (instanceState && typeof instanceState === 'object') {
277
+ Object.assign(identity, instanceState);
278
+ }
279
+ return identity;
280
+ }
281
+ /** 加载身份信息,不存在时返回 null */
282
+ loadIdentityOrNone(aid) {
283
+ try {
284
+ return this.loadIdentity(aid);
285
+ }
286
+ catch (e) {
287
+ if (e instanceof StateError)
288
+ return null;
289
+ throw e;
290
+ }
291
+ }
292
+ /** 与 Browser/JS SDK 对齐的别名:加载身份信息,不存在时返回 null */
293
+ loadIdentityOrNull(aid) {
294
+ return this.loadIdentityOrNone(aid);
295
+ }
296
+ /** 获取 access_token 的过期时间戳(秒) */
297
+ getAccessTokenExpiry(identity) {
298
+ const expiresAt = identity.access_token_expires_at;
299
+ if (typeof expiresAt === 'number')
300
+ return expiresAt;
301
+ return null;
302
+ }
303
+ setInstanceContext(opts) {
304
+ this._deviceId = String(opts.deviceId ?? '').trim();
305
+ this._slotId = String(opts.slotId ?? '').trim();
306
+ }
307
+ /**
308
+ * 创建 AID 并注册到 Gateway。
309
+ * 如果 AID 已在服务端注册但本地证书丢失,尝试从 PKI 端点下载恢复。
310
+ */
311
+ async createAid(gatewayUrl, aid) {
312
+ AuthFlow._validateAidName(aid);
313
+ let identity = this._ensureLocalIdentity(aid);
314
+ if (identity.cert) {
315
+ return { aid: identity.aid, cert: identity.cert };
316
+ }
317
+ // 本地有密钥但无证书——尝试注册
318
+ try {
319
+ const created = await this._createAid(gatewayUrl, identity);
320
+ Object.assign(identity, created);
321
+ }
322
+ catch (e) {
323
+ if (!(e instanceof AUNError) || !String(e.message).includes('already exists')) {
324
+ throw e;
325
+ }
326
+ // AID 已在服务端注册,尝试从 PKI 下载恢复证书
327
+ try {
328
+ identity = await this._recoverCertViaDownload(gatewayUrl, identity);
329
+ }
330
+ catch {
331
+ throw new StateError(`AID ${aid} already registered on server but local certificate is missing. ` +
332
+ `Certificate download recovery failed. Options: ` +
333
+ `(1) use a different AID name, or ` +
334
+ `(2) restart Kite server to clear registration.`);
335
+ }
336
+ }
337
+ this._persistIdentity(identity);
338
+ this._aid = String(identity.aid);
339
+ return { aid: identity.aid, cert: identity.cert };
340
+ }
341
+ /**
342
+ * 认证(登录)到 Gateway。
343
+ * 执行两阶段挑战-应答认证,返回 token 信息。
344
+ */
345
+ async authenticate(gatewayUrl, opts) {
346
+ let identity = this._loadIdentityOrRaise(opts?.aid);
347
+ if (!identity.cert) {
348
+ // 本地有密钥但无证书——尝试从 PKI 下载恢复
349
+ try {
350
+ identity = await this._recoverCertViaDownload(gatewayUrl, identity);
351
+ this._persistIdentity(identity);
352
+ }
353
+ catch (e) {
354
+ throw new StateError(`local certificate missing and recovery failed: ${e instanceof Error ? e.message : String(e)}. ` +
355
+ `Run auth.createAid() to register a new identity.`);
356
+ }
357
+ }
358
+ let login;
359
+ try {
360
+ login = await this._login(gatewayUrl, identity);
361
+ }
362
+ catch (e) {
363
+ // 证书未在服务端注册或公钥不匹配 — 自动重新注册
364
+ if (e instanceof AuthError && (String(e.message).includes('not registered') || String(e.message).includes('public key mismatch'))) {
365
+ _authLog('warn', '证书未在服务端注册,自动重新注册: aid=%s', identity.aid);
366
+ const created = await this._createAid(gatewayUrl, identity);
367
+ identity.cert = created.cert;
368
+ this._persistIdentity(identity);
369
+ login = await this._login(gatewayUrl, identity);
370
+ }
371
+ else {
372
+ throw e;
373
+ }
374
+ }
375
+ AuthFlow._rememberTokens(identity, login);
376
+ await this._validateNewCert(identity, gatewayUrl);
377
+ this._persistIdentity(identity);
378
+ this._aid = String(identity.aid);
379
+ return {
380
+ aid: identity.aid,
381
+ access_token: identity.access_token,
382
+ refresh_token: identity.refresh_token,
383
+ expires_at: identity.access_token_expires_at,
384
+ gateway: gatewayUrl,
385
+ };
386
+ }
387
+ /**
388
+ * 确保已认证(自动创建 + 登录)。
389
+ * 如果没有本地身份则创建,然后执行登录流程。
390
+ */
391
+ async ensureAuthenticated(gatewayUrl) {
392
+ const identity = this._ensureIdentity();
393
+ if (!identity.cert) {
394
+ const created = await this._createAid(gatewayUrl, identity);
395
+ Object.assign(identity, created);
396
+ this._persistIdentity(identity);
397
+ }
398
+ const login = await this._login(gatewayUrl, identity);
399
+ AuthFlow._rememberTokens(identity, login);
400
+ await this._validateNewCert(identity, gatewayUrl);
401
+ this._persistIdentity(identity);
402
+ const token = identity.access_token || identity.token || identity.kite_token;
403
+ if (!token) {
404
+ throw new AuthError('login2 did not return access token');
405
+ }
406
+ return { token, identity };
407
+ }
408
+ /**
409
+ * 刷新缓存的 token。
410
+ * 使用 refresh_token 获取新的 access_token。
411
+ */
412
+ async refreshCachedTokens(gatewayUrl, identity) {
413
+ const refreshToken = String(identity.refresh_token || '');
414
+ if (!refreshToken) {
415
+ throw new AuthError('missing refresh_token');
416
+ }
417
+ const refreshed = await this._refreshAccessToken(gatewayUrl, refreshToken);
418
+ AuthFlow._rememberTokens(identity, refreshed);
419
+ await this._validateNewCert(identity, gatewayUrl);
420
+ this._persistIdentity(identity);
421
+ return identity;
422
+ }
423
+ /**
424
+ * 使用 token 初始化 WebSocket 会话。
425
+ * 发送 auth.connect RPC 完成会话握手。
426
+ */
427
+ async initializeWithToken(transport, challenge, accessToken, opts) {
428
+ const nonce = this._extractChallengeNonce(challenge);
429
+ this.setInstanceContext({
430
+ deviceId: String(opts?.deviceId ?? ''),
431
+ slotId: String(opts?.slotId ?? ''),
432
+ });
433
+ await this._initializeSession(transport, nonce, accessToken, {
434
+ deviceId: String(opts?.deviceId ?? ''),
435
+ slotId: String(opts?.slotId ?? ''),
436
+ deliveryMode: opts?.deliveryMode ?? null,
437
+ });
438
+ }
439
+ /**
440
+ * 连接会话(自动选择认证策略)。
441
+ * 依次尝试:显式 token → 缓存 token → 刷新 token → 完整重认证。
442
+ */
443
+ async connectSession(transport, challenge, gatewayUrl, opts) {
444
+ const nonce = this._extractChallengeNonce(challenge);
445
+ const deviceId = String(opts?.deviceId ?? '');
446
+ const slotId = String(opts?.slotId ?? '');
447
+ const deliveryMode = opts?.deliveryMode ?? null;
448
+ this.setInstanceContext({ deviceId, slotId });
449
+ let identity;
450
+ try {
451
+ identity = this.loadIdentity();
452
+ }
453
+ catch {
454
+ identity = null;
455
+ }
456
+ // 策略 1:显式 token
457
+ const explicitToken = String(opts?.accessToken || '');
458
+ if (explicitToken && identity !== null) {
459
+ try {
460
+ await this._initializeSession(transport, nonce, explicitToken, {
461
+ deviceId,
462
+ slotId,
463
+ deliveryMode,
464
+ });
465
+ identity.access_token = explicitToken;
466
+ this._persistIdentity(identity);
467
+ return { token: explicitToken, identity };
468
+ }
469
+ catch (exc) {
470
+ if (!(exc instanceof AuthError))
471
+ throw exc;
472
+ _authLog('debug', 'explicit_token 认证失败,尝试下一方式: %s', exc.message);
473
+ }
474
+ }
475
+ // 无本地身份时,执行完整认证
476
+ if (identity === null) {
477
+ const authContext = await this.ensureAuthenticated(gatewayUrl);
478
+ const token = String(authContext.token);
479
+ await this._initializeSession(transport, nonce, token, {
480
+ deviceId,
481
+ slotId,
482
+ deliveryMode,
483
+ });
484
+ return authContext;
485
+ }
486
+ // 策略 2:缓存的 access_token
487
+ const cachedToken = AuthFlow._getCachedAccessToken(identity);
488
+ if (cachedToken) {
489
+ try {
490
+ await this._initializeSession(transport, nonce, cachedToken, {
491
+ deviceId,
492
+ slotId,
493
+ deliveryMode,
494
+ });
495
+ return { token: cachedToken, identity };
496
+ }
497
+ catch (exc) {
498
+ if (!(exc instanceof AuthError))
499
+ throw exc;
500
+ _authLog('debug', 'cached_token 认证失败,尝试刷新: %s', exc.message);
501
+ }
502
+ }
503
+ // 策略 3:refresh_token
504
+ const refreshToken = String(identity.refresh_token || '');
505
+ if (refreshToken) {
506
+ try {
507
+ identity = await this.refreshCachedTokens(gatewayUrl, identity);
508
+ const newCachedToken = AuthFlow._getCachedAccessToken(identity);
509
+ if (newCachedToken) {
510
+ await this._initializeSession(transport, nonce, newCachedToken, {
511
+ deviceId,
512
+ slotId,
513
+ deliveryMode,
514
+ });
515
+ return { token: newCachedToken, identity };
516
+ }
517
+ }
518
+ catch (exc) {
519
+ if (!(exc instanceof AuthError))
520
+ throw exc;
521
+ _authLog('debug', 'refresh_token 认证失败,将重新登录: %s', exc.message);
522
+ }
523
+ }
524
+ // 策略 4:完整重认证
525
+ const login = await this.authenticate(gatewayUrl, { aid: identity.aid });
526
+ const token = String(login.access_token || '');
527
+ if (!token) {
528
+ throw new AuthError('authenticate did not return access_token');
529
+ }
530
+ await this._initializeSession(transport, nonce, token, {
531
+ deviceId,
532
+ slotId,
533
+ deliveryMode,
534
+ });
535
+ identity = this.loadIdentity(identity.aid);
536
+ return { token, identity };
537
+ }
538
+ /**
539
+ * 验证对端证书:时间有效性 + 链验证 + CRL + OCSP + AID 绑定。
540
+ * 用于 E2EE 握手中验证通信对端的身份证书。
541
+ */
542
+ async verifyPeerCertificate(gatewayUrl, certPem, expectedAid) {
543
+ const cert = _loadX509(certPem);
544
+ const nowMs = Date.now();
545
+ _ensureCertTimeValid(cert, 'peer certificate', nowMs);
546
+ await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
547
+ try {
548
+ await this._verifyAuthCertRevocation(gatewayUrl, cert, expectedAid);
549
+ }
550
+ catch (e) {
551
+ const errMsg = e instanceof Error ? e.message : String(e);
552
+ if (/revoked/i.test(errMsg))
553
+ throw e instanceof AuthError ? e : new AuthError(errMsg);
554
+ _authLog('warn', 'CRL 检查不可用,降级继续: %s', errMsg);
555
+ }
556
+ try {
557
+ await this._verifyAuthCertOcsp(gatewayUrl, cert, expectedAid);
558
+ }
559
+ catch (exc) {
560
+ const errMsg = exc instanceof Error ? exc.message : String(exc);
561
+ if (/revoked/i.test(errMsg))
562
+ throw exc instanceof AuthError ? exc : new AuthError(errMsg);
563
+ _authLog('warn', 'OCSP 校验不可用,降级继续: %s', errMsg);
564
+ }
565
+ // 检查 CN 匹配
566
+ const cn = _certSubjectCN(cert);
567
+ if (cn !== expectedAid) {
568
+ throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}, got ${cn || 'none'}`);
569
+ }
570
+ }
571
+ // ── 内部方法:短连接 RPC ────────────────────────────────────
572
+ /**
573
+ * 通过临时 WebSocket 发送单次 JSON-RPC 请求。
574
+ * 流程:连接 → 接收 challenge → 发送请求 → 接收响应 → 关闭。
575
+ */
576
+ async _shortRpc(gatewayUrl, method, params) {
577
+ const ws = await this._connectionFactory(gatewayUrl);
578
+ try {
579
+ // 接收 challenge(第一条消息)
580
+ await this._wsRecv(ws);
581
+ // 发送 RPC 请求
582
+ const payload = JSON.stringify({
583
+ jsonrpc: '2.0',
584
+ id: `pre-${method}`,
585
+ method,
586
+ params,
587
+ });
588
+ ws.send(payload);
589
+ // 接收响应
590
+ const raw = await this._wsRecv(ws);
591
+ const message = typeof raw === 'string' ? JSON.parse(raw) : raw;
592
+ if (message.error) {
593
+ throw mapRemoteError(message.error);
594
+ }
595
+ const result = isJsonObject(message.result) ? message.result : null;
596
+ if (result === null) {
597
+ throw new ValidationError(`invalid pre-auth response for ${method}`);
598
+ }
599
+ if (result.success === false) {
600
+ throw new AuthError(String(result.error || `${method} failed`));
601
+ }
602
+ return result;
603
+ }
604
+ finally {
605
+ try {
606
+ ws.close();
607
+ }
608
+ catch {
609
+ // 忽略关闭错误
610
+ }
611
+ }
612
+ }
613
+ /** 从 WebSocket 接收一条消息(Promise 封装) */
614
+ _wsRecv(ws) {
615
+ return new Promise((resolve, reject) => {
616
+ const timer = setTimeout(() => {
617
+ reject(new AuthError('websocket recv timeout'));
618
+ }, 10000);
619
+ const handler = (data) => {
620
+ clearTimeout(timer);
621
+ ws.off('message', handler);
622
+ ws.off('error', errHandler);
623
+ if (Buffer.isBuffer(data)) {
624
+ resolve(data.toString('utf-8'));
625
+ }
626
+ else if (data instanceof ArrayBuffer) {
627
+ resolve(Buffer.from(data).toString('utf-8'));
628
+ }
629
+ else {
630
+ resolve(String(data));
631
+ }
632
+ };
633
+ const errHandler = (err) => {
634
+ clearTimeout(timer);
635
+ ws.off('message', handler);
636
+ reject(new AuthError(`websocket error: ${err.message}`));
637
+ };
638
+ ws.on('message', handler);
639
+ ws.on('error', errHandler);
640
+ });
641
+ }
642
+ // ── 内部方法:认证流程 ──────────────────────────────────────
643
+ /** 注册 AID 到 Gateway */
644
+ async _createAid(gatewayUrl, identity) {
645
+ const response = await this._shortRpc(gatewayUrl, 'auth.create_aid', {
646
+ aid: identity.aid,
647
+ public_key: identity.public_key_der_b64,
648
+ curve: identity.curve || 'P-256',
649
+ });
650
+ return { cert: response.cert };
651
+ }
652
+ /** 两阶段登录 */
653
+ async _login(gatewayUrl, identity) {
654
+ const clientNonce = this._crypto.newClientNonce();
655
+ // Phase 1: 发送 AID + 证书 + 客户端 nonce
656
+ const phase1 = await this._shortRpc(gatewayUrl, 'auth.aid_login1', {
657
+ aid: identity.aid,
658
+ cert: identity.cert,
659
+ client_nonce: clientNonce,
660
+ });
661
+ // 验证 Phase 1 响应(证书链 + 签名)
662
+ await this._verifyPhase1Response(gatewayUrl, phase1, clientNonce);
663
+ // Phase 2: 用私钥签名 nonce
664
+ const [signature, clientTime] = this._crypto.signLoginNonce(String(identity.private_key_pem), String(phase1.nonce));
665
+ const phase2 = await this._shortRpc(gatewayUrl, 'auth.aid_login2', {
666
+ aid: identity.aid,
667
+ request_id: phase1.request_id,
668
+ nonce: phase1.nonce,
669
+ client_time: clientTime,
670
+ signature,
671
+ });
672
+ return phase2;
673
+ }
674
+ /** 刷新 access_token */
675
+ async _refreshAccessToken(gatewayUrl, refreshToken) {
676
+ const result = await this._shortRpc(gatewayUrl, 'auth.refresh_token', {
677
+ refresh_token: refreshToken,
678
+ });
679
+ if (!result.success) {
680
+ throw new AuthError(String(result.error || 'refresh failed'));
681
+ }
682
+ return result;
683
+ }
684
+ /** 会话初始化:发送 auth.connect RPC */
685
+ async _initializeSession(transport, nonce, token, opts) {
686
+ const result = await transport.call('auth.connect', {
687
+ nonce,
688
+ auth: { method: 'kite_token', token },
689
+ protocol: { min: '1.0', max: '1.0' },
690
+ device: { id: String(opts?.deviceId ?? ''), type: 'sdk' },
691
+ client: { slot_id: String(opts?.slotId ?? '') },
692
+ delivery_mode: opts?.deliveryMode ?? { mode: 'fanout' },
693
+ capabilities: {
694
+ e2ee: true,
695
+ group_e2ee: true,
696
+ },
697
+ });
698
+ const status = isJsonObject(result) ? result.status : undefined;
699
+ if (status !== 'ok') {
700
+ throw new AuthError(`initialize failed: ${JSON.stringify(result)}`);
701
+ }
702
+ }
703
+ // ── 内部方法:证书验证 ──────────────────────────────────────
704
+ /**
705
+ * 验证 Phase 1 响应:
706
+ * 1. 解析 auth_cert
707
+ * 2. 验证证书链 + CRL + OCSP
708
+ * 3. 验证 client_nonce_signature
709
+ */
710
+ async _verifyPhase1Response(gatewayUrl, result, clientNonce) {
711
+ const authCertPem = String(result.auth_cert || '');
712
+ const signatureB64 = String(result.client_nonce_signature || '');
713
+ if (!authCertPem) {
714
+ throw new AuthError('aid_login1 missing auth_cert');
715
+ }
716
+ if (!signatureB64) {
717
+ throw new AuthError('aid_login1 missing client_nonce_signature');
718
+ }
719
+ let authCert;
720
+ try {
721
+ authCert = _loadX509(authCertPem);
722
+ }
723
+ catch {
724
+ throw new AuthError('aid_login1 returned invalid auth_cert');
725
+ }
726
+ // 验证证书链、CRL、OCSP
727
+ await this._verifyAuthCertChain(gatewayUrl, authCert);
728
+ try {
729
+ await this._verifyAuthCertRevocation(gatewayUrl, authCert);
730
+ }
731
+ catch (e) {
732
+ const errMsg = e instanceof Error ? e.message : String(e);
733
+ if (/revoked/i.test(errMsg))
734
+ throw e instanceof AuthError ? e : new AuthError(errMsg);
735
+ _authLog('warn', 'CRL 检查不可用,降级继续: %s', errMsg);
736
+ }
737
+ try {
738
+ await this._verifyAuthCertOcsp(gatewayUrl, authCert);
739
+ }
740
+ catch (exc) {
741
+ const errMsg = exc instanceof Error ? exc.message : String(exc);
742
+ if (/revoked/i.test(errMsg))
743
+ throw exc instanceof AuthError ? exc : new AuthError(errMsg);
744
+ _authLog('warn', 'OCSP 校验不可用,降级继续: %s', errMsg);
745
+ }
746
+ // 验证 client_nonce 签名
747
+ try {
748
+ const signature = Buffer.from(signatureB64, 'base64');
749
+ const pubKey = _extractPublicKey(authCert);
750
+ _verifySignature(pubKey, signature, Buffer.from(clientNonce, 'utf-8'));
751
+ }
752
+ catch (e) {
753
+ if (e instanceof AuthError && e.message.includes('signature verification failed')) {
754
+ throw new AuthError('aid_login1 server auth signature verification failed');
755
+ }
756
+ throw new AuthError('aid_login1 server auth signature verification failed');
757
+ }
758
+ }
759
+ /**
760
+ * 验证认证证书链。
761
+ * 1. 检查缓存
762
+ * 2. 时间有效性
763
+ * 3. 签名链验证
764
+ * 4. BasicConstraints 检查
765
+ * 5. 根证书自签 + 受信根锚定
766
+ */
767
+ async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '') {
768
+ const certSerial = _certSerialHex(authCert);
769
+ // 检查缓存
770
+ const cachedAt = this._chainVerifiedCache.get(certSerial);
771
+ if (cachedAt && Date.now() - cachedAt < this._chainCacheTtl) {
772
+ return;
773
+ }
774
+ const nowMs = Date.now();
775
+ _ensureCertTimeValid(authCert, 'auth certificate', nowMs);
776
+ const chain = await this._loadGatewayCaChain(gatewayUrl, chainAid);
777
+ if (!chain.length) {
778
+ throw new AuthError('unable to verify auth certificate chain: missing CA chain');
779
+ }
780
+ const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
781
+ // 快速路径:CA 链已通过完整验证
782
+ if (this._gatewayCaVerified.get(cacheKey)) {
783
+ const issuer = chain[0];
784
+ _ensureCertTimeValid(issuer, 'Issuer CA', nowMs);
785
+ if (!_isCaCert(issuer)) {
786
+ throw new AuthError('Issuer CA is not marked as CA (fast path)');
787
+ }
788
+ if (!_checkIssuedBy(authCert, issuer)) {
789
+ throw new AuthError('auth certificate issuer mismatch');
790
+ }
791
+ if (!_verifyCertSignedBy(authCert, issuer)) {
792
+ throw new AuthError('auth certificate signature verification failed');
793
+ }
794
+ this._chainVerifiedCache.set(certSerial, Date.now());
795
+ return;
796
+ }
797
+ // 首次验证:完整验证流程
798
+ let current = authCert;
799
+ for (let i = 0; i < chain.length; i++) {
800
+ const caCert = chain[i];
801
+ _ensureCertTimeValid(caCert, `CA certificate[${i}]`, nowMs);
802
+ if (!_checkIssuedBy(current, caCert)) {
803
+ throw new AuthError(`auth certificate issuer mismatch at chain level ${i}`);
804
+ }
805
+ if (!_verifyCertSignedBy(current, caCert)) {
806
+ throw new AuthError(`auth certificate signature verification failed at chain level ${i}`);
807
+ }
808
+ if (!_isCaCert(caCert)) {
809
+ throw new AuthError(`CA certificate[${i}] is not marked as CA`);
810
+ }
811
+ current = caCert;
812
+ }
813
+ // 验证根证书自签
814
+ const root = chain[chain.length - 1];
815
+ if (!_isSelfSigned(root)) {
816
+ throw new AuthError('auth certificate chain root is not self-signed');
817
+ }
818
+ if (!_verifyCertSignedBy(root, root)) {
819
+ throw new AuthError('auth certificate chain root self-signature verification failed');
820
+ }
821
+ // 验证根证书在受信列表中
822
+ const trustedRoots = this._loadTrustedRoots();
823
+ const rootDer = _certDer(root);
824
+ const isTrusted = trustedRoots.some((tr) => _certDer(tr).equals(rootDer));
825
+ if (!isTrusted) {
826
+ throw new AuthError('auth certificate chain is not anchored by a trusted root');
827
+ }
828
+ // 验证成功,更新缓存
829
+ this._chainVerifiedCache.set(certSerial, Date.now());
830
+ this._gatewayCaVerified.set(cacheKey, true);
831
+ }
832
+ /** 加载 Gateway CA 链(带缓存) */
833
+ async _loadGatewayCaChain(gatewayUrl, chainAid = '') {
834
+ const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
835
+ let cached = this._gatewayChainCache.get(cacheKey);
836
+ if (!cached) {
837
+ cached = await this._fetchGatewayCaChain(gatewayUrl, chainAid);
838
+ this._gatewayChainCache.set(cacheKey, cached);
839
+ }
840
+ return cached.map((pem) => _loadX509(pem));
841
+ }
842
+ /** 从 Gateway PKI 端点获取 CA 链 */
843
+ async _fetchGatewayCaChain(gatewayUrl, _chainAid = '') {
844
+ const url = _gatewayHttpUrl(gatewayUrl, '/pki/chain');
845
+ const text = await _fetchText(url, this._verifySsl);
846
+ return _splitPemBundle(text);
847
+ }
848
+ /**
849
+ * CRL 吊销检查。
850
+ * 从 Gateway 的 /pki/crl.json 端点获取 CRL,
851
+ * 验证签发者签名,检查证书序列号是否在吊销列表中。
852
+ */
853
+ async _verifyAuthCertRevocation(gatewayUrl, authCert, chainAid = '') {
854
+ const chain = await this._loadGatewayCaChain(gatewayUrl, chainAid);
855
+ if (!chain.length) {
856
+ throw new AuthError('unable to verify auth certificate revocation: missing issuer certificate');
857
+ }
858
+ // 跨域时:CRL 请求发到 peer 所在域的 Gateway
859
+ let crlGatewayUrl = gatewayUrl;
860
+ if (chainAid && chainAid.includes('.')) {
861
+ const peerIssuer = chainAid.split('.').slice(1).join('.');
862
+ const localMatch = gatewayUrl.match(/gateway\.([^:/]+)/);
863
+ const localIssuer = localMatch ? localMatch[1] : '';
864
+ if (localIssuer && peerIssuer !== localIssuer) {
865
+ crlGatewayUrl = gatewayUrl.replace(`gateway.${localIssuer}`, `gateway.${peerIssuer}`);
866
+ }
867
+ }
868
+ const revokedSerials = await this._loadGatewayRevokedSerials(crlGatewayUrl, chain[0]);
869
+ const serialHex = _certSerialHex(authCert);
870
+ if (revokedSerials.has(serialHex)) {
871
+ throw new AuthError('auth certificate has been revoked');
872
+ }
873
+ }
874
+ /** 加载 Gateway 吊销列表(带缓存) */
875
+ async _loadGatewayRevokedSerials(gatewayUrl, issuerCert) {
876
+ const cached = this._gatewayCrlCache.get(gatewayUrl);
877
+ const now = Date.now();
878
+ if (cached && cached.nextRefreshAt > now) {
879
+ return cached.revokedSerials;
880
+ }
881
+ const entry = await this._fetchGatewayCrl(gatewayUrl, issuerCert);
882
+ this._gatewayCrlCache.set(gatewayUrl, entry);
883
+ return entry.revokedSerials;
884
+ }
885
+ /**
886
+ * 从 Gateway /pki/crl.json 获取 CRL 并解析。
887
+ *
888
+ * 响应格式: { crl_pem: "...", revoked_serials?: [...] }
889
+ *
890
+ * 注意:完整的 CRL PEM 签名验证需要 ASN.1/DER 解析,
891
+ * Node.js 标准库不直接支持 CRL 解析。
892
+ * 这里使用 JSON 响应中的 revoked_serials 字段作为可信数据源,
893
+ * 并验证 crl_pem 存在性。完整的 CRL 签名验证需要依赖
894
+ * @peculiar/x509 或手动 ASN.1 解析。
895
+ */
896
+ async _fetchGatewayCrl(gatewayUrl, _issuerCert) {
897
+ const url = _gatewayHttpUrl(gatewayUrl, '/pki/crl.json');
898
+ const payload = await _fetchJson(url, this._verifySsl);
899
+ const crlPem = String(payload.crl_pem || '');
900
+ if (!crlPem) {
901
+ throw new AuthError('gateway CRL endpoint returned no signed CRL');
902
+ }
903
+ // 从 JSON 响应中提取吊销序列号列表
904
+ // Gateway 的 crl.json 同时包含 crl_pem(签名 CRL)和 revoked_serials(方便客户端解析)
905
+ let revokedSerials = new Set();
906
+ // 尝试解析 CRL PEM 以提取吊销序列号
907
+ // Node.js 不直接支持 CRL 解析,尝试从 DER 中手动提取
908
+ try {
909
+ revokedSerials = this._parseCrlRevokedSerials(crlPem, _issuerCert);
910
+ }
911
+ catch {
912
+ // CRL 解析失败时,降级使用 JSON 响应中的 revoked_serials(如果提供)
913
+ const serialsArr = payload.revoked_serials;
914
+ if (Array.isArray(serialsArr)) {
915
+ revokedSerials = new Set(serialsArr.map((s) => String(s).toLowerCase()));
916
+ }
917
+ // 如果两者都没有,返回空集合(无吊销记录)
918
+ _authLog('debug', 'CRL PEM 解析失败,使用 JSON revoked_serials 降级');
919
+ }
920
+ // 缓存 TTL:默认 5 分钟,最大 24 小时
921
+ const now = Date.now();
922
+ let nextRefreshAt = now + 300_000; // 5 分钟
923
+ const maxRefreshAt = now + 86400_000; // 24 小时
924
+ nextRefreshAt = Math.min(nextRefreshAt, maxRefreshAt);
925
+ return { revokedSerials, nextRefreshAt };
926
+ }
927
+ /**
928
+ * 从 CRL PEM 解析吊销序列号。
929
+ * 简化的 ASN.1 DER 解析:提取 TBSCertList 中的 revokedCertificates 序列号。
930
+ *
931
+ * CRL ASN.1 结构(简化):
932
+ * CertificateList ::= SEQUENCE {
933
+ * tbsCertList TBSCertList,
934
+ * signatureAlgorithm AlgorithmIdentifier,
935
+ * signature BIT STRING
936
+ * }
937
+ *
938
+ * TBSCertList ::= SEQUENCE {
939
+ * version INTEGER OPTIONAL,
940
+ * signature AlgorithmIdentifier,
941
+ * issuer Name,
942
+ * thisUpdate Time,
943
+ * nextUpdate Time OPTIONAL,
944
+ * revokedCertificates SEQUENCE OF SEQUENCE { ... } OPTIONAL,
945
+ * ...
946
+ * }
947
+ */
948
+ _parseCrlRevokedSerials(crlPem, issuerCert) {
949
+ // 提取 DER 数据
950
+ const b64 = crlPem
951
+ .replace(/-----BEGIN X509 CRL-----/g, '')
952
+ .replace(/-----END X509 CRL-----/g, '')
953
+ .replace(/\s/g, '');
954
+ const der = Buffer.from(b64, 'base64');
955
+ // 简化 ASN.1 解析:提取 TBSCertList 和签名
956
+ // 外层 SEQUENCE
957
+ const outer = this._readAsn1Sequence(der, 0);
958
+ if (!outer)
959
+ throw new Error('invalid CRL: not a SEQUENCE');
960
+ // TBSCertList SEQUENCE
961
+ const tbsStart = outer.contentOffset;
962
+ const tbs = this._readAsn1Sequence(der, tbsStart);
963
+ if (!tbs)
964
+ throw new Error('invalid CRL: TBSCertList not a SEQUENCE');
965
+ // 验证签发者签名
966
+ const tbsBytes = der.subarray(tbsStart, tbsStart + tbs.totalLength);
967
+ // 签名在 TBSCertList 之后:先跳过 AlgorithmIdentifier,再读取 BIT STRING
968
+ let sigOffset = tbsStart + tbs.totalLength;
969
+ // 跳过 signatureAlgorithm SEQUENCE
970
+ const sigAlg = this._readAsn1Tag(der, sigOffset);
971
+ if (sigAlg)
972
+ sigOffset += sigAlg.totalLength;
973
+ // 读取 signature BIT STRING
974
+ const sigBitString = this._readAsn1Tag(der, sigOffset);
975
+ if (sigBitString && der[sigOffset] === 0x03) {
976
+ // BIT STRING 第一个字节是 padding bits 数(通常为 0)
977
+ const sigBytes = der.subarray(sigBitString.contentOffset + 1, sigBitString.contentOffset + sigBitString.contentLength);
978
+ try {
979
+ const pubKey = _extractPublicKey(issuerCert);
980
+ _verifySignature(pubKey, Buffer.from(sigBytes), Buffer.from(tbsBytes));
981
+ }
982
+ catch {
983
+ throw new AuthError('gateway CRL signature verification failed');
984
+ }
985
+ }
986
+ // 解析 TBSCertList 内部字段以找到 revokedCertificates
987
+ const revokedSerials = new Set();
988
+ let offset = tbs.contentOffset;
989
+ const tbsEnd = tbs.contentOffset + tbs.contentLength;
990
+ let fieldIdx = 0;
991
+ while (offset < tbsEnd) {
992
+ const tag = this._readAsn1Tag(der, offset);
993
+ if (!tag)
994
+ break;
995
+ // revokedCertificates 是 TBSCertList 中的第 6 个字段(index 5,version 从 0 开始)
996
+ // 或者根据 tag 判断:它是一个 SEQUENCE OF SEQUENCE
997
+ // 实际上字段顺序为:version?, signatureAlg, issuer, thisUpdate, nextUpdate?, revokedCerts?
998
+ // 简化方式:找到包含 INTEGER(序列号)的嵌套 SEQUENCE 结构
999
+ if (fieldIdx >= 4 && der[offset] === 0x30) {
1000
+ // 这可能是 revokedCertificates SEQUENCE OF
1001
+ const revokedSeq = this._readAsn1Sequence(der, offset);
1002
+ if (revokedSeq) {
1003
+ let rOffset = revokedSeq.contentOffset;
1004
+ const rEnd = revokedSeq.contentOffset + revokedSeq.contentLength;
1005
+ while (rOffset < rEnd) {
1006
+ // 每个条目是 SEQUENCE { serialNumber INTEGER, ... }
1007
+ const entry = this._readAsn1Sequence(der, rOffset);
1008
+ if (!entry)
1009
+ break;
1010
+ // 条目的第一个元素是 INTEGER(序列号)
1011
+ const serialTag = this._readAsn1Tag(der, entry.contentOffset);
1012
+ if (serialTag && der[entry.contentOffset] === 0x02) {
1013
+ const serialBytes = der.subarray(serialTag.contentOffset, serialTag.contentOffset + serialTag.contentLength);
1014
+ const serialHex = Buffer.from(serialBytes).toString('hex').toLowerCase();
1015
+ // 去掉前导零
1016
+ const cleaned = serialHex.replace(/^0+/, '') || '0';
1017
+ revokedSerials.add(cleaned);
1018
+ }
1019
+ rOffset += entry.totalLength;
1020
+ }
1021
+ break; // 找到 revokedCertificates 后退出
1022
+ }
1023
+ }
1024
+ offset += tag.totalLength;
1025
+ fieldIdx++;
1026
+ }
1027
+ return revokedSerials;
1028
+ }
1029
+ /**
1030
+ * OCSP 状态检查。
1031
+ * 从 Gateway 的 /pki/ocsp/{serial_hex} 端点获取 OCSP 响应。
1032
+ */
1033
+ async _verifyAuthCertOcsp(gatewayUrl, authCert, chainAid = '') {
1034
+ const chain = await this._loadGatewayCaChain(gatewayUrl, chainAid);
1035
+ if (!chain.length) {
1036
+ throw new AuthError('unable to verify auth certificate OCSP status: missing issuer certificate');
1037
+ }
1038
+ const status = await this._loadGatewayOcspStatus(gatewayUrl, authCert, chain[0]);
1039
+ if (status === 'revoked') {
1040
+ throw new AuthError('auth certificate OCSP status is revoked');
1041
+ }
1042
+ if (status !== 'good') {
1043
+ throw new AuthError(`auth certificate OCSP status is ${status}`);
1044
+ }
1045
+ }
1046
+ /** 加载 Gateway OCSP 状态(带缓存) */
1047
+ async _loadGatewayOcspStatus(gatewayUrl, authCert, issuerCert) {
1048
+ const serialHex = _certSerialHex(authCert);
1049
+ let gatewayCache = this._gatewayOcspCache.get(gatewayUrl);
1050
+ if (!gatewayCache) {
1051
+ gatewayCache = new Map();
1052
+ this._gatewayOcspCache.set(gatewayUrl, gatewayCache);
1053
+ }
1054
+ const cached = gatewayCache.get(serialHex);
1055
+ const now = Date.now();
1056
+ if (cached && cached.nextRefreshAt > now) {
1057
+ return cached.status;
1058
+ }
1059
+ const entry = await this._fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert);
1060
+ gatewayCache.set(serialHex, entry);
1061
+ return entry.status;
1062
+ }
1063
+ /**
1064
+ * 从 Gateway /pki/ocsp/{serial_hex} 获取 OCSP 状态。
1065
+ *
1066
+ * 响应格式: { status: "good"|"revoked"|"unknown", ocsp_response: "base64..." }
1067
+ *
1068
+ * 注意:完整的 OCSP DER 响应解析需要 ASN.1 解析。
1069
+ * 这里从 JSON 响应中提取 status 字段,并验证 ocsp_response 存在性。
1070
+ * 对于 ocsp_response 的签名验证,实现简化的 DER 解析以提取关键字段。
1071
+ */
1072
+ async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert) {
1073
+ const serialHex = _certSerialHex(authCert);
1074
+ const url = _gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${serialHex}`);
1075
+ const payload = await _fetchJson(url, this._verifySsl);
1076
+ const status = String(payload.status || '');
1077
+ const ocspB64 = String(payload.ocsp_response || '');
1078
+ if (!ocspB64) {
1079
+ throw new AuthError('gateway OCSP endpoint returned no ocsp_response');
1080
+ }
1081
+ let effectiveStatus;
1082
+ try {
1083
+ // 尝试解析 DER OCSP 响应
1084
+ const ocspDer = Buffer.from(ocspB64, 'base64');
1085
+ effectiveStatus = this._parseOcspResponse(ocspDer, authCert, issuerCert);
1086
+ }
1087
+ catch (e) {
1088
+ // OCSP DER 解析失败时,降级信赖 JSON status 字段
1089
+ if (e instanceof AuthError)
1090
+ throw e;
1091
+ _authLog('debug', 'OCSP DER 解析失败,使用 JSON status 降级: %s', e instanceof Error ? e.message : String(e));
1092
+ if (!status) {
1093
+ throw new AuthError('gateway OCSP endpoint returned invalid response and no status field');
1094
+ }
1095
+ effectiveStatus = status;
1096
+ }
1097
+ // 如果 JSON status 和 DER 解析结果不一致则报错
1098
+ if (status && status !== effectiveStatus) {
1099
+ throw new AuthError('gateway OCSP status mismatch');
1100
+ }
1101
+ // 缓存 TTL:默认 5 分钟,最大 24 小时
1102
+ const now = Date.now();
1103
+ const nextRefreshAt = Math.min(now + 300_000, now + 86400_000);
1104
+ return { status: effectiveStatus, nextRefreshAt };
1105
+ }
1106
+ /**
1107
+ * 简化的 OCSP DER 响应解析。
1108
+ *
1109
+ * OCSPResponse ::= SEQUENCE {
1110
+ * responseStatus ENUMERATED { successful(0), ... },
1111
+ * responseBytes [0] EXPLICIT SEQUENCE {
1112
+ * responseType OID,
1113
+ * response OCTET STRING (BasicOCSPResponse DER)
1114
+ * } OPTIONAL
1115
+ * }
1116
+ *
1117
+ * BasicOCSPResponse ::= SEQUENCE {
1118
+ * tbsResponseData ResponseData,
1119
+ * signatureAlgorithm AlgorithmIdentifier,
1120
+ * signature BIT STRING,
1121
+ * ...
1122
+ * }
1123
+ *
1124
+ * ResponseData ::= SEQUENCE {
1125
+ * version [0] EXPLICIT INTEGER DEFAULT v1,
1126
+ * responderID ...,
1127
+ * producedAt GeneralizedTime,
1128
+ * responses SEQUENCE OF SingleResponse,
1129
+ * ...
1130
+ * }
1131
+ *
1132
+ * SingleResponse ::= SEQUENCE {
1133
+ * certID CertID,
1134
+ * certStatus CHOICE { good [0], revoked [1], unknown [2] },
1135
+ * thisUpdate GeneralizedTime,
1136
+ * nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL,
1137
+ * }
1138
+ *
1139
+ * 这里只做关键字段提取:responseStatus、certStatus。
1140
+ * 完整签名验证依赖更全面的 ASN.1 库。
1141
+ */
1142
+ _parseOcspResponse(ocspDer, _authCert, _issuerCert) {
1143
+ // 外层 SEQUENCE
1144
+ const outer = this._readAsn1Sequence(ocspDer, 0);
1145
+ if (!outer)
1146
+ throw new Error('invalid OCSP response: not a SEQUENCE');
1147
+ // responseStatus ENUMERATED
1148
+ let offset = outer.contentOffset;
1149
+ const statusTag = this._readAsn1Tag(ocspDer, offset);
1150
+ if (!statusTag || ocspDer[offset] !== 0x0a) {
1151
+ throw new Error('invalid OCSP response: missing responseStatus');
1152
+ }
1153
+ const responseStatus = ocspDer[statusTag.contentOffset];
1154
+ if (responseStatus !== 0) {
1155
+ // 非 successful(如 unauthorized=6 表示 responder 无法回答,常见于 unknown 证书)
1156
+ // 抛普通 Error 而非 AuthError,让外层 catch 降级到 JSON status 字段
1157
+ const statusNames = ['successful', 'malformedRequest', 'internalError', 'tryLater', '', 'sigRequired', 'unauthorized'];
1158
+ const statusName = statusNames[responseStatus] || `unknown(${responseStatus})`;
1159
+ throw new Error(`OCSP responseStatus is ${statusName} (non-successful), fallback to JSON status`);
1160
+ }
1161
+ offset += statusTag.totalLength;
1162
+ // responseBytes [0] EXPLICIT
1163
+ if (offset >= outer.contentOffset + outer.contentLength) {
1164
+ throw new Error('invalid OCSP response: missing responseBytes');
1165
+ }
1166
+ const respBytesTag = this._readAsn1Tag(ocspDer, offset);
1167
+ if (!respBytesTag)
1168
+ throw new Error('invalid OCSP response: cannot read responseBytes');
1169
+ // 内部 SEQUENCE { responseType OID, response OCTET STRING }
1170
+ const respBytesSeq = this._readAsn1Sequence(ocspDer, respBytesTag.contentOffset);
1171
+ if (!respBytesSeq)
1172
+ throw new Error('invalid OCSP response: responseBytes not a SEQUENCE');
1173
+ // 跳过 responseType OID
1174
+ let innerOffset = respBytesSeq.contentOffset;
1175
+ const oidTag = this._readAsn1Tag(ocspDer, innerOffset);
1176
+ if (!oidTag)
1177
+ throw new Error('invalid OCSP response: missing responseType OID');
1178
+ innerOffset += oidTag.totalLength;
1179
+ // response OCTET STRING (包含 BasicOCSPResponse DER)
1180
+ const octetTag = this._readAsn1Tag(ocspDer, innerOffset);
1181
+ if (!octetTag || ocspDer[innerOffset] !== 0x04) {
1182
+ throw new Error('invalid OCSP response: missing response OCTET STRING');
1183
+ }
1184
+ const basicDer = ocspDer.subarray(octetTag.contentOffset, octetTag.contentOffset + octetTag.contentLength);
1185
+ // BasicOCSPResponse SEQUENCE
1186
+ const basicSeq = this._readAsn1Sequence(basicDer, 0);
1187
+ if (!basicSeq)
1188
+ throw new Error('invalid OCSP response: BasicOCSPResponse not a SEQUENCE');
1189
+ // tbsResponseData SEQUENCE
1190
+ const tbsSeq = this._readAsn1Sequence(basicDer, basicSeq.contentOffset);
1191
+ if (!tbsSeq)
1192
+ throw new Error('invalid OCSP response: tbsResponseData not a SEQUENCE');
1193
+ // 解析 tbsResponseData 以找到 responses
1194
+ // 字段顺序:version?, responderID, producedAt, responses, extensions?
1195
+ let tbsOffset = tbsSeq.contentOffset;
1196
+ const tbsEnd = tbsSeq.contentOffset + tbsSeq.contentLength;
1197
+ // 跳过 version [0] EXPLICIT(如果存在)
1198
+ if (tbsOffset < tbsEnd && (basicDer[tbsOffset] & 0xe0) === 0xa0) {
1199
+ const versionTag = this._readAsn1Tag(basicDer, tbsOffset);
1200
+ if (versionTag)
1201
+ tbsOffset += versionTag.totalLength;
1202
+ }
1203
+ // 跳过 responderID (CHOICE)
1204
+ if (tbsOffset < tbsEnd) {
1205
+ const respIdTag = this._readAsn1Tag(basicDer, tbsOffset);
1206
+ if (respIdTag)
1207
+ tbsOffset += respIdTag.totalLength;
1208
+ }
1209
+ // 跳过 producedAt GeneralizedTime
1210
+ if (tbsOffset < tbsEnd) {
1211
+ const prodAtTag = this._readAsn1Tag(basicDer, tbsOffset);
1212
+ if (prodAtTag)
1213
+ tbsOffset += prodAtTag.totalLength;
1214
+ }
1215
+ // responses SEQUENCE OF SingleResponse
1216
+ if (tbsOffset >= tbsEnd)
1217
+ throw new Error('invalid OCSP response: missing responses');
1218
+ const responsesSeq = this._readAsn1Sequence(basicDer, tbsOffset);
1219
+ if (!responsesSeq)
1220
+ throw new Error('invalid OCSP response: responses not a SEQUENCE');
1221
+ // 解析第一个 SingleResponse
1222
+ const singleSeq = this._readAsn1Sequence(basicDer, responsesSeq.contentOffset);
1223
+ if (!singleSeq)
1224
+ throw new Error('invalid OCSP response: SingleResponse not a SEQUENCE');
1225
+ // SingleResponse: { certID SEQUENCE, certStatus CHOICE, thisUpdate, nextUpdate? }
1226
+ let srOffset = singleSeq.contentOffset;
1227
+ // 跳过 certID SEQUENCE
1228
+ const certIdTag = this._readAsn1Tag(basicDer, srOffset);
1229
+ if (!certIdTag)
1230
+ throw new Error('invalid OCSP response: missing certID');
1231
+ srOffset += certIdTag.totalLength;
1232
+ // certStatus CHOICE: good [0] IMPLICIT NULL, revoked [1] IMPLICIT ..., unknown [2] IMPLICIT NULL
1233
+ if (srOffset >= singleSeq.contentOffset + singleSeq.contentLength) {
1234
+ throw new Error('invalid OCSP response: missing certStatus');
1235
+ }
1236
+ const certStatusByte = basicDer[srOffset];
1237
+ // context-specific tag class (bits 7-6 = 10), constructed bit (bit 5)
1238
+ const tagNumber = certStatusByte & 0x1f;
1239
+ if (tagNumber === 0)
1240
+ return 'good';
1241
+ if (tagNumber === 1)
1242
+ return 'revoked';
1243
+ if (tagNumber === 2)
1244
+ return 'unknown';
1245
+ return 'unknown';
1246
+ }
1247
+ // ── ASN.1 DER 辅助解析 ─────────────────────────────────────
1248
+ /** 读取 ASN.1 SEQUENCE 标签,返回内容偏移和长度 */
1249
+ _readAsn1Sequence(buf, offset) {
1250
+ if (offset >= buf.length)
1251
+ return null;
1252
+ const tag = buf[offset];
1253
+ // SEQUENCE 或 CONSTRUCTED SEQUENCE (0x30)
1254
+ if (tag !== 0x30) {
1255
+ // 也接受 context-specific constructed tags
1256
+ if ((tag & 0x20) === 0)
1257
+ return null;
1258
+ }
1259
+ return this._readAsn1Tag(buf, offset);
1260
+ }
1261
+ /** 读取任意 ASN.1 标签的长度信息 */
1262
+ _readAsn1Tag(buf, offset) {
1263
+ if (offset >= buf.length)
1264
+ return null;
1265
+ let pos = offset + 1; // 跳过 tag byte
1266
+ if (pos >= buf.length)
1267
+ return null;
1268
+ // 读取长度
1269
+ const firstLenByte = buf[pos];
1270
+ let contentLength;
1271
+ let lenBytes;
1272
+ if (firstLenByte < 0x80) {
1273
+ // 短格式
1274
+ contentLength = firstLenByte;
1275
+ lenBytes = 1;
1276
+ }
1277
+ else if (firstLenByte === 0x80) {
1278
+ // 不定长格式(不支持)
1279
+ return null;
1280
+ }
1281
+ else {
1282
+ // 长格式
1283
+ const numLenBytes = firstLenByte & 0x7f;
1284
+ if (pos + numLenBytes >= buf.length)
1285
+ return null;
1286
+ contentLength = 0;
1287
+ for (let i = 0; i < numLenBytes; i++) {
1288
+ contentLength = (contentLength << 8) | buf[pos + 1 + i];
1289
+ }
1290
+ lenBytes = 1 + numLenBytes;
1291
+ }
1292
+ const contentOffset = offset + 1 + lenBytes;
1293
+ const totalLength = 1 + lenBytes + contentLength;
1294
+ return { contentOffset, contentLength, totalLength };
1295
+ }
1296
+ // ── 内部方法:证书恢复 ─────────────────────────────────────
1297
+ /**
1298
+ * 从 PKI HTTP 端点下载证书恢复。
1299
+ * 本地有密钥但无证书、服务端已注册时使用。
1300
+ */
1301
+ async _recoverCertViaDownload(gatewayUrl, identity) {
1302
+ const certUrl = _gatewayHttpUrl(gatewayUrl, `/pki/cert/${identity.aid}`);
1303
+ const certPem = await _fetchText(certUrl, this._verifySsl);
1304
+ if (!certPem || !certPem.includes('BEGIN CERTIFICATE')) {
1305
+ throw new AuthError(`failed to download certificate for ${identity.aid}`);
1306
+ }
1307
+ // 验证下载的证书公钥与本地密钥对匹配
1308
+ const cert = _loadX509(certPem);
1309
+ const certPubKey = _extractPublicKey(cert);
1310
+ const certPubDer = certPubKey.export({ type: 'spki', format: 'der' });
1311
+ const localPubDer = Buffer.from(String(identity.public_key_der_b64), 'base64');
1312
+ if (!certPubDer.equals(localPubDer)) {
1313
+ throw new AuthError(`downloaded certificate public key does not match local key pair for ${identity.aid}. ` +
1314
+ `The server has a different key registered — this AID cannot be recovered with the current key.`);
1315
+ }
1316
+ identity.cert = certPem;
1317
+ return identity;
1318
+ }
1319
+ // ── 内部方法:new_cert 验证 ────────────────────────────────
1320
+ /**
1321
+ * 验证服务端返回的 new_cert,通过后才正式接受。
1322
+ * 安全要点:CN/公钥/时间 + 完整链验证 + 受信根锚定 + CRL/OCSP。
1323
+ */
1324
+ async _validateNewCert(identity, gatewayUrl = '') {
1325
+ const newCertPem = identity._pending_new_cert;
1326
+ delete identity._pending_new_cert;
1327
+ if (!newCertPem)
1328
+ return;
1329
+ try {
1330
+ const certPemStr = typeof newCertPem === 'string' ? newCertPem : String(newCertPem);
1331
+ const cert = _loadX509(certPemStr);
1332
+ const aid = String(identity.aid || '');
1333
+ // 1. CN 必须匹配当前 AID
1334
+ const cn = _certSubjectCN(cert);
1335
+ if (cn !== aid) {
1336
+ throw new AuthError(`new_cert CN mismatch: expected ${aid}, got ${cn || 'none'}`);
1337
+ }
1338
+ // 2. 公钥必须匹配本地私钥
1339
+ const certPubKey = _extractPublicKey(cert);
1340
+ const certPubDer = certPubKey.export({ type: 'spki', format: 'der' });
1341
+ const localPubB64 = String(identity.public_key_der_b64 || '');
1342
+ if (localPubB64) {
1343
+ const localPubDer = Buffer.from(localPubB64, 'base64');
1344
+ if (!certPubDer.equals(localPubDer)) {
1345
+ throw new AuthError('new_cert public key does not match local identity key');
1346
+ }
1347
+ }
1348
+ // 3. 时间有效性
1349
+ _ensureCertTimeValid(cert, 'new_cert', Date.now());
1350
+ // 4. 完整证书链验证 + 受信根锚定 + CRL/OCSP
1351
+ if (gatewayUrl) {
1352
+ await this._verifyAuthCertChain(gatewayUrl, cert);
1353
+ try {
1354
+ await this._verifyAuthCertRevocation(gatewayUrl, cert);
1355
+ }
1356
+ catch (e) {
1357
+ const errMsg = e instanceof Error ? e.message : String(e);
1358
+ if (/revoked/i.test(errMsg))
1359
+ throw e instanceof AuthError ? e : new AuthError(errMsg);
1360
+ _authLog('warn', 'CRL 检查不可用,降级继续: %s', errMsg);
1361
+ }
1362
+ try {
1363
+ await this._verifyAuthCertOcsp(gatewayUrl, cert);
1364
+ }
1365
+ catch (exc) {
1366
+ const errMsg = exc instanceof Error ? exc.message : String(exc);
1367
+ if (/revoked/i.test(errMsg))
1368
+ throw exc instanceof AuthError ? exc : new AuthError(errMsg);
1369
+ _authLog('warn', 'OCSP 校验不可用,降级继续: %s', errMsg);
1370
+ }
1371
+ }
1372
+ // 验证通过,正式接受
1373
+ identity.cert = certPemStr;
1374
+ }
1375
+ catch (e) {
1376
+ if (e instanceof AuthError) {
1377
+ _authLog('warn', '拒绝服务端返回的 new_cert (%s): %s', identity.aid, e.message);
1378
+ }
1379
+ else {
1380
+ _authLog('warn', 'new_cert 验证异常 (%s): %s', identity.aid, e instanceof Error ? e.message : String(e));
1381
+ }
1382
+ }
1383
+ // active_cert 同步:验证公钥匹配后更新本地 cert
1384
+ const activeCertPem = identity._pending_active_cert;
1385
+ delete identity._pending_active_cert;
1386
+ if (typeof activeCertPem === 'string' && activeCertPem) {
1387
+ try {
1388
+ const actCert = _loadX509(activeCertPem);
1389
+ const actPubDer = _extractPublicKey(actCert).export({ type: 'spki', format: 'der' });
1390
+ const localPubB64 = String(identity.public_key_der_b64 || '');
1391
+ if (localPubB64) {
1392
+ const localPubDer = Buffer.from(localPubB64, 'base64');
1393
+ if (actPubDer.equals(localPubDer)) {
1394
+ identity.cert = activeCertPem;
1395
+ }
1396
+ else {
1397
+ _authLog('warn', '服务端 active_cert 公钥与本地私钥不匹配,拒绝同步 (aid=%s)', identity.aid);
1398
+ }
1399
+ }
1400
+ }
1401
+ catch (e) {
1402
+ _authLog('warn', 'active_cert 同步异常 (%s): %s', identity.aid, e instanceof Error ? e.message : String(e));
1403
+ }
1404
+ }
1405
+ }
1406
+ // ── 内部方法:Token 管理 ───────────────────────────────────
1407
+ /**
1408
+ * 从认证结果中提取并保存 token 到 identity。
1409
+ * 与 Python SDK 的 _remember_tokens 对齐。
1410
+ */
1411
+ static _rememberTokens(identity, authResult) {
1412
+ const accessToken = authResult.access_token || authResult.token || authResult.kite_token;
1413
+ const refreshToken = authResult.refresh_token;
1414
+ const expiresIn = authResult.expires_in;
1415
+ if (typeof accessToken === 'string' && accessToken)
1416
+ identity.access_token = accessToken;
1417
+ if (typeof refreshToken === 'string' && refreshToken)
1418
+ identity.refresh_token = refreshToken;
1419
+ if (typeof authResult.token === 'string' && authResult.token)
1420
+ identity.kite_token = authResult.token;
1421
+ if (typeof expiresIn === 'number') {
1422
+ identity.access_token_expires_at = Math.floor(Date.now() / 1000 + expiresIn);
1423
+ }
1424
+ // 协议要求:login2 响应含 new_cert 时(证书过半自动续期),客户端必须保存
1425
+ // 先暂存到 _pending_new_cert,由 _validate_new_cert 验证后再正式接受
1426
+ const newCert = authResult.new_cert;
1427
+ if (typeof newCert === 'string' && newCert) {
1428
+ identity._pending_new_cert = newCert;
1429
+ }
1430
+ // 服务端返回 active_cert 用于同步本地 cert.pem
1431
+ const activeCert = authResult.active_cert;
1432
+ if (typeof activeCert === 'string' && activeCert) {
1433
+ identity._pending_active_cert = activeCert;
1434
+ }
1435
+ }
1436
+ /**
1437
+ * 获取缓存的 access_token(30 秒提前过期余量)。
1438
+ * 返回空字符串表示 token 不可用。
1439
+ */
1440
+ static _getCachedAccessToken(identity) {
1441
+ const accessToken = String(identity.access_token || '');
1442
+ if (!accessToken)
1443
+ return '';
1444
+ const expiresAt = identity.access_token_expires_at;
1445
+ if (typeof expiresAt === 'number' && expiresAt <= Date.now() / 1000 + 30) {
1446
+ return '';
1447
+ }
1448
+ return accessToken;
1449
+ }
1450
+ // ── 内部方法:身份管理 ─────────────────────────────────────
1451
+ // AID name 验证:4-64 字符,仅 [a-z0-9_-],首字符不为 -,不以 guest 开头
1452
+ static _AID_NAME_RE = /^[a-z0-9_][a-z0-9_-]{3,63}$/;
1453
+ static _validateAidName(aid) {
1454
+ const name = aid.includes('.') ? aid.split('.')[0] : aid;
1455
+ if (!AuthFlow._AID_NAME_RE.test(name)) {
1456
+ throw new ValidationError(`Invalid AID name '${name}': must be 4-64 characters, only [a-z0-9_-], cannot start with '-'`);
1457
+ }
1458
+ if (name.startsWith('guest')) {
1459
+ throw new ValidationError("AID name must not start with 'guest'");
1460
+ }
1461
+ }
1462
+ /** 确保本地有指定 AID 的身份(不存在则创建密钥对) */
1463
+ _ensureLocalIdentity(aid) {
1464
+ const existing = this._keystore.loadIdentity(aid);
1465
+ if (existing) {
1466
+ this._aid = aid;
1467
+ return existing;
1468
+ }
1469
+ const identity = this._crypto.generateIdentity();
1470
+ identity.aid = aid;
1471
+ this._persistIdentity(identity); // 立即持久化密钥对
1472
+ this._aid = aid;
1473
+ return identity;
1474
+ }
1475
+ /** 加载身份信息,不存在时抛出 StateError */
1476
+ _loadIdentityOrRaise(aid) {
1477
+ const requestedAid = aid ?? this._aid;
1478
+ if (requestedAid) {
1479
+ const existing = this._keystore.loadIdentity(requestedAid);
1480
+ if (existing === null) {
1481
+ throw new StateError(`identity not found for aid: ${requestedAid}`);
1482
+ }
1483
+ this._aid = requestedAid;
1484
+ if (!existing.aid)
1485
+ existing.aid = requestedAid;
1486
+ return existing;
1487
+ }
1488
+ // 尝试加载任意身份
1489
+ const ks = this._keystore;
1490
+ if (typeof ks.loadAnyIdentity === 'function') {
1491
+ const existing = ks.loadAnyIdentity();
1492
+ if (existing !== null && existing !== undefined) {
1493
+ const loadedAid = existing.aid;
1494
+ if (typeof loadedAid === 'string' && loadedAid) {
1495
+ this._aid = loadedAid;
1496
+ }
1497
+ return existing;
1498
+ }
1499
+ }
1500
+ throw new StateError('no local identity found, call auth.createAid() first');
1501
+ }
1502
+ /** 确保有身份(不存在时自动创建密钥对) */
1503
+ _ensureIdentity() {
1504
+ try {
1505
+ return this._loadIdentityOrRaise();
1506
+ }
1507
+ catch (e) {
1508
+ if (!(e instanceof StateError))
1509
+ throw e;
1510
+ if (!this._aid) {
1511
+ throw new StateError('no local identity found, call auth.createAid() first');
1512
+ }
1513
+ const identity = this._crypto.generateIdentity();
1514
+ identity.aid = this._aid;
1515
+ this._persistIdentity(identity); // 立即持久化
1516
+ return identity;
1517
+ }
1518
+ }
1519
+ _loadInstanceState(aid) {
1520
+ if (!this._deviceId) {
1521
+ return null;
1522
+ }
1523
+ const loader = this._keystore.loadInstanceState;
1524
+ if (typeof loader !== 'function') {
1525
+ return null;
1526
+ }
1527
+ return loader.call(this._keystore, aid, this._deviceId, this._slotId);
1528
+ }
1529
+ _persistIdentity(identity) {
1530
+ const aid = String(identity.aid ?? '');
1531
+ if (!aid) {
1532
+ throw new StateError('identity missing aid');
1533
+ }
1534
+ const persisted = { ...identity };
1535
+ const instanceState = {};
1536
+ for (const key of AuthFlow._INSTANCE_STATE_FIELDS) {
1537
+ if (key in persisted) {
1538
+ instanceState[key] = persisted[key];
1539
+ delete persisted[key];
1540
+ }
1541
+ }
1542
+ this._keystore.saveIdentity(aid, persisted);
1543
+ if (this._deviceId) {
1544
+ // 从共享 metadata_kv 中移除实例级字段(它们已保存到 instance_state)
1545
+ const db = this._keystore._getDB?.(aid);
1546
+ if (db && typeof db.deleteMetadata === 'function') {
1547
+ for (const key of AuthFlow._INSTANCE_STATE_FIELDS) {
1548
+ db.deleteMetadata(key);
1549
+ db.deleteMetadata(`${key}_protection`);
1550
+ }
1551
+ }
1552
+ }
1553
+ if (!this._deviceId || Object.keys(instanceState).length === 0) {
1554
+ return;
1555
+ }
1556
+ const updater = this._keystore.updateInstanceState;
1557
+ if (typeof updater !== 'function') {
1558
+ return;
1559
+ }
1560
+ updater.call(this._keystore, aid, this._deviceId, this._slotId, (current) => {
1561
+ Object.assign(current, instanceState);
1562
+ return current;
1563
+ });
1564
+ }
1565
+ /** 从 challenge 消息中提取 nonce */
1566
+ _extractChallengeNonce(challenge) {
1567
+ const params = isJsonObject(challenge?.params) ? challenge.params : {};
1568
+ const nonce = String(params.nonce || '');
1569
+ if (!nonce) {
1570
+ throw new AuthError('gateway challenge missing nonce');
1571
+ }
1572
+ return nonce;
1573
+ }
1574
+ // ── 内部方法:根证书管理 ───────────────────────────────────
1575
+ /** 加载受信根证书列表 */
1576
+ _loadTrustedRoots() {
1577
+ if (!this._rootCerts.length) {
1578
+ throw new AuthError('no trusted roots available for auth certificate verification');
1579
+ }
1580
+ return this._rootCerts;
1581
+ }
1582
+ /**
1583
+ * 加载根证书:内置 + 自定义路径。
1584
+ * 在 SDK 的 certs/ 目录下查找 *.crt 文件。
1585
+ */
1586
+ _loadRootCerts(rootCaPath) {
1587
+ const candidatePaths = [];
1588
+ if (rootCaPath) {
1589
+ candidatePaths.push(rootCaPath);
1590
+ }
1591
+ // 内置 certs 目录:多种路径策略确保找到
1592
+ let normalizedSrcDir;
1593
+ try {
1594
+ normalizedSrcDir = path.dirname(fileURLToPath(import.meta.url));
1595
+ }
1596
+ catch {
1597
+ normalizedSrcDir = __dirname ?? process.cwd();
1598
+ }
1599
+ const bundledDirs = [
1600
+ path.join(normalizedSrcDir, 'certs'),
1601
+ path.join(normalizedSrcDir, '..', 'certs'),
1602
+ path.join(normalizedSrcDir, '..', 'src', 'certs'),
1603
+ // 从 process.cwd() 出发(vitest 运行时 cwd 通常是包根目录)
1604
+ path.join(process.cwd(), 'src', 'certs'),
1605
+ ];
1606
+ for (const dir of bundledDirs) {
1607
+ try {
1608
+ if (fs.existsSync(dir)) {
1609
+ const files = fs.readdirSync(dir)
1610
+ .filter((f) => f.endsWith('.crt'))
1611
+ .sort()
1612
+ .map((f) => path.join(dir, f));
1613
+ candidatePaths.push(...files);
1614
+ }
1615
+ }
1616
+ catch {
1617
+ // 目录不存在或不可访问
1618
+ }
1619
+ }
1620
+ const certs = [];
1621
+ const seenDer = new Set();
1622
+ for (const certPath of candidatePaths) {
1623
+ let text;
1624
+ try {
1625
+ text = fs.readFileSync(certPath, 'utf-8');
1626
+ }
1627
+ catch (e) {
1628
+ throw new AuthError(`failed to read root certificate bundle: ${certPath}`);
1629
+ }
1630
+ const pems = _splitPemBundle(text);
1631
+ for (const pem of pems) {
1632
+ try {
1633
+ const cert = _loadX509(pem);
1634
+ const der = _certDer(cert).toString('hex');
1635
+ if (seenDer.has(der))
1636
+ continue;
1637
+ seenDer.add(der);
1638
+ certs.push(cert);
1639
+ }
1640
+ catch {
1641
+ // 跳过无法解析的证书
1642
+ }
1643
+ }
1644
+ }
1645
+ return certs;
1646
+ }
1647
+ /** 清理过期的 gateway 缓存条目(供外部定时调用) */
1648
+ cleanExpiredCaches() {
1649
+ const now = Date.now();
1650
+ for (const [k, v] of this._gatewayCrlCache) {
1651
+ if (v.nextRefreshAt <= now)
1652
+ this._gatewayCrlCache.delete(k);
1653
+ }
1654
+ for (const [k, v] of this._gatewayOcspCache) {
1655
+ for (const [serial, entry] of v) {
1656
+ if (entry.nextRefreshAt <= now)
1657
+ v.delete(serial);
1658
+ }
1659
+ if (v.size === 0)
1660
+ this._gatewayOcspCache.delete(k);
1661
+ }
1662
+ for (const [k, v] of this._chainVerifiedCache) {
1663
+ if (now - v >= 300_000)
1664
+ this._chainVerifiedCache.delete(k);
1665
+ }
1666
+ }
1667
+ }
1668
+ //# sourceMappingURL=auth.js.map