@agentunion/fastaun 0.5.22 → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/_packed_docs/CHANGELOG.md +33 -0
- package/_packed_docs/INDEX.md +32 -16
- package/_packed_docs/KITE_DOCS_GUIDE.md +22 -18
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +2 -2
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -7
- package/_packed_docs/sdk/INDEX.md +21 -6
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +49 -2
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +123 -72
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +173 -96
- package/dist/agent-md.d.ts +16 -2
- package/dist/agent-md.js +60 -29
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -0
- package/dist/aid-store.js +76 -19
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +205 -69
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +12 -10
- package/dist/client.js +193 -112
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/keystore/aid-db.d.ts +1 -8
- package/dist/keystore/aid-db.js +46 -204
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +0 -15
- package/dist/keystore/local-token-store.d.ts +1 -6
- package/dist/keystore/local-token-store.js +0 -15
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +10 -0
- package/dist/org-protocol.js +100 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +2 -2
- package/dist/ownership-v21.js +25 -12
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +30 -59
- package/dist/ownership-workflow.js +260 -1667
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +1 -2
- package/dist/tools/cross-sdk-agent.js +7 -14
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -348,7 +348,7 @@ function _strictCrlRevokedSerials(pem, issuer, now) {
|
|
|
348
348
|
throw new AuthError('service event CRL entry is invalid');
|
|
349
349
|
revoked.add(serial.content.toString('hex').replace(/^0+/, '') || '0');
|
|
350
350
|
}
|
|
351
|
-
return revoked;
|
|
351
|
+
return { revokedSerials: revoked, nextRefreshAt: nextUpdate * 1000 };
|
|
352
352
|
}
|
|
353
353
|
function _verifyOwnershipOcsp(der, certificate, issuer, now) {
|
|
354
354
|
const outer = _ownershipDer(der);
|
|
@@ -405,6 +405,7 @@ function _verifyOwnershipOcsp(der, certificate, issuer, now) {
|
|
|
405
405
|
throw new StateError('OCSP response is not current', { stringCode: 'unavailable', retryable: true });
|
|
406
406
|
const responses = _ownershipDerChildren(tbs[offset++], 0x30);
|
|
407
407
|
let status;
|
|
408
|
+
let nextRefreshAt = 0;
|
|
408
409
|
for (const entry of responses) {
|
|
409
410
|
const single = _ownershipDerChildren(entry, 0x30);
|
|
410
411
|
if (single.length < 4)
|
|
@@ -425,6 +426,7 @@ function _verifyOwnershipOcsp(der, certificate, issuer, now) {
|
|
|
425
426
|
if (single[3].tag !== 0xa0)
|
|
426
427
|
throw new StateError('OCSP nextUpdate is missing', { stringCode: 'unavailable', retryable: true });
|
|
427
428
|
const nextUpdate = _ownershipOcspTime(_ownershipDerChildren(single[3])[0]);
|
|
429
|
+
nextRefreshAt = nextUpdate * 1000;
|
|
428
430
|
if (thisUpdate > now + 5 || thisUpdate < now - 30 || nextUpdate <= now || nextUpdate <= thisUpdate)
|
|
429
431
|
throw new StateError('OCSP validity window is not current', { stringCode: 'unavailable', retryable: true });
|
|
430
432
|
if (single[1].tag === 0x80 && single[1].content.length === 0)
|
|
@@ -438,7 +440,7 @@ function _verifyOwnershipOcsp(der, certificate, issuer, now) {
|
|
|
438
440
|
}
|
|
439
441
|
if (!status)
|
|
440
442
|
throw new ValidationError('OCSP does not match the requested certificate', { stringCode: 'invalid' });
|
|
441
|
-
return status;
|
|
443
|
+
return { status, nextRefreshAt };
|
|
442
444
|
}
|
|
443
445
|
// ── HTTP 辅助 ─────────────────────────────────────────────────
|
|
444
446
|
/** 将 WebSocket URL 转换为 HTTP URL + 路径 */
|
|
@@ -531,16 +533,21 @@ export class AuthFlow {
|
|
|
531
533
|
_rootCerts;
|
|
532
534
|
// Gateway CA 链缓存:gateway_url -> PEM 字符串数组
|
|
533
535
|
_gatewayChainCache = new Map();
|
|
536
|
+
_gatewayChainLoads = new Map();
|
|
534
537
|
// Gateway CRL 缓存:gateway_url -> CRL 缓存条目
|
|
535
538
|
_gatewayCrlCache = new Map();
|
|
536
539
|
// Gateway OCSP 缓存:gateway_url -> (serial_hex -> OCSP 缓存条目)
|
|
537
540
|
_gatewayOcspCache = new Map();
|
|
538
|
-
//
|
|
541
|
+
// 证书链验证结果缓存:材料与用途 -> expires_at (ms)
|
|
539
542
|
_chainVerifiedCache = new Map();
|
|
543
|
+
_strictPkiCache = new Map();
|
|
544
|
+
_peerPkiCache = new Map();
|
|
545
|
+
_strictPkiEpoch = 0;
|
|
540
546
|
// Gateway CA 链预验证标记:cache_key -> verified
|
|
541
547
|
_gatewayCaVerified = new Map();
|
|
542
548
|
// 调用方注入的内存私钥(由 AUNClient.loadIdentity 传入,不走 tokenStore 解密)
|
|
543
549
|
_memIdentity = null;
|
|
550
|
+
_identityPkiContext = '';
|
|
544
551
|
constructor(opts) {
|
|
545
552
|
this._logger = opts.logger ?? _noopLogger;
|
|
546
553
|
this._tokenStore = opts.tokenStore;
|
|
@@ -552,7 +559,7 @@ export class AuthFlow {
|
|
|
552
559
|
this._rootCaPath = opts.rootCaPath ?? null;
|
|
553
560
|
this._net = opts.net ?? null;
|
|
554
561
|
this._verifySsl = opts.verifySsl ?? false;
|
|
555
|
-
this._chainCacheTtl = (opts.chainCacheTtl ??
|
|
562
|
+
this._chainCacheTtl = Math.min(opts.chainCacheTtl ?? 3600, 3600) * 1000; // 转为毫秒
|
|
556
563
|
this._rootCerts = this._loadRootCerts(this._rootCaPath);
|
|
557
564
|
}
|
|
558
565
|
// ── 公开方法 ────────────────────────────────────────────────
|
|
@@ -561,6 +568,10 @@ export class AuthFlow {
|
|
|
561
568
|
* AuthFlow 内部不再从 tokenStore 解密私钥,只使用此处注入的明文。
|
|
562
569
|
*/
|
|
563
570
|
setIdentity(identity) {
|
|
571
|
+
const context = JSON.stringify([identity?.aid ?? '', identity?.cert ?? '']);
|
|
572
|
+
if (this._identityPkiContext !== context)
|
|
573
|
+
this.invalidateStrictPkiCache();
|
|
574
|
+
this._identityPkiContext = context;
|
|
564
575
|
this._memIdentity = identity;
|
|
565
576
|
if (identity?.aid)
|
|
566
577
|
this._aid = String(identity.aid);
|
|
@@ -936,7 +947,13 @@ export class AuthFlow {
|
|
|
936
947
|
const nowMs = Date.now();
|
|
937
948
|
_ensureCertTimeValid(cert, 'peer certificate', nowMs);
|
|
938
949
|
if (strictRevocation) {
|
|
939
|
-
const
|
|
950
|
+
const epoch = this._strictPkiEpoch;
|
|
951
|
+
const cacheKey = JSON.stringify([gatewayUrl, expectedAid, cert.fingerprint256, this._loadTrustedRoots().map((root) => root.fingerprint256)]);
|
|
952
|
+
const cached = this._strictPkiCache.get(cacheKey);
|
|
953
|
+
if (cached && cached.epoch === epoch && cached.status === 'good' && cached.expiresAt > Date.now())
|
|
954
|
+
return;
|
|
955
|
+
this._strictPkiCache.delete(cacheKey);
|
|
956
|
+
const chain = await this._loadGatewayCaChain(gatewayUrl, expectedAid, false, true);
|
|
940
957
|
if (!chain.length)
|
|
941
958
|
throw new AuthError('service event certificate chain is unavailable');
|
|
942
959
|
const current = Math.trunc(Date.now() / 1000);
|
|
@@ -946,12 +963,26 @@ export class AuthFlow {
|
|
|
946
963
|
const crl = await this._fetchGatewayCrl(gatewayUrl, chain[0], true);
|
|
947
964
|
if (crl.revokedSerials.has(_certSerialHex(cert)))
|
|
948
965
|
throw new AuthError('service event certificate has been revoked');
|
|
949
|
-
await this._fetchGatewayOcspStatus(gatewayUrl, cert, chain[0], true);
|
|
966
|
+
const ocsp = await this._fetchGatewayOcspStatus(gatewayUrl, cert, chain[0], true);
|
|
950
967
|
if (_certSubjectCN(cert) !== expectedAid)
|
|
951
968
|
throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
|
|
969
|
+
const expiresAt = Math.min(tStart + 3600_000, Date.parse(cert.validTo), ...chain.map((ca) => Date.parse(ca.validTo)), crl.nextRefreshAt, ocsp.nextRefreshAt);
|
|
970
|
+
if (epoch === this._strictPkiEpoch && expiresAt > Date.now()) {
|
|
971
|
+
this.cleanExpiredCaches();
|
|
972
|
+
if (this._strictPkiCache.size >= 2048)
|
|
973
|
+
this._strictPkiCache.delete(this._strictPkiCache.keys().next().value);
|
|
974
|
+
this._strictPkiCache.set(cacheKey, { epoch, expiresAt, status: ocsp.status });
|
|
975
|
+
}
|
|
952
976
|
return;
|
|
953
977
|
}
|
|
978
|
+
const epoch = this._strictPkiEpoch;
|
|
979
|
+
const cacheKey = JSON.stringify([gatewayUrl, expectedAid, cert.fingerprint256, this._loadTrustedRoots().map((root) => root.fingerprint256)]);
|
|
980
|
+
const cached = this._peerPkiCache.get(cacheKey);
|
|
981
|
+
if (cached && cached.epoch === epoch && cached.expiresAt > Date.now())
|
|
982
|
+
return;
|
|
983
|
+
this._peerPkiCache.delete(cacheKey);
|
|
954
984
|
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
|
|
985
|
+
let revocationChecked = true;
|
|
955
986
|
try {
|
|
956
987
|
await this._verifyAuthCertRevocation(gatewayUrl, cert, expectedAid);
|
|
957
988
|
}
|
|
@@ -959,6 +990,7 @@ export class AuthFlow {
|
|
|
959
990
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
960
991
|
if (/revoked/i.test(errMsg))
|
|
961
992
|
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
993
|
+
revocationChecked = false;
|
|
962
994
|
this._logger.warn(`CRL verification unavailable, skipping: ${errMsg}`);
|
|
963
995
|
}
|
|
964
996
|
try {
|
|
@@ -968,6 +1000,7 @@ export class AuthFlow {
|
|
|
968
1000
|
const errMsg = exc instanceof Error ? exc.message : String(exc);
|
|
969
1001
|
if (/revoked/i.test(errMsg))
|
|
970
1002
|
throw exc instanceof AuthError ? exc : new AuthError(errMsg);
|
|
1003
|
+
revocationChecked = false;
|
|
971
1004
|
this._logger.warn(`OCSP verification unavailable, skipping: ${errMsg}`);
|
|
972
1005
|
}
|
|
973
1006
|
// 检查 CN 匹配
|
|
@@ -975,6 +1008,20 @@ export class AuthFlow {
|
|
|
975
1008
|
if (cn !== expectedAid) {
|
|
976
1009
|
throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}, got ${cn || 'none'}`);
|
|
977
1010
|
}
|
|
1011
|
+
if (revocationChecked && epoch === this._strictPkiEpoch) {
|
|
1012
|
+
const chain = this._gatewayChainCache.get(`${gatewayUrl}:${expectedAid}`);
|
|
1013
|
+
const crl = this._gatewayCrlCache.get(gatewayUrl);
|
|
1014
|
+
const ocsp = this._gatewayOcspCache.get(gatewayUrl)?.get(_certSerialHex(cert));
|
|
1015
|
+
if (chain && crl && ocsp) {
|
|
1016
|
+
const expiresAt = Math.min(tStart + 3600_000, Date.parse(cert.validTo), chain.expiresAt, crl.nextRefreshAt, ocsp.nextRefreshAt);
|
|
1017
|
+
if (expiresAt > Date.now()) {
|
|
1018
|
+
this.cleanExpiredCaches();
|
|
1019
|
+
if (this._peerPkiCache.size >= 2048)
|
|
1020
|
+
this._peerPkiCache.delete(this._peerPkiCache.keys().next().value);
|
|
1021
|
+
this._peerPkiCache.set(cacheKey, { epoch, expiresAt });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
978
1025
|
this._logger.debug(`verifyPeerCertificate exit: elapsed=${Date.now() - tStart}ms aid=${expectedAid}`);
|
|
979
1026
|
}
|
|
980
1027
|
catch (err) {
|
|
@@ -1056,30 +1103,12 @@ export class AuthFlow {
|
|
|
1056
1103
|
validateAgentMdCertificate(response.body, aid, Math.trunc(now), { now: Math.trunc(now), requireCurrentValidity: true });
|
|
1057
1104
|
if (certFingerprint(response.body) !== fingerprint)
|
|
1058
1105
|
throw new ValidationError('ownership certificate DER fingerprint mismatch', { stringCode: 'invalid' });
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
throw unavailable('ownership certificate chain is unavailable');
|
|
1062
|
-
let chain;
|
|
1063
|
-
try {
|
|
1064
|
-
chain = _splitPemBundle(chainResponse.body).map(_loadX509);
|
|
1065
|
-
}
|
|
1066
|
-
catch {
|
|
1067
|
-
throw new ValidationError('ownership certificate chain PEM is invalid', { stringCode: 'invalid' });
|
|
1068
|
-
}
|
|
1106
|
+
const epoch = this._strictPkiEpoch;
|
|
1107
|
+
const chain = [...await this._loadGatewayCaChain(gatewayUrl, aid, true)];
|
|
1069
1108
|
if (chain[0] && !chain[0].ca && _certSubjectCN(chain[0]) === aid)
|
|
1070
1109
|
chain.shift();
|
|
1071
1110
|
if (!chain.length)
|
|
1072
1111
|
throw unavailable('ownership issuer chain is unavailable');
|
|
1073
|
-
let current = cert;
|
|
1074
|
-
for (let index = 0; index < chain.length; index++) {
|
|
1075
|
-
const issuer = chain[index];
|
|
1076
|
-
const constraints = _ownershipCertConstraints(issuer);
|
|
1077
|
-
if (constraints.pathLen !== undefined && index > constraints.pathLen)
|
|
1078
|
-
throw new ValidationError('ownership certificate path length is invalid', { stringCode: 'invalid' });
|
|
1079
|
-
if (!constraints.basicPresent || !issuer.ca || !constraints.keyUsagePresent || !constraints.keyCertSign || Date.now() < Date.parse(issuer.validFrom) || Date.now() > Date.parse(issuer.validTo) || !current.checkIssued(issuer) || !current.verify(issuer.publicKey))
|
|
1080
|
-
throw new ValidationError('ownership certificate chain is invalid', { stringCode: 'invalid' });
|
|
1081
|
-
current = issuer;
|
|
1082
|
-
}
|
|
1083
1112
|
let roots;
|
|
1084
1113
|
try {
|
|
1085
1114
|
roots = this._loadTrustedRoots();
|
|
@@ -1087,8 +1116,23 @@ export class AuthFlow {
|
|
|
1087
1116
|
catch {
|
|
1088
1117
|
throw unavailable('ownership trust anchors are unavailable');
|
|
1089
1118
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1119
|
+
const staticKey = JSON.stringify(['ownership', gatewayUrl, aid, cert.fingerprint256, chain.map((item) => item.fingerprint256), roots.map((root) => root.fingerprint256)]);
|
|
1120
|
+
if ((this._chainVerifiedCache.get(staticKey) ?? 0) <= Date.now()) {
|
|
1121
|
+
let current = cert;
|
|
1122
|
+
for (let index = 0; index < chain.length; index++) {
|
|
1123
|
+
const issuer = chain[index];
|
|
1124
|
+
const constraints = _ownershipCertConstraints(issuer);
|
|
1125
|
+
if (constraints.pathLen !== undefined && index > constraints.pathLen)
|
|
1126
|
+
throw new ValidationError('ownership certificate path length is invalid', { stringCode: 'invalid' });
|
|
1127
|
+
if (!constraints.basicPresent || !issuer.ca || !constraints.keyUsagePresent || !constraints.keyCertSign || Date.now() < Date.parse(issuer.validFrom) || Date.now() > Date.parse(issuer.validTo) || !current.checkIssued(issuer) || !current.verify(issuer.publicKey))
|
|
1128
|
+
throw new ValidationError('ownership certificate chain is invalid', { stringCode: 'invalid' });
|
|
1129
|
+
current = issuer;
|
|
1130
|
+
}
|
|
1131
|
+
if (!current.checkIssued(current) || !current.verify(current.publicKey) || !roots.some((root) => root.raw.equals(current.raw)))
|
|
1132
|
+
throw new ValidationError('ownership certificate chain is not anchored by a trusted root', { stringCode: 'invalid' });
|
|
1133
|
+
if (epoch === this._strictPkiEpoch)
|
|
1134
|
+
this._chainVerifiedCache.set(staticKey, Math.min(Date.now() + this._chainCacheTtl, Date.parse(cert.validTo), ...chain.map((item) => Date.parse(item.validTo))));
|
|
1135
|
+
}
|
|
1092
1136
|
const ocspResponse = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${cert.serialNumber.toLowerCase()}`));
|
|
1093
1137
|
if (ocspResponse.status !== 200)
|
|
1094
1138
|
throw unavailable('ownership OCSP is unavailable');
|
|
@@ -1109,7 +1153,7 @@ export class AuthFlow {
|
|
|
1109
1153
|
throw new ValidationError('ownership OCSP Base64 is invalid', { stringCode: 'invalid' });
|
|
1110
1154
|
let status;
|
|
1111
1155
|
try {
|
|
1112
|
-
status = _verifyOwnershipOcsp(Buffer.from(ocsp.ocsp_response, 'base64'), cert, chain[0], Date.now() / 1000);
|
|
1156
|
+
status = _verifyOwnershipOcsp(Buffer.from(ocsp.ocsp_response, 'base64'), cert, chain[0], Date.now() / 1000).status;
|
|
1113
1157
|
}
|
|
1114
1158
|
catch (error) {
|
|
1115
1159
|
if (error instanceof ValidationError || error instanceof StateError)
|
|
@@ -1185,17 +1229,16 @@ export class AuthFlow {
|
|
|
1185
1229
|
.filter((pem) => pem.length > 0);
|
|
1186
1230
|
if (normalized.length === 0)
|
|
1187
1231
|
return;
|
|
1188
|
-
|
|
1189
|
-
_loadX509(pem);
|
|
1232
|
+
const chain = normalized.map(_loadX509);
|
|
1190
1233
|
const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
|
|
1191
|
-
this._gatewayChainCache.
|
|
1192
|
-
|
|
1234
|
+
const previous = this._gatewayChainCache.get(cacheKey);
|
|
1235
|
+
if (previous && JSON.stringify(previous.chain.map((pem) => _loadX509(pem).fingerprint256)) !== JSON.stringify(chain.map((cert) => cert.fingerprint256)))
|
|
1236
|
+
this.invalidateStrictPkiCache();
|
|
1237
|
+
this._gatewayChainCache.set(cacheKey, { chain: normalized, expiresAt: Math.min(Date.now() + 3600_000, ...chain.map((cert) => Date.parse(cert.validTo))) });
|
|
1193
1238
|
}
|
|
1194
1239
|
/** 丢弃预置或缓存的 Gateway CA 链材料。 */
|
|
1195
1240
|
discardGatewayCaChain(gatewayUrl, chainAid = '') {
|
|
1196
|
-
|
|
1197
|
-
this._gatewayChainCache.delete(cacheKey);
|
|
1198
|
-
this._gatewayCaVerified.delete(cacheKey);
|
|
1241
|
+
this.invalidateStrictPkiCache();
|
|
1199
1242
|
}
|
|
1200
1243
|
// ── 内部方法:短连接 RPC ────────────────────────────────────
|
|
1201
1244
|
/**
|
|
@@ -1557,22 +1600,27 @@ export class AuthFlow {
|
|
|
1557
1600
|
* 4. BasicConstraints 检查
|
|
1558
1601
|
* 5. 根证书自签 + 受信根锚定
|
|
1559
1602
|
*/
|
|
1560
|
-
async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '', signedAt,
|
|
1561
|
-
const
|
|
1562
|
-
// 检查缓存
|
|
1563
|
-
const cachedAt = this._chainVerifiedCache.get(certSerial);
|
|
1564
|
-
if (!fresh && cachedAt && Date.now() - cachedAt < this._chainCacheTtl) {
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1603
|
+
async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '', signedAt, _fresh = false, suppliedChain) {
|
|
1604
|
+
const epoch = this._strictPkiEpoch;
|
|
1567
1605
|
const nowMs = signedAt === undefined ? Date.now() : signedAt * 1000;
|
|
1568
1606
|
_ensureCertTimeValid(authCert, 'auth certificate', nowMs);
|
|
1569
1607
|
const chain = suppliedChain ?? await this._loadGatewayCaChain(gatewayUrl, chainAid);
|
|
1570
1608
|
if (!chain.length) {
|
|
1571
1609
|
throw new AuthError('unable to verify auth certificate chain: missing CA chain');
|
|
1572
1610
|
}
|
|
1573
|
-
const
|
|
1611
|
+
const trustedRoots = this._loadTrustedRoots();
|
|
1612
|
+
const cacheKey = JSON.stringify([gatewayUrl, chain.map((cert) => cert.fingerprint256), trustedRoots.map((cert) => cert.fingerprint256), signedAt === undefined ? 'current' : 'historical']);
|
|
1613
|
+
const certKey = JSON.stringify([cacheKey, chainAid, authCert.fingerprint256]);
|
|
1614
|
+
const now = Date.now();
|
|
1615
|
+
const expiresAt = Math.min(now + this._chainCacheTtl, Date.parse(authCert.validTo), ...chain.map((cert) => Date.parse(cert.validTo)));
|
|
1616
|
+
const chainExpiresAt = Math.min(now + this._chainCacheTtl, ...chain.map((cert) => Date.parse(cert.validTo)));
|
|
1617
|
+
for (const caCert of chain)
|
|
1618
|
+
_ensureCertTimeValid(caCert, 'CA certificate', nowMs);
|
|
1619
|
+
// 检查缓存
|
|
1620
|
+
if ((this._chainVerifiedCache.get(certKey) ?? 0) > now)
|
|
1621
|
+
return;
|
|
1574
1622
|
// 快速路径:CA 链已通过完整验证
|
|
1575
|
-
if (
|
|
1623
|
+
if ((this._gatewayCaVerified.get(cacheKey) ?? 0) > now) {
|
|
1576
1624
|
const issuer = chain[0];
|
|
1577
1625
|
_ensureCertTimeValid(issuer, 'Issuer CA', nowMs);
|
|
1578
1626
|
if (!_isCaCert(issuer)) {
|
|
@@ -1584,7 +1632,8 @@ export class AuthFlow {
|
|
|
1584
1632
|
if (!_verifyCertSignedBy(authCert, issuer)) {
|
|
1585
1633
|
throw new AuthError('auth certificate signature verification failed');
|
|
1586
1634
|
}
|
|
1587
|
-
|
|
1635
|
+
if (epoch === this._strictPkiEpoch)
|
|
1636
|
+
this._chainVerifiedCache.set(certKey, Math.min(expiresAt, this._gatewayCaVerified.get(cacheKey)));
|
|
1588
1637
|
return;
|
|
1589
1638
|
}
|
|
1590
1639
|
// 首次验证:完整验证流程
|
|
@@ -1612,25 +1661,60 @@ export class AuthFlow {
|
|
|
1612
1661
|
throw new AuthError('auth certificate chain root self-signature verification failed');
|
|
1613
1662
|
}
|
|
1614
1663
|
// 验证根证书在受信列表中
|
|
1615
|
-
const trustedRoots = this._loadTrustedRoots();
|
|
1616
1664
|
const rootDer = _certDer(root);
|
|
1617
1665
|
const isTrusted = trustedRoots.some((tr) => _certDer(tr).equals(rootDer));
|
|
1618
1666
|
if (!isTrusted) {
|
|
1619
1667
|
throw new AuthError('auth certificate chain is not anchored by a trusted root');
|
|
1620
1668
|
}
|
|
1621
1669
|
// 验证成功,更新缓存
|
|
1622
|
-
|
|
1623
|
-
|
|
1670
|
+
if (epoch === this._strictPkiEpoch) {
|
|
1671
|
+
this._chainVerifiedCache.set(certKey, expiresAt);
|
|
1672
|
+
this._gatewayCaVerified.set(cacheKey, chainExpiresAt);
|
|
1673
|
+
}
|
|
1624
1674
|
}
|
|
1625
1675
|
/** 加载 Gateway CA 链(带缓存) */
|
|
1626
|
-
async _loadGatewayCaChain(gatewayUrl, chainAid = '') {
|
|
1627
|
-
const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
cached
|
|
1631
|
-
|
|
1676
|
+
async _loadGatewayCaChain(gatewayUrl, chainAid = '', ownership = false, fresh = false) {
|
|
1677
|
+
const cacheKey = (ownership ? 'ownership:' : '') + (chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl);
|
|
1678
|
+
const cached = this._gatewayChainCache.get(cacheKey);
|
|
1679
|
+
if (cached && cached.expiresAt > Date.now())
|
|
1680
|
+
return cached.chain.map(_loadX509);
|
|
1681
|
+
const pending = this._gatewayChainLoads.get(cacheKey);
|
|
1682
|
+
if (pending)
|
|
1683
|
+
return pending;
|
|
1684
|
+
const epoch = this._strictPkiEpoch;
|
|
1685
|
+
const load = (async () => {
|
|
1686
|
+
let pems;
|
|
1687
|
+
if (ownership) {
|
|
1688
|
+
const response = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/chain/${encodeURIComponent(chainAid)}`));
|
|
1689
|
+
if (response.status !== 200 || !response.body.trim())
|
|
1690
|
+
throw new StateError('ownership certificate chain is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
1691
|
+
pems = _splitPemBundle(response.body);
|
|
1692
|
+
if (!pems.length)
|
|
1693
|
+
throw new ValidationError('ownership certificate chain PEM is invalid', { stringCode: 'invalid' });
|
|
1694
|
+
}
|
|
1695
|
+
else
|
|
1696
|
+
pems = await this._fetchGatewayCaChain(gatewayUrl, chainAid, fresh);
|
|
1697
|
+
let chain;
|
|
1698
|
+
try {
|
|
1699
|
+
chain = pems.map(_loadX509);
|
|
1700
|
+
}
|
|
1701
|
+
catch (error) {
|
|
1702
|
+
if (ownership)
|
|
1703
|
+
throw new ValidationError('ownership certificate chain PEM is invalid', { stringCode: 'invalid' });
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
if (epoch === this._strictPkiEpoch && chain.length)
|
|
1707
|
+
this._gatewayChainCache.set(cacheKey, { chain: pems, expiresAt: Math.min(Date.now() + 3600_000, ...chain.map((cert) => Date.parse(cert.validTo))) });
|
|
1708
|
+
return chain;
|
|
1709
|
+
})();
|
|
1710
|
+
this._gatewayChainLoads.set(cacheKey, load);
|
|
1711
|
+
try {
|
|
1712
|
+
return await load;
|
|
1713
|
+
}
|
|
1714
|
+
finally {
|
|
1715
|
+
if (this._gatewayChainLoads.get(cacheKey) === load)
|
|
1716
|
+
this._gatewayChainLoads.delete(cacheKey);
|
|
1632
1717
|
}
|
|
1633
|
-
return cached.map((pem) => _loadX509(pem));
|
|
1634
1718
|
}
|
|
1635
1719
|
/** 从 Gateway PKI 端点获取 CA 链 */
|
|
1636
1720
|
async _fetchFreshPkiJson(url) {
|
|
@@ -1683,8 +1767,10 @@ export class AuthFlow {
|
|
|
1683
1767
|
if (cached && cached.nextRefreshAt > now) {
|
|
1684
1768
|
return cached.revokedSerials;
|
|
1685
1769
|
}
|
|
1770
|
+
const epoch = this._strictPkiEpoch;
|
|
1686
1771
|
const entry = await this._fetchGatewayCrl(gatewayUrl, issuerCert);
|
|
1687
|
-
this.
|
|
1772
|
+
if (epoch === this._strictPkiEpoch)
|
|
1773
|
+
this._gatewayCrlCache.set(gatewayUrl, entry);
|
|
1688
1774
|
return entry.revokedSerials;
|
|
1689
1775
|
}
|
|
1690
1776
|
/**
|
|
@@ -1706,14 +1792,15 @@ export class AuthFlow {
|
|
|
1706
1792
|
throw new AuthError('gateway CRL endpoint returned no signed CRL');
|
|
1707
1793
|
}
|
|
1708
1794
|
if (fresh)
|
|
1709
|
-
return
|
|
1795
|
+
return _strictCrlRevokedSerials(crlPem, _issuerCert, Date.now() / 1000);
|
|
1710
1796
|
// 从 JSON 响应中提取吊销序列号列表
|
|
1711
1797
|
// Gateway 的 crl.json 同时包含 crl_pem(签名 CRL)和 revoked_serials(方便客户端解析)
|
|
1712
1798
|
let revokedSerials = new Set();
|
|
1799
|
+
const bounds = { nextRefreshAt: Date.now() + 300_000 };
|
|
1713
1800
|
// 尝试解析 CRL PEM 以提取吊销序列号
|
|
1714
1801
|
// Node.js 不直接支持 CRL 解析,尝试从 DER 中手动提取
|
|
1715
1802
|
try {
|
|
1716
|
-
revokedSerials = this._parseCrlRevokedSerials(crlPem, _issuerCert);
|
|
1803
|
+
revokedSerials = this._parseCrlRevokedSerials(crlPem, _issuerCert, bounds);
|
|
1717
1804
|
}
|
|
1718
1805
|
catch {
|
|
1719
1806
|
// CRL 解析失败时,降级使用 JSON 响应中的 revoked_serials(如果提供)
|
|
@@ -1728,7 +1815,7 @@ export class AuthFlow {
|
|
|
1728
1815
|
const now = Date.now();
|
|
1729
1816
|
let nextRefreshAt = now + 300_000; // 5 分钟
|
|
1730
1817
|
const maxRefreshAt = now + 86400_000; // 24 小时
|
|
1731
|
-
nextRefreshAt = Math.min(nextRefreshAt, maxRefreshAt);
|
|
1818
|
+
nextRefreshAt = Math.min(nextRefreshAt, maxRefreshAt, bounds.nextRefreshAt);
|
|
1732
1819
|
return { revokedSerials, nextRefreshAt };
|
|
1733
1820
|
}
|
|
1734
1821
|
/**
|
|
@@ -1752,7 +1839,7 @@ export class AuthFlow {
|
|
|
1752
1839
|
* ...
|
|
1753
1840
|
* }
|
|
1754
1841
|
*/
|
|
1755
|
-
_parseCrlRevokedSerials(crlPem, issuerCert) {
|
|
1842
|
+
_parseCrlRevokedSerials(crlPem, issuerCert, bounds) {
|
|
1756
1843
|
// 提取 DER 数据
|
|
1757
1844
|
const b64 = crlPem
|
|
1758
1845
|
.replace(/-----BEGIN X509 CRL-----/g, '')
|
|
@@ -1790,6 +1877,17 @@ export class AuthFlow {
|
|
|
1790
1877
|
throw new AuthError('gateway CRL signature verification failed');
|
|
1791
1878
|
}
|
|
1792
1879
|
}
|
|
1880
|
+
if (bounds) {
|
|
1881
|
+
const fields = _ownershipDerChildren(_ownershipDer(der.subarray(tbsStart, tbsStart + tbs.totalLength)), 0x30);
|
|
1882
|
+
const version = fields[0]?.tag === 2 ? 1 : 0;
|
|
1883
|
+
let nextUpdate = fields[version + 3];
|
|
1884
|
+
if (nextUpdate?.tag === 23) {
|
|
1885
|
+
const text = nextUpdate.content.toString('ascii');
|
|
1886
|
+
nextUpdate = { ...nextUpdate, tag: 24, content: Buffer.from(`${Number(text.slice(0, 2)) >= 50 ? '19' : '20'}${text}`) };
|
|
1887
|
+
}
|
|
1888
|
+
if (nextUpdate && nextUpdate.tag === 24)
|
|
1889
|
+
bounds.nextRefreshAt = _ownershipOcspTime(nextUpdate) * 1000;
|
|
1890
|
+
}
|
|
1793
1891
|
// 解析 TBSCertList 内部字段以找到 revokedCertificates
|
|
1794
1892
|
const revokedSerials = new Set();
|
|
1795
1893
|
let offset = tbs.contentOffset;
|
|
@@ -1886,20 +1984,21 @@ export class AuthFlow {
|
|
|
1886
1984
|
if (typeof payload.ocsp_response !== 'string' || !isStrictBase64(ocspB64))
|
|
1887
1985
|
throw new AuthError('service event signed current OCSP is unavailable or invalid');
|
|
1888
1986
|
const verified = _verifyOwnershipOcsp(Buffer.from(ocspB64, 'base64'), authCert, issuerCert, Date.now() / 1000);
|
|
1889
|
-
if (payload.status !== undefined && payload.status !== verified)
|
|
1987
|
+
if (payload.status !== undefined && payload.status !== verified.status)
|
|
1890
1988
|
throw new AuthError('service event OCSP status mismatch');
|
|
1891
|
-
if (verified !== 'good')
|
|
1892
|
-
throw new AuthError(`service event certificate OCSP status is ${verified}`);
|
|
1893
|
-
return
|
|
1989
|
+
if (verified.status !== 'good')
|
|
1990
|
+
throw new AuthError(`service event certificate OCSP status is ${verified.status}`);
|
|
1991
|
+
return verified;
|
|
1894
1992
|
}
|
|
1895
1993
|
if (!ocspB64) {
|
|
1896
1994
|
throw new AuthError('gateway OCSP endpoint returned no ocsp_response');
|
|
1897
1995
|
}
|
|
1898
1996
|
let effectiveStatus;
|
|
1997
|
+
const bounds = { nextRefreshAt: Date.now() + 300_000 };
|
|
1899
1998
|
try {
|
|
1900
1999
|
// 尝试解析 DER OCSP 响应
|
|
1901
2000
|
const ocspDer = Buffer.from(ocspB64, 'base64');
|
|
1902
|
-
effectiveStatus = this._parseOcspResponse(ocspDer, authCert, issuerCert);
|
|
2001
|
+
effectiveStatus = this._parseOcspResponse(ocspDer, authCert, issuerCert, bounds);
|
|
1903
2002
|
}
|
|
1904
2003
|
catch (e) {
|
|
1905
2004
|
// OCSP DER 解析失败时,降级信赖 JSON status 字段
|
|
@@ -1917,7 +2016,7 @@ export class AuthFlow {
|
|
|
1917
2016
|
}
|
|
1918
2017
|
// 缓存 TTL:默认 5 分钟,最大 24 小时
|
|
1919
2018
|
const now = Date.now();
|
|
1920
|
-
const nextRefreshAt = Math.min(now + 300_000, now + 86400_000);
|
|
2019
|
+
const nextRefreshAt = Math.min(now + 300_000, now + 86400_000, bounds.nextRefreshAt);
|
|
1921
2020
|
return { status: effectiveStatus, nextRefreshAt };
|
|
1922
2021
|
}
|
|
1923
2022
|
/**
|
|
@@ -1956,7 +2055,7 @@ export class AuthFlow {
|
|
|
1956
2055
|
* 这里只做关键字段提取:responseStatus、certStatus。
|
|
1957
2056
|
* 完整签名验证依赖更全面的 ASN.1 库。
|
|
1958
2057
|
*/
|
|
1959
|
-
_parseOcspResponse(ocspDer, _authCert, _issuerCert) {
|
|
2058
|
+
_parseOcspResponse(ocspDer, _authCert, _issuerCert, bounds) {
|
|
1960
2059
|
// 外层 SEQUENCE
|
|
1961
2060
|
const outer = this._readAsn1Sequence(ocspDer, 0);
|
|
1962
2061
|
if (!outer)
|
|
@@ -2055,6 +2154,15 @@ export class AuthFlow {
|
|
|
2055
2154
|
const certStatusByte = basicDer[srOffset];
|
|
2056
2155
|
// context-specific tag class (bits 7-6 = 10), constructed bit (bit 5)
|
|
2057
2156
|
const tagNumber = certStatusByte & 0x1f;
|
|
2157
|
+
if (bounds) {
|
|
2158
|
+
const status = this._readAsn1Tag(basicDer, srOffset);
|
|
2159
|
+
const thisUpdate = status && this._readAsn1Tag(basicDer, srOffset + status.totalLength);
|
|
2160
|
+
const nextOffset = status && thisUpdate ? srOffset + status.totalLength + thisUpdate.totalLength : singleSeq.contentOffset + singleSeq.contentLength;
|
|
2161
|
+
if (nextOffset < singleSeq.contentOffset + singleSeq.contentLength && basicDer[nextOffset] === 0xa0) {
|
|
2162
|
+
const nextUpdate = _ownershipDerChildren(_ownershipDer(basicDer.subarray(nextOffset)))[0];
|
|
2163
|
+
bounds.nextRefreshAt = _ownershipOcspTime(nextUpdate) * 1000;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2058
2166
|
if (tagNumber === 0)
|
|
2059
2167
|
return 'good';
|
|
2060
2168
|
if (tagNumber === 1)
|
|
@@ -2493,6 +2601,7 @@ export class AuthFlow {
|
|
|
2493
2601
|
// ── 内部方法:根证书管理 ───────────────────────────────────
|
|
2494
2602
|
/** 重新从磁盘加载信任根证书,供 meta.import_trust_roots 后刷新使用。 */
|
|
2495
2603
|
reloadTrustedRoots() {
|
|
2604
|
+
this.invalidateStrictPkiCache();
|
|
2496
2605
|
this._rootCerts = this._loadRootCerts(this._rootCaPath);
|
|
2497
2606
|
this._gatewayCaVerified.clear();
|
|
2498
2607
|
this._chainVerifiedCache.clear();
|
|
@@ -2574,9 +2683,28 @@ export class AuthFlow {
|
|
|
2574
2683
|
}
|
|
2575
2684
|
return certs;
|
|
2576
2685
|
}
|
|
2686
|
+
invalidateStrictPkiCache() {
|
|
2687
|
+
this._strictPkiEpoch++;
|
|
2688
|
+
this._strictPkiCache.clear();
|
|
2689
|
+
this._peerPkiCache.clear();
|
|
2690
|
+
this._chainVerifiedCache.clear();
|
|
2691
|
+
this._gatewayCaVerified.clear();
|
|
2692
|
+
this._gatewayChainCache.clear();
|
|
2693
|
+
this._gatewayChainLoads.clear();
|
|
2694
|
+
this._gatewayCrlCache.clear();
|
|
2695
|
+
this._gatewayOcspCache.clear();
|
|
2696
|
+
}
|
|
2577
2697
|
/** 清理过期的 gateway 缓存条目(供外部定时调用) */
|
|
2578
2698
|
cleanExpiredCaches() {
|
|
2579
2699
|
const now = Date.now();
|
|
2700
|
+
for (const [k, v] of this._strictPkiCache) {
|
|
2701
|
+
if (v.epoch !== this._strictPkiEpoch || v.expiresAt <= now)
|
|
2702
|
+
this._strictPkiCache.delete(k);
|
|
2703
|
+
}
|
|
2704
|
+
for (const [k, v] of this._peerPkiCache) {
|
|
2705
|
+
if (v.epoch !== this._strictPkiEpoch || v.expiresAt <= now)
|
|
2706
|
+
this._peerPkiCache.delete(k);
|
|
2707
|
+
}
|
|
2580
2708
|
for (const [k, v] of this._gatewayCrlCache) {
|
|
2581
2709
|
if (v.nextRefreshAt <= now)
|
|
2582
2710
|
this._gatewayCrlCache.delete(k);
|
|
@@ -2590,9 +2718,17 @@ export class AuthFlow {
|
|
|
2590
2718
|
this._gatewayOcspCache.delete(k);
|
|
2591
2719
|
}
|
|
2592
2720
|
for (const [k, v] of this._chainVerifiedCache) {
|
|
2593
|
-
if (
|
|
2721
|
+
if (v <= now)
|
|
2594
2722
|
this._chainVerifiedCache.delete(k);
|
|
2595
2723
|
}
|
|
2724
|
+
for (const [k, v] of this._gatewayCaVerified) {
|
|
2725
|
+
if (v <= now)
|
|
2726
|
+
this._gatewayCaVerified.delete(k);
|
|
2727
|
+
}
|
|
2728
|
+
for (const [k, v] of this._gatewayChainCache) {
|
|
2729
|
+
if (v.expiresAt <= now)
|
|
2730
|
+
this._gatewayChainCache.delete(k);
|
|
2731
|
+
}
|
|
2596
2732
|
}
|
|
2597
2733
|
}
|
|
2598
2734
|
//# sourceMappingURL=auth.js.map
|