@agentunion/fastaun-browser 0.5.18 → 0.5.19

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 (118) hide show
  1. package/CHANGELOG.md +44 -2
  2. package/_packed_docs/CHANGELOG.md +44 -2
  3. package/_packed_docs/INDEX.md +136 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +42 -15
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
  8. 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 +45 -134
  9. 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 +5 -20
  10. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
  11. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +22 -27
  12. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +250 -283
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +74 -64
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +76 -17
  20. package/dist/agent-md-schema.d.ts +3 -0
  21. package/dist/agent-md-schema.d.ts.map +1 -1
  22. package/dist/agent-md-schema.js +45 -11
  23. package/dist/agent-md-schema.js.map +1 -1
  24. package/dist/agent-md.d.ts +45 -2
  25. package/dist/agent-md.d.ts.map +1 -1
  26. package/dist/agent-md.js +519 -30
  27. package/dist/agent-md.js.map +1 -1
  28. package/dist/aid-store.d.ts +12 -46
  29. package/dist/aid-store.d.ts.map +1 -1
  30. package/dist/aid-store.js +113 -246
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/aid.d.ts.map +1 -1
  33. package/dist/aid.js +20 -7
  34. package/dist/aid.js.map +1 -1
  35. package/dist/auth.d.ts +4 -1
  36. package/dist/auth.d.ts.map +1 -1
  37. package/dist/auth.js +343 -23
  38. package/dist/auth.js.map +1 -1
  39. package/dist/bundle.js +37330 -32005
  40. package/dist/cert-utils.d.ts +1 -1
  41. package/dist/cert-utils.d.ts.map +1 -1
  42. package/dist/cert-utils.js +19 -7
  43. package/dist/cert-utils.js.map +1 -1
  44. package/dist/client/delivery.d.ts +15 -1
  45. package/dist/client/delivery.d.ts.map +1 -1
  46. package/dist/client/delivery.js +147 -51
  47. package/dist/client/delivery.js.map +1 -1
  48. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  49. package/dist/client/rpc-pipeline.js +19 -5
  50. package/dist/client/rpc-pipeline.js.map +1 -1
  51. package/dist/client/v2-e2ee.d.ts +1 -0
  52. package/dist/client/v2-e2ee.d.ts.map +1 -1
  53. package/dist/client/v2-e2ee.js +104 -30
  54. package/dist/client/v2-e2ee.js.map +1 -1
  55. package/dist/client.d.ts +100 -1
  56. package/dist/client.d.ts.map +1 -1
  57. package/dist/client.js +1204 -23
  58. package/dist/client.js.map +1 -1
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +3 -2
  61. package/dist/errors.js.map +1 -1
  62. package/dist/events.d.ts +4 -0
  63. package/dist/events.d.ts.map +1 -1
  64. package/dist/events.js +41 -1
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +82 -2
  67. package/dist/facades.d.ts.map +1 -1
  68. package/dist/facades.js +98 -4
  69. package/dist/facades.js.map +1 -1
  70. package/dist/index.d.ts +7 -3
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +3 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/keystore/index.d.ts +17 -0
  75. package/dist/keystore/index.d.ts.map +1 -1
  76. package/dist/keystore/indexeddb-shared.d.ts +10 -1
  77. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  78. package/dist/keystore/indexeddb-shared.js +111 -0
  79. package/dist/keystore/indexeddb-shared.js.map +1 -1
  80. package/dist/keystore/indexeddb-token-store.d.ts +7 -1
  81. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  82. package/dist/keystore/indexeddb-token-store.js +30 -1
  83. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  84. package/dist/org-protocol.d.ts +98 -0
  85. package/dist/org-protocol.d.ts.map +1 -0
  86. package/dist/org-protocol.js +1742 -0
  87. package/dist/org-protocol.js.map +1 -0
  88. package/dist/ownership-proof.d.ts +93 -5
  89. package/dist/ownership-proof.d.ts.map +1 -1
  90. package/dist/ownership-proof.js +239 -53
  91. package/dist/ownership-proof.js.map +1 -1
  92. package/dist/ownership-v21.d.ts +177 -0
  93. package/dist/ownership-v21.d.ts.map +1 -0
  94. package/dist/ownership-v21.js +609 -0
  95. package/dist/ownership-v21.js.map +1 -0
  96. package/dist/ownership-workflow.d.ts +146 -0
  97. package/dist/ownership-workflow.d.ts.map +1 -0
  98. package/dist/ownership-workflow.js +1066 -0
  99. package/dist/ownership-workflow.js.map +1 -0
  100. package/dist/seq-tracker.d.ts +1 -0
  101. package/dist/seq-tracker.d.ts.map +1 -1
  102. package/dist/seq-tracker.js +10 -2
  103. package/dist/seq-tracker.js.map +1 -1
  104. package/dist/tools/cross-sdk-agent.d.ts +82 -0
  105. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  106. package/dist/tools/cross-sdk-agent.js +332 -7
  107. package/dist/tools/cross-sdk-agent.js.map +1 -1
  108. package/dist/transport.d.ts +6 -0
  109. package/dist/transport.d.ts.map +1 -1
  110. package/dist/transport.js +88 -2
  111. package/dist/transport.js.map +1 -1
  112. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  113. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  114. package/dist/v2/e2ee/decrypt.js +9 -1
  115. package/dist/v2/e2ee/decrypt.js.map +1 -1
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/package.json +1 -1
package/dist/auth.js CHANGED
@@ -6,6 +6,7 @@ import { AuthError, StateError, ValidationError, mapRemoteError } from './errors
6
6
  import { ROOT_CA_PEM } from './certs/root.js';
