@agentunion/fastaun 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.
- package/CHANGELOG.md +44 -2
- package/_packed_docs/CHANGELOG.md +44 -2
- package/_packed_docs/INDEX.md +136 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- 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
- 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
- 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
- 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
- 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 +22 -27
- 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 +250 -283
- 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 +74 -64
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +76 -17
- package/dist/agent-md-schema.d.ts +2 -0
- package/dist/agent-md-schema.js +45 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +47 -2
- package/dist/agent-md.js +493 -33
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +9 -59
- package/dist/aid-store.js +135 -245
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +19 -5
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +6 -1
- package/dist/auth.js +428 -15
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.js +19 -7
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +2 -1
- package/dist/client/delivery.js +143 -52
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.js +22 -5
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.js +93 -29
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +84 -1
- package/dist/client.js +749 -28
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +5 -1
- package/dist/events.js +47 -6
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +83 -3
- package/dist/facades.js +98 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +5 -0
- package/dist/keystore/aid-db.js +48 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +8 -0
- package/dist/keystore/local-identity-store.js +10 -3
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +6 -1
- package/dist/keystore/local-token-store.js +24 -1
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.js +1689 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +82 -5
- package/dist/ownership-proof.js +327 -31
- package/dist/ownership-proof.js.map +1 -1
- package/dist/ownership-v21.d.ts +178 -0
- package/dist/ownership-v21.js +635 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +82 -0
- package/dist/ownership-workflow.js +872 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +10 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +89 -1
- package/dist/tools/cross-sdk-agent.js +338 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +108 -4
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- 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 +1 -1
package/dist/auth.js
CHANGED
|
@@ -16,10 +16,12 @@ import * as http from 'node:http';
|
|
|
16
16
|
import * as https from 'node:https';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { URL, fileURLToPath } from 'node:url';
|
|
19
|
-
import { AuthError, StateError, ValidationError, mapRemoteError } from './errors.js';
|
|
19
|
+
import { AuthError, StateError, ValidationError, AUNError, mapRemoteError } from './errors.js';
|
|
20
20
|
import { shortRpcViaNetworkActor } from './transport.js';
|
|
21
21
|
import { isJsonObject, } from './types.js';
|
|
22
22
|
import { VERSION as AUN_SDK_VERSION } from './version.js';
|
|
23
|
+
import { certFingerprint, isStrictBase64 } from './cert-utils.js';
|
|
24
|
+
import { validateAgentMdCertificate } from './agent-md-schema.js';
|
|
23
25
|
const _noopLogger = {
|
|
24
26
|
error: () => { },
|
|
25
27
|
warn: () => { },
|
|
@@ -182,6 +184,195 @@ function _certSubjectCN(cert) {
|
|
|
182
184
|
function _certDer(cert) {
|
|
183
185
|
return Buffer.from(cert.raw);
|
|
184
186
|
}
|
|
187
|
+
function _ownershipDer(buffer, offset = 0) {
|
|
188
|
+
let cursor = offset + 2;
|
|
189
|
+
if (cursor > buffer.length || (buffer[offset] & 31) === 31)
|
|
190
|
+
throw new ValidationError('invalid OCSP DER', { stringCode: 'invalid' });
|
|
191
|
+
let length = buffer[offset + 1];
|
|
192
|
+
if (length & 128) {
|
|
193
|
+
const count = length & 127;
|
|
194
|
+
length = 0;
|
|
195
|
+
if (!count || count > 4 || cursor + count > buffer.length || buffer[cursor] === 0)
|
|
196
|
+
throw new ValidationError('invalid OCSP DER length', { stringCode: 'invalid' });
|
|
197
|
+
for (let i = 0; i < count; i++)
|
|
198
|
+
length = length * 256 + buffer[cursor++];
|
|
199
|
+
if (length < 128)
|
|
200
|
+
throw new ValidationError('non-canonical OCSP DER length', { stringCode: 'invalid' });
|
|
201
|
+
}
|
|
202
|
+
if (cursor + length > buffer.length)
|
|
203
|
+
throw new ValidationError('truncated OCSP DER', { stringCode: 'invalid' });
|
|
204
|
+
return { tag: buffer[offset], raw: buffer.subarray(offset, cursor + length), content: buffer.subarray(cursor, cursor + length) };
|
|
205
|
+
}
|
|
206
|
+
function _ownershipDerChildren(value, tag) {
|
|
207
|
+
if (!value)
|
|
208
|
+
throw new StateError('OCSP field is missing', { stringCode: 'unavailable', retryable: true });
|
|
209
|
+
if (tag !== undefined && value.tag !== tag)
|
|
210
|
+
throw new ValidationError('unexpected OCSP DER field', { stringCode: 'invalid' });
|
|
211
|
+
const children = [];
|
|
212
|
+
for (let offset = 0; offset < value.content.length;) {
|
|
213
|
+
const child = _ownershipDer(value.content, offset);
|
|
214
|
+
children.push(child);
|
|
215
|
+
offset += child.raw.length;
|
|
216
|
+
}
|
|
217
|
+
return children;
|
|
218
|
+
}
|
|
219
|
+
function _ownershipCertParts(cert) {
|
|
220
|
+
const tbs = _ownershipDerChildren(_ownershipDerChildren(_ownershipDer(cert.raw), 0x30)[0], 0x30);
|
|
221
|
+
const version = tbs[0]?.tag === 0xa0 ? 1 : 0;
|
|
222
|
+
const spki = _ownershipDerChildren(tbs[version + 5], 0x30);
|
|
223
|
+
if (spki[1]?.tag !== 3 || spki[1].content[0] !== 0)
|
|
224
|
+
throw new ValidationError('invalid issuer public key', { stringCode: 'invalid' });
|
|
225
|
+
return { subject: tbs[version + 4].raw, key: spki[1].content.subarray(1) };
|
|
226
|
+
}
|
|
227
|
+
function _ownershipOcspTime(node) {
|
|
228
|
+
if (!node)
|
|
229
|
+
throw new StateError('OCSP time is missing', { stringCode: 'unavailable', retryable: true });
|
|
230
|
+
const text = node.content.toString('ascii');
|
|
231
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text);
|
|
232
|
+
if (node.tag !== 24 || !match)
|
|
233
|
+
throw new ValidationError('invalid OCSP time', { stringCode: 'invalid' });
|
|
234
|
+
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6])));
|
|
235
|
+
if (date.toISOString().replace(/[-:T]/g, '').replace('.000', '') !== text)
|
|
236
|
+
throw new ValidationError('invalid OCSP calendar time', { stringCode: 'invalid' });
|
|
237
|
+
return date.getTime() / 1000;
|
|
238
|
+
}
|
|
239
|
+
function _strictCrlRevokedSerials(pem, issuer, now) {
|
|
240
|
+
const match = /^-----BEGIN X509 CRL-----\s+([A-Za-z0-9+/=\s]+)-----END X509 CRL-----\s*$/.exec(pem);
|
|
241
|
+
if (!match || !isStrictBase64(match[1].replace(/\s/g, '')))
|
|
242
|
+
throw new AuthError('service event signed CRL is invalid');
|
|
243
|
+
const der = Buffer.from(match[1].replace(/\s/g, ''), 'base64');
|
|
244
|
+
const outer = _ownershipDer(der);
|
|
245
|
+
if (outer.raw.length !== der.length)
|
|
246
|
+
throw new AuthError('service event signed CRL has trailing data');
|
|
247
|
+
const fields = _ownershipDerChildren(outer, 0x30);
|
|
248
|
+
if (fields.length !== 3 || fields[2].tag !== 3 || fields[2].content[0] !== 0)
|
|
249
|
+
throw new AuthError('service event signed CRL is incomplete');
|
|
250
|
+
const tbs = _ownershipDerChildren(fields[0], 0x30);
|
|
251
|
+
const offset = tbs[0]?.tag === 2 ? 1 : 0;
|
|
252
|
+
const algorithm = _ownershipDerChildren(fields[1], 0x30)[0];
|
|
253
|
+
const oid = algorithm?.tag === 6 ? algorithm.content.toString('hex') : '';
|
|
254
|
+
const hash = { '2a8648ce3d040302': 'sha256', '2a8648ce3d040303': 'sha384', '2a864886f70d01010b': 'sha256', '2a864886f70d01010c': 'sha384', '2b6570': null }[oid];
|
|
255
|
+
if (hash === undefined || !tbs[offset]?.raw.equals(fields[1].raw) || !crypto.verify(hash, fields[0].raw, issuer.publicKey, fields[2].content.subarray(1)))
|
|
256
|
+
throw new AuthError('service event CRL signature is invalid');
|
|
257
|
+
if (!tbs[offset + 1]?.raw.equals(_ownershipCertParts(issuer).subject))
|
|
258
|
+
throw new AuthError('service event CRL issuer is invalid');
|
|
259
|
+
const crlTime = (node) => {
|
|
260
|
+
if (!node || ![23, 24].includes(node.tag))
|
|
261
|
+
throw new AuthError('service event CRL time is invalid');
|
|
262
|
+
const value = node.content.toString('ascii');
|
|
263
|
+
const digits = node.tag === 23 ? /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(value) : /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(value);
|
|
264
|
+
if (!digits)
|
|
265
|
+
throw new AuthError('service event CRL time is invalid');
|
|
266
|
+
const year = node.tag === 23 ? (Number(digits[1]) >= 50 ? 1900 : 2000) + Number(digits[1]) : Number(digits[1]);
|
|
267
|
+
const date = new Date(Date.UTC(year, Number(digits[2]) - 1, Number(digits[3]), Number(digits[4]), Number(digits[5]), Number(digits[6])));
|
|
268
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() + 1 !== Number(digits[2]) || date.getUTCDate() !== Number(digits[3]) || date.getUTCHours() !== Number(digits[4]) || date.getUTCMinutes() !== Number(digits[5]) || date.getUTCSeconds() !== Number(digits[6]))
|
|
269
|
+
throw new AuthError('service event CRL time is invalid');
|
|
270
|
+
return date.getTime() / 1000;
|
|
271
|
+
};
|
|
272
|
+
const thisUpdate = crlTime(tbs[offset + 2]);
|
|
273
|
+
const nextUpdate = crlTime(tbs[offset + 3]);
|
|
274
|
+
if (thisUpdate > now + 5 || nextUpdate <= now || nextUpdate <= thisUpdate)
|
|
275
|
+
throw new AuthError('service event CRL is not current');
|
|
276
|
+
const revoked = new Set();
|
|
277
|
+
if (tbs[offset + 4]?.tag === 0x30)
|
|
278
|
+
for (const entry of _ownershipDerChildren(tbs[offset + 4], 0x30)) {
|
|
279
|
+
const serial = _ownershipDerChildren(entry, 0x30)[0];
|
|
280
|
+
if (!serial || serial.tag !== 2)
|
|
281
|
+
throw new AuthError('service event CRL entry is invalid');
|
|
282
|
+
revoked.add(serial.content.toString('hex').replace(/^0+/, '') || '0');
|
|
283
|
+
}
|
|
284
|
+
return revoked;
|
|
285
|
+
}
|
|
286
|
+
function _verifyOwnershipOcsp(der, certificate, issuer, now) {
|
|
287
|
+
const outer = _ownershipDer(der);
|
|
288
|
+
if (outer.raw.length !== der.length)
|
|
289
|
+
throw new ValidationError('trailing OCSP data', { stringCode: 'invalid' });
|
|
290
|
+
const fields = _ownershipDerChildren(outer, 0x30);
|
|
291
|
+
if (!fields[0])
|
|
292
|
+
throw new StateError('OCSP response status is missing', { stringCode: 'unavailable', retryable: true });
|
|
293
|
+
if (fields[0].tag !== 10 || fields[0].content.length !== 1)
|
|
294
|
+
throw new ValidationError('invalid OCSP response status', { stringCode: 'invalid' });
|
|
295
|
+
if (!fields[0].content.equals(Buffer.from([0])) || !fields[1])
|
|
296
|
+
throw new StateError('OCSP response is not successful', { stringCode: 'unavailable', retryable: true });
|
|
297
|
+
if (fields.length !== 2 || fields[1].tag !== 0xa0)
|
|
298
|
+
throw new ValidationError('invalid OCSP response fields', { stringCode: 'invalid' });
|
|
299
|
+
const response = _ownershipDerChildren(_ownershipDerChildren(fields[1])[0], 0x30);
|
|
300
|
+
if (response.length < 2)
|
|
301
|
+
throw new StateError('OCSP Basic response is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
302
|
+
if (response.length !== 2 || response[0].tag !== 6 || response[1].tag !== 4)
|
|
303
|
+
throw new ValidationError('invalid OCSP Basic response', { stringCode: 'invalid' });
|
|
304
|
+
if (response[0].content.toString('hex') !== '2b0601050507300101')
|
|
305
|
+
throw new StateError('unsupported OCSP response type', { stringCode: 'unavailable', retryable: true });
|
|
306
|
+
const basicRoot = _ownershipDer(response[1].content);
|
|
307
|
+
if (basicRoot.raw.length !== response[1].content.length)
|
|
308
|
+
throw new ValidationError('invalid OCSP Basic response length', { stringCode: 'invalid' });
|
|
309
|
+
const basic = _ownershipDerChildren(basicRoot, 0x30);
|
|
310
|
+
if (basic.length < 3 || basic[2].content.length <= 1)
|
|
311
|
+
throw new StateError('OCSP signature is missing', { stringCode: 'unavailable', retryable: true });
|
|
312
|
+
if (basic.length > 4 || basic[2].tag !== 3 || basic[2].content[0] !== 0)
|
|
313
|
+
throw new ValidationError('invalid OCSP signature field', { stringCode: 'invalid' });
|
|
314
|
+
if (basic.length === 4) {
|
|
315
|
+
const certs = _ownershipDerChildren(basic[3], 0xa0);
|
|
316
|
+
if (certs.length !== 1 || certs[0].tag !== 0x30)
|
|
317
|
+
throw new ValidationError('invalid OCSP embedded certificates', { stringCode: 'invalid' });
|
|
318
|
+
const embedded = _ownershipDerChildren(certs[0], 0x30);
|
|
319
|
+
if (embedded.length !== 1 || !embedded[0].raw.equals(issuer.raw))
|
|
320
|
+
throw new ValidationError('OCSP embedded certificate is not the verified issuer', { stringCode: 'invalid' });
|
|
321
|
+
}
|
|
322
|
+
const algorithms = { '2a8648ce3d040302': 'sha256', '2a8648ce3d040303': 'sha384', '2a864886f70d01010b': 'sha256', '2a864886f70d01010c': 'sha384', '2b6570': null };
|
|
323
|
+
const algorithm = _ownershipDerChildren(basic[1], 0x30)[0];
|
|
324
|
+
const oid = algorithm?.tag === 6 ? algorithm.content.toString('hex') : '';
|
|
325
|
+
if (!(oid in algorithms))
|
|
326
|
+
throw new StateError('unsupported OCSP signature algorithm', { stringCode: 'unavailable', retryable: true });
|
|
327
|
+
if (!crypto.verify(algorithms[oid], basic[0].raw, issuer.publicKey, basic[2].content.subarray(1)))
|
|
328
|
+
throw new ValidationError('OCSP signature verification failed', { stringCode: 'invalid' });
|
|
329
|
+
const tbs = _ownershipDerChildren(basic[0], 0x30);
|
|
330
|
+
let offset = tbs[0]?.tag === 0xa0 ? 1 : 0;
|
|
331
|
+
const responder = tbs[offset++];
|
|
332
|
+
const issuerParts = _ownershipCertParts(issuer);
|
|
333
|
+
const responderValid = responder?.tag === 0xa1 ? responder.content.equals(issuerParts.subject) : responder?.tag === 0xa2 && _ownershipDerChildren(responder)[0]?.tag === 4 && _ownershipDerChildren(responder)[0].content.equals(crypto.createHash('sha1').update(issuerParts.key).digest());
|
|
334
|
+
if (!responderValid)
|
|
335
|
+
throw new ValidationError('OCSP responder is not the verified issuer', { stringCode: 'invalid' });
|
|
336
|
+
const producedAt = _ownershipOcspTime(tbs[offset++]);
|
|
337
|
+
if (producedAt > now + 5 || producedAt < now - 90)
|
|
338
|
+
throw new StateError('OCSP response is not current', { stringCode: 'unavailable', retryable: true });
|
|
339
|
+
const responses = _ownershipDerChildren(tbs[offset++], 0x30);
|
|
340
|
+
let status;
|
|
341
|
+
for (const entry of responses) {
|
|
342
|
+
const single = _ownershipDerChildren(entry, 0x30);
|
|
343
|
+
if (single.length < 4)
|
|
344
|
+
throw new StateError('OCSP update bounds are missing', { stringCode: 'unavailable', retryable: true });
|
|
345
|
+
const id = _ownershipDerChildren(single[0], 0x30);
|
|
346
|
+
if (id.length !== 4 || id[1].tag !== 4 || id[2].tag !== 4 || id[3].tag !== 2)
|
|
347
|
+
throw new ValidationError('invalid OCSP CertID', { stringCode: 'invalid' });
|
|
348
|
+
const hashOid = _ownershipDerChildren(id[0], 0x30)[0];
|
|
349
|
+
const hashName = hashOid?.tag === 6 ? { '2b0e03021a': 'sha1', '608648016503040201': 'sha256' }[hashOid.content.toString('hex')] : undefined;
|
|
350
|
+
if (!hashName)
|
|
351
|
+
throw new StateError('unsupported OCSP issuer hash', { stringCode: 'unavailable', retryable: true });
|
|
352
|
+
const serial = id[3].content.toString('hex').replace(/^0+/, '') || '0';
|
|
353
|
+
if (serial !== certificate.serialNumber.toLowerCase().replace(/^0+/, ''))
|
|
354
|
+
continue;
|
|
355
|
+
if (status !== undefined || !id[1].content.equals(crypto.createHash(hashName).update(issuerParts.subject).digest()) || !id[2].content.equals(crypto.createHash(hashName).update(issuerParts.key).digest()))
|
|
356
|
+
throw new ValidationError('OCSP CertID issuer mismatch', { stringCode: 'invalid' });
|
|
357
|
+
const thisUpdate = _ownershipOcspTime(single[2]);
|
|
358
|
+
if (single[3].tag !== 0xa0)
|
|
359
|
+
throw new StateError('OCSP nextUpdate is missing', { stringCode: 'unavailable', retryable: true });
|
|
360
|
+
const nextUpdate = _ownershipOcspTime(_ownershipDerChildren(single[3])[0]);
|
|
361
|
+
if (thisUpdate > now + 5 || thisUpdate < now - 30 || nextUpdate <= now || nextUpdate <= thisUpdate)
|
|
362
|
+
throw new StateError('OCSP validity window is not current', { stringCode: 'unavailable', retryable: true });
|
|
363
|
+
if (single[1].tag === 0x80 && single[1].content.length === 0)
|
|
364
|
+
status = 'good';
|
|
365
|
+
else if (single[1].tag === 0xa1)
|
|
366
|
+
status = 'revoked';
|
|
367
|
+
else if (single[1].tag === 0x82 && single[1].content.length === 0)
|
|
368
|
+
status = 'unknown';
|
|
369
|
+
else
|
|
370
|
+
throw new ValidationError('invalid OCSP certificate status', { stringCode: 'invalid' });
|
|
371
|
+
}
|
|
372
|
+
if (!status)
|
|
373
|
+
throw new ValidationError('OCSP does not match the requested certificate', { stringCode: 'invalid' });
|
|
374
|
+
return status;
|
|
375
|
+
}
|
|
185
376
|
// ── HTTP 辅助 ─────────────────────────────────────────────────
|
|
186
377
|
/** 将 WebSocket URL 转换为 HTTP URL + 路径 */
|
|
187
378
|
function _gatewayHttpUrl(gatewayUrl, urlPath) {
|
|
@@ -663,13 +854,36 @@ export class AuthFlow {
|
|
|
663
854
|
* 验证对端证书:时间有效性 + 链验证 + CRL + OCSP + AID 绑定。
|
|
664
855
|
* 用于 E2EE 握手中验证通信对端的身份证书。
|
|
665
856
|
*/
|
|
666
|
-
async verifyPeerCertificate(gatewayUrl, certPem, expectedAid) {
|
|
857
|
+
async verifyPeerCertificate(gatewayUrl, certPem, expectedAid, signedAt, strictRevocation = false) {
|
|
667
858
|
const tStart = Date.now();
|
|
668
859
|
this._logger.debug(`verifyPeerCertificate enter: aid=${expectedAid}, gateway=${gatewayUrl}`);
|
|
669
860
|
try {
|
|
670
861
|
const cert = _loadX509(certPem);
|
|
862
|
+
if (signedAt !== undefined) {
|
|
863
|
+
_ensureCertTimeValid(cert, 'peer certificate', signedAt * 1000);
|
|
864
|
+
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, signedAt);
|
|
865
|
+
if (_certSubjectCN(cert) !== expectedAid)
|
|
866
|
+
throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
671
869
|
const nowMs = Date.now();
|
|
672
870
|
_ensureCertTimeValid(cert, 'peer certificate', nowMs);
|
|
871
|
+
if (strictRevocation) {
|
|
872
|
+
const chain = (await this._fetchGatewayCaChain(gatewayUrl, expectedAid, true)).map(_loadX509);
|
|
873
|
+
if (!chain.length)
|
|
874
|
+
throw new AuthError('service event certificate chain is unavailable');
|
|
875
|
+
const current = Math.trunc(Date.now() / 1000);
|
|
876
|
+
if ([cert, ...chain].some((item) => current < Math.trunc(Date.parse(item.validFrom) / 1000) || current > Math.trunc(Date.parse(item.validTo) / 1000)))
|
|
877
|
+
throw new AuthError('service event certificate is outside its validity window');
|
|
878
|
+
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid, undefined, true, chain);
|
|
879
|
+
const crl = await this._fetchGatewayCrl(gatewayUrl, chain[0], true);
|
|
880
|
+
if (crl.revokedSerials.has(_certSerialHex(cert)))
|
|
881
|
+
throw new AuthError('service event certificate has been revoked');
|
|
882
|
+
await this._fetchGatewayOcspStatus(gatewayUrl, cert, chain[0], true);
|
|
883
|
+
if (_certSubjectCN(cert) !== expectedAid)
|
|
884
|
+
throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
673
887
|
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
|
|
674
888
|
try {
|
|
675
889
|
await this._verifyAuthCertRevocation(gatewayUrl, cert, expectedAid);
|
|
@@ -701,6 +915,181 @@ export class AuthFlow {
|
|
|
701
915
|
throw err;
|
|
702
916
|
}
|
|
703
917
|
}
|
|
918
|
+
async resolveCurrentEntityCertificate(gatewayUrl, aid, type) {
|
|
919
|
+
try {
|
|
920
|
+
const query = new URLSearchParams({ consistency: 'authoritative', ...(type === 'group' ? { key_purpose: 'group_identity' } : {}) });
|
|
921
|
+
const response = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/cert/${encodeURIComponent(aid)}?${query}`));
|
|
922
|
+
if (response.status !== 200 || !response.body.trim())
|
|
923
|
+
throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
924
|
+
let fingerprint;
|
|
925
|
+
try {
|
|
926
|
+
new crypto.X509Certificate(response.body);
|
|
927
|
+
fingerprint = certFingerprint(response.body);
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
throw new ValidationError('entity current certificate is invalid', { stringCode: 'invalid' });
|
|
931
|
+
}
|
|
932
|
+
return this.resolveOwnershipCertificate(gatewayUrl, aid, fingerprint, type === 'group' ? 'group_identity' : 'signing');
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
if (error instanceof AUNError && error.stringCode)
|
|
936
|
+
throw error;
|
|
937
|
+
throw new StateError(error instanceof Error ? error.message : String(error), { stringCode: 'unavailable', retryable: true });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async resolveOwnershipCertificate(gatewayUrl, aid, fingerprint, purpose) {
|
|
941
|
+
const began = Date.now();
|
|
942
|
+
const unavailable = (reason) => new StateError(reason, { stringCode: 'unavailable', retryable: true });
|
|
943
|
+
try {
|
|
944
|
+
if (!/^[a-z0-9_][a-z0-9_-]*(?:\.[a-z0-9_][a-z0-9_-]*)+$/.test(aid) || !/^sha256:[0-9a-f]{64}$/.test(fingerprint))
|
|
945
|
+
throw new ValidationError('ownership certificate query requires exact AID and full fingerprint', { stringCode: 'invalid' });
|
|
946
|
+
const query = new URLSearchParams({ cert_fingerprint: fingerprint, consistency: 'authoritative' });
|
|
947
|
+
if (purpose === 'group_identity')
|
|
948
|
+
query.set('key_purpose', purpose);
|
|
949
|
+
const response = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/cert/${encodeURIComponent(aid)}?${query}`));
|
|
950
|
+
if (response.status !== 200)
|
|
951
|
+
throw unavailable(`ownership certificate HTTP ${response.status}`);
|
|
952
|
+
const headers = response.headers;
|
|
953
|
+
const state = headers['x-aun-cert-lifecycle-state'];
|
|
954
|
+
const observedText = headers['x-aun-cert-observed-at'] ?? '';
|
|
955
|
+
const observed = Date.parse(observedText) / 1000;
|
|
956
|
+
const now = Date.now() / 1000;
|
|
957
|
+
if (headers['x-aun-cert-consistency'] !== 'authoritative' || typeof state !== 'string' || !state || !headers['x-aun-cert-fingerprint'] || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(observedText) || !Number.isFinite(observed) || observed > now + 5 || observed < now - 30)
|
|
958
|
+
throw unavailable('ownership certificate authoritative current metadata is unavailable');
|
|
959
|
+
if (state !== 'active_signing' || headers['x-aun-cert-fingerprint'] !== fingerprint)
|
|
960
|
+
throw new ValidationError('ownership certificate lifecycle or fingerprint is invalid', { stringCode: 'invalid' });
|
|
961
|
+
if (!response.body.trim())
|
|
962
|
+
throw unavailable('ownership certificate is missing');
|
|
963
|
+
let cert;
|
|
964
|
+
try {
|
|
965
|
+
cert = new crypto.X509Certificate(response.body);
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
throw new ValidationError('ownership certificate PEM is invalid', { stringCode: 'invalid' });
|
|
969
|
+
}
|
|
970
|
+
if (cert.subject.match(/(?:^|\n)CN=([^\n]+)/)?.[1] !== aid)
|
|
971
|
+
throw new ValidationError('ownership certificate AID mismatch', { stringCode: 'invalid' });
|
|
972
|
+
validateAgentMdCertificate(response.body, aid, Math.trunc(now), { now: Math.trunc(now), requireCurrentValidity: true });
|
|
973
|
+
if (certFingerprint(response.body) !== fingerprint)
|
|
974
|
+
throw new ValidationError('ownership certificate DER fingerprint mismatch', { stringCode: 'invalid' });
|
|
975
|
+
const chainResponse = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/chain/${encodeURIComponent(aid)}`));
|
|
976
|
+
if (chainResponse.status !== 200 || !chainResponse.body.trim())
|
|
977
|
+
throw unavailable('ownership certificate chain is unavailable');
|
|
978
|
+
let chain;
|
|
979
|
+
try {
|
|
980
|
+
chain = _splitPemBundle(chainResponse.body).map(_loadX509);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
throw new ValidationError('ownership certificate chain PEM is invalid', { stringCode: 'invalid' });
|
|
984
|
+
}
|
|
985
|
+
if (chain[0] && !chain[0].ca && _certSubjectCN(chain[0]) === aid)
|
|
986
|
+
chain.shift();
|
|
987
|
+
if (!chain.length)
|
|
988
|
+
throw unavailable('ownership issuer chain is unavailable');
|
|
989
|
+
let current = cert;
|
|
990
|
+
for (const issuer of chain) {
|
|
991
|
+
if (!issuer.ca || Date.now() < Date.parse(issuer.validFrom) || Date.now() > Date.parse(issuer.validTo) || !current.checkIssued(issuer) || !current.verify(issuer.publicKey))
|
|
992
|
+
throw new ValidationError('ownership certificate chain is invalid', { stringCode: 'invalid' });
|
|
993
|
+
current = issuer;
|
|
994
|
+
}
|
|
995
|
+
let roots;
|
|
996
|
+
try {
|
|
997
|
+
roots = this._loadTrustedRoots();
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
throw unavailable('ownership trust anchors are unavailable');
|
|
1001
|
+
}
|
|
1002
|
+
if (!current.checkIssued(current) || !current.verify(current.publicKey) || !roots.some((root) => root.raw.equals(current.raw)))
|
|
1003
|
+
throw new ValidationError('ownership certificate chain is not anchored by a trusted root', { stringCode: 'invalid' });
|
|
1004
|
+
const ocspResponse = await this._fetchOwnershipPki(_gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${cert.serialNumber.toLowerCase()}`));
|
|
1005
|
+
if (ocspResponse.status !== 200)
|
|
1006
|
+
throw unavailable('ownership OCSP is unavailable');
|
|
1007
|
+
let ocsp;
|
|
1008
|
+
if (!ocspResponse.body.trim())
|
|
1009
|
+
throw unavailable('ownership OCSP response is missing');
|
|
1010
|
+
try {
|
|
1011
|
+
ocsp = JSON.parse(ocspResponse.body);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
throw new ValidationError('ownership OCSP response is invalid', { stringCode: 'invalid' });
|
|
1015
|
+
}
|
|
1016
|
+
if (!ocsp || typeof ocsp !== 'object' || Array.isArray(ocsp))
|
|
1017
|
+
throw new ValidationError('ownership OCSP response must be an object', { stringCode: 'invalid' });
|
|
1018
|
+
if (ocsp.ocsp_response === undefined || ocsp.ocsp_response === '')
|
|
1019
|
+
throw unavailable('signed ownership OCSP is missing');
|
|
1020
|
+
if (typeof ocsp.ocsp_response !== 'string' || !isStrictBase64(ocsp.ocsp_response))
|
|
1021
|
+
throw new ValidationError('ownership OCSP Base64 is invalid', { stringCode: 'invalid' });
|
|
1022
|
+
let status;
|
|
1023
|
+
try {
|
|
1024
|
+
status = _verifyOwnershipOcsp(Buffer.from(ocsp.ocsp_response, 'base64'), cert, chain[0], Date.now() / 1000);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (error instanceof ValidationError || error instanceof StateError)
|
|
1028
|
+
throw error;
|
|
1029
|
+
throw new ValidationError('ownership OCSP evidence is invalid', { stringCode: 'invalid' });
|
|
1030
|
+
}
|
|
1031
|
+
if (ocsp.status !== undefined && ocsp.status !== status)
|
|
1032
|
+
throw new ValidationError('ownership OCSP status mismatch', { stringCode: 'invalid' });
|
|
1033
|
+
if (status === 'revoked')
|
|
1034
|
+
throw new ValidationError('ownership certificate is revoked', { stringCode: 'invalid' });
|
|
1035
|
+
if (status !== 'good')
|
|
1036
|
+
throw unavailable('ownership certificate revocation status is unknown');
|
|
1037
|
+
const finalNow = Date.now();
|
|
1038
|
+
if ([cert, ...chain].some((item) => finalNow < Date.parse(item.validFrom) || finalNow > Date.parse(item.validTo)))
|
|
1039
|
+
throw new ValidationError('ownership certificate chain is invalid', { stringCode: 'invalid' });
|
|
1040
|
+
if (finalNow - began > 30_000 || finalNow / 1000 - observed > 30)
|
|
1041
|
+
throw unavailable('ownership certificate verification window elapsed');
|
|
1042
|
+
return { cert_pem: response.body, chain_valid: true, state, revoked: false, revocation_checked: true, observed_at: Math.trunc(observed) };
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
if (error instanceof ValidationError)
|
|
1046
|
+
throw new ValidationError(error.message, { stringCode: 'invalid' });
|
|
1047
|
+
if (error instanceof StateError && error.stringCode)
|
|
1048
|
+
throw error;
|
|
1049
|
+
throw unavailable(error instanceof Error ? error.message : String(error));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async _fetchOwnershipPki(url) {
|
|
1053
|
+
const fetch = (target, originalHost) => new Promise((resolve, reject) => {
|
|
1054
|
+
const parsed = new URL(target);
|
|
1055
|
+
const module = parsed.protocol === 'https:' ? https : http;
|
|
1056
|
+
const request = module.get(target, { timeout: 5_000, rejectUnauthorized: this._verifySsl,
|
|
1057
|
+
servername: originalHost, headers: { 'Cache-Control': 'no-cache, no-store', ...(originalHost ? { Host: new URL(url).host } : {}) } }, (response) => {
|
|
1058
|
+
const chunks = [];
|
|
1059
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
1060
|
+
response.on('error', reject);
|
|
1061
|
+
response.on('end', () => {
|
|
1062
|
+
try {
|
|
1063
|
+
const headers = {};
|
|
1064
|
+
for (const [key, value] of Object.entries(response.headers))
|
|
1065
|
+
if (typeof value === 'string')
|
|
1066
|
+
headers[key] = value;
|
|
1067
|
+
resolve({ status: response.statusCode ?? 0, headers, body: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks)) });
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
reject(error);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
request.on('error', reject);
|
|
1075
|
+
request.on('timeout', () => request.destroy(new Error('ownership PKI request timed out')));
|
|
1076
|
+
});
|
|
1077
|
+
try {
|
|
1078
|
+
const result = await fetch(url);
|
|
1079
|
+
if (this._net)
|
|
1080
|
+
void this._net.refreshDnsCacheAfterSuccess(url);
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
if (!this._net?.isDnsError(error))
|
|
1085
|
+
throw error;
|
|
1086
|
+
const original = new URL(url);
|
|
1087
|
+
const cached = this._net.loadDnsCache(original.hostname);
|
|
1088
|
+
if (!cached)
|
|
1089
|
+
throw error;
|
|
1090
|
+
return fetch(this._net.replaceHostWithIP(url, cached.ip), original.hostname);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
704
1093
|
/** 预置 Gateway CA 链材料,但不标记为已验证。 */
|
|
705
1094
|
cacheGatewayCaChain(gatewayUrl, caChainPems, chainAid = '') {
|
|
706
1095
|
const normalized = caChainPems
|
|
@@ -1076,22 +1465,22 @@ export class AuthFlow {
|
|
|
1076
1465
|
* 4. BasicConstraints 检查
|
|
1077
1466
|
* 5. 根证书自签 + 受信根锚定
|
|
1078
1467
|
*/
|
|
1079
|
-
async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '') {
|
|
1080
|
-
const certSerial = _certSerialHex(authCert);
|
|
1468
|
+
async _verifyAuthCertChain(gatewayUrl, authCert, chainAid = '', signedAt, fresh = false, suppliedChain) {
|
|
1469
|
+
const certSerial = _certSerialHex(authCert) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
|
|
1081
1470
|
// 检查缓存
|
|
1082
1471
|
const cachedAt = this._chainVerifiedCache.get(certSerial);
|
|
1083
|
-
if (cachedAt && Date.now() - cachedAt < this._chainCacheTtl) {
|
|
1472
|
+
if (!fresh && cachedAt && Date.now() - cachedAt < this._chainCacheTtl) {
|
|
1084
1473
|
return;
|
|
1085
1474
|
}
|
|
1086
|
-
const nowMs = Date.now();
|
|
1475
|
+
const nowMs = signedAt === undefined ? Date.now() : signedAt * 1000;
|
|
1087
1476
|
_ensureCertTimeValid(authCert, 'auth certificate', nowMs);
|
|
1088
|
-
const chain = await this._loadGatewayCaChain(gatewayUrl, chainAid);
|
|
1477
|
+
const chain = suppliedChain ?? await this._loadGatewayCaChain(gatewayUrl, chainAid);
|
|
1089
1478
|
if (!chain.length) {
|
|
1090
1479
|
throw new AuthError('unable to verify auth certificate chain: missing CA chain');
|
|
1091
1480
|
}
|
|
1092
|
-
const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
|
|
1481
|
+
const cacheKey = (chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
|
|
1093
1482
|
// 快速路径:CA 链已通过完整验证
|
|
1094
|
-
if (this._gatewayCaVerified.get(cacheKey)) {
|
|
1483
|
+
if (!fresh && this._gatewayCaVerified.get(cacheKey)) {
|
|
1095
1484
|
const issuer = chain[0];
|
|
1096
1485
|
_ensureCertTimeValid(issuer, 'Issuer CA', nowMs);
|
|
1097
1486
|
if (!_isCaCert(issuer)) {
|
|
@@ -1152,9 +1541,21 @@ export class AuthFlow {
|
|
|
1152
1541
|
return cached.map((pem) => _loadX509(pem));
|
|
1153
1542
|
}
|
|
1154
1543
|
/** 从 Gateway PKI 端点获取 CA 链 */
|
|
1155
|
-
async
|
|
1544
|
+
async _fetchFreshPkiJson(url) {
|
|
1545
|
+
const response = await this._fetchOwnershipPki(url);
|
|
1546
|
+
if (response.status !== 200)
|
|
1547
|
+
throw new AuthError('service event current PKI is unavailable');
|
|
1548
|
+
const payload = JSON.parse(response.body);
|
|
1549
|
+
if (!isJsonObject(payload))
|
|
1550
|
+
throw new AuthError('service event current PKI response is invalid');
|
|
1551
|
+
return payload;
|
|
1552
|
+
}
|
|
1553
|
+
async _fetchGatewayCaChain(gatewayUrl, _chainAid = '', fresh = false) {
|
|
1156
1554
|
const url = _gatewayHttpUrl(gatewayUrl, '/pki/chain');
|
|
1157
|
-
const
|
|
1555
|
+
const response = fresh ? await this._fetchOwnershipPki(url) : undefined;
|
|
1556
|
+
if (response && response.status !== 200)
|
|
1557
|
+
throw new AuthError('service event certificate chain is unavailable');
|
|
1558
|
+
const text = response ? response.body : await _fetchText(url, this._verifySsl, this._net);
|
|
1158
1559
|
return _splitPemBundle(text);
|
|
1159
1560
|
}
|
|
1160
1561
|
/**
|
|
@@ -1205,13 +1606,15 @@ export class AuthFlow {
|
|
|
1205
1606
|
* 并验证 crl_pem 存在性。完整的 CRL 签名验证需要依赖
|
|
1206
1607
|
* @peculiar/x509 或手动 ASN.1 解析。
|
|
1207
1608
|
*/
|
|
1208
|
-
async _fetchGatewayCrl(gatewayUrl, _issuerCert) {
|
|
1609
|
+
async _fetchGatewayCrl(gatewayUrl, _issuerCert, fresh = false) {
|
|
1209
1610
|
const url = _gatewayHttpUrl(gatewayUrl, '/pki/crl.json');
|
|
1210
|
-
const payload = await _fetchJson(url, this._verifySsl, this._net);
|
|
1611
|
+
const payload = fresh ? await this._fetchFreshPkiJson(url) : await _fetchJson(url, this._verifySsl, this._net);
|
|
1211
1612
|
const crlPem = String(payload.crl_pem || '');
|
|
1212
1613
|
if (!crlPem) {
|
|
1213
1614
|
throw new AuthError('gateway CRL endpoint returned no signed CRL');
|
|
1214
1615
|
}
|
|
1616
|
+
if (fresh)
|
|
1617
|
+
return { revokedSerials: _strictCrlRevokedSerials(crlPem, _issuerCert, Date.now() / 1000), nextRefreshAt: Date.now() };
|
|
1215
1618
|
// 从 JSON 响应中提取吊销序列号列表
|
|
1216
1619
|
// Gateway 的 crl.json 同时包含 crl_pem(签名 CRL)和 revoked_serials(方便客户端解析)
|
|
1217
1620
|
let revokedSerials = new Set();
|
|
@@ -1381,12 +1784,22 @@ export class AuthFlow {
|
|
|
1381
1784
|
* 这里从 JSON 响应中提取 status 字段,并验证 ocsp_response 存在性。
|
|
1382
1785
|
* 对于 ocsp_response 的签名验证,实现简化的 DER 解析以提取关键字段。
|
|
1383
1786
|
*/
|
|
1384
|
-
async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert) {
|
|
1787
|
+
async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert, strictCurrent = false) {
|
|
1385
1788
|
const serialHex = _certSerialHex(authCert);
|
|
1386
1789
|
const url = _gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${serialHex}`);
|
|
1387
|
-
const payload = await _fetchJson(url, this._verifySsl, this._net);
|
|
1790
|
+
const payload = strictCurrent ? await this._fetchFreshPkiJson(url) : await _fetchJson(url, this._verifySsl, this._net);
|
|
1388
1791
|
const status = String(payload.status || '');
|
|
1389
1792
|
const ocspB64 = String(payload.ocsp_response || '');
|
|
1793
|
+
if (strictCurrent) {
|
|
1794
|
+
if (typeof payload.ocsp_response !== 'string' || !isStrictBase64(ocspB64))
|
|
1795
|
+
throw new AuthError('service event signed current OCSP is unavailable or invalid');
|
|
1796
|
+
const verified = _verifyOwnershipOcsp(Buffer.from(ocspB64, 'base64'), authCert, issuerCert, Date.now() / 1000);
|
|
1797
|
+
if (payload.status !== undefined && payload.status !== verified)
|
|
1798
|
+
throw new AuthError('service event OCSP status mismatch');
|
|
1799
|
+
if (verified !== 'good')
|
|
1800
|
+
throw new AuthError(`service event certificate OCSP status is ${verified}`);
|
|
1801
|
+
return { status: verified, nextRefreshAt: Date.now() };
|
|
1802
|
+
}
|
|
1390
1803
|
if (!ocspB64) {
|
|
1391
1804
|
throw new AuthError('gateway OCSP endpoint returned no ocsp_response');
|
|
1392
1805
|
}
|