@gerege-systems/eid-mongolia-sdk 0.9.1 → 0.10.0
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/README.md +65 -4
- package/dist/{browser-B_XtyuFG.d.cts → browser-BWCDj8xO.d.cts} +104 -5
- package/dist/{browser-B_XtyuFG.d.ts → browser-BWCDj8xO.d.ts} +104 -5
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/{chunk-NXDKQZIW.js → chunk-YDVPZMAL.js} +71 -2
- package/dist/index.cjs +547 -43
- package/dist/index.d.cts +331 -5
- package/dist/index.d.ts +331 -5
- package/dist/index.js +468 -37
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -6,12 +6,15 @@ import {
|
|
|
6
6
|
ForbiddenError,
|
|
7
7
|
NetworkError,
|
|
8
8
|
ON_BEHALF_ERROR_CODES,
|
|
9
|
+
RETRYABLE_ERROR_CODES,
|
|
9
10
|
SessionFailedError,
|
|
10
11
|
ValidationError,
|
|
11
12
|
deviceLink,
|
|
12
13
|
isMobileUserAgent,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
maskPath,
|
|
15
|
+
openOrShowQR,
|
|
16
|
+
parseRetryAfter
|
|
17
|
+
} from "./chunk-YDVPZMAL.js";
|
|
15
18
|
|
|
16
19
|
// src/http.ts
|
|
17
20
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -54,26 +57,32 @@ var Http = class {
|
|
|
54
57
|
};
|
|
55
58
|
if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
|
|
56
59
|
let res;
|
|
60
|
+
const op = `${method} ${maskPath(path)}`;
|
|
57
61
|
try {
|
|
58
62
|
res = await f(url, init);
|
|
59
63
|
} catch (e) {
|
|
60
64
|
const err = e;
|
|
61
65
|
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
62
|
-
throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${
|
|
66
|
+
throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${op}`, { op, timeout: true });
|
|
63
67
|
}
|
|
64
|
-
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${
|
|
68
|
+
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${op}): ${err.message.split(url).join(op)}`, { op, timeout: false });
|
|
65
69
|
}
|
|
66
70
|
if (res.status < 300) return res;
|
|
67
71
|
const text = await res.text();
|
|
68
72
|
const { error, code } = errorBody(text);
|
|
69
|
-
|
|
73
|
+
const headers = res.headers;
|
|
74
|
+
const retryAfterMs = parseRetryAfter(headers.get("Retry-After"));
|
|
75
|
+
if (res.status === 401) {
|
|
76
|
+
throw new AuthenticationError(`API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401${code ? ` ${code}` : ""})`, { code, serverMessage: error, body: text, headers });
|
|
77
|
+
}
|
|
70
78
|
if (res.status === 403) {
|
|
71
79
|
throw new ForbiddenError(
|
|
72
80
|
error ? `RP-API 403${code ? ` ${code}` : ""}: ${error}` : "IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)",
|
|
73
|
-
code
|
|
81
|
+
code,
|
|
82
|
+
{ headers, retryAfterMs }
|
|
74
83
|
);
|
|
75
84
|
}
|
|
76
|
-
throw new ApiError(res.status, text, code);
|
|
85
|
+
throw new ApiError(res.status, text, code, { headers, retryAfterMs });
|
|
77
86
|
}
|
|
78
87
|
};
|
|
79
88
|
function errorBody(text) {
|
|
@@ -90,6 +99,48 @@ function errorBody(text) {
|
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
// src/contract.ts
|
|
103
|
+
var RP_ROUTES = {
|
|
104
|
+
authNotificationEtsi: { method: "POST", path: "/authentication/notification/etsi/{semanticsIdentifier}" },
|
|
105
|
+
authNotificationDocument: { method: "POST", path: "/authentication/notification/document/{documentNumber}" },
|
|
106
|
+
authDeviceLinkAnonymous: { method: "POST", path: "/authentication/device-link/anonymous" },
|
|
107
|
+
authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
|
|
108
|
+
signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
|
|
109
|
+
signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
|
|
110
|
+
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
111
|
+
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
112
|
+
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
113
|
+
session: { method: "GET", path: "/session/{sessionId}" },
|
|
114
|
+
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
115
|
+
personDevices: { method: "GET", path: "/devices/etsi/{personEtsi}" },
|
|
116
|
+
personActivity: { method: "GET", path: "/rp/activity/etsi/{personEtsi}" },
|
|
117
|
+
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
118
|
+
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
119
|
+
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
120
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
121
|
+
};
|
|
122
|
+
var RP_ROUTES_UNDISCOVERED = {
|
|
123
|
+
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
124
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
125
|
+
};
|
|
126
|
+
function routePath(route, ...args) {
|
|
127
|
+
let i = 0;
|
|
128
|
+
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
129
|
+
const a = args[i++];
|
|
130
|
+
if (a === void 0) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u0443\u0442\u0443\u0443`);
|
|
131
|
+
return encodeURIComponent(a);
|
|
132
|
+
});
|
|
133
|
+
if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
var OIDC_EXPECT = {
|
|
137
|
+
defaultScope: "openid",
|
|
138
|
+
pkceMethod: "S256",
|
|
139
|
+
idTokenAlg: "ES256",
|
|
140
|
+
/** `Authorization: Basic` (RFC 6749 §2.3.1). */
|
|
141
|
+
tokenEndpointAuthMethod: "client_secret_basic"
|
|
142
|
+
};
|
|
143
|
+
|
|
93
144
|
// src/crypto.ts
|
|
94
145
|
import { createHash, randomBytes } from "crypto";
|
|
95
146
|
function sha256Base64(data) {
|
|
@@ -192,25 +243,25 @@ var AuthApi = class {
|
|
|
192
243
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
193
244
|
async notificationByEtsi(id, interactions, opts) {
|
|
194
245
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
195
|
-
const raw = await this.http.post(
|
|
246
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationEtsi, id), body);
|
|
196
247
|
return this.parseNotification(raw, acsp);
|
|
197
248
|
}
|
|
198
249
|
/** Push — баримтын дугаараар (тодорхой төхөөрөмж рүү). */
|
|
199
250
|
async notificationByDocument(documentNumber, interactions, opts) {
|
|
200
251
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
201
|
-
const raw = await this.http.post(
|
|
252
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationDocument, documentNumber), body);
|
|
202
253
|
return this.parseNotification(raw, acsp);
|
|
203
254
|
}
|
|
204
255
|
/** QR/App2App — нэр томьёололгүй (иргэн хэн болохыг урьдчилж мэдэхгүй). QR код үүсгэхэд. */
|
|
205
256
|
async deviceLinkAnonymous(interactions, opts) {
|
|
206
257
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
207
|
-
const raw = await this.http.post(
|
|
258
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkAnonymous), body);
|
|
208
259
|
return this.parseDeviceLink(raw, acsp);
|
|
209
260
|
}
|
|
210
261
|
/** QR/App2App — тодорхой иргэн (РД/civil_id/ETSI). */
|
|
211
262
|
async deviceLinkByEtsi(id, interactions, opts) {
|
|
212
263
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
213
|
-
const raw = await this.http.post(
|
|
264
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkEtsi, id), body);
|
|
214
265
|
return this.parseDeviceLink(raw, acsp);
|
|
215
266
|
}
|
|
216
267
|
};
|
|
@@ -243,6 +294,7 @@ var SignApi = class {
|
|
|
243
294
|
};
|
|
244
295
|
if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
|
|
245
296
|
if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
|
|
297
|
+
if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
|
|
246
298
|
return body;
|
|
247
299
|
}
|
|
248
300
|
parse(raw) {
|
|
@@ -255,12 +307,12 @@ var SignApi = class {
|
|
|
255
307
|
* digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
|
|
256
308
|
*/
|
|
257
309
|
async digestByEtsi(id, digestB64, interactions, opts) {
|
|
258
|
-
const raw = await this.http.post(
|
|
310
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
|
|
259
311
|
return this.parse(raw);
|
|
260
312
|
}
|
|
261
313
|
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
|
|
262
314
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
263
|
-
const raw = await this.http.post(
|
|
315
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
|
|
264
316
|
return this.parse(raw);
|
|
265
317
|
}
|
|
266
318
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
@@ -301,9 +353,44 @@ function parseSessionResult(raw) {
|
|
|
301
353
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
302
354
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
303
355
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
304
|
-
pdf: parsePdf(r.pdf)
|
|
356
|
+
pdf: parsePdf(r.pdf),
|
|
357
|
+
person: parsePerson(r.person),
|
|
358
|
+
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
359
|
+
raw
|
|
305
360
|
};
|
|
306
361
|
}
|
|
362
|
+
var PERSON_STRINGS = [
|
|
363
|
+
"firstName",
|
|
364
|
+
"lastName",
|
|
365
|
+
"familyName",
|
|
366
|
+
"firstNameEn",
|
|
367
|
+
"lastNameEn",
|
|
368
|
+
"civilId",
|
|
369
|
+
"regNo",
|
|
370
|
+
"birthDate",
|
|
371
|
+
"gender"
|
|
372
|
+
];
|
|
373
|
+
function parsePerson(raw) {
|
|
374
|
+
const p = asRecord(raw);
|
|
375
|
+
const out = {};
|
|
376
|
+
for (const k of PERSON_STRINGS) if (typeof p[k] === "string" && p[k]) out[k] = p[k];
|
|
377
|
+
if (typeof p.geID === "number" && p.geID !== 0) out.geID = p.geID;
|
|
378
|
+
return Object.keys(out).length ? out : null;
|
|
379
|
+
}
|
|
380
|
+
function parseTokenCert(raw) {
|
|
381
|
+
const t = asRecord(raw);
|
|
382
|
+
const status = str(t.status);
|
|
383
|
+
if (!status) return null;
|
|
384
|
+
const out = { status };
|
|
385
|
+
if (t.errorCode) out.errorCode = String(t.errorCode);
|
|
386
|
+
if (t.authSerialHex) out.authSerialHex = String(t.authSerialHex);
|
|
387
|
+
if (t.signSerialHex) out.signSerialHex = String(t.signSerialHex);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function personMatchesCertificate(person, subject) {
|
|
391
|
+
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
392
|
+
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
393
|
+
}
|
|
307
394
|
function parsePdf(raw) {
|
|
308
395
|
const p = asRecord(raw);
|
|
309
396
|
const docID = str(p.docID);
|
|
@@ -357,7 +444,7 @@ var SessionApi = class {
|
|
|
357
444
|
/** Нэг удаагийн long-poll. serverPollMs хүртэл хүлээнэ. */
|
|
358
445
|
async poll(sessionId, serverPollMs = SERVER_POLL_MS) {
|
|
359
446
|
const raw = await this.http.get(
|
|
360
|
-
|
|
447
|
+
`${routePath(RP_ROUTES.session, sessionId)}?timeoutMs=${serverPollMs}`,
|
|
361
448
|
serverPollMs + 1e4
|
|
362
449
|
// HTTP timeout нь серверийн poll-оос урт
|
|
363
450
|
);
|
|
@@ -382,6 +469,7 @@ var SessionApi = class {
|
|
|
382
469
|
|
|
383
470
|
// src/pdf.ts
|
|
384
471
|
import { createHash as createHash2 } from "crypto";
|
|
472
|
+
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
385
473
|
function rec3(v) {
|
|
386
474
|
return v && typeof v === "object" ? v : {};
|
|
387
475
|
}
|
|
@@ -394,7 +482,10 @@ var PdfApi = class {
|
|
|
394
482
|
http;
|
|
395
483
|
creds;
|
|
396
484
|
defaults;
|
|
397
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
487
|
+
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
488
|
+
*/
|
|
398
489
|
async prepare(input) {
|
|
399
490
|
const request = {
|
|
400
491
|
relyingPartyUUID: this.creds.rpUUID,
|
|
@@ -410,7 +501,7 @@ var PdfApi = class {
|
|
|
410
501
|
const form = new FormData();
|
|
411
502
|
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
412
503
|
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
413
|
-
const r = rec3(await this.http.postMultipart(
|
|
504
|
+
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS));
|
|
414
505
|
const receivedAt = Date.now();
|
|
415
506
|
const vc = rec3(r.vc).value;
|
|
416
507
|
const dl = rec3(r.deviceLink);
|
|
@@ -432,10 +523,12 @@ var PdfApi = class {
|
|
|
432
523
|
}
|
|
433
524
|
/**
|
|
434
525
|
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
435
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
|
|
526
|
+
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
527
|
+
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
528
|
+
* дараа дахин татна (`retryable`).
|
|
436
529
|
*/
|
|
437
530
|
document(sessionId) {
|
|
438
|
-
return this.http.getBytes(
|
|
531
|
+
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS);
|
|
439
532
|
}
|
|
440
533
|
/**
|
|
441
534
|
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
@@ -467,14 +560,54 @@ var OrganizationApi = class {
|
|
|
467
560
|
http;
|
|
468
561
|
/** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
|
|
469
562
|
async getRepresentations(personEtsi) {
|
|
470
|
-
const raw = await this.http.get(
|
|
563
|
+
const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
|
|
471
564
|
const list = Array.isArray(raw.representations) ? raw.representations : [];
|
|
472
565
|
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
|
|
473
566
|
}
|
|
474
567
|
};
|
|
475
568
|
|
|
476
|
-
// src/
|
|
477
|
-
|
|
569
|
+
// src/person.ts
|
|
570
|
+
function rec4(v) {
|
|
571
|
+
return v && typeof v === "object" ? v : {};
|
|
572
|
+
}
|
|
573
|
+
var PersonApi = class {
|
|
574
|
+
constructor(http) {
|
|
575
|
+
this.http = http;
|
|
576
|
+
}
|
|
577
|
+
http;
|
|
578
|
+
async get(path) {
|
|
579
|
+
const raw = await this.http.get(path);
|
|
580
|
+
return { ...rec4(raw), raw };
|
|
581
|
+
}
|
|
582
|
+
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
583
|
+
certificates(personEtsi) {
|
|
584
|
+
return this.get(routePath(RP_ROUTES.personCertificates, personEtsi));
|
|
585
|
+
}
|
|
586
|
+
/** Иргэний холбоотой төхөөрөмжүүд (идэвхтэй + идэвхгүй). */
|
|
587
|
+
devices(personEtsi) {
|
|
588
|
+
return this.get(routePath(RP_ROUTES.personDevices, personEtsi));
|
|
589
|
+
}
|
|
590
|
+
/** Иргэний үйл ажиллагааны түүх. */
|
|
591
|
+
activity(personEtsi, query) {
|
|
592
|
+
const q = new URLSearchParams();
|
|
593
|
+
if (query?.flow) q.set("flow", query.flow);
|
|
594
|
+
if (query?.limit && query.limit > 0) q.set("limit", String(query.limit));
|
|
595
|
+
if (query?.offset && query.offset > 0) q.set("offset", String(query.offset));
|
|
596
|
+
const qs = q.toString();
|
|
597
|
+
return this.get(routePath(RP_ROUTES.personActivity, personEtsi) + (qs ? `?${qs}` : ""));
|
|
598
|
+
}
|
|
599
|
+
/** Самбарын нэгдсэн тоо. */
|
|
600
|
+
summary(personEtsi) {
|
|
601
|
+
return this.get(routePath(RP_ROUTES.personSummary, personEtsi));
|
|
602
|
+
}
|
|
603
|
+
/** Асран хамгаалж буй хүүхдүүд (зөвхөн унших). */
|
|
604
|
+
children(personEtsi) {
|
|
605
|
+
return this.get(routePath(RP_ROUTES.personChildren, personEtsi));
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/token.ts
|
|
610
|
+
import { X509Certificate as X509Certificate2, createHash as createHash3, createPublicKey, timingSafeEqual } from "crypto";
|
|
478
611
|
|
|
479
612
|
// src/subject.ts
|
|
480
613
|
import { X509Certificate } from "crypto";
|
|
@@ -580,8 +713,207 @@ function parseSubject(cert) {
|
|
|
580
713
|
out.name = full || out.commonName || null;
|
|
581
714
|
return out;
|
|
582
715
|
}
|
|
716
|
+
function certExtension(der, oid) {
|
|
717
|
+
const top = tlv(der, 0, der.length);
|
|
718
|
+
const tbs = tlv(der, top.start, top.end);
|
|
719
|
+
let f = tlv(der, tbs.start, tbs.end);
|
|
720
|
+
if (f.tag === 160) f = tlv(der, f.end, tbs.end);
|
|
721
|
+
for (let k = 0; k < 5; k++) f = tlv(der, f.end, tbs.end);
|
|
722
|
+
for (let pos = f.end; pos < tbs.end; ) {
|
|
723
|
+
const t = tlv(der, pos, tbs.end);
|
|
724
|
+
if (t.tag === 163) {
|
|
725
|
+
const seq = tlv(der, t.start, t.end);
|
|
726
|
+
for (let ep = seq.start; ep < seq.end; ) {
|
|
727
|
+
const ext = tlv(der, ep, seq.end);
|
|
728
|
+
const id = tlv(der, ext.start, ext.end);
|
|
729
|
+
let v = tlv(der, id.end, ext.end);
|
|
730
|
+
if (v.tag === 1) v = tlv(der, v.end, ext.end);
|
|
731
|
+
if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
|
|
732
|
+
if (oidOf(der, id) === oid) return der.subarray(v.start, v.end);
|
|
733
|
+
ep = ext.end;
|
|
734
|
+
}
|
|
735
|
+
return void 0;
|
|
736
|
+
}
|
|
737
|
+
pos = t.end;
|
|
738
|
+
}
|
|
739
|
+
return void 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/token.ts
|
|
743
|
+
function rec5(v) {
|
|
744
|
+
return v && typeof v === "object" ? v : {};
|
|
745
|
+
}
|
|
746
|
+
function hexSha256Equal(hex, data) {
|
|
747
|
+
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
748
|
+
return timingSafeEqual(createHash3("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
749
|
+
}
|
|
750
|
+
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
751
|
+
var TokenApi = class {
|
|
752
|
+
constructor(http, creds) {
|
|
753
|
+
this.http = http;
|
|
754
|
+
this.creds = creds;
|
|
755
|
+
}
|
|
756
|
+
http;
|
|
757
|
+
creds;
|
|
758
|
+
/**
|
|
759
|
+
* POST /token/certificate — токены AUTH + SIGN гэрчилгээ олголт эхлүүлнэ (иргэн утсаараа батална). CA-ийн буцаасан
|
|
760
|
+
* CSR hash-ууд илгээсэнтэй таарахгүй бол ValidationError.
|
|
761
|
+
*/
|
|
762
|
+
async issueCertificate(input) {
|
|
763
|
+
if (!input.signerEtsi?.trim() || !input.csrAuth?.length || !input.csrSign?.length || !input.tokenSerial?.trim()) {
|
|
764
|
+
throw new Error("token: signerEtsi, csrAuth, csrSign, tokenSerial \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
765
|
+
}
|
|
766
|
+
const body = {
|
|
767
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
768
|
+
relyingPartyName: this.creds.rpName,
|
|
769
|
+
signer: { etsi: input.signerEtsi.trim() },
|
|
770
|
+
csrAuth: Buffer.from(input.csrAuth).toString("base64"),
|
|
771
|
+
csrSign: Buffer.from(input.csrSign).toString("base64"),
|
|
772
|
+
tokenSerial: input.tokenSerial.trim(),
|
|
773
|
+
onBehalfOf: input.onBehalfOf?.trim() ?? "",
|
|
774
|
+
flow: "notification"
|
|
775
|
+
};
|
|
776
|
+
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
777
|
+
const r = rec5(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
778
|
+
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
779
|
+
throw new ValidationError("token: CA-\u0438\u0439\u043D csrAuthSha256/csrSignSha256 \u0438\u043B\u0433\u044D\u044D\u0441\u044D\u043D CSR-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
780
|
+
}
|
|
781
|
+
const vc = rec5(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
782
|
+
return {
|
|
783
|
+
sessionId: String(r.sessionID ?? ""),
|
|
784
|
+
vc: vc != null ? String(vc) : null,
|
|
785
|
+
csrAuthSha256: String(r.csrAuthSha256).toLowerCase(),
|
|
786
|
+
csrSignSha256: String(r.csrSignSha256).toLowerCase(),
|
|
787
|
+
...typeof r.expiresAt === "string" ? { expiresAt: r.expiresAt } : {}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* GET /token/certificate/{sessionID} — олгосон AUTH + SIGN гэрчилгээ + гинж. Токенд бичихээс өмнө
|
|
792
|
+
* {@link verifyTokenCertificates}-ээр CSR-ийн түлхүүртэй тулга.
|
|
793
|
+
*/
|
|
794
|
+
async certificateResult(sessionId) {
|
|
795
|
+
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
796
|
+
const r = rec5(raw);
|
|
797
|
+
const item = (v) => v && typeof v === "object" ? v : null;
|
|
798
|
+
return {
|
|
799
|
+
status: String(r.status ?? ""),
|
|
800
|
+
auth: item(r.auth),
|
|
801
|
+
sign: item(r.sign),
|
|
802
|
+
chain: Array.isArray(r.chain) ? r.chain.map(String) : [],
|
|
803
|
+
chainPem: String(r.chainPem ?? ""),
|
|
804
|
+
organization: r.organization && typeof r.organization === "object" ? r.organization : null,
|
|
805
|
+
keyMedium: String(r.keyMedium ?? ""),
|
|
806
|
+
certificateLevel: String(r.certificateLevel ?? ""),
|
|
807
|
+
raw
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
811
|
+
async authChallenge() {
|
|
812
|
+
const r = rec5(
|
|
813
|
+
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
814
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
815
|
+
relyingPartyName: this.creds.rpName
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
return {
|
|
819
|
+
challengeId: String(r.challengeId ?? ""),
|
|
820
|
+
challenge: String(r.challenge ?? ""),
|
|
821
|
+
rpUUID: String(r.rpUUID ?? ""),
|
|
822
|
+
expiresAt: String(r.expiresAt ?? "")
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* POST /token/auth/verify — токены AUTH гэрчилгээ (DER) + challenge дээрх түүхий r||s. CA гинж, төлөв, цуцлалт, гарын
|
|
827
|
+
* үсгийг шалгана; SDK нэмж endResult === OK ба identity.etsi === гэрчилгээний SERIALNUMBER-ийг тулгана (эс бөгөөс
|
|
828
|
+
* ValidationError). Challenge нэг удаагийн — алдааны дараа шинээр авна.
|
|
829
|
+
*/
|
|
830
|
+
async authVerify(challengeId, certDer, signature) {
|
|
831
|
+
let serial;
|
|
832
|
+
try {
|
|
833
|
+
serial = parseSubject(new X509Certificate2(certDer)).serialNumber;
|
|
834
|
+
} catch {
|
|
835
|
+
throw new Error("token auth: \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
836
|
+
}
|
|
837
|
+
if (!challengeId || !signature?.length) throw new Error("token auth: challengeId \u0431\u0430 signature \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
838
|
+
const raw = await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthVerify), {
|
|
839
|
+
challengeId,
|
|
840
|
+
certificate: Buffer.from(certDer).toString("base64"),
|
|
841
|
+
signature: Buffer.from(signature).toString("base64")
|
|
842
|
+
});
|
|
843
|
+
const r = rec5(raw);
|
|
844
|
+
if (rec5(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
845
|
+
const id = rec5(r.identity);
|
|
846
|
+
if (!id.etsi || id.etsi !== serial) throw new ValidationError("token auth: identity.etsi \u043D\u044C \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 SERIALNUMBER \u0431\u0438\u0448");
|
|
847
|
+
const org = rec5(r.organization);
|
|
848
|
+
const cert = rec5(r.cert);
|
|
849
|
+
return {
|
|
850
|
+
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
851
|
+
serialHex: String(cert.serialHex ?? ""),
|
|
852
|
+
notAfter: String(cert.notAfter ?? ""),
|
|
853
|
+
organization: org.etsi ? { etsi: String(org.etsi), name: String(org.name ?? "") } : null,
|
|
854
|
+
raw
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
function csrSpki(csr) {
|
|
859
|
+
const top = tlv(csr, 0, csr.length);
|
|
860
|
+
const info = tlv(csr, top.start, top.end);
|
|
861
|
+
const version = tlv(csr, info.start, info.end);
|
|
862
|
+
const subject = tlv(csr, version.end, info.end);
|
|
863
|
+
const spki = tlv(csr, subject.end, info.end);
|
|
864
|
+
return createPublicKey({ key: Buffer.from(csr.subarray(subject.end, spki.end)), format: "der", type: "spki" }).export({
|
|
865
|
+
format: "der",
|
|
866
|
+
type: "spki"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function verifyTokenCertificates(result, csrAuth, csrSign) {
|
|
870
|
+
if (!result?.auth || !result.sign) throw new ValidationError("token: auth/sign \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
871
|
+
for (const [name, item, csr] of [
|
|
872
|
+
["auth", result.auth, csrAuth],
|
|
873
|
+
["sign", result.sign, csrSign]
|
|
874
|
+
]) {
|
|
875
|
+
let want;
|
|
876
|
+
try {
|
|
877
|
+
want = csrSpki(csr);
|
|
878
|
+
} catch {
|
|
879
|
+
throw new Error(`token: ${name} CSR \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
880
|
+
}
|
|
881
|
+
let got;
|
|
882
|
+
try {
|
|
883
|
+
got = new X509Certificate2(Buffer.from(item.certificate, "base64")).publicKey.export({ format: "der", type: "spki" });
|
|
884
|
+
} catch {
|
|
885
|
+
throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
886
|
+
}
|
|
887
|
+
if (!got.equals(want)) throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440 CSR-\u0438\u0439\u043D\u0445\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function tokenAuthDigest(rpUUID, challengeB64) {
|
|
891
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(rpUUID)) {
|
|
892
|
+
throw new Error("token auth: rpUUID \u0436\u0438\u0436\u0438\u0433 \u04AF\u0441\u0433\u0438\u0439\u043D 36 \u0442\u044D\u043C\u0434\u044D\u0433\u0442 UUID \u0431\u0438\u0448");
|
|
893
|
+
}
|
|
894
|
+
const ch = Buffer.from(challengeB64, "base64");
|
|
895
|
+
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
896
|
+
return createHash3("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
897
|
+
}
|
|
898
|
+
function tokenLimitOf(err) {
|
|
899
|
+
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
900
|
+
let b;
|
|
901
|
+
try {
|
|
902
|
+
b = rec5(JSON.parse(err.body));
|
|
903
|
+
} catch {
|
|
904
|
+
return void 0;
|
|
905
|
+
}
|
|
906
|
+
if (typeof b.limit !== "number" || !Number.isInteger(b.limit) || b.limit < 0) return void 0;
|
|
907
|
+
if (b.kind === "PERSONAL") return { kind: "PERSONAL", limit: b.limit };
|
|
908
|
+
if (b.kind === "ORGANIZATION" && typeof b.organization === "string" && b.organization.startsWith("NTRMN-")) {
|
|
909
|
+
return { kind: "ORGANIZATION", limit: b.limit, organization: b.organization };
|
|
910
|
+
}
|
|
911
|
+
return void 0;
|
|
912
|
+
}
|
|
583
913
|
|
|
584
914
|
// src/validator.ts
|
|
915
|
+
import { X509Certificate as X509Certificate3, constants, createHash as createHash4, publicDecrypt, verify } from "crypto";
|
|
916
|
+
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
585
917
|
var ResponseValidator = class {
|
|
586
918
|
anchors;
|
|
587
919
|
intermediates;
|
|
@@ -590,14 +922,28 @@ var ResponseValidator = class {
|
|
|
590
922
|
allowUntrusted;
|
|
591
923
|
revocation;
|
|
592
924
|
revocationMode;
|
|
925
|
+
allowedIssuers;
|
|
926
|
+
checkKeyUsage;
|
|
927
|
+
schemeName;
|
|
593
928
|
constructor(trust = {}) {
|
|
594
|
-
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new
|
|
595
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
929
|
+
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate3(p));
|
|
930
|
+
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate3(p));
|
|
596
931
|
this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
|
|
597
932
|
this.skew = trust.clockSkewMs ?? 0;
|
|
598
933
|
this.allowUntrusted = trust.allowUntrusted ?? false;
|
|
599
934
|
this.revocation = trust.revocation;
|
|
600
935
|
this.revocationMode = trust.revocationMode ?? "hard-fail";
|
|
936
|
+
if (trust.allowedIssuerSkis?.length) {
|
|
937
|
+
this.allowedIssuers = new Set(
|
|
938
|
+
trust.allowedIssuerSkis.map((s) => {
|
|
939
|
+
const ski = s.trim().replace(/:/g, "").toLowerCase();
|
|
940
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(ski)) throw new Error(`ResponseValidator: allowedIssuerSkis: "${s}" hex key identifier \u0431\u0438\u0448`);
|
|
941
|
+
return ski;
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
this.checkKeyUsage = trust.checkKeyUsage ?? false;
|
|
946
|
+
this.schemeName = trust.schemeName;
|
|
601
947
|
}
|
|
602
948
|
// ── нийтийн API ──
|
|
603
949
|
/**
|
|
@@ -625,6 +971,7 @@ var ResponseValidator = class {
|
|
|
625
971
|
if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
|
|
626
972
|
const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
|
|
627
973
|
const payload = buildAcspV2Payload({
|
|
974
|
+
...this.schemeName ? { schemeName: this.schemeName } : {},
|
|
628
975
|
serverRandom,
|
|
629
976
|
rpChallenge: acsp.rpChallenge,
|
|
630
977
|
userChallenge,
|
|
@@ -728,9 +1075,10 @@ var ResponseValidator = class {
|
|
|
728
1075
|
if (!result.certificateDerB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
729
1076
|
if (!result.signatureValueB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0430\u043B\u0433\u0430");
|
|
730
1077
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
731
|
-
const leaf = new
|
|
1078
|
+
const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
|
|
732
1079
|
this.checkValidity(leaf);
|
|
733
1080
|
this.checkLevel(String(result.certificateLevel ?? ""));
|
|
1081
|
+
this.checkPurpose(leaf, protocol);
|
|
734
1082
|
if (this.anchors.length === 0) {
|
|
735
1083
|
if (!this.allowUntrusted) {
|
|
736
1084
|
throw new ValidationError(
|
|
@@ -740,8 +1088,35 @@ var ResponseValidator = class {
|
|
|
740
1088
|
} else {
|
|
741
1089
|
this.verifyChain(leaf);
|
|
742
1090
|
}
|
|
1091
|
+
this.checkIssuer(leaf);
|
|
743
1092
|
return leaf;
|
|
744
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* checkKeyUsage үед гэрчилгээний зориулалт протоколд тохирох эсэх (CA crypto/ca_profile.go): ACSP_V2 (auth) → KU
|
|
1096
|
+
* digitalSignature + EKU clientAuth (эсвэл anyExtendedKeyUsage); RAW_DIGEST_SIGNATURE (sign) → KU contentCommitment.
|
|
1097
|
+
*/
|
|
1098
|
+
checkPurpose(leaf, protocol) {
|
|
1099
|
+
if (!this.checkKeyUsage) return;
|
|
1100
|
+
const ku = keyUsageBits(leaf);
|
|
1101
|
+
if (protocol === "ACSP_V2") {
|
|
1102
|
+
if (!(ku & 128)) throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage digitalSignature \u0430\u043B\u0433\u0430");
|
|
1103
|
+
const eku = extKeyUsage(leaf);
|
|
1104
|
+
if (!eku.includes("1.3.6.1.5.5.7.3.2") && !eku.includes("2.5.29.37.0")) {
|
|
1105
|
+
throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 ExtKeyUsage clientAuth \u0430\u043B\u0433\u0430 (\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)");
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (!(ku & 64)) throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage nonRepudiation (contentCommitment) \u0430\u043B\u0433\u0430");
|
|
1110
|
+
}
|
|
1111
|
+
/** allowedIssuerSkis үед leaf-ийн шууд issuer-ийн SKI (issuer-т алга бол leaf-ийн AKI) жагсаалтад байх ёстой. */
|
|
1112
|
+
checkIssuer(leaf) {
|
|
1113
|
+
if (!this.allowedIssuers) return;
|
|
1114
|
+
const issuer = this.findIssuer(leaf);
|
|
1115
|
+
const ski = (issuer && subjectKeyId(issuer)) ?? authorityKeyId(leaf);
|
|
1116
|
+
if (!ski || !this.allowedIssuers.has(ski)) {
|
|
1117
|
+
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u0433 \u0433\u0430\u0440\u0433\u0430\u0441\u0430\u043D CA \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u0433\u0434\u0441\u04E9\u043D \u0438\u0440\u0433\u044D\u043D\u0438\u0439 issuer \u0431\u0438\u0448 (allowedIssuerSkis)");
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
745
1120
|
checkValidity(cert) {
|
|
746
1121
|
const now = Date.now();
|
|
747
1122
|
const from = Date.parse(cert.validFrom);
|
|
@@ -784,14 +1159,23 @@ var ResponseValidator = class {
|
|
|
784
1159
|
* дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
|
|
785
1160
|
*/
|
|
786
1161
|
async checkRevocation(cert) {
|
|
787
|
-
|
|
1162
|
+
await this.checkRevocationStatus(cert);
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* {@link checkRevocation} + юу шийдсэнийг буцаана (лог/аудитад): "good" | "unchecked" (unknown, soft-fail) |
|
|
1166
|
+
* "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
|
|
1167
|
+
*/
|
|
1168
|
+
async checkRevocationStatus(cert) {
|
|
1169
|
+
if (!this.revocation) return "skipped";
|
|
788
1170
|
const status = await this.revocation(cert, this.findIssuer(cert));
|
|
1171
|
+
if (status === "good") return "good";
|
|
789
1172
|
if (status === "revoked") {
|
|
790
1173
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
|
|
791
1174
|
}
|
|
792
|
-
if (
|
|
1175
|
+
if (this.revocationMode === "hard-fail") {
|
|
793
1176
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 revocation \u0442\u04E9\u043B\u04E9\u0432 \u0442\u043E\u0433\u0442\u043E\u043E\u0433\u0434\u0441\u043E\u043D\u0433\u04AF\u0439 (hard-fail)");
|
|
794
1177
|
}
|
|
1178
|
+
return "unchecked";
|
|
795
1179
|
}
|
|
796
1180
|
};
|
|
797
1181
|
function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
@@ -806,7 +1190,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
806
1190
|
return fromResponse;
|
|
807
1191
|
}
|
|
808
1192
|
function userChallengeOf(verifier) {
|
|
809
|
-
return
|
|
1193
|
+
return createHash4("sha256").update(verifier, "utf8").digest("base64url");
|
|
810
1194
|
}
|
|
811
1195
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
812
1196
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -908,7 +1292,7 @@ function mgf1(hash, seed, length) {
|
|
|
908
1292
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
909
1293
|
const c = Buffer.alloc(4);
|
|
910
1294
|
c.writeUInt32BE(i, 0);
|
|
911
|
-
out.push(
|
|
1295
|
+
out.push(createHash4(nodeHash(hash)).update(seed).update(c).digest());
|
|
912
1296
|
}
|
|
913
1297
|
return Buffer.concat(out).subarray(0, length);
|
|
914
1298
|
}
|
|
@@ -932,9 +1316,42 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
932
1316
|
const psLen = emLen - hLen - sLen - 2;
|
|
933
1317
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
934
1318
|
const salt = db.subarray(psLen + 1);
|
|
935
|
-
const h2 =
|
|
1319
|
+
const h2 = createHash4(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
936
1320
|
return h.equals(h2);
|
|
937
1321
|
}
|
|
1322
|
+
function keyUsageBits(cert) {
|
|
1323
|
+
const v = certExtension(cert.raw, "2.5.29.15");
|
|
1324
|
+
if (!v) return 0;
|
|
1325
|
+
const bs = tlv(v, 0, v.length);
|
|
1326
|
+
if (bs.tag !== 3 || bs.end - bs.start < 2) return 0;
|
|
1327
|
+
return v[bs.start + 1];
|
|
1328
|
+
}
|
|
1329
|
+
function extKeyUsage(cert) {
|
|
1330
|
+
const v = certExtension(cert.raw, "2.5.29.37");
|
|
1331
|
+
if (!v) return [];
|
|
1332
|
+
const seq = tlv(v, 0, v.length);
|
|
1333
|
+
const out = [];
|
|
1334
|
+
for (let p = seq.start; p < seq.end; ) {
|
|
1335
|
+
const t = tlv(v, p, seq.end);
|
|
1336
|
+
out.push(oidOf(v, t));
|
|
1337
|
+
p = t.end;
|
|
1338
|
+
}
|
|
1339
|
+
return out;
|
|
1340
|
+
}
|
|
1341
|
+
function subjectKeyId(cert) {
|
|
1342
|
+
const v = certExtension(cert.raw, "2.5.29.14");
|
|
1343
|
+
if (!v) return void 0;
|
|
1344
|
+
const o = tlv(v, 0, v.length);
|
|
1345
|
+
return o.tag === 4 && o.end > o.start ? Buffer.from(v.subarray(o.start, o.end)).toString("hex") : void 0;
|
|
1346
|
+
}
|
|
1347
|
+
function authorityKeyId(cert) {
|
|
1348
|
+
const v = certExtension(cert.raw, "2.5.29.35");
|
|
1349
|
+
if (!v) return void 0;
|
|
1350
|
+
const seq = tlv(v, 0, v.length);
|
|
1351
|
+
if (seq.start >= seq.end) return void 0;
|
|
1352
|
+
const k = tlv(v, seq.start, seq.end);
|
|
1353
|
+
return k.tag === 128 && k.end > k.start ? Buffer.from(v.subarray(k.start, k.end)).toString("hex") : void 0;
|
|
1354
|
+
}
|
|
938
1355
|
function isCa(cert) {
|
|
939
1356
|
return cert.ca === true;
|
|
940
1357
|
}
|
|
@@ -962,6 +1379,8 @@ var EidClient = class {
|
|
|
962
1379
|
sign;
|
|
963
1380
|
pdf;
|
|
964
1381
|
organization;
|
|
1382
|
+
person;
|
|
1383
|
+
token;
|
|
965
1384
|
session;
|
|
966
1385
|
validator;
|
|
967
1386
|
constructor(cfg) {
|
|
@@ -977,8 +1396,10 @@ var EidClient = class {
|
|
|
977
1396
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
978
1397
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
979
1398
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
980
|
-
this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level });
|
|
1399
|
+
this.pdf = new PdfApi(http, cfg.credentials, { certificateLevel: level, timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS });
|
|
981
1400
|
this.organization = new OrganizationApi(http);
|
|
1401
|
+
this.person = new PersonApi(http);
|
|
1402
|
+
this.token = new TokenApi(http, cfg.credentials);
|
|
982
1403
|
this.session = new SessionApi(http);
|
|
983
1404
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
984
1405
|
}
|
|
@@ -1055,7 +1476,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1055
1476
|
}
|
|
1056
1477
|
|
|
1057
1478
|
// src/oidc.ts
|
|
1058
|
-
import { timingSafeEqual } from "crypto";
|
|
1479
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1059
1480
|
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
1060
1481
|
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
1061
1482
|
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
@@ -1092,7 +1513,7 @@ var EidOidcClient = class {
|
|
|
1092
1513
|
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1093
1514
|
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1094
1515
|
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1095
|
-
this.scope = cfg.scope ??
|
|
1516
|
+
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1096
1517
|
}
|
|
1097
1518
|
cfg;
|
|
1098
1519
|
issuer;
|
|
@@ -1140,7 +1561,7 @@ var EidOidcClient = class {
|
|
|
1140
1561
|
state,
|
|
1141
1562
|
nonce,
|
|
1142
1563
|
code_challenge: await pkceChallenge(codeVerifier),
|
|
1143
|
-
code_challenge_method:
|
|
1564
|
+
code_challenge_method: OIDC_EXPECT.pkceMethod
|
|
1144
1565
|
});
|
|
1145
1566
|
if (opts.display) p.set("display", opts.display);
|
|
1146
1567
|
u.search = p.toString();
|
|
@@ -1219,7 +1640,7 @@ var EidOidcClient = class {
|
|
|
1219
1640
|
const header = decodeJsonPart(parts[0]);
|
|
1220
1641
|
const payload = decodeJsonPart(parts[1]);
|
|
1221
1642
|
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1222
|
-
if (header.alg !==
|
|
1643
|
+
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1223
1644
|
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1224
1645
|
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1225
1646
|
const pick = (ks) => ks.find(
|
|
@@ -1312,7 +1733,7 @@ function formEncode(s) {
|
|
|
1312
1733
|
function timingSafeEqualStr(a, b) {
|
|
1313
1734
|
const x = Buffer.from(a, "utf8");
|
|
1314
1735
|
const y = Buffer.from(b, "utf8");
|
|
1315
|
-
return x.length === y.length &&
|
|
1736
|
+
return x.length === y.length && timingSafeEqual2(x, y);
|
|
1316
1737
|
}
|
|
1317
1738
|
function decodeJsonPart(s) {
|
|
1318
1739
|
try {
|
|
@@ -1334,7 +1755,9 @@ export {
|
|
|
1334
1755
|
ApiError,
|
|
1335
1756
|
AuthApi,
|
|
1336
1757
|
AuthenticationError,
|
|
1758
|
+
CITIZEN_ISSUING_CA_SKI,
|
|
1337
1759
|
DEFAULT_BASE_URL,
|
|
1760
|
+
DEFAULT_PDF_TIMEOUT_MS,
|
|
1338
1761
|
DeviceLinkError,
|
|
1339
1762
|
EID_ACR_ADVANCED,
|
|
1340
1763
|
EID_ACR_QUALIFIED,
|
|
@@ -1349,10 +1772,13 @@ export {
|
|
|
1349
1772
|
ON_BEHALF_ERROR_CODES,
|
|
1350
1773
|
OrganizationApi,
|
|
1351
1774
|
PdfApi,
|
|
1775
|
+
PersonApi,
|
|
1776
|
+
RETRYABLE_ERROR_CODES,
|
|
1352
1777
|
ResponseValidator,
|
|
1353
1778
|
SessionApi,
|
|
1354
1779
|
SessionFailedError,
|
|
1355
1780
|
SignApi,
|
|
1781
|
+
TokenApi,
|
|
1356
1782
|
ValidationError,
|
|
1357
1783
|
acspV2Digest,
|
|
1358
1784
|
buildAcspV2Payload,
|
|
@@ -1361,14 +1787,19 @@ export {
|
|
|
1361
1787
|
interactionsToBase64,
|
|
1362
1788
|
isMobileUserAgent,
|
|
1363
1789
|
openOrShowQR,
|
|
1790
|
+
parseRetryAfter,
|
|
1364
1791
|
parseSessionResult,
|
|
1365
1792
|
parseSubject,
|
|
1793
|
+
personMatchesCertificate,
|
|
1366
1794
|
pkceChallenge,
|
|
1367
1795
|
qrDeviceLinkTicker,
|
|
1368
1796
|
randomChallenge,
|
|
1369
1797
|
resolveBaseUrl,
|
|
1370
1798
|
sha256Base64,
|
|
1799
|
+
tokenAuthDigest,
|
|
1800
|
+
tokenLimitOf,
|
|
1371
1801
|
userChallengeOf,
|
|
1372
1802
|
verifyPayloadSignature,
|
|
1373
|
-
verifyPrehashedSignature
|
|
1803
|
+
verifyPrehashedSignature,
|
|
1804
|
+
verifyTokenCertificates
|
|
1374
1805
|
};
|