7
7
  import { VERSION as AUN_SDK_VERSION } from './version.js';
8
8
  import { createTransportWebSocket } from './transport.js';
9
+ import { certFingerprint, isStrictBase64 } from './cert-utils.js';
9
10
  const _noopLog = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
10
11
  const AUN_SDK_LANG = 'javascript';
11
12
  const SENSITIVE_RPC_LOG_KEYS = new Set([
@@ -209,7 +210,7 @@ function parseCertDer(pem) {
209
210
  const serialBytes = der.slice(serialStart, serialStart + serialLen);
210
211
  const serialHex = Array.from(serialBytes)
211
212
  .map((b) => b.toString(16).padStart(2, '0'))
212
- .join('');
213
+ .join('').replace(/^0+/, '') || '0';
213
214
  pos = serialStart + serialLen;
214
215
  // 签名算法(SEQUENCE)
215
216
  const [, sigAlgStart, sigAlgLen] = readTlv(der, pos);
@@ -333,6 +334,112 @@ async function verifyEcdsaSignature(spkiDer, signatureDer, data) {
333
334
  const p1363Sig = derToP1363(signatureDer, curveLen);
334
335
  return crypto.subtle.verify({ name: 'ECDSA', hash }, pubKey, toBufferSource(p1363Sig), toBufferSource(data));
335
336
  }
337
+ function ownershipDerNode(data, offset, limit = data.length) {
338
+ if (offset + 2 > limit || data[offset + 1] === 0x80 || data[offset + 1] > 0x84)
339
+ throw new AuthError('invalid OCSP DER length');
340
+ const lengthBytes = data[offset + 1] & 0x80 ? data[offset + 1] & 0x7f : 0;
341
+ if (offset + 2 + lengthBytes > limit || lengthBytes && data[offset + 2] === 0)
342
+ throw new AuthError('invalid OCSP DER length');
343
+ const [tag, valueStart, length] = readTlv(data, offset);
344
+ if (length < 0 || valueStart + length > limit || lengthBytes && length < 128)
345
+ throw new AuthError('invalid OCSP DER bounds');
346
+ return { tag, start: offset, valueStart, end: valueStart + length };
347
+ }
348
+ function ownershipDerChildren(data, node) {
349
+ const children = [];
350
+ for (let offset = node.valueStart; offset < node.end;) {
351
+ const child = ownershipDerNode(data, offset, node.end);
352
+ children.push(child);
353
+ offset = child.end;
354
+ }
355
+ return children;
356
+ }
357
+ async function verifyOwnershipOcsp(raw, cert, issuer) {
358
+ const outer = ownershipDerNode(raw, 0);
359
+ if (outer.tag !== 0x30 || outer.end !== raw.length)
360
+ throw new AuthError('invalid OCSP response container');
361
+ const fields = ownershipDerChildren(raw, outer);
362
+ if (!fields[0])
363
+ throw new StateError('OCSP response status is missing', { stringCode: 'unavailable', retryable: true });
364
+ if (fields[0].tag !== 0x0a || fields[0].end - fields[0].valueStart !== 1)
365
+ throw new AuthError('invalid OCSP response status');
366
+ if (raw[fields[0].valueStart] !== 0 || !fields[1])
367
+ throw new StateError('OCSP current signed status unavailable', { stringCode: 'unavailable', retryable: true });
368
+ if (fields.length !== 2 || fields[1].tag !== 0xa0)
369
+ throw new AuthError('invalid OCSP response fields');
370
+ const response = ownershipDerChildren(raw, ownershipDerChildren(raw, fields[1])[0]);
371
+ if (response.length !== 2 || response[0].tag !== 0x06 || parseOid(raw.slice(response[0].valueStart, response[0].end)) !== '1.3.6.1.5.5.7.48.1.1' || response[1].tag !== 0x04)
372
+ throw new AuthError('invalid OCSP basic response');
373
+ const basic = raw.slice(response[1].valueStart, response[1].end);
374
+ const basicNode = ownershipDerNode(basic, 0);
375
+ const items = ownershipDerChildren(basic, basicNode);
376
+ if (basicNode.tag !== 0x30 || basicNode.end !== basic.length || (items.length !== 3 && items.length !== 4) || items[0].tag !== 0x30 || items[1].tag !== 0x30 || items[2].tag !== 0x03 || basic[items[2].valueStart] !== 0)
377
+ throw new AuthError('invalid OCSP signed response');
378
+ if (items[3]) {
379
+ if (items[3].tag !== 0xa0)
380
+ throw new AuthError('invalid OCSP embedded certificates');
381
+ const sequences = ownershipDerChildren(basic, items[3]);
382
+ if (sequences.length !== 1 || sequences[0].tag !== 0x30)
383
+ throw new AuthError('invalid OCSP embedded certificates');
384
+ const certificates = ownershipDerChildren(basic, sequences[0]);
385
+ const issuerDer = new Uint8Array(pemToArrayBuffer(issuer.pem));
386
+ if (certificates.length !== 1 || certificates[0].tag !== 0x30 || !arraysEqual(basic.slice(certificates[0].start, certificates[0].end), issuerDer))
387
+ throw new AuthError('OCSP embedded certificate is not the verified issuer');
388
+ }
389
+ const algorithm = ownershipDerChildren(basic, items[1])[0];
390
+ await verifyCertSignature(issuer.spkiBytes, { ...cert,
391
+ tbsBytes: basic.slice(items[0].start, items[0].end), signatureBytes: basic.slice(items[2].valueStart + 1, items[2].end),
392
+ signatureAlgorithmOid: parseOid(basic.slice(algorithm.valueStart, algorithm.end)),
393
+ });
394
+ const data = ownershipDerChildren(basic, items[0]);
395
+ if (data[0]?.tag === 0xa0)
396
+ data.shift();
397
+ if (data.length < 3 || ![0xa1, 0xa2, 0x82].includes(data[0].tag) || data[1].tag !== 0x18 || data[2].tag !== 0x30)
398
+ throw new AuthError('invalid OCSP response data');
399
+ const publicBits = ownershipDerChildren(issuer.spkiBytes, ownershipDerNode(issuer.spkiBytes, 0))[1];
400
+ const responder = data[0], keyHash = new Uint8Array(await crypto.subtle.digest('SHA-1', toBufferSource(issuer.spkiBytes.slice(publicBits.valueStart + 1, publicBits.end))));
401
+ const responderKey = responder.tag === 0xa2 ? ownershipDerChildren(basic, responder) : [];
402
+ const responderValid = responder.tag === 0xa1 ? arraysEqual(basic.slice(responder.valueStart, responder.end), issuer.subjectRaw)
403
+ : responder.tag === 0x82 ? arraysEqual(basic.slice(responder.valueStart, responder.end), keyHash)
404
+ : responderKey.length === 1 && responderKey[0].tag === 0x04 && arraysEqual(basic.slice(responderKey[0].valueStart, responderKey[0].end), keyHash);
405
+ if (!responderValid)
406
+ throw new AuthError('OCSP responder is not the verified issuer');
407
+ const responses = ownershipDerChildren(basic, data[2]);
408
+ if (responses.length !== 1)
409
+ throw new AuthError('OCSP response must identify one certificate');
410
+ const single = ownershipDerChildren(basic, responses[0]);
411
+ if (single.length < 4 || single[3].tag !== 0xa0)
412
+ throw new StateError('OCSP current time evidence unavailable', { stringCode: 'unavailable', retryable: true });
413
+ if (single[0].tag !== 0x30 || single[2].tag !== 0x18)
414
+ throw new AuthError('invalid OCSP current time evidence');
415
+ const certId = ownershipDerChildren(basic, single[0]);
416
+ if (certId.length !== 4 || certId[1].tag !== 0x04 || certId[2].tag !== 0x04 || certId[3].tag !== 0x02)
417
+ throw new AuthError('invalid OCSP certificate identity');
418
+ const hashAlgorithm = ownershipDerChildren(basic, certId[0])[0];
419
+ const hashOid = parseOid(basic.slice(hashAlgorithm.valueStart, hashAlgorithm.end));
420
+ const hashName = hashOid === '1.3.14.3.2.26' ? 'SHA-1' : hashOid === '2.16.840.1.101.3.4.2.1' ? 'SHA-256' : '';
421
+ if (!hashName)
422
+ throw new StateError('unsupported OCSP certificate identity hash', { stringCode: 'unavailable', retryable: true });
423
+ const digest = async (bytes) => new Uint8Array(await crypto.subtle.digest(hashName, toBufferSource(bytes)));
424
+ if (!arraysEqual(await digest(issuer.subjectRaw), basic.slice(certId[1].valueStart, certId[1].end)) || !arraysEqual(await digest(issuer.spkiBytes.slice(publicBits.valueStart + 1, publicBits.end)), basic.slice(certId[2].valueStart, certId[2].end)))
425
+ throw new AuthError('OCSP issuer identity mismatch');
426
+ const serial = Array.from(basic.slice(certId[3].valueStart, certId[3].end), (byte) => byte.toString(16).padStart(2, '0')).join('').replace(/^0+/, '') || '0';
427
+ if (serial !== (cert.serialHex.replace(/^0+/, '') || '0'))
428
+ throw new AuthError('OCSP certificate serial mismatch');
429
+ const status = single[1].tag === 0xa1 ? 'revoked' : single[1].tag === 0x80 && single[1].end === single[1].valueStart ? 'good' : single[1].tag === 0x82 && single[1].end === single[1].valueStart ? 'unknown' : '';
430
+ if (!status)
431
+ throw new AuthError('invalid OCSP certificate status');
432
+ const next = ownershipDerChildren(basic, single[3])[0];
433
+ const time = (node) => {
434
+ if (node.tag !== 0x18 || !/^\d{14}Z$/.test(new TextDecoder().decode(basic.slice(node.valueStart, node.end))))
435
+ throw new AuthError('invalid OCSP UTC time');
436
+ return parseAsn1Time(basic, node.start);
437
+ };
438
+ const now = Date.now() / 1000, producedAt = time(data[1]), thisUpdate = time(single[2]), nextUpdate = time(next);
439
+ if (producedAt > now + 5 || producedAt < now - 90 || thisUpdate > now + 5 || thisUpdate < now - 30 || nextUpdate <= now || nextUpdate <= thisUpdate)
440
+ throw new StateError('OCSP current status evidence is stale', { stringCode: 'unavailable', retryable: true });
441
+ return status;
442
+ }
336
443
  // ── Gateway URL 工具 ────────────────────────────────────
337
444
  /** 将 WebSocket URL 转为对应的 HTTP URL */
338
445
  function gatewayHttpUrl(gatewayUrl, path) {
@@ -754,13 +861,136 @@ export class AuthFlow {
754
861
  /**
755
862
  * 统一的对端证书验证入口:时间有效性 + 链验证 + CRL + OCSP + AID 绑定。
756
863
  */
757
- async verifyPeerCertificate(gatewayUrl, certPem, expectedAid) {
864
+ async resolveCurrentEntityCertificate(gatewayUrl, aid, type) {
865
+ const url = new URL(gatewayHttpUrl(gatewayUrl, `/pki/cert/${encodeURIComponent(aid)}`));
866
+ url.searchParams.set('consistency', 'authoritative');
867
+ if (type === 'group')
868
+ url.searchParams.set('key_purpose', 'group_identity');
869
+ const controller = new AbortController();
870
+ const timeout = setTimeout(() => controller.abort(), 5000);
871
+ try {
872
+ const response = await fetch(url.toString(), { signal: controller.signal, cache: 'no-store', redirect: 'error' });
873
+ if (!response.ok)
874
+ throw new StateError(`entity current certificate unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
875
+ const certPem = await response.text();
876
+ if (!certPem.trim())
877
+ throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
878
+ let fingerprint;
879
+ try {
880
+ parseCertDer(certPem);
881
+ fingerprint = await certFingerprint(certPem);
882
+ }
883
+ catch {
884
+ throw new ValidationError('entity current certificate is invalid', { stringCode: 'invalid' });
885
+ }
886
+ return await this.resolveOwnershipCertificate(gatewayUrl, aid, fingerprint, type === 'group' ? 'group_identity' : undefined);
887
+ }
888
+ catch (error) {
889
+ if (error instanceof StateError || error instanceof ValidationError)
890
+ throw error;
891
+ throw new StateError(`entity current certificate unavailable: ${String(error)}`, { stringCode: 'unavailable', retryable: true });
892
+ }
893
+ finally {
894
+ clearTimeout(timeout);
895
+ }
896
+ }
897
+ async resolveOwnershipCertificate(gatewayUrl, expectedAid, fingerprint, keyPurpose) {
898
+ if (!/^sha256:[0-9a-f]{64}$/.test(fingerprint))
899
+ throw new ValidationError('ownership certificate fingerprint is invalid', { stringCode: 'invalid' });
900
+ const began = Date.now();
901
+ const url = new URL(gatewayHttpUrl(gatewayUrl, `/pki/cert/${encodeURIComponent(expectedAid)}`));
902
+ url.searchParams.set('cert_fingerprint', fingerprint);
903
+ url.searchParams.set('consistency', 'authoritative');
904
+ if (keyPurpose)
905
+ url.searchParams.set('key_purpose', keyPurpose);
906
+ const controller = new AbortController();
907
+ const timeout = setTimeout(() => controller.abort(), 5000);
908
+ try {
909
+ const response = await fetch(url.toString(), { signal: controller.signal, cache: 'no-store', redirect: 'error' });
910
+ if (!response.ok)
911
+ throw new StateError(`ownership certificate unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
912
+ const state = response.headers.get('X-AUN-Cert-Lifecycle-State') ?? '';
913
+ const observed = response.headers.get('X-AUN-Cert-Observed-At') ?? '';
914
+ const observedAt = Math.trunc(Date.parse(observed) / 1000);
915
+ const now = Math.trunc(Date.now() / 1000);
916
+ if (response.headers.get('X-AUN-Cert-Consistency') !== 'authoritative' || !state || !response.headers.get('X-AUN-Cert-Fingerprint') || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(observed) || !Number.isSafeInteger(observedAt) || observedAt < now - 30 || observedAt > now + 5)
917
+ throw new StateError('ownership current certificate metadata unavailable', { stringCode: 'unavailable', retryable: true });
918
+ if (state !== 'active_signing' || response.headers.get('X-AUN-Cert-Fingerprint') !== fingerprint)
919
+ throw new ValidationError('ownership certificate lifecycle or fingerprint is invalid', { stringCode: 'invalid' });
920
+ const certPem = await response.text();
921
+ if (!certPem.trim())
922
+ throw new StateError('ownership certificate is missing', { stringCode: 'unavailable', retryable: true });
923
+ let cert;
924
+ try {
925
+ cert = parseCertDer(certPem);
926
+ }
927
+ catch {
928
+ throw new ValidationError('ownership certificate PEM is invalid', { stringCode: 'invalid' });
929
+ }
930
+ if (cert.subjectCN !== expectedAid || await certFingerprint(certPem) !== fingerprint || cert.notBefore > now || cert.notAfter <= now)
931
+ throw new ValidationError('ownership current certificate identity or validity mismatch', { stringCode: 'invalid' });
932
+ const chainPems = await this._fetchGatewayCaChain(gatewayUrl, true);
933
+ let chain;
934
+ try {
935
+ chain = chainPems.map((pem) => parseCertDer(pem));
936
+ }
937
+ catch {
938
+ throw new ValidationError('ownership certificate chain PEM is invalid', { stringCode: 'invalid' });
939
+ }
940
+ if (!chain.length)
941
+ throw new StateError('ownership certificate chain unavailable', { stringCode: 'unavailable', retryable: true });
942
+ if (chain.some((ca) => ca.notBefore > now || ca.notAfter <= now))
943
+ throw new ValidationError('ownership current CA certificate validity mismatch', { stringCode: 'invalid' });
944
+ await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, undefined, true, chain);
945
+ await this._fetchGatewayOcspStatus(gatewayUrl, cert, chain[0], true);
946
+ const finalNow = Date.now();
947
+ if ([cert, ...chain].some((item) => finalNow / 1000 < item.notBefore || finalNow / 1000 >= item.notAfter))
948
+ throw new ValidationError('ownership current certificate chain validity mismatch', { stringCode: 'invalid' });
949
+ if (finalNow - began > 30_000 || finalNow / 1000 - observedAt > 30)
950
+ throw new StateError('ownership current certificate evidence became stale', { stringCode: 'unavailable', retryable: true });
951
+ return { cert_pem: certPem, chain_valid: true, state, revoked: false, revocation_checked: true, observed_at: observedAt };
952
+ }
953
+ catch (error) {
954
+ if (error instanceof StateError || error instanceof ValidationError)
955
+ throw error;
956
+ if (error instanceof AuthError)
957
+ throw new ValidationError(error.message, { stringCode: 'invalid' });
958
+ throw new StateError(`ownership certificate unavailable: ${error instanceof Error ? error.message : String(error)}`, { stringCode: 'unavailable', retryable: true });
959
+ }
960
+ finally {
961
+ clearTimeout(timeout);
962
+ }
963
+ }
964
+ async verifyPeerCertificate(gatewayUrl, certPem, expectedAid, signedAt, strictRevocation = false) {
758
965
  const tStart = Date.now();
759
966
  this._log.debug(`verifyPeerCertificate enter: aid=${expectedAid} gateway=${gatewayUrl}`);
760
967
  try {
761
968
  const cert = parseCertDer(certPem);
969
+ if (signedAt !== undefined) {
970
+ ensureCertTimeValid(cert, 'peer certificate', signedAt);
971
+ await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, signedAt);
972
+ if (cert.subjectCN !== expectedAid)
973
+ throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
974
+ return;
975
+ }
762
976
  const now = Date.now() / 1000;
763
977
  ensureCertTimeValid(cert, 'peer certificate', now);
978
+ if (strictRevocation) {
979
+ const chain = (await this._fetchGatewayCaChain(gatewayUrl, true)).map((pem) => parseCertDer(pem));
980
+ if (!chain.length)
981
+ throw new AuthError('service event certificate chain is unavailable');
982
+ const current = Math.trunc(Date.now() / 1000);
983
+ if ([cert, ...chain].some((item) => current < item.notBefore || current > item.notAfter))
984
+ throw new AuthError('service event certificate is outside its validity window');
985
+ await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, undefined, true, chain);
986
+ const crl = await this._fetchGatewayCrl(gatewayUrl, chain[0], true);
987
+ if (crl.revokedSerials.has(cert.serialHex.toLowerCase()))
988
+ throw new AuthError('service event certificate has been revoked');
989
+ await this._fetchGatewayOcspStatus(gatewayUrl, cert, chain[0], true);
990
+ if (cert.subjectCN !== expectedAid)
991
+ throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
992
+ return;
993
+ }
764
994
  await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
765
995
  try {
766
996
  await this._verifyAuthCertRevocation(gatewayUrl, cert, expectedAid);
@@ -902,17 +1132,19 @@ export class AuthFlow {
902
1132
  }
903
1133
  // ── 内部方法:HTTP 请求 ───────────────────────────
904
1134
  /** fetch GET 返回文本 */
905
- async _fetchText(url) {
1135
+ async _fetchText(url, fresh = false) {
906
1136
  // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
907
1137
  const controller = new AbortController();
908
1138
  const timeoutId = setTimeout(() => controller.abort(), 5000);
909
1139
  try {
910
- const resp = await fetch(url, { signal: controller.signal });
1140
+ const resp = await fetch(url, { signal: controller.signal, ...(fresh ? { cache: 'no-store', redirect: 'error' } : {}) });
911
1141
  if (!resp.ok)
912
1142
  throw new Error(`HTTP ${resp.status}`);
913
1143
  return await resp.text();
914
1144
  }
915
1145
  catch (e) {
1146
+ if (fresh)
1147
+ throw new StateError(`ownership certificate material unavailable: ${e}`, { stringCode: 'unavailable', retryable: true });
916
1148
  throw new AuthError(`failed to fetch ${url}: ${e}`);
917
1149
  }
918
1150
  finally {
@@ -920,12 +1152,27 @@ export class AuthFlow {
920
1152
  }
921
1153
  }
922
1154
  /** fetch GET 返回 JSON */
923
- async _fetchJson(url) {
1155
+ async _fetchJson(url, fresh = false) {
1156
+ if (fresh) {
1157
+ const text = await this._fetchText(url, true);
1158
+ if (!text.trim())
1159
+ throw new StateError('ownership certificate material is missing', { stringCode: 'unavailable', retryable: true });
1160
+ let payload;
1161
+ try {
1162
+ payload = JSON.parse(text);
1163
+ }
1164
+ catch {
1165
+ throw new ValidationError('ownership certificate JSON is invalid', { stringCode: 'invalid' });
1166
+ }
1167
+ if (!isJsonObject(payload))
1168
+ throw new ValidationError('ownership certificate JSON is invalid', { stringCode: 'invalid' });
1169
+ return payload;
1170
+ }
924
1171
  // 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
925
1172
  const controller = new AbortController();
926
1173
  const timeoutId = setTimeout(() => controller.abort(), 5000);
927
1174
  try {
928
- const resp = await fetch(url, { signal: controller.signal });
1175
+ const resp = await fetch(url, { signal: controller.signal, ...(fresh ? { cache: 'no-store', redirect: 'error' } : {}) });
929
1176
  if (!resp.ok)
930
1177
  throw new Error(`HTTP ${resp.status}`);
931
1178
  const payload = await resp.json();
@@ -935,6 +1182,8 @@ export class AuthFlow {
935
1182
  return payload;
936
1183
  }
937
1184
  catch (e) {
1185
+ if (fresh)
1186
+ throw new StateError(`ownership certificate material unavailable: ${e}`, { stringCode: 'unavailable', retryable: true });
938
1187
  if (e instanceof AuthError)
939
1188
  throw e;
940
1189
  throw new AuthError(`failed to fetch ${url}: ${e}`);
@@ -1165,20 +1414,21 @@ export class AuthFlow {
1165
1414
  }
1166
1415
  }
1167
1416
  // ── 内部方法:证书链验证 ──────────────────────────
1168
- async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '') {
1417
+ async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '', signedAt, fresh = false, suppliedChain) {
1169
1418
  // 检查缓存
1170
- const cachedAt = this._chainVerifiedCache.get(authCert.serialHex);
1171
- const now = Date.now() / 1000;
1172
- if (cachedAt && now - cachedAt < this._chainCacheTtl)
1419
+ const certKey = authCert.serialHex + (signedAt === undefined ? '' : `:signed:${signedAt}`);
1420
+ const cachedAt = this._chainVerifiedCache.get(certKey);
1421
+ const now = signedAt === undefined ? Date.now() / 1000 : signedAt;
1422
+ if (!fresh && cachedAt && Date.now() / 1000 - cachedAt < this._chainCacheTtl)
1173
1423
  return;
1174
1424
  ensureCertTimeValid(authCert, 'auth certificate', now);
1175
- const chain = await this._loadGatewayCaChain(gatewayUrl, chainAid);
1425
+ const chain = suppliedChain ?? await this._loadGatewayCaChain(gatewayUrl, chainAid);
1176
1426
  if (!chain.length) {
1177
1427
  throw new AuthError('unable to verify auth certificate chain: missing CA chain');
1178
1428
  }
1179
- const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
1429
+ const cacheKey = (chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
1180
1430
  // 快速路径:CA 链已预验证过
1181
- if (this._gatewayCaVerified.get(cacheKey)) {
1431
+ if (!fresh && this._gatewayCaVerified.get(cacheKey)) {
1182
1432
  const issuer = chain[0];
1183
1433
  ensureCertTimeValid(issuer, 'Issuer CA', now);
1184
1434
  if (!issuer.isCA)
@@ -1187,7 +1437,7 @@ export class AuthFlow {
1187
1437
  throw new AuthError('auth certificate issuer mismatch');
1188
1438
  }
1189
1439
  await verifyCertSignature(issuer.spkiBytes, authCert);
1190
- this._chainVerifiedCache.set(authCert.serialHex, Date.now() / 1000);
1440
+ this._chainVerifiedCache.set(certKey, Date.now() / 1000);
1191
1441
  return;
1192
1442
  }
1193
1443
  // 完整验证路径
@@ -1211,15 +1461,25 @@ export class AuthFlow {
1211
1461
  }
1212
1462
  await verifyCertSignature(root.spkiBytes, root);
1213
1463
  // 验证根证书在受信列表中
1214
- const trustedRoots = this._loadTrustedRoots();
1464
+ let trustedRoots;
1465
+ try {
1466
+ trustedRoots = this._loadTrustedRoots();
1467
+ }
1468
+ catch (error) {
1469
+ if (fresh)
1470
+ throw new StateError('ownership trust anchors are unavailable', { stringCode: 'unavailable', retryable: true });
1471
+ throw error;
1472
+ }
1215
1473
  const rootSpkiB64 = uint8ToBase64(root.spkiBytes);
1216
1474
  const isTrusted = trustedRoots.some((trusted) => uint8ToBase64(trusted.spkiBytes) === rootSpkiB64
1217
1475
  && trusted.subjectCN === root.subjectCN);
1218
1476
  if (!isTrusted) {
1219
1477
  throw new AuthError('auth certificate chain is not anchored by a trusted root');
1220
1478
  }
1221
- this._chainVerifiedCache.set(authCert.serialHex, Date.now() / 1000);
1222
- this._gatewayCaVerified.set(cacheKey, true);
1479
+ if (!fresh) {
1480
+ this._chainVerifiedCache.set(certKey, Date.now() / 1000);
1481
+ this._gatewayCaVerified.set(cacheKey, true);
1482
+ }
1223
1483
  }
1224
1484
  /** 加载 Gateway CA 链(带缓存) */
1225
1485
  async _loadGatewayCaChain(gatewayUrl, chainAid = '') {
@@ -1232,9 +1492,9 @@ export class AuthFlow {
1232
1492
  return cached.map((pem) => parseCertDer(pem));
1233
1493
  }
1234
1494
  /** 从 Gateway PKI 端点下载 CA 链 */
1235
- async _fetchGatewayCaChain(gatewayUrl) {
1495
+ async _fetchGatewayCaChain(gatewayUrl, fresh = false) {
1236
1496
  const url = gatewayHttpUrl(gatewayUrl, '/pki/chain');
1237
- const text = await this._fetchText(url);
1497
+ const text = await this._fetchText(url, fresh);
1238
1498
  return splitPemBundle(text);
1239
1499
  }
1240
1500
  // ── 内部方法:CRL 验证 ────────────────────────────
@@ -1270,9 +1530,47 @@ export class AuthFlow {
1270
1530
  return fresh.revokedSerials;
1271
1531
  }
1272
1532
  /** 从 Gateway PKI 端点获取并验证 CRL */
1273
- async _fetchGatewayCrl(gatewayUrl, issuerCert) {
1533
+ async _fetchGatewayCrl(gatewayUrl, issuerCert, fresh = false) {
1274
1534
  const url = gatewayHttpUrl(gatewayUrl, '/pki/crl.json');
1275
- const payload = await this._fetchJson(url);
1535
+ const payload = await this._fetchJson(url, fresh);
1536
+ if (fresh) {
1537
+ const pem = payload.crl_pem;
1538
+ if (typeof pem !== 'string' || !/^-----BEGIN X509 CRL-----\s+([A-Za-z0-9+/=\s]+)-----END X509 CRL-----\s*$/.test(pem))
1539
+ throw new AuthError('signed current CRL is unavailable');
1540
+ const encoded = pem.replace(/-----BEGIN X509 CRL-----|-----END X509 CRL-----|\s/g, '');
1541
+ if (encoded.length % 4 !== 0 || btoa(atob(encoded)) !== encoded)
1542
+ throw new AuthError('signed current CRL encoding is invalid');
1543
+ const der = base64ToUint8(encoded);
1544
+ const outer = ownershipDerNode(der, 0);
1545
+ if (outer.tag !== 0x30 || outer.end !== der.length)
1546
+ throw new AuthError('signed current CRL is invalid');
1547
+ const fields = ownershipDerChildren(der, outer);
1548
+ if (fields.length !== 3 || fields[0].tag !== 0x30 || fields[1].tag !== 0x30 || fields[2].tag !== 0x03 || der[fields[2].valueStart] !== 0)
1549
+ throw new AuthError('signed current CRL is invalid');
1550
+ const algorithm = ownershipDerChildren(der, fields[1]);
1551
+ if (algorithm[0]?.tag !== 0x06)
1552
+ throw new AuthError('signed current CRL algorithm is invalid');
1553
+ await verifyCertSignature(issuerCert.spkiBytes, { ...issuerCert, tbsBytes: der.slice(fields[0].start, fields[0].end), signatureBytes: der.slice(fields[2].valueStart + 1, fields[2].end), signatureAlgorithmOid: parseOid(der.slice(algorithm[0].valueStart, algorithm[0].end)) });
1554
+ const body = ownershipDerChildren(der, fields[0]);
1555
+ let index = body[0]?.tag === 0x02 ? 1 : 0;
1556
+ if (body[index++]?.tag !== 0x30 || body[index]?.tag !== 0x30 || !arraysEqual(der.slice(body[index].start, body[index].end), issuerCert.subjectRaw))
1557
+ throw new AuthError('signed current CRL issuer is invalid');
1558
+ index++;
1559
+ const thisUpdate = body[index++], nextUpdate = body[index++];
1560
+ const now = Date.now() / 1000;
1561
+ if (!thisUpdate || !nextUpdate || ![0x17, 0x18].includes(thisUpdate.tag) || ![0x17, 0x18].includes(nextUpdate.tag) || !Number.isFinite(parseAsn1Time(der, thisUpdate.start)) || parseAsn1Time(der, thisUpdate.start) > now || !Number.isFinite(parseAsn1Time(der, nextUpdate.start)) || now >= parseAsn1Time(der, nextUpdate.start))
1562
+ throw new AuthError('signed current CRL time window is invalid');
1563
+ const revokedSerials = new Set();
1564
+ const revoked = body[index];
1565
+ if (revoked?.tag === 0x30)
1566
+ for (const entry of ownershipDerChildren(der, revoked)) {
1567
+ const serial = ownershipDerChildren(der, entry)[0];
1568
+ if (entry.tag !== 0x30 || serial?.tag !== 0x02)
1569
+ throw new AuthError('signed current CRL revoked entry is invalid');
1570
+ revokedSerials.add(Array.from(der.slice(serial.valueStart, serial.end), (byte) => byte.toString(16).padStart(2, '0')).join('').replace(/^0+/, '') || '0');
1571
+ }
1572
+ return { revokedSerials, nextRefreshAt: parseAsn1Time(der, nextUpdate.start) };
1573
+ }
1276
1574
  // 简化 CRL 验证:JSON 格式 CRL
1277
1575
  // payload 包含 revoked_serials 数组和签名
1278
1576
  const revokedList = payload.revoked_serials;
@@ -1335,12 +1633,34 @@ export class AuthFlow {
1335
1633
  return fresh.status;
1336
1634
  }
1337
1635
  /** 从 Gateway PKI 端点获取并验证 OCSP 状态 */
1338
- async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert) {
1636
+ async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert, strictCurrent = false) {
1339
1637
  const serialHex = authCert.serialHex.toLowerCase();
1340
1638
  const url = gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${serialHex}`);
1341
- const payload = await this._fetchJson(url);
1639
+ const payload = await this._fetchJson(url, strictCurrent);
1342
1640
  const status = String(payload.status ?? '');
1343
1641
  const ocspB64 = String(payload.ocsp_response ?? '');
1642
+ if (strictCurrent) {
1643
+ if (payload.ocsp_response === undefined || payload.ocsp_response === '')
1644
+ throw new StateError('OCSP signed current evidence unavailable', { stringCode: 'unavailable', retryable: true });
1645
+ if (typeof payload.ocsp_response !== 'string' || !isStrictBase64(ocspB64))
1646
+ throw new ValidationError('ownership OCSP Base64 is invalid', { stringCode: 'invalid' });
1647
+ let verifiedStatus;
1648
+ try {
1649
+ verifiedStatus = await verifyOwnershipOcsp(base64ToUint8(ocspB64), authCert, issuerCert);
1650
+ }
1651
+ catch (error) {
1652
+ if (error instanceof StateError || error instanceof ValidationError)
1653
+ throw error;
1654
+ throw new ValidationError(error instanceof AuthError ? error.message : 'ownership OCSP evidence is invalid', { stringCode: 'invalid' });
1655
+ }
1656
+ if (payload.status !== undefined && payload.status !== verifiedStatus)
1657
+ throw new ValidationError('ownership OCSP status mismatch', { stringCode: 'invalid' });
1658
+ if (verifiedStatus === 'revoked')
1659
+ throw new ValidationError('ownership certificate is revoked', { stringCode: 'invalid' });
1660
+ if (verifiedStatus !== 'good')
1661
+ throw new StateError('ownership certificate revocation status is unknown', { stringCode: 'unavailable', retryable: true });
1662
+ return { status: verifiedStatus, nextRefreshAt: Date.now() / 1000 };
1663
+ }
1344
1664
  if (!ocspB64)
1345
1665
  throw new AuthError('gateway OCSP endpoint returned no ocsp_response');
1346
1666
  // 简化 OCSP 验证:检查 JSON 响应中的签名