@gerege-systems/eid-mongolia-sdk 0.9.1 → 0.11.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 +82 -5
- package/dist/{browser-B_XtyuFG.d.cts → browser-BVR6gtrx.d.cts} +115 -5
- package/dist/{browser-B_XtyuFG.d.ts → browser-BVR6gtrx.d.ts} +115 -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-QPJDZLHR.js} +81 -2
- package/dist/index.cjs +785 -63
- package/dist/index.d.cts +402 -8
- package/dist/index.d.ts +402 -8
- package/dist/index.js +696 -57
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -6,15 +6,20 @@ import {
|
|
|
6
6
|
ForbiddenError,
|
|
7
7
|
NetworkError,
|
|
8
8
|
ON_BEHALF_ERROR_CODES,
|
|
9
|
+
RETRYABLE_ERROR_CODES,
|
|
10
|
+
ResponseTooLargeError,
|
|
9
11
|
SessionFailedError,
|
|
10
12
|
ValidationError,
|
|
11
13
|
deviceLink,
|
|
12
14
|
isMobileUserAgent,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
maskPath,
|
|
16
|
+
openOrShowQR,
|
|
17
|
+
parseRetryAfter
|
|
18
|
+
} from "./chunk-QPJDZLHR.js";
|
|
15
19
|
|
|
16
20
|
// src/http.ts
|
|
17
21
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
22
|
+
var MAX_JSON_BYTES = 4 << 20;
|
|
18
23
|
var Http = class {
|
|
19
24
|
constructor(cfg) {
|
|
20
25
|
this.cfg = cfg;
|
|
@@ -30,14 +35,14 @@ var Http = class {
|
|
|
30
35
|
postMultipart(path, form, timeoutMs) {
|
|
31
36
|
return this.json("POST", path, form, void 0, timeoutMs);
|
|
32
37
|
}
|
|
33
|
-
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
|
|
34
|
-
async getBytes(path, timeoutMs) {
|
|
38
|
+
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
|
|
39
|
+
async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
|
|
35
40
|
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
36
|
-
return
|
|
41
|
+
return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
|
|
37
42
|
}
|
|
38
43
|
async json(method, path, body, contentType, timeoutMs) {
|
|
39
44
|
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
40
|
-
const text = await res
|
|
45
|
+
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
|
|
41
46
|
return text ? JSON.parse(text) : {};
|
|
42
47
|
}
|
|
43
48
|
async send(method, path, body, contentType, timeoutMs) {
|
|
@@ -54,28 +59,62 @@ var Http = class {
|
|
|
54
59
|
};
|
|
55
60
|
if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
|
|
56
61
|
let res;
|
|
62
|
+
const op = `${method} ${maskPath(path)}`;
|
|
57
63
|
try {
|
|
58
64
|
res = await f(url, init);
|
|
59
65
|
} catch (e) {
|
|
60
66
|
const err = e;
|
|
61
67
|
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: ${
|
|
68
|
+
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
69
|
}
|
|
64
|
-
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${
|
|
70
|
+
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
71
|
}
|
|
66
72
|
if (res.status < 300) return res;
|
|
67
73
|
const text = await res.text();
|
|
68
74
|
const { error, code } = errorBody(text);
|
|
69
|
-
|
|
75
|
+
const headers = res.headers;
|
|
76
|
+
const retryAfterMs = parseRetryAfter(headers.get("Retry-After"));
|
|
77
|
+
if (res.status === 401) {
|
|
78
|
+
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 });
|
|
79
|
+
}
|
|
70
80
|
if (res.status === 403) {
|
|
71
81
|
throw new ForbiddenError(
|
|
72
82
|
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
|
|
83
|
+
code,
|
|
84
|
+
{ headers, retryAfterMs }
|
|
74
85
|
);
|
|
75
86
|
}
|
|
76
|
-
throw new ApiError(res.status, text, code);
|
|
87
|
+
throw new ApiError(res.status, text, code, { headers, retryAfterMs });
|
|
77
88
|
}
|
|
78
89
|
};
|
|
90
|
+
async function readLimited(res, limit, op) {
|
|
91
|
+
const declared = Number(res.headers.get("content-length") ?? NaN);
|
|
92
|
+
if (declared > limit) {
|
|
93
|
+
await res.body?.cancel().catch(() => void 0);
|
|
94
|
+
throw new ResponseTooLargeError(op, limit);
|
|
95
|
+
}
|
|
96
|
+
if (!res.body) return new Uint8Array(0);
|
|
97
|
+
const reader = res.body.getReader();
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let total = 0;
|
|
100
|
+
for (; ; ) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) break;
|
|
103
|
+
total += value.byteLength;
|
|
104
|
+
if (total > limit) {
|
|
105
|
+
await reader.cancel().catch(() => void 0);
|
|
106
|
+
throw new ResponseTooLargeError(op, limit);
|
|
107
|
+
}
|
|
108
|
+
chunks.push(value);
|
|
109
|
+
}
|
|
110
|
+
const out = new Uint8Array(total);
|
|
111
|
+
let off = 0;
|
|
112
|
+
for (const c of chunks) {
|
|
113
|
+
out.set(c, off);
|
|
114
|
+
off += c.byteLength;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
79
118
|
function errorBody(text) {
|
|
80
119
|
try {
|
|
81
120
|
const v = JSON.parse(text);
|
|
@@ -90,6 +129,51 @@ function errorBody(text) {
|
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
131
|
|
|
132
|
+
// src/contract.ts
|
|
133
|
+
var RP_ROUTES = {
|
|
134
|
+
authNotificationEtsi: { method: "POST", path: "/authentication/notification/etsi/{semanticsIdentifier}" },
|
|
135
|
+
authNotificationDocument: { method: "POST", path: "/authentication/notification/document/{documentNumber}" },
|
|
136
|
+
authDeviceLinkAnonymous: { method: "POST", path: "/authentication/device-link/anonymous" },
|
|
137
|
+
authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
|
|
138
|
+
signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
|
|
139
|
+
signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
|
|
140
|
+
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
141
|
+
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
142
|
+
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
143
|
+
session: { method: "GET", path: "/session/{sessionId}" },
|
|
144
|
+
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
145
|
+
personDevices: { method: "GET", path: "/devices/etsi/{personEtsi}" },
|
|
146
|
+
personActivity: { method: "GET", path: "/rp/activity/etsi/{personEtsi}" },
|
|
147
|
+
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
148
|
+
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
149
|
+
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
150
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
151
|
+
};
|
|
152
|
+
var RP_ROUTES_UNDISCOVERED = {
|
|
153
|
+
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
154
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" },
|
|
155
|
+
pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
|
|
156
|
+
pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
|
|
157
|
+
pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
|
|
158
|
+
};
|
|
159
|
+
function routePath(route, ...args) {
|
|
160
|
+
let i = 0;
|
|
161
|
+
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
162
|
+
const a = args[i++];
|
|
163
|
+
if (a === void 0) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u0443\u0442\u0443\u0443`);
|
|
164
|
+
return encodeURIComponent(a);
|
|
165
|
+
});
|
|
166
|
+
if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
var OIDC_EXPECT = {
|
|
170
|
+
defaultScope: "openid",
|
|
171
|
+
pkceMethod: "S256",
|
|
172
|
+
idTokenAlg: "ES256",
|
|
173
|
+
/** `Authorization: Basic` (RFC 6749 §2.3.1). */
|
|
174
|
+
tokenEndpointAuthMethod: "client_secret_basic"
|
|
175
|
+
};
|
|
176
|
+
|
|
93
177
|
// src/crypto.ts
|
|
94
178
|
import { createHash, randomBytes } from "crypto";
|
|
95
179
|
function sha256Base64(data) {
|
|
@@ -192,25 +276,25 @@ var AuthApi = class {
|
|
|
192
276
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
193
277
|
async notificationByEtsi(id, interactions, opts) {
|
|
194
278
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
195
|
-
const raw = await this.http.post(
|
|
279
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationEtsi, id), body);
|
|
196
280
|
return this.parseNotification(raw, acsp);
|
|
197
281
|
}
|
|
198
282
|
/** Push — баримтын дугаараар (тодорхой төхөөрөмж рүү). */
|
|
199
283
|
async notificationByDocument(documentNumber, interactions, opts) {
|
|
200
284
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
201
|
-
const raw = await this.http.post(
|
|
285
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationDocument, documentNumber), body);
|
|
202
286
|
return this.parseNotification(raw, acsp);
|
|
203
287
|
}
|
|
204
288
|
/** QR/App2App — нэр томьёололгүй (иргэн хэн болохыг урьдчилж мэдэхгүй). QR код үүсгэхэд. */
|
|
205
289
|
async deviceLinkAnonymous(interactions, opts) {
|
|
206
290
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
207
|
-
const raw = await this.http.post(
|
|
291
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkAnonymous), body);
|
|
208
292
|
return this.parseDeviceLink(raw, acsp);
|
|
209
293
|
}
|
|
210
294
|
/** QR/App2App — тодорхой иргэн (РД/civil_id/ETSI). */
|
|
211
295
|
async deviceLinkByEtsi(id, interactions, opts) {
|
|
212
296
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
213
|
-
const raw = await this.http.post(
|
|
297
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkEtsi, id), body);
|
|
214
298
|
return this.parseDeviceLink(raw, acsp);
|
|
215
299
|
}
|
|
216
300
|
};
|
|
@@ -243,6 +327,7 @@ var SignApi = class {
|
|
|
243
327
|
};
|
|
244
328
|
if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
|
|
245
329
|
if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
|
|
330
|
+
if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
|
|
246
331
|
return body;
|
|
247
332
|
}
|
|
248
333
|
parse(raw) {
|
|
@@ -255,12 +340,12 @@ var SignApi = class {
|
|
|
255
340
|
* digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
|
|
256
341
|
*/
|
|
257
342
|
async digestByEtsi(id, digestB64, interactions, opts) {
|
|
258
|
-
const raw = await this.http.post(
|
|
343
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
|
|
259
344
|
return this.parse(raw);
|
|
260
345
|
}
|
|
261
346
|
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
|
|
262
347
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
263
|
-
const raw = await this.http.post(
|
|
348
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
|
|
264
349
|
return this.parse(raw);
|
|
265
350
|
}
|
|
266
351
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
@@ -301,9 +386,44 @@ function parseSessionResult(raw) {
|
|
|
301
386
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
302
387
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
303
388
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
304
|
-
pdf: parsePdf(r.pdf)
|
|
389
|
+
pdf: parsePdf(r.pdf),
|
|
390
|
+
person: parsePerson(r.person),
|
|
391
|
+
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
392
|
+
raw
|
|
305
393
|
};
|
|
306
394
|
}
|
|
395
|
+
var PERSON_STRINGS = [
|
|
396
|
+
"firstName",
|
|
397
|
+
"lastName",
|
|
398
|
+
"familyName",
|
|
399
|
+
"firstNameEn",
|
|
400
|
+
"lastNameEn",
|
|
401
|
+
"civilId",
|
|
402
|
+
"regNo",
|
|
403
|
+
"birthDate",
|
|
404
|
+
"gender"
|
|
405
|
+
];
|
|
406
|
+
function parsePerson(raw) {
|
|
407
|
+
const p = asRecord(raw);
|
|
408
|
+
const out = {};
|
|
409
|
+
for (const k of PERSON_STRINGS) if (typeof p[k] === "string" && p[k]) out[k] = p[k];
|
|
410
|
+
if (typeof p.geID === "number" && p.geID !== 0) out.geID = p.geID;
|
|
411
|
+
return Object.keys(out).length ? out : null;
|
|
412
|
+
}
|
|
413
|
+
function parseTokenCert(raw) {
|
|
414
|
+
const t = asRecord(raw);
|
|
415
|
+
const status = str(t.status);
|
|
416
|
+
if (!status) return null;
|
|
417
|
+
const out = { status };
|
|
418
|
+
if (t.errorCode) out.errorCode = String(t.errorCode);
|
|
419
|
+
if (t.authSerialHex) out.authSerialHex = String(t.authSerialHex);
|
|
420
|
+
if (t.signSerialHex) out.signSerialHex = String(t.signSerialHex);
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
function personMatchesCertificate(person, subject) {
|
|
424
|
+
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
425
|
+
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
426
|
+
}
|
|
307
427
|
function parsePdf(raw) {
|
|
308
428
|
const p = asRecord(raw);
|
|
309
429
|
const docID = str(p.docID);
|
|
@@ -357,7 +477,7 @@ var SessionApi = class {
|
|
|
357
477
|
/** Нэг удаагийн long-poll. serverPollMs хүртэл хүлээнэ. */
|
|
358
478
|
async poll(sessionId, serverPollMs = SERVER_POLL_MS) {
|
|
359
479
|
const raw = await this.http.get(
|
|
360
|
-
|
|
480
|
+
`${routePath(RP_ROUTES.session, sessionId)}?timeoutMs=${serverPollMs}`,
|
|
361
481
|
serverPollMs + 1e4
|
|
362
482
|
// HTTP timeout нь серверийн poll-оос урт
|
|
363
483
|
);
|
|
@@ -382,6 +502,8 @@ var SessionApi = class {
|
|
|
382
502
|
|
|
383
503
|
// src/pdf.ts
|
|
384
504
|
import { createHash as createHash2 } from "crypto";
|
|
505
|
+
var DEFAULT_PDF_MAX_BYTES = 64 << 20;
|
|
506
|
+
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
385
507
|
function rec3(v) {
|
|
386
508
|
return v && typeof v === "object" ? v : {};
|
|
387
509
|
}
|
|
@@ -394,7 +516,10 @@ var PdfApi = class {
|
|
|
394
516
|
http;
|
|
395
517
|
creds;
|
|
396
518
|
defaults;
|
|
397
|
-
/**
|
|
519
|
+
/**
|
|
520
|
+
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
521
|
+
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
522
|
+
*/
|
|
398
523
|
async prepare(input) {
|
|
399
524
|
const request = {
|
|
400
525
|
relyingPartyUUID: this.creds.rpUUID,
|
|
@@ -410,7 +535,7 @@ var PdfApi = class {
|
|
|
410
535
|
const form = new FormData();
|
|
411
536
|
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
412
537
|
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
413
|
-
const r = rec3(await this.http.postMultipart(
|
|
538
|
+
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
|
|
414
539
|
const receivedAt = Date.now();
|
|
415
540
|
const vc = rec3(r.vc).value;
|
|
416
541
|
const dl = rec3(r.deviceLink);
|
|
@@ -432,10 +557,80 @@ var PdfApi = class {
|
|
|
432
557
|
}
|
|
433
558
|
/**
|
|
434
559
|
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
435
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
|
|
560
|
+
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
561
|
+
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
562
|
+
* дараа дахин татна (`retryable`).
|
|
436
563
|
*/
|
|
437
564
|
document(sessionId) {
|
|
438
|
-
return this.http.getBytes(
|
|
565
|
+
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
|
|
566
|
+
}
|
|
567
|
+
get timeout() {
|
|
568
|
+
return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
|
|
569
|
+
}
|
|
570
|
+
get maxBytes() {
|
|
571
|
+
return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
|
|
575
|
+
* (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
|
|
576
|
+
* зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
|
|
577
|
+
* LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
|
|
578
|
+
* TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
|
|
579
|
+
*/
|
|
580
|
+
async tokenPrepare(input) {
|
|
581
|
+
if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
|
|
582
|
+
throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
583
|
+
}
|
|
584
|
+
const request = {
|
|
585
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
586
|
+
relyingPartyName: this.creds.rpName,
|
|
587
|
+
signer: { etsi: input.signerEtsi.trim() },
|
|
588
|
+
certificate: Buffer.from(input.certificate).toString("base64"),
|
|
589
|
+
fileName: input.fileName
|
|
590
|
+
};
|
|
591
|
+
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
592
|
+
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
593
|
+
const form = new FormData();
|
|
594
|
+
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
595
|
+
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
596
|
+
const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
|
|
597
|
+
const r = rec3(raw);
|
|
598
|
+
const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
|
|
599
|
+
if (!r.signingID || hashToSign.length !== 32) {
|
|
600
|
+
throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
signingId: String(r.signingID),
|
|
604
|
+
docId: String(r.docID ?? ""),
|
|
605
|
+
hashToSign,
|
|
606
|
+
hashAlgorithm: String(r.hashAlgorithm ?? ""),
|
|
607
|
+
signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
|
|
608
|
+
signatureFormat: String(r.signatureFormat ?? ""),
|
|
609
|
+
expiresAt: String(r.expiresAt ?? ""),
|
|
610
|
+
raw
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
|
|
615
|
+
* Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
|
|
616
|
+
* Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
|
|
617
|
+
* TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
|
|
618
|
+
*/
|
|
619
|
+
async tokenFinalize(signingId, signature) {
|
|
620
|
+
if (signature?.length !== 64) throw new Error("eid: pdf token: signature \u043D\u044C \u0442\u04AF\u04AF\u0445\u0438\u0439 r||s 64 \u0431\u0430\u0439\u0442 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 (P-256, DER \u0431\u0438\u0448)");
|
|
621
|
+
const r = await this.http.post(
|
|
622
|
+
routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
|
|
623
|
+
{ signature: Buffer.from(signature).toString("base64") },
|
|
624
|
+
this.timeout
|
|
625
|
+
);
|
|
626
|
+
return r;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
|
|
630
|
+
* хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
|
|
631
|
+
*/
|
|
632
|
+
tokenDocument(signingId) {
|
|
633
|
+
return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
|
|
439
634
|
}
|
|
440
635
|
/**
|
|
441
636
|
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
@@ -467,14 +662,54 @@ var OrganizationApi = class {
|
|
|
467
662
|
http;
|
|
468
663
|
/** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
|
|
469
664
|
async getRepresentations(personEtsi) {
|
|
470
|
-
const raw = await this.http.get(
|
|
665
|
+
const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
|
|
471
666
|
const list = Array.isArray(raw.representations) ? raw.representations : [];
|
|
472
|
-
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
|
|
667
|
+
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list, raw };
|
|
473
668
|
}
|
|
474
669
|
};
|
|
475
670
|
|
|
476
|
-
// src/
|
|
477
|
-
|
|
671
|
+
// src/person.ts
|
|
672
|
+
function rec4(v) {
|
|
673
|
+
return v && typeof v === "object" ? v : {};
|
|
674
|
+
}
|
|
675
|
+
var PersonApi = class {
|
|
676
|
+
constructor(http) {
|
|
677
|
+
this.http = http;
|
|
678
|
+
}
|
|
679
|
+
http;
|
|
680
|
+
async get(path) {
|
|
681
|
+
const raw = await this.http.get(path);
|
|
682
|
+
return { ...rec4(raw), raw };
|
|
683
|
+
}
|
|
684
|
+
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
685
|
+
certificates(personEtsi) {
|
|
686
|
+
return this.get(routePath(RP_ROUTES.personCertificates, personEtsi));
|
|
687
|
+
}
|
|
688
|
+
/** Иргэний холбоотой төхөөрөмжүүд (идэвхтэй + идэвхгүй). */
|
|
689
|
+
devices(personEtsi) {
|
|
690
|
+
return this.get(routePath(RP_ROUTES.personDevices, personEtsi));
|
|
691
|
+
}
|
|
692
|
+
/** Иргэний үйл ажиллагааны түүх. */
|
|
693
|
+
activity(personEtsi, query) {
|
|
694
|
+
const q = new URLSearchParams();
|
|
695
|
+
if (query?.flow) q.set("flow", query.flow);
|
|
696
|
+
if (query?.limit && query.limit > 0) q.set("limit", String(query.limit));
|
|
697
|
+
if (query?.offset && query.offset > 0) q.set("offset", String(query.offset));
|
|
698
|
+
const qs = q.toString();
|
|
699
|
+
return this.get(routePath(RP_ROUTES.personActivity, personEtsi) + (qs ? `?${qs}` : ""));
|
|
700
|
+
}
|
|
701
|
+
/** Самбарын нэгдсэн тоо. */
|
|
702
|
+
summary(personEtsi) {
|
|
703
|
+
return this.get(routePath(RP_ROUTES.personSummary, personEtsi));
|
|
704
|
+
}
|
|
705
|
+
/** Асран хамгаалж буй хүүхдүүд (зөвхөн унших). */
|
|
706
|
+
children(personEtsi) {
|
|
707
|
+
return this.get(routePath(RP_ROUTES.personChildren, personEtsi));
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/token.ts
|
|
712
|
+
import { X509Certificate as X509Certificate2, createHash as createHash3, createPublicKey, timingSafeEqual } from "crypto";
|
|
478
713
|
|
|
479
714
|
// src/subject.ts
|
|
480
715
|
import { X509Certificate } from "crypto";
|
|
@@ -580,8 +815,215 @@ function parseSubject(cert) {
|
|
|
580
815
|
out.name = full || out.commonName || null;
|
|
581
816
|
return out;
|
|
582
817
|
}
|
|
818
|
+
function certExtension(der, oid) {
|
|
819
|
+
return certExtensions(der).find((e) => e.oid === oid)?.value;
|
|
820
|
+
}
|
|
821
|
+
function certExtensions(der) {
|
|
822
|
+
const out = [];
|
|
823
|
+
const top = tlv(der, 0, der.length);
|
|
824
|
+
const tbs = tlv(der, top.start, top.end);
|
|
825
|
+
let f = tlv(der, tbs.start, tbs.end);
|
|
826
|
+
if (f.tag === 160) f = tlv(der, f.end, tbs.end);
|
|
827
|
+
for (let k = 0; k < 5; k++) f = tlv(der, f.end, tbs.end);
|
|
828
|
+
for (let pos = f.end; pos < tbs.end; ) {
|
|
829
|
+
const t = tlv(der, pos, tbs.end);
|
|
830
|
+
if (t.tag === 163) {
|
|
831
|
+
const seq = tlv(der, t.start, t.end);
|
|
832
|
+
for (let ep = seq.start; ep < seq.end; ) {
|
|
833
|
+
const ext = tlv(der, ep, seq.end);
|
|
834
|
+
const id = tlv(der, ext.start, ext.end);
|
|
835
|
+
let v = tlv(der, id.end, ext.end);
|
|
836
|
+
let critical = false;
|
|
837
|
+
if (v.tag === 1) {
|
|
838
|
+
critical = v.end > v.start && der[v.start] !== 0;
|
|
839
|
+
v = tlv(der, v.end, ext.end);
|
|
840
|
+
}
|
|
841
|
+
if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
|
|
842
|
+
out.push({ oid: oidOf(der, id), critical, value: der.subarray(v.start, v.end) });
|
|
843
|
+
ep = ext.end;
|
|
844
|
+
}
|
|
845
|
+
return out;
|
|
846
|
+
}
|
|
847
|
+
pos = t.end;
|
|
848
|
+
}
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/token.ts
|
|
853
|
+
function rec5(v) {
|
|
854
|
+
return v && typeof v === "object" ? v : {};
|
|
855
|
+
}
|
|
856
|
+
function hexSha256Equal(hex, data) {
|
|
857
|
+
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
858
|
+
return timingSafeEqual(createHash3("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
859
|
+
}
|
|
860
|
+
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
861
|
+
var TokenApi = class {
|
|
862
|
+
constructor(http, creds) {
|
|
863
|
+
this.http = http;
|
|
864
|
+
this.creds = creds;
|
|
865
|
+
}
|
|
866
|
+
http;
|
|
867
|
+
creds;
|
|
868
|
+
/**
|
|
869
|
+
* POST /token/certificate — токены AUTH + SIGN гэрчилгээ олголт эхлүүлнэ (иргэн утсаараа батална). CA-ийн буцаасан
|
|
870
|
+
* CSR hash-ууд илгээсэнтэй таарахгүй бол ValidationError.
|
|
871
|
+
*/
|
|
872
|
+
async issueCertificate(input) {
|
|
873
|
+
if (!input.signerEtsi?.trim() || !input.csrAuth?.length || !input.csrSign?.length || !input.tokenSerial?.trim()) {
|
|
874
|
+
throw new Error("token: signerEtsi, csrAuth, csrSign, tokenSerial \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
875
|
+
}
|
|
876
|
+
const body = {
|
|
877
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
878
|
+
relyingPartyName: this.creds.rpName,
|
|
879
|
+
signer: { etsi: input.signerEtsi.trim() },
|
|
880
|
+
csrAuth: Buffer.from(input.csrAuth).toString("base64"),
|
|
881
|
+
csrSign: Buffer.from(input.csrSign).toString("base64"),
|
|
882
|
+
tokenSerial: input.tokenSerial.trim(),
|
|
883
|
+
onBehalfOf: input.onBehalfOf?.trim() ?? "",
|
|
884
|
+
flow: "notification"
|
|
885
|
+
};
|
|
886
|
+
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
887
|
+
const r = rec5(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
888
|
+
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
889
|
+
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");
|
|
890
|
+
}
|
|
891
|
+
const vc = rec5(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
892
|
+
return {
|
|
893
|
+
sessionId: String(r.sessionID ?? ""),
|
|
894
|
+
vc: vc != null ? String(vc) : null,
|
|
895
|
+
csrAuthSha256: String(r.csrAuthSha256).toLowerCase(),
|
|
896
|
+
csrSignSha256: String(r.csrSignSha256).toLowerCase(),
|
|
897
|
+
...typeof r.expiresAt === "string" ? { expiresAt: r.expiresAt } : {}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* GET /token/certificate/{sessionID} — олгосон AUTH + SIGN гэрчилгээ + гинж. Токенд бичихээс өмнө
|
|
902
|
+
* {@link verifyTokenCertificates}-ээр CSR-ийн түлхүүртэй тулга.
|
|
903
|
+
*/
|
|
904
|
+
async certificateResult(sessionId) {
|
|
905
|
+
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
906
|
+
const r = rec5(raw);
|
|
907
|
+
const item = (v) => v && typeof v === "object" ? v : null;
|
|
908
|
+
return {
|
|
909
|
+
status: String(r.status ?? ""),
|
|
910
|
+
auth: item(r.auth),
|
|
911
|
+
sign: item(r.sign),
|
|
912
|
+
chain: Array.isArray(r.chain) ? r.chain.map(String) : [],
|
|
913
|
+
chainPem: String(r.chainPem ?? ""),
|
|
914
|
+
organization: r.organization && typeof r.organization === "object" ? r.organization : null,
|
|
915
|
+
keyMedium: String(r.keyMedium ?? ""),
|
|
916
|
+
certificateLevel: String(r.certificateLevel ?? ""),
|
|
917
|
+
raw
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
921
|
+
async authChallenge() {
|
|
922
|
+
const r = rec5(
|
|
923
|
+
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
924
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
925
|
+
relyingPartyName: this.creds.rpName
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
return {
|
|
929
|
+
challengeId: String(r.challengeId ?? ""),
|
|
930
|
+
challenge: String(r.challenge ?? ""),
|
|
931
|
+
rpUUID: String(r.rpUUID ?? ""),
|
|
932
|
+
expiresAt: String(r.expiresAt ?? "")
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* POST /token/auth/verify — токены AUTH гэрчилгээ (DER) + challenge дээрх түүхий r||s. CA гинж, төлөв, цуцлалт, гарын
|
|
937
|
+
* үсгийг шалгана; SDK нэмж endResult === OK ба identity.etsi === гэрчилгээний SERIALNUMBER-ийг тулгана (эс бөгөөс
|
|
938
|
+
* ValidationError). Challenge нэг удаагийн — алдааны дараа шинээр авна.
|
|
939
|
+
*/
|
|
940
|
+
async authVerify(challengeId, certDer, signature) {
|
|
941
|
+
let serial;
|
|
942
|
+
try {
|
|
943
|
+
serial = parseSubject(new X509Certificate2(certDer)).serialNumber;
|
|
944
|
+
} catch {
|
|
945
|
+
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");
|
|
946
|
+
}
|
|
947
|
+
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");
|
|
948
|
+
const raw = await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthVerify), {
|
|
949
|
+
challengeId,
|
|
950
|
+
certificate: Buffer.from(certDer).toString("base64"),
|
|
951
|
+
signature: Buffer.from(signature).toString("base64")
|
|
952
|
+
});
|
|
953
|
+
const r = rec5(raw);
|
|
954
|
+
if (rec5(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
955
|
+
const id = rec5(r.identity);
|
|
956
|
+
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");
|
|
957
|
+
const org = rec5(r.organization);
|
|
958
|
+
const cert = rec5(r.cert);
|
|
959
|
+
return {
|
|
960
|
+
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
961
|
+
serialHex: String(cert.serialHex ?? ""),
|
|
962
|
+
notAfter: String(cert.notAfter ?? ""),
|
|
963
|
+
organization: org.etsi ? { etsi: String(org.etsi), name: String(org.name ?? "") } : null,
|
|
964
|
+
raw
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
function csrSpki(csr) {
|
|
969
|
+
const top = tlv(csr, 0, csr.length);
|
|
970
|
+
const info = tlv(csr, top.start, top.end);
|
|
971
|
+
const version = tlv(csr, info.start, info.end);
|
|
972
|
+
const subject = tlv(csr, version.end, info.end);
|
|
973
|
+
const spki = tlv(csr, subject.end, info.end);
|
|
974
|
+
return createPublicKey({ key: Buffer.from(csr.subarray(subject.end, spki.end)), format: "der", type: "spki" }).export({
|
|
975
|
+
format: "der",
|
|
976
|
+
type: "spki"
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function verifyTokenCertificates(result, csrAuth, csrSign) {
|
|
980
|
+
if (!result?.auth || !result.sign) throw new ValidationError("token: auth/sign \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
981
|
+
for (const [name, item, csr] of [
|
|
982
|
+
["auth", result.auth, csrAuth],
|
|
983
|
+
["sign", result.sign, csrSign]
|
|
984
|
+
]) {
|
|
985
|
+
let want;
|
|
986
|
+
try {
|
|
987
|
+
want = csrSpki(csr);
|
|
988
|
+
} catch {
|
|
989
|
+
throw new Error(`token: ${name} CSR \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
990
|
+
}
|
|
991
|
+
let got;
|
|
992
|
+
try {
|
|
993
|
+
got = new X509Certificate2(Buffer.from(item.certificate, "base64")).publicKey.export({ format: "der", type: "spki" });
|
|
994
|
+
} catch {
|
|
995
|
+
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`);
|
|
996
|
+
}
|
|
997
|
+
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`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function tokenAuthDigest(rpUUID, challengeB64) {
|
|
1001
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(rpUUID)) {
|
|
1002
|
+
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");
|
|
1003
|
+
}
|
|
1004
|
+
const ch = Buffer.from(challengeB64, "base64");
|
|
1005
|
+
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
1006
|
+
return createHash3("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
1007
|
+
}
|
|
1008
|
+
function tokenLimitOf(err) {
|
|
1009
|
+
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
1010
|
+
let b;
|
|
1011
|
+
try {
|
|
1012
|
+
b = rec5(JSON.parse(err.body));
|
|
1013
|
+
} catch {
|
|
1014
|
+
return void 0;
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof b.limit !== "number" || !Number.isInteger(b.limit) || b.limit < 0) return void 0;
|
|
1017
|
+
if (b.kind === "PERSONAL") return { kind: "PERSONAL", limit: b.limit };
|
|
1018
|
+
if (b.kind === "ORGANIZATION" && typeof b.organization === "string" && b.organization.startsWith("NTRMN-")) {
|
|
1019
|
+
return { kind: "ORGANIZATION", limit: b.limit, organization: b.organization };
|
|
1020
|
+
}
|
|
1021
|
+
return void 0;
|
|
1022
|
+
}
|
|
583
1023
|
|
|
584
1024
|
// src/validator.ts
|
|
1025
|
+
import { X509Certificate as X509Certificate3, constants, createHash as createHash4, publicDecrypt, verify } from "crypto";
|
|
1026
|
+
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
585
1027
|
var ResponseValidator = class {
|
|
586
1028
|
anchors;
|
|
587
1029
|
intermediates;
|
|
@@ -590,14 +1032,28 @@ var ResponseValidator = class {
|
|
|
590
1032
|
allowUntrusted;
|
|
591
1033
|
revocation;
|
|
592
1034
|
revocationMode;
|
|
1035
|
+
allowedIssuers;
|
|
1036
|
+
checkKeyUsage;
|
|
1037
|
+
schemeName;
|
|
593
1038
|
constructor(trust = {}) {
|
|
594
|
-
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new
|
|
595
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
1039
|
+
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate3(p));
|
|
1040
|
+
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate3(p));
|
|
596
1041
|
this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
|
|
597
1042
|
this.skew = trust.clockSkewMs ?? 0;
|
|
598
1043
|
this.allowUntrusted = trust.allowUntrusted ?? false;
|
|
599
1044
|
this.revocation = trust.revocation;
|
|
600
1045
|
this.revocationMode = trust.revocationMode ?? "hard-fail";
|
|
1046
|
+
if (trust.allowedIssuerSkis?.length) {
|
|
1047
|
+
this.allowedIssuers = new Set(
|
|
1048
|
+
trust.allowedIssuerSkis.map((s) => {
|
|
1049
|
+
const ski = s.trim().replace(/:/g, "").toLowerCase();
|
|
1050
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(ski)) throw new Error(`ResponseValidator: allowedIssuerSkis: "${s}" hex key identifier \u0431\u0438\u0448`);
|
|
1051
|
+
return ski;
|
|
1052
|
+
})
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
this.checkKeyUsage = trust.checkKeyUsage ?? false;
|
|
1056
|
+
this.schemeName = trust.schemeName;
|
|
601
1057
|
}
|
|
602
1058
|
// ── нийтийн API ──
|
|
603
1059
|
/**
|
|
@@ -625,6 +1081,7 @@ var ResponseValidator = class {
|
|
|
625
1081
|
if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
|
|
626
1082
|
const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
|
|
627
1083
|
const payload = buildAcspV2Payload({
|
|
1084
|
+
...this.schemeName ? { schemeName: this.schemeName } : {},
|
|
628
1085
|
serverRandom,
|
|
629
1086
|
rpChallenge: acsp.rpChallenge,
|
|
630
1087
|
userChallenge,
|
|
@@ -728,9 +1185,10 @@ var ResponseValidator = class {
|
|
|
728
1185
|
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
1186
|
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
1187
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
731
|
-
const leaf = new
|
|
1188
|
+
const leaf = new X509Certificate3(Buffer.from(result.certificateDerB64, "base64"));
|
|
732
1189
|
this.checkValidity(leaf);
|
|
733
1190
|
this.checkLevel(String(result.certificateLevel ?? ""));
|
|
1191
|
+
this.checkPurpose(leaf, protocol);
|
|
734
1192
|
if (this.anchors.length === 0) {
|
|
735
1193
|
if (!this.allowUntrusted) {
|
|
736
1194
|
throw new ValidationError(
|
|
@@ -738,10 +1196,37 @@ var ResponseValidator = class {
|
|
|
738
1196
|
);
|
|
739
1197
|
}
|
|
740
1198
|
} else {
|
|
741
|
-
this.verifyChain(leaf);
|
|
1199
|
+
this.verifyChain(leaf, protocol === "ACSP_V2" ? EKU_CLIENT_AUTH : EKU_EMAIL_PROTECTION);
|
|
742
1200
|
}
|
|
1201
|
+
this.checkIssuer(leaf);
|
|
743
1202
|
return leaf;
|
|
744
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* checkKeyUsage үед гэрчилгээний зориулалт протоколд тохирох эсэх (CA crypto/ca_profile.go): ACSP_V2 (auth) → KU
|
|
1206
|
+
* digitalSignature + EKU clientAuth (эсвэл anyExtendedKeyUsage); RAW_DIGEST_SIGNATURE (sign) → KU contentCommitment.
|
|
1207
|
+
*/
|
|
1208
|
+
checkPurpose(leaf, protocol) {
|
|
1209
|
+
if (!this.checkKeyUsage) return;
|
|
1210
|
+
const ku = keyUsageBits(leaf);
|
|
1211
|
+
if (protocol === "ACSP_V2") {
|
|
1212
|
+
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");
|
|
1213
|
+
const eku = extKeyUsage(leaf);
|
|
1214
|
+
if (!eku.includes("1.3.6.1.5.5.7.3.2") && !eku.includes("2.5.29.37.0")) {
|
|
1215
|
+
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)");
|
|
1216
|
+
}
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
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");
|
|
1220
|
+
}
|
|
1221
|
+
/** allowedIssuerSkis үед leaf-ийн шууд issuer-ийн SKI (issuer-т алга бол leaf-ийн AKI) жагсаалтад байх ёстой. */
|
|
1222
|
+
checkIssuer(leaf) {
|
|
1223
|
+
if (!this.allowedIssuers) return;
|
|
1224
|
+
const issuer = this.findIssuer(leaf);
|
|
1225
|
+
const ski = (issuer && subjectKeyId(issuer)) ?? authorityKeyId(leaf);
|
|
1226
|
+
if (!ski || !this.allowedIssuers.has(ski)) {
|
|
1227
|
+
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)");
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
745
1230
|
checkValidity(cert) {
|
|
746
1231
|
const now = Date.now();
|
|
747
1232
|
const from = Date.parse(cert.validFrom);
|
|
@@ -755,21 +1240,40 @@ var ResponseValidator = class {
|
|
|
755
1240
|
const need = rank[this.requiredLevel] ?? 2;
|
|
756
1241
|
if (got < need) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u0432\u0448\u0438\u043D \u0445\u04AF\u0440\u044D\u043B\u0446\u044D\u0445\u0433\u04AF\u0439: ${level} < ${this.requiredLevel}`);
|
|
757
1242
|
}
|
|
758
|
-
/**
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1243
|
+
/**
|
|
1244
|
+
* RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
|
|
1245
|
+
* замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
|
|
1246
|
+
* гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
|
|
1247
|
+
* завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
|
|
1248
|
+
* бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
|
|
1249
|
+
* fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
|
|
1250
|
+
* хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
|
|
1251
|
+
*/
|
|
1252
|
+
verifyChain(leaf, usage) {
|
|
1253
|
+
const now = Math.min(Math.max(Date.now(), Date.parse(leaf.validFrom)), Date.parse(leaf.validTo));
|
|
1254
|
+
const leafReason = criticalReason(leaf) ?? ekuReason(leaf, usage);
|
|
1255
|
+
if (leafReason) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${leafReason}`);
|
|
1256
|
+
let reason;
|
|
1257
|
+
const extend = (cur, cas) => {
|
|
1258
|
+
if (cas.length >= 8) return false;
|
|
1259
|
+
for (const [cand, isAnchor] of [
|
|
1260
|
+
...this.anchors.map((c) => [c, true]),
|
|
1261
|
+
...this.intermediates.map((c) => [c, false])
|
|
1262
|
+
]) {
|
|
1263
|
+
if (cas.includes(cand) || !signedBy(cur, cand)) continue;
|
|
1264
|
+
const bad = caReason(cand, cas.length, now, usage);
|
|
1265
|
+
if (bad) {
|
|
1266
|
+
reason ??= bad;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (isAnchor || extend(cand, [...cas, cand])) return true;
|
|
765
1270
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439");
|
|
1271
|
+
return false;
|
|
1272
|
+
};
|
|
1273
|
+
if (extend(leaf, [])) return;
|
|
1274
|
+
throw new ValidationError(
|
|
1275
|
+
reason ? `\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${reason}` : "\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439"
|
|
1276
|
+
);
|
|
773
1277
|
}
|
|
774
1278
|
/** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
|
|
775
1279
|
findIssuer(leaf) {
|
|
@@ -784,14 +1288,23 @@ var ResponseValidator = class {
|
|
|
784
1288
|
* дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
|
|
785
1289
|
*/
|
|
786
1290
|
async checkRevocation(cert) {
|
|
787
|
-
|
|
1291
|
+
await this.checkRevocationStatus(cert);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* {@link checkRevocation} + юу шийдсэнийг буцаана (лог/аудитад): "good" | "unchecked" (unknown, soft-fail) |
|
|
1295
|
+
* "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
|
|
1296
|
+
*/
|
|
1297
|
+
async checkRevocationStatus(cert) {
|
|
1298
|
+
if (!this.revocation) return "skipped";
|
|
788
1299
|
const status = await this.revocation(cert, this.findIssuer(cert));
|
|
1300
|
+
if (status === "good") return "good";
|
|
789
1301
|
if (status === "revoked") {
|
|
790
1302
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
|
|
791
1303
|
}
|
|
792
|
-
if (
|
|
1304
|
+
if (this.revocationMode === "hard-fail") {
|
|
793
1305
|
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
1306
|
}
|
|
1307
|
+
return "unchecked";
|
|
795
1308
|
}
|
|
796
1309
|
};
|
|
797
1310
|
function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
@@ -806,7 +1319,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
806
1319
|
return fromResponse;
|
|
807
1320
|
}
|
|
808
1321
|
function userChallengeOf(verifier) {
|
|
809
|
-
return
|
|
1322
|
+
return createHash4("sha256").update(verifier, "utf8").digest("base64url");
|
|
810
1323
|
}
|
|
811
1324
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
812
1325
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -908,7 +1421,7 @@ function mgf1(hash, seed, length) {
|
|
|
908
1421
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
909
1422
|
const c = Buffer.alloc(4);
|
|
910
1423
|
c.writeUInt32BE(i, 0);
|
|
911
|
-
out.push(
|
|
1424
|
+
out.push(createHash4(nodeHash(hash)).update(seed).update(c).digest());
|
|
912
1425
|
}
|
|
913
1426
|
return Buffer.concat(out).subarray(0, length);
|
|
914
1427
|
}
|
|
@@ -932,12 +1445,118 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
932
1445
|
const psLen = emLen - hLen - sLen - 2;
|
|
933
1446
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
934
1447
|
const salt = db.subarray(psLen + 1);
|
|
935
|
-
const h2 =
|
|
1448
|
+
const h2 = createHash4(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
936
1449
|
return h.equals(h2);
|
|
937
1450
|
}
|
|
1451
|
+
function keyUsageBits(cert) {
|
|
1452
|
+
const v = certExtension(cert.raw, "2.5.29.15");
|
|
1453
|
+
if (!v) return 0;
|
|
1454
|
+
const bs = tlv(v, 0, v.length);
|
|
1455
|
+
if (bs.tag !== 3 || bs.end - bs.start < 2) return 0;
|
|
1456
|
+
return v[bs.start + 1];
|
|
1457
|
+
}
|
|
1458
|
+
function extKeyUsage(cert) {
|
|
1459
|
+
const v = certExtension(cert.raw, "2.5.29.37");
|
|
1460
|
+
if (!v) return [];
|
|
1461
|
+
const seq = tlv(v, 0, v.length);
|
|
1462
|
+
const out = [];
|
|
1463
|
+
for (let p = seq.start; p < seq.end; ) {
|
|
1464
|
+
const t = tlv(v, p, seq.end);
|
|
1465
|
+
out.push(oidOf(v, t));
|
|
1466
|
+
p = t.end;
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
function subjectKeyId(cert) {
|
|
1471
|
+
const v = certExtension(cert.raw, "2.5.29.14");
|
|
1472
|
+
if (!v) return void 0;
|
|
1473
|
+
const o = tlv(v, 0, v.length);
|
|
1474
|
+
return o.tag === 4 && o.end > o.start ? Buffer.from(v.subarray(o.start, o.end)).toString("hex") : void 0;
|
|
1475
|
+
}
|
|
1476
|
+
function authorityKeyId(cert) {
|
|
1477
|
+
const v = certExtension(cert.raw, "2.5.29.35");
|
|
1478
|
+
if (!v) return void 0;
|
|
1479
|
+
const seq = tlv(v, 0, v.length);
|
|
1480
|
+
if (seq.start >= seq.end) return void 0;
|
|
1481
|
+
const k = tlv(v, seq.start, seq.end);
|
|
1482
|
+
return k.tag === 128 && k.end > k.start ? Buffer.from(v.subarray(k.start, k.end)).toString("hex") : void 0;
|
|
1483
|
+
}
|
|
938
1484
|
function isCa(cert) {
|
|
939
1485
|
return cert.ca === true;
|
|
940
1486
|
}
|
|
1487
|
+
var EKU_ANY = "2.5.29.37.0";
|
|
1488
|
+
var EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
|
|
1489
|
+
var EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
|
|
1490
|
+
var OID_NAME_CONSTRAINTS = "2.5.29.30";
|
|
1491
|
+
var HANDLED_CRITICAL = /* @__PURE__ */ new Set([
|
|
1492
|
+
"2.5.29.14",
|
|
1493
|
+
// subjectKeyIdentifier
|
|
1494
|
+
"2.5.29.15",
|
|
1495
|
+
// keyUsage
|
|
1496
|
+
"2.5.29.17",
|
|
1497
|
+
// subjectAltName
|
|
1498
|
+
"2.5.29.19",
|
|
1499
|
+
// basicConstraints
|
|
1500
|
+
"2.5.29.30",
|
|
1501
|
+
// nameConstraints (CA-д — caReason fail-closed)
|
|
1502
|
+
"2.5.29.31",
|
|
1503
|
+
// cRLDistributionPoints
|
|
1504
|
+
"2.5.29.32",
|
|
1505
|
+
// certificatePolicies
|
|
1506
|
+
"2.5.29.35",
|
|
1507
|
+
// authorityKeyIdentifier
|
|
1508
|
+
"2.5.29.37",
|
|
1509
|
+
// extKeyUsage
|
|
1510
|
+
"1.3.6.1.5.5.7.1.1"
|
|
1511
|
+
// authorityInfoAccess
|
|
1512
|
+
]);
|
|
1513
|
+
function signedBy(child, cand) {
|
|
1514
|
+
if (child.issuer !== cand.subject) return false;
|
|
1515
|
+
const aki = authorityKeyId(child);
|
|
1516
|
+
const ski = subjectKeyId(cand);
|
|
1517
|
+
if (aki && ski && aki !== ski) return false;
|
|
1518
|
+
try {
|
|
1519
|
+
return child.verify(cand.publicKey);
|
|
1520
|
+
} catch {
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function criticalReason(cert) {
|
|
1525
|
+
return certExtensions(cert.raw).some((e) => e.critical && !HANDLED_CRITICAL.has(e.oid)) ? "\u04AF\u043B \u0442\u0430\u043D\u0438\u0445 critical extension" : void 0;
|
|
1526
|
+
}
|
|
1527
|
+
function ekuReason(cert, usage) {
|
|
1528
|
+
const eku = extKeyUsage(cert);
|
|
1529
|
+
return eku.length && !eku.includes(usage) && !eku.includes(EKU_ANY) ? "\u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 ExtKeyUsage \u044D\u043D\u044D \u04AF\u0439\u043B\u0434\u044D\u043B\u0434 \u0442\u043E\u0445\u0438\u0440\u043E\u0445\u0433\u04AF\u0439 (\u0433\u0438\u043D\u0436\u0438\u0439\u043D EKU)" : void 0;
|
|
1530
|
+
}
|
|
1531
|
+
function caReason(ca, below, now, usage) {
|
|
1532
|
+
if (!isCa(ca)) return "\u0437\u0430\u0432\u0441\u0440\u044B\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D CA \u0431\u0438\u0448 \u044D\u0441\u0432\u044D\u043B keyCertSign \u044D\u0440\u0445\u0433\u04AF\u0439";
|
|
1533
|
+
if (certExtension(ca.raw, "2.5.29.15") && !(keyUsageBits(ca) & 4)) return "CA-\u0438\u0439\u043D KeyUsage-\u0434 keyCertSign \u0430\u043B\u0433\u0430";
|
|
1534
|
+
const from = Date.parse(ca.validFrom);
|
|
1535
|
+
const to = Date.parse(ca.validTo);
|
|
1536
|
+
if (Number.isNaN(from) || Number.isNaN(to) || now < from || now > to) {
|
|
1537
|
+
return "\u0433\u0438\u043D\u0436\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D \u044D\u0441\u0432\u044D\u043B \u0445\u04AF\u0447\u0438\u043D\u0442\u044D\u0439 \u0431\u043E\u043B\u043E\u043E\u0433\u04AF\u0439";
|
|
1538
|
+
}
|
|
1539
|
+
const pathLen = basicConstraintsPathLen(ca);
|
|
1540
|
+
if (pathLen !== void 0 && below > pathLen) return "pathLenConstraint \u0437\u04E9\u0440\u0447\u0441\u04E9\u043D (\u0437\u0430\u0432\u0441\u0440\u044B\u043D CA \u0445\u044D\u0442 \u043E\u043B\u043E\u043D)";
|
|
1541
|
+
if (certExtension(ca.raw, OID_NAME_CONSTRAINTS)) return "CA-\u0434 name constraints \u0431\u0430\u0439\u043D\u0430 (SDK \u04AF\u043D\u044D\u043B\u0434\u044D\u0433\u0433\u04AF\u0439 \u2014 fail-closed)";
|
|
1542
|
+
return criticalReason(ca) ?? ekuReason(ca, usage);
|
|
1543
|
+
}
|
|
1544
|
+
function basicConstraintsPathLen(cert) {
|
|
1545
|
+
const v = certExtension(cert.raw, "2.5.29.19");
|
|
1546
|
+
if (!v) return void 0;
|
|
1547
|
+
const seq = tlv(v, 0, v.length);
|
|
1548
|
+
for (let p = seq.start; p < seq.end; ) {
|
|
1549
|
+
const t = tlv(v, p, seq.end);
|
|
1550
|
+
if (t.tag === 2) {
|
|
1551
|
+
if (t.end - t.start > 4 || (v[t.start] & 128) !== 0) throw new ValidationError("basicConstraints: pathLen \u0431\u0443\u0440\u0443\u0443");
|
|
1552
|
+
let n = 0;
|
|
1553
|
+
for (let i = t.start; i < t.end; i++) n = n * 256 + v[i];
|
|
1554
|
+
return n;
|
|
1555
|
+
}
|
|
1556
|
+
p = t.end;
|
|
1557
|
+
}
|
|
1558
|
+
return void 0;
|
|
1559
|
+
}
|
|
941
1560
|
|
|
942
1561
|
// src/client.ts
|
|
943
1562
|
var DEFAULT_BASE_URL = "https://rp.eidmongolia.mn";
|
|
@@ -962,6 +1581,8 @@ var EidClient = class {
|
|
|
962
1581
|
sign;
|
|
963
1582
|
pdf;
|
|
964
1583
|
organization;
|
|
1584
|
+
person;
|
|
1585
|
+
token;
|
|
965
1586
|
session;
|
|
966
1587
|
validator;
|
|
967
1588
|
constructor(cfg) {
|
|
@@ -977,8 +1598,14 @@ var EidClient = class {
|
|
|
977
1598
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
978
1599
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
979
1600
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
980
|
-
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1601
|
+
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1602
|
+
certificateLevel: level,
|
|
1603
|
+
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
1604
|
+
maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
|
|
1605
|
+
});
|
|
981
1606
|
this.organization = new OrganizationApi(http);
|
|
1607
|
+
this.person = new PersonApi(http);
|
|
1608
|
+
this.token = new TokenApi(http, cfg.credentials);
|
|
982
1609
|
this.session = new SessionApi(http);
|
|
983
1610
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
984
1611
|
}
|
|
@@ -1055,7 +1682,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1055
1682
|
}
|
|
1056
1683
|
|
|
1057
1684
|
// src/oidc.ts
|
|
1058
|
-
import { timingSafeEqual } from "crypto";
|
|
1685
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1059
1686
|
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
1060
1687
|
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
1061
1688
|
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
@@ -1092,7 +1719,7 @@ var EidOidcClient = class {
|
|
|
1092
1719
|
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1093
1720
|
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1094
1721
|
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1095
|
-
this.scope = cfg.scope ??
|
|
1722
|
+
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1096
1723
|
}
|
|
1097
1724
|
cfg;
|
|
1098
1725
|
issuer;
|
|
@@ -1140,7 +1767,7 @@ var EidOidcClient = class {
|
|
|
1140
1767
|
state,
|
|
1141
1768
|
nonce,
|
|
1142
1769
|
code_challenge: await pkceChallenge(codeVerifier),
|
|
1143
|
-
code_challenge_method:
|
|
1770
|
+
code_challenge_method: OIDC_EXPECT.pkceMethod
|
|
1144
1771
|
});
|
|
1145
1772
|
if (opts.display) p.set("display", opts.display);
|
|
1146
1773
|
u.search = p.toString();
|
|
@@ -1219,7 +1846,7 @@ var EidOidcClient = class {
|
|
|
1219
1846
|
const header = decodeJsonPart(parts[0]);
|
|
1220
1847
|
const payload = decodeJsonPart(parts[1]);
|
|
1221
1848
|
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1222
|
-
if (header.alg !==
|
|
1849
|
+
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1223
1850
|
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1224
1851
|
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1225
1852
|
const pick = (ks) => ks.find(
|
|
@@ -1312,7 +1939,7 @@ function formEncode(s) {
|
|
|
1312
1939
|
function timingSafeEqualStr(a, b) {
|
|
1313
1940
|
const x = Buffer.from(a, "utf8");
|
|
1314
1941
|
const y = Buffer.from(b, "utf8");
|
|
1315
|
-
return x.length === y.length &&
|
|
1942
|
+
return x.length === y.length && timingSafeEqual2(x, y);
|
|
1316
1943
|
}
|
|
1317
1944
|
function decodeJsonPart(s) {
|
|
1318
1945
|
try {
|
|
@@ -1334,7 +1961,10 @@ export {
|
|
|
1334
1961
|
ApiError,
|
|
1335
1962
|
AuthApi,
|
|
1336
1963
|
AuthenticationError,
|
|
1964
|
+
CITIZEN_ISSUING_CA_SKI,
|
|
1337
1965
|
DEFAULT_BASE_URL,
|
|
1966
|
+
DEFAULT_PDF_MAX_BYTES,
|
|
1967
|
+
DEFAULT_PDF_TIMEOUT_MS,
|
|
1338
1968
|
DeviceLinkError,
|
|
1339
1969
|
EID_ACR_ADVANCED,
|
|
1340
1970
|
EID_ACR_QUALIFIED,
|
|
@@ -1349,10 +1979,14 @@ export {
|
|
|
1349
1979
|
ON_BEHALF_ERROR_CODES,
|
|
1350
1980
|
OrganizationApi,
|
|
1351
1981
|
PdfApi,
|
|
1982
|
+
PersonApi,
|
|
1983
|
+
RETRYABLE_ERROR_CODES,
|
|
1984
|
+
ResponseTooLargeError,
|
|
1352
1985
|
ResponseValidator,
|
|
1353
1986
|
SessionApi,
|
|
1354
1987
|
SessionFailedError,
|
|
1355
1988
|
SignApi,
|
|
1989
|
+
TokenApi,
|
|
1356
1990
|
ValidationError,
|
|
1357
1991
|
acspV2Digest,
|
|
1358
1992
|
buildAcspV2Payload,
|
|
@@ -1361,14 +1995,19 @@ export {
|
|
|
1361
1995
|
interactionsToBase64,
|
|
1362
1996
|
isMobileUserAgent,
|
|
1363
1997
|
openOrShowQR,
|
|
1998
|
+
parseRetryAfter,
|
|
1364
1999
|
parseSessionResult,
|
|
1365
2000
|
parseSubject,
|
|
2001
|
+
personMatchesCertificate,
|
|
1366
2002
|
pkceChallenge,
|
|
1367
2003
|
qrDeviceLinkTicker,
|
|
1368
2004
|
randomChallenge,
|
|
1369
2005
|
resolveBaseUrl,
|
|
1370
2006
|
sha256Base64,
|
|
2007
|
+
tokenAuthDigest,
|
|
2008
|
+
tokenLimitOf,
|
|
1371
2009
|
userChallengeOf,
|
|
1372
2010
|
verifyPayloadSignature,
|
|
1373
|
-
verifyPrehashedSignature
|
|
2011
|
+
verifyPrehashedSignature,
|
|
2012
|
+
verifyTokenCertificates
|
|
1374
2013
|
};
|