@agentunion/fastaun-browser 0.5.17 → 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.
- package/CHANGELOG.md +83 -0
- package/_packed_docs/CHANGELOG.md +83 -0
- package/_packed_docs/INDEX.md +163 -16
- package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
- package/_packed_docs/agent.md/SCHEMA.md +266 -207
- 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 +64 -31
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
- 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 +39 -39
- 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 +1 -1
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
- package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
- package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
- package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +78 -18
- package/dist/agent-md-schema.d.ts +3 -0
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +46 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +44 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +518 -26
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +12 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +113 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +69 -9
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +4 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +343 -23
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +37513 -31501
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +24 -10
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +16 -1
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +198 -58
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +28 -6
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +858 -484
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +100 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1216 -24
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +41 -1
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +84 -2
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +104 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +17 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +10 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +111 -0
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +7 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +30 -1
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.d.ts.map +1 -0
- package/dist/org-protocol.js +1742 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +118 -0
- package/dist/ownership-proof.d.ts.map +1 -0
- package/dist/ownership-proof.js +283 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/ownership-v21.d.ts +177 -0
- package/dist/ownership-v21.d.ts.map +1 -0
- package/dist/ownership-v21.js +609 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +146 -0
- package/dist/ownership-workflow.d.ts.map +1 -0
- package/dist/ownership-workflow.js +1066 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +10 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +82 -0
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +332 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +6 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +88 -2
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
- package/dist/v2/e2ee/decrypt.js +9 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
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
|
|
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
|
|
1171
|
-
const
|
|
1172
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
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 响应中的签名
|