@gerege-systems/eid-mongolia-sdk 0.9.0 → 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 +78 -6
- 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 +553 -45
- package/dist/index.d.cts +347 -7
- package/dist/index.d.ts +347 -7
- package/dist/index.js +474 -39
- 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,
|
|
@@ -648,10 +995,14 @@ var ResponseValidator = class {
|
|
|
648
995
|
}
|
|
649
996
|
/**
|
|
650
997
|
* Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
|
|
651
|
-
* @param
|
|
998
|
+
* @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
|
|
652
999
|
*/
|
|
653
|
-
validateSign(result, digestB64,
|
|
1000
|
+
validateSign(result, digestB64, opts = {}) {
|
|
1001
|
+
const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
|
|
654
1002
|
const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
|
|
1003
|
+
if (expectedFlowType && result.flowType !== expectedFlowType) {
|
|
1004
|
+
throw new ValidationError(`flowType ${result.flowType} \u2260 \u044D\u0445\u043B\u04AF\u04AF\u043B\u0441\u044D\u043D \u0443\u0440\u0441\u0433\u0430\u043B ${expectedFlowType} (relay \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)`);
|
|
1005
|
+
}
|
|
655
1006
|
const digest = Buffer.from(digestB64, "base64");
|
|
656
1007
|
const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
|
|
657
1008
|
if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
@@ -724,9 +1075,10 @@ var ResponseValidator = class {
|
|
|
724
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");
|
|
725
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");
|
|
726
1077
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
727
|
-
const leaf = new
|
|
1078
|
+
const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
|
|
728
1079
|
this.checkValidity(leaf);
|
|
729
1080
|
this.checkLevel(String(result.certificateLevel ?? ""));
|
|
1081
|
+
this.checkPurpose(leaf, protocol);
|
|
730
1082
|
if (this.anchors.length === 0) {
|
|
731
1083
|
if (!this.allowUntrusted) {
|
|
732
1084
|
throw new ValidationError(
|
|
@@ -736,8 +1088,35 @@ var ResponseValidator = class {
|
|
|
736
1088
|
} else {
|
|
737
1089
|
this.verifyChain(leaf);
|
|
738
1090
|
}
|
|
1091
|
+
this.checkIssuer(leaf);
|
|
739
1092
|
return leaf;
|
|
740
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
|
+
}
|
|
741
1120
|
checkValidity(cert) {
|
|
742
1121
|
const now = Date.now();
|
|
743
1122
|
const from = Date.parse(cert.validFrom);
|
|
@@ -780,14 +1159,23 @@ var ResponseValidator = class {
|
|
|
780
1159
|
* дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
|
|
781
1160
|
*/
|
|
782
1161
|
async checkRevocation(cert) {
|
|
783
|
-
|
|
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";
|
|
784
1170
|
const status = await this.revocation(cert, this.findIssuer(cert));
|
|
1171
|
+
if (status === "good") return "good";
|
|
785
1172
|
if (status === "revoked") {
|
|
786
1173
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
|
|
787
1174
|
}
|
|
788
|
-
if (
|
|
1175
|
+
if (this.revocationMode === "hard-fail") {
|
|
789
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)");
|
|
790
1177
|
}
|
|
1178
|
+
return "unchecked";
|
|
791
1179
|
}
|
|
792
1180
|
};
|
|
793
1181
|
function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
@@ -802,7 +1190,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
802
1190
|
return fromResponse;
|
|
803
1191
|
}
|
|
804
1192
|
function userChallengeOf(verifier) {
|
|
805
|
-
return
|
|
1193
|
+
return createHash4("sha256").update(verifier, "utf8").digest("base64url");
|
|
806
1194
|
}
|
|
807
1195
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
808
1196
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -904,7 +1292,7 @@ function mgf1(hash, seed, length) {
|
|
|
904
1292
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
905
1293
|
const c = Buffer.alloc(4);
|
|
906
1294
|
c.writeUInt32BE(i, 0);
|
|
907
|
-
out.push(
|
|
1295
|
+
out.push(createHash4(nodeHash(hash)).update(seed).update(c).digest());
|
|
908
1296
|
}
|
|
909
1297
|
return Buffer.concat(out).subarray(0, length);
|
|
910
1298
|
}
|
|
@@ -928,9 +1316,42 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
928
1316
|
const psLen = emLen - hLen - sLen - 2;
|
|
929
1317
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
930
1318
|
const salt = db.subarray(psLen + 1);
|
|
931
|
-
const h2 =
|
|
1319
|
+
const h2 = createHash4(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
932
1320
|
return h.equals(h2);
|
|
933
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
|
+
}
|
|
934
1355
|
function isCa(cert) {
|
|
935
1356
|
return cert.ca === true;
|
|
936
1357
|
}
|
|
@@ -958,6 +1379,8 @@ var EidClient = class {
|
|
|
958
1379
|
sign;
|
|
959
1380
|
pdf;
|
|
960
1381
|
organization;
|
|
1382
|
+
person;
|
|
1383
|
+
token;
|
|
961
1384
|
session;
|
|
962
1385
|
validator;
|
|
963
1386
|
constructor(cfg) {
|
|
@@ -973,8 +1396,10 @@ var EidClient = class {
|
|
|
973
1396
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
974
1397
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
975
1398
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
976
|
-
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 });
|
|
977
1400
|
this.organization = new OrganizationApi(http);
|
|
1401
|
+
this.person = new PersonApi(http);
|
|
1402
|
+
this.token = new TokenApi(http, cfg.credentials);
|
|
978
1403
|
this.session = new SessionApi(http);
|
|
979
1404
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
980
1405
|
}
|
|
@@ -1051,7 +1476,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1051
1476
|
}
|
|
1052
1477
|
|
|
1053
1478
|
// src/oidc.ts
|
|
1054
|
-
import { timingSafeEqual } from "crypto";
|
|
1479
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1055
1480
|
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
1056
1481
|
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
1057
1482
|
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
@@ -1088,7 +1513,7 @@ var EidOidcClient = class {
|
|
|
1088
1513
|
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1089
1514
|
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1090
1515
|
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1091
|
-
this.scope = cfg.scope ??
|
|
1516
|
+
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1092
1517
|
}
|
|
1093
1518
|
cfg;
|
|
1094
1519
|
issuer;
|
|
@@ -1136,7 +1561,7 @@ var EidOidcClient = class {
|
|
|
1136
1561
|
state,
|
|
1137
1562
|
nonce,
|
|
1138
1563
|
code_challenge: await pkceChallenge(codeVerifier),
|
|
1139
|
-
code_challenge_method:
|
|
1564
|
+
code_challenge_method: OIDC_EXPECT.pkceMethod
|
|
1140
1565
|
});
|
|
1141
1566
|
if (opts.display) p.set("display", opts.display);
|
|
1142
1567
|
u.search = p.toString();
|
|
@@ -1215,7 +1640,7 @@ var EidOidcClient = class {
|
|
|
1215
1640
|
const header = decodeJsonPart(parts[0]);
|
|
1216
1641
|
const payload = decodeJsonPart(parts[1]);
|
|
1217
1642
|
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1218
|
-
if (header.alg !==
|
|
1643
|
+
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1219
1644
|
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1220
1645
|
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1221
1646
|
const pick = (ks) => ks.find(
|
|
@@ -1308,7 +1733,7 @@ function formEncode(s) {
|
|
|
1308
1733
|
function timingSafeEqualStr(a, b) {
|
|
1309
1734
|
const x = Buffer.from(a, "utf8");
|
|
1310
1735
|
const y = Buffer.from(b, "utf8");
|
|
1311
|
-
return x.length === y.length &&
|
|
1736
|
+
return x.length === y.length && timingSafeEqual2(x, y);
|
|
1312
1737
|
}
|
|
1313
1738
|
function decodeJsonPart(s) {
|
|
1314
1739
|
try {
|
|
@@ -1330,7 +1755,9 @@ export {
|
|
|
1330
1755
|
ApiError,
|
|
1331
1756
|
AuthApi,
|
|
1332
1757
|
AuthenticationError,
|
|
1758
|
+
CITIZEN_ISSUING_CA_SKI,
|
|
1333
1759
|
DEFAULT_BASE_URL,
|
|
1760
|
+
DEFAULT_PDF_TIMEOUT_MS,
|
|
1334
1761
|
DeviceLinkError,
|
|
1335
1762
|
EID_ACR_ADVANCED,
|
|
1336
1763
|
EID_ACR_QUALIFIED,
|
|
@@ -1345,10 +1772,13 @@ export {
|
|
|
1345
1772
|
ON_BEHALF_ERROR_CODES,
|
|
1346
1773
|
OrganizationApi,
|
|
1347
1774
|
PdfApi,
|
|
1775
|
+
PersonApi,
|
|
1776
|
+
RETRYABLE_ERROR_CODES,
|
|
1348
1777
|
ResponseValidator,
|
|
1349
1778
|
SessionApi,
|
|
1350
1779
|
SessionFailedError,
|
|
1351
1780
|
SignApi,
|
|
1781
|
+
TokenApi,
|
|
1352
1782
|
ValidationError,
|
|
1353
1783
|
acspV2Digest,
|
|
1354
1784
|
buildAcspV2Payload,
|
|
@@ -1357,14 +1787,19 @@ export {
|
|
|
1357
1787
|
interactionsToBase64,
|
|
1358
1788
|
isMobileUserAgent,
|
|
1359
1789
|
openOrShowQR,
|
|
1790
|
+
parseRetryAfter,
|
|
1360
1791
|
parseSessionResult,
|
|
1361
1792
|
parseSubject,
|
|
1793
|
+
personMatchesCertificate,
|
|
1362
1794
|
pkceChallenge,
|
|
1363
1795
|
qrDeviceLinkTicker,
|
|
1364
1796
|
randomChallenge,
|
|
1365
1797
|
resolveBaseUrl,
|
|
1366
1798
|
sha256Base64,
|
|
1799
|
+
tokenAuthDigest,
|
|
1800
|
+
tokenLimitOf,
|
|
1367
1801
|
userChallengeOf,
|
|
1368
1802
|
verifyPayloadSignature,
|
|
1369
|
-
verifyPrehashedSignature
|
|
1803
|
+
verifyPrehashedSignature,
|
|
1804
|
+
verifyTokenCertificates
|
|
1370
1805
|
};
|