@gerege-systems/eid-mongolia-sdk 0.14.2 → 0.15.1
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 +176 -54
- package/dist/{browser-CSZjH9Xs.d.cts → browser-8k_VDSP8.d.cts} +8 -183
- package/dist/{browser-CSZjH9Xs.d.ts → browser-BZ3hvv6-.d.ts} +8 -183
- package/dist/browser.d.cts +2 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +5 -3
- package/dist/button.cjs +55 -45
- package/dist/button.js +55 -45
- package/dist/chunk-REWNE7PO.js +51 -0
- package/dist/{chunk-DXVHOZO7.js → chunk-TB2KQH3W.js} +1 -46
- package/dist/errors-Z7SgLVTr.d.cts +149 -0
- package/dist/errors-Z7SgLVTr.d.ts +149 -0
- package/dist/index.cjs +270 -256
- package/dist/index.d.cts +169 -159
- package/dist/index.d.ts +169 -159
- package/dist/index.js +256 -241
- package/dist/pades.cjs +3520 -0
- package/dist/pades.d.cts +128 -0
- package/dist/pades.d.ts +128 -0
- package/dist/pades.js +3484 -0
- package/package.json +16 -6
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deviceLink,
|
|
3
|
+
isMobileUserAgent,
|
|
4
|
+
openOrShowQR
|
|
5
|
+
} from "./chunk-REWNE7PO.js";
|
|
1
6
|
import {
|
|
2
7
|
ApiError,
|
|
3
8
|
AuthenticationError,
|
|
@@ -13,12 +18,9 @@ import {
|
|
|
13
18
|
ResponseTooLargeError,
|
|
14
19
|
SessionFailedError,
|
|
15
20
|
ValidationError,
|
|
16
|
-
deviceLink,
|
|
17
|
-
isMobileUserAgent,
|
|
18
21
|
maskPath,
|
|
19
|
-
openOrShowQR,
|
|
20
22
|
parseRetryAfter
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TB2KQH3W.js";
|
|
22
24
|
|
|
23
25
|
// src/http.ts
|
|
24
26
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -34,15 +36,6 @@ var Http = class {
|
|
|
34
36
|
get(path, timeoutMs) {
|
|
35
37
|
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
36
38
|
}
|
|
37
|
-
/** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
|
|
38
|
-
postMultipart(path, form, timeoutMs) {
|
|
39
|
-
return this.json("POST", path, form, void 0, timeoutMs);
|
|
40
|
-
}
|
|
41
|
-
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
|
|
42
|
-
async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
|
|
43
|
-
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
44
|
-
return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
|
|
45
|
-
}
|
|
46
39
|
async json(method, path, body, contentType, timeoutMs) {
|
|
47
40
|
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
48
41
|
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
|
|
@@ -147,8 +140,6 @@ var RP_ROUTES = {
|
|
|
147
140
|
signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
|
|
148
141
|
certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
|
|
149
142
|
certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
|
|
150
|
-
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
151
|
-
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
152
143
|
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
153
144
|
session: { method: "GET", path: "/session/{sessionId}" },
|
|
154
145
|
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
@@ -157,15 +148,29 @@ var RP_ROUTES = {
|
|
|
157
148
|
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
158
149
|
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
159
150
|
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
160
|
-
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
151
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" },
|
|
152
|
+
pdfSignatures: { method: "POST", path: "/pdf/signatures" }
|
|
161
153
|
};
|
|
162
154
|
var RP_ROUTES_UNDISCOVERED = {
|
|
163
155
|
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
164
|
-
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
165
|
-
pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
|
|
166
|
-
pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
|
|
167
|
-
pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
|
|
156
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
168
157
|
};
|
|
158
|
+
var PDF_SIGNATURE_REGISTER_FIELDS = [
|
|
159
|
+
"docID",
|
|
160
|
+
"revIndex",
|
|
161
|
+
"medium",
|
|
162
|
+
"sessionID",
|
|
163
|
+
"cms",
|
|
164
|
+
"fileName",
|
|
165
|
+
"inSha256",
|
|
166
|
+
"pageSha256",
|
|
167
|
+
"sigRevSha256",
|
|
168
|
+
"outSha256",
|
|
169
|
+
"priorCmsSha256",
|
|
170
|
+
"priorRevisionSha256",
|
|
171
|
+
"onBehalfOf",
|
|
172
|
+
"signerEtsi"
|
|
173
|
+
];
|
|
169
174
|
function routePath(route, ...args) {
|
|
170
175
|
let i = 0;
|
|
171
176
|
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
@@ -328,6 +333,84 @@ var AuthApi = class {
|
|
|
328
333
|
};
|
|
329
334
|
|
|
330
335
|
// src/sign.ts
|
|
336
|
+
function registrationFromCompleted(done, extra) {
|
|
337
|
+
const out = {
|
|
338
|
+
docID: done.docId,
|
|
339
|
+
revIndex: done.revIndex,
|
|
340
|
+
cms: done.cms,
|
|
341
|
+
fileName: extra.fileName,
|
|
342
|
+
inSha256: done.inSha256,
|
|
343
|
+
sigRevSha256: done.sigRevSha256,
|
|
344
|
+
outSha256: done.outSha256
|
|
345
|
+
};
|
|
346
|
+
if (extra.medium) out.medium = extra.medium;
|
|
347
|
+
if (extra.sessionID) out.sessionID = extra.sessionID;
|
|
348
|
+
if (done.hasPage && done.pageSha256) out.pageSha256 = done.pageSha256;
|
|
349
|
+
if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
|
|
350
|
+
if (extra.priorRevisionSha256?.length) out.priorRevisionSha256 = extra.priorRevisionSha256;
|
|
351
|
+
if (extra.onBehalfOf) out.onBehalfOf = extra.onBehalfOf;
|
|
352
|
+
if (extra.signerEtsi) out.signerEtsi = extra.signerEtsi;
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
356
|
+
var MAX_CMS_BYTES = 28672;
|
|
357
|
+
function buildRegisterBody(input) {
|
|
358
|
+
const fail2 = (m) => {
|
|
359
|
+
throw new ValidationError(`registerSignature: ${m}`);
|
|
360
|
+
};
|
|
361
|
+
if (!UUID_RE.test(input.docID ?? "")) fail2("docID UUID \u0431\u0438\u0448");
|
|
362
|
+
if (!Number.isInteger(input.revIndex) || input.revIndex < 1 || input.revIndex > 16) fail2("revIndex 1\u201316");
|
|
363
|
+
const hex = (name, v) => {
|
|
364
|
+
const x = v?.trim().toLowerCase() ?? "";
|
|
365
|
+
if (!HEX64.test(x)) fail2(`${name} SHA-256 hex (64) \u0431\u0438\u0448`);
|
|
366
|
+
return x;
|
|
367
|
+
};
|
|
368
|
+
let cmsB64;
|
|
369
|
+
if (typeof input.cms === "string") {
|
|
370
|
+
const raw = Buffer.from(input.cms, "base64");
|
|
371
|
+
if (raw.length === 0 || raw.toString("base64") !== input.cms) fail2("cms \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 base64 \u0431\u0438\u0448");
|
|
372
|
+
cmsB64 = input.cms;
|
|
373
|
+
if (raw.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
374
|
+
} else {
|
|
375
|
+
if (!input.cms?.length) fail2("cms \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
376
|
+
if (input.cms.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
377
|
+
cmsB64 = Buffer.from(input.cms).toString("base64");
|
|
378
|
+
}
|
|
379
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
380
|
+
if (!fileName || Array.from(fileName).length > PDF_MAX_FILE_NAME) fail2(`fileName \u0437\u0430\u0430\u0432\u0430\u043B, \u2264 ${PDF_MAX_FILE_NAME} \u0442\u044D\u043C\u0434\u044D\u0433\u0442`);
|
|
381
|
+
const sessionID = input.sessionID?.trim() ?? "";
|
|
382
|
+
const medium = input.medium ?? (sessionID ? "phone" : "token");
|
|
383
|
+
if (medium !== "phone" && medium !== "token") fail2("medium: phone | token");
|
|
384
|
+
if (medium === "phone" && !sessionID) fail2("phone-\u0434 sessionID \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
385
|
+
const priorCms = (input.priorCmsSha256 ?? []).map((h, i) => hex(`priorCmsSha256[${i}]`, h));
|
|
386
|
+
const priorRev = (input.priorRevisionSha256 ?? []).map((h, i) => hex(`priorRevisionSha256[${i}]`, h));
|
|
387
|
+
if (priorCms.length + priorRev.length !== input.revIndex - 1) fail2("priorCmsSha256 + priorRevisionSha256-\u0438\u0439\u043D \u0442\u043E\u043E = revIndex \u2212 1");
|
|
388
|
+
if (input.revIndex === 1 && !input.pageSha256) fail2("revIndex 1-\u0434 pageSha256 \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
389
|
+
const body = { docID: input.docID, revIndex: input.revIndex, medium };
|
|
390
|
+
if (sessionID) body.sessionID = sessionID;
|
|
391
|
+
body.cms = cmsB64;
|
|
392
|
+
body.fileName = fileName;
|
|
393
|
+
body.inSha256 = hex("inSha256", input.inSha256);
|
|
394
|
+
if (input.pageSha256) body.pageSha256 = hex("pageSha256", input.pageSha256);
|
|
395
|
+
body.sigRevSha256 = hex("sigRevSha256", input.sigRevSha256);
|
|
396
|
+
body.outSha256 = hex("outSha256", input.outSha256);
|
|
397
|
+
if (priorCms.length) body.priorCmsSha256 = priorCms;
|
|
398
|
+
if (priorRev.length) body.priorRevisionSha256 = priorRev;
|
|
399
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
400
|
+
if (input.signerEtsi?.trim()) body.signerEtsi = input.signerEtsi.trim();
|
|
401
|
+
for (const k of Object.keys(body)) if (!PDF_SIGNATURE_REGISTER_FIELDS.includes(k)) fail2(`\u0434\u043E\u0442\u043E\u043E\u0434: \u04AF\u043B \u043C\u044D\u0434\u044D\u0433\u0434\u044D\u0445 \u0442\u0430\u043B\u0431\u0430\u0440 ${k}`);
|
|
402
|
+
return body;
|
|
403
|
+
}
|
|
404
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
405
|
+
var PDF_DISPLAY_TEXT = "\u0411\u0430\u0440\u0438\u043C\u0442\u0430\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0437\u0443\u0440\u0430\u0445: ";
|
|
406
|
+
var PDF_MAX_FILE_NAME = 120;
|
|
407
|
+
function dtbsBase64(v) {
|
|
408
|
+
const raw = typeof v === "string" ? Buffer.from(v, "base64") : Buffer.from(v);
|
|
409
|
+
if (raw.length !== 32 || typeof v === "string" && raw.toString("base64") !== v) {
|
|
410
|
+
throw new ValidationError("pdfDigest: dtbs \u043D\u044C 32 \u0431\u0430\u0439\u0442 (SHA-256) \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
411
|
+
}
|
|
412
|
+
return raw.toString("base64");
|
|
413
|
+
}
|
|
331
414
|
function rec2(v) {
|
|
332
415
|
return v && typeof v === "object" ? v : {};
|
|
333
416
|
}
|
|
@@ -412,6 +495,77 @@ var SignApi = class {
|
|
|
412
495
|
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
413
496
|
return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
|
|
414
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Клиент талын PAdES (`@gerege-systems/eid-mongolia-sdk/pades`): `prepare()`-ийн DTBS-д иргэнээр гарын үсэг зуруулна.
|
|
500
|
+
* PDF CA руу ОЧИХГҮЙ — зөвхөн 32 байт. CA-ийн өөрийн PAdES session-тэй ижил: `RAW_DIGEST_SIGNATURE`,
|
|
501
|
+
* `sha256WithRSAEncryption` (CMS-д PKCS#1 v1.5; legacy ECDSA иргэнд сервер `ecdsa-with-SHA256`), `displayTextAndPIN`
|
|
502
|
+
* «Баримтад гарын үсэг зурах: <fileName>» (60 тэмдэгтэд тайрна).
|
|
503
|
+
*
|
|
504
|
+
* Дараа нь `session.waitForResult(sessionId)` → `validator.validateSign(result, s.digest)` → `signature.value`-г
|
|
505
|
+
* (base64 → байт) `complete(state, signature)`-д.
|
|
506
|
+
*/
|
|
507
|
+
async pdfDigest(input) {
|
|
508
|
+
const digest = dtbsBase64(input.dtbs);
|
|
509
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
510
|
+
if (!fileName || Array.from(fileName).length > PDF_MAX_FILE_NAME) {
|
|
511
|
+
throw new ValidationError(`pdfDigest: fileName \u0437\u0430\u0430\u0432\u0430\u043B, \u2264 ${PDF_MAX_FILE_NAME} \u0442\u044D\u043C\u0434\u044D\u0433\u0442`);
|
|
512
|
+
}
|
|
513
|
+
const signer = input.signer;
|
|
514
|
+
const etsi = signer?.etsi?.trim();
|
|
515
|
+
const doc = signer?.documentNumber?.trim();
|
|
516
|
+
if (!etsi === !doc) throw new ValidationError("pdfDigest: signer \u2014 etsi \u044D\u0441\u0432\u044D\u043B documentNumber-\u0438\u0439\u043D \u044F\u0433 \u043D\u044D\u0433 \u043D\u044C");
|
|
517
|
+
const deviceLink2 = input.flow === "device-link";
|
|
518
|
+
const display = Array.from(PDF_DISPLAY_TEXT + fileName).slice(0, 60).join("");
|
|
519
|
+
const interactions = interactionsToBase64([{ type: "displayTextAndPIN", displayText60: display }]);
|
|
520
|
+
const body = {
|
|
521
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
522
|
+
relyingPartyName: this.creds.rpName,
|
|
523
|
+
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
524
|
+
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
525
|
+
signatureProtocolParameters: {
|
|
526
|
+
digest,
|
|
527
|
+
signatureAlgorithm: "sha256WithRSAEncryption",
|
|
528
|
+
signatureAlgorithmParameters: { hashAlgorithm: "SHA-256" }
|
|
529
|
+
},
|
|
530
|
+
interactions,
|
|
531
|
+
fileName
|
|
532
|
+
};
|
|
533
|
+
if (input.initialCallbackUrl?.trim()) body.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
534
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
535
|
+
const route = deviceLink2 ? etsi ? RP_ROUTES.signDeviceLinkEtsi : RP_ROUTES.signDeviceLinkDocument : etsi ? RP_ROUTES.signNotificationEtsi : RP_ROUTES.signNotificationDocument;
|
|
536
|
+
const r = rec2(await this.http.post(routePath(route, etsi ?? doc), body));
|
|
537
|
+
const receivedAt = Date.now();
|
|
538
|
+
const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
|
|
539
|
+
let dl = null;
|
|
540
|
+
if (deviceLink2) {
|
|
541
|
+
if (!r.sessionToken || !r.sessionSecret || !r.deviceLinkBase) {
|
|
542
|
+
throw new ValidationError("pdfDigest: device-link \u0445\u0430\u0440\u0438\u0443\u043D\u0434 sessionToken/sessionSecret/deviceLinkBase \u0430\u043B\u0433\u0430");
|
|
543
|
+
}
|
|
544
|
+
dl = { sessionToken: String(r.sessionToken), sessionSecret: String(r.sessionSecret), deviceLinkBase: String(r.deviceLinkBase) };
|
|
545
|
+
}
|
|
546
|
+
return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, digest, interactions, deviceLink: dl, receivedAt };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* `POST /pdf/signatures` — `pades.complete()`-ийн дараа CMS-ийг CA-д бүртгэнэ (`/verify/{docID}`, иргэний түүх). PDF
|
|
550
|
+
* илгээгдэхгүй. SIGN эрх. CA CMS-ийг шалгана: 04-ийн SIGN cert, session digest = SHA-256(signedAttrs), claimedRole ↔
|
|
551
|
+
* төлөөлөл. Алдаа: 403 `REPRESENTATION_REVOKED`, 409 `CLAIMED_ROLE_MISMATCH` / `REGISTRY_CONFLICT` /
|
|
552
|
+
* `DOC_ID_CONFLICT` / `PDF_FOREIGN_SIGNATURE` (`ApiError.code`). Бүртгэлгүй файл ч Adobe/EU DSS-д хүчинтэй — зөвхөн
|
|
553
|
+
* `/verify` «бүртгэлгүй» гэнэ.
|
|
554
|
+
*/
|
|
555
|
+
async registerSignature(input) {
|
|
556
|
+
return this.registerBody(buildRegisterBody(input));
|
|
557
|
+
}
|
|
558
|
+
async registerBody(body) {
|
|
559
|
+
const raw = await this.http.post(routePath(RP_ROUTES.pdfSignatures), body);
|
|
560
|
+
const r = rec2(raw);
|
|
561
|
+
return {
|
|
562
|
+
docID: String(r.docID ?? body.docID),
|
|
563
|
+
revIndex: Number(r.revIndex ?? 0),
|
|
564
|
+
registeredAt: typeof r.registeredAt === "string" ? r.registeredAt : "",
|
|
565
|
+
verifyUrl: String(r.verifyUrl ?? ""),
|
|
566
|
+
raw
|
|
567
|
+
};
|
|
568
|
+
}
|
|
415
569
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
416
570
|
async textByEtsi(id, text, interactions, opts) {
|
|
417
571
|
return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
|
|
@@ -527,7 +681,6 @@ function parseSessionResult(raw) {
|
|
|
527
681
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
528
682
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
529
683
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
530
|
-
pdf: parsePdf(r.pdf),
|
|
531
684
|
person: parsePerson(r.person),
|
|
532
685
|
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
533
686
|
raw
|
|
@@ -565,28 +718,6 @@ function personMatchesCertificate(person, subject) {
|
|
|
565
718
|
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
566
719
|
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
567
720
|
}
|
|
568
|
-
function parsePdf(raw) {
|
|
569
|
-
const p = asRecord(raw);
|
|
570
|
-
const docID = str(p.docID);
|
|
571
|
-
if (!docID) return null;
|
|
572
|
-
const out = { docID, documentStatus: String(p.documentStatus ?? "") };
|
|
573
|
-
if (p.errorCode != null) out.errorCode = String(p.errorCode);
|
|
574
|
-
if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
|
|
575
|
-
if (p.size != null) out.size = Number(p.size);
|
|
576
|
-
if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
|
|
577
|
-
if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
|
|
578
|
-
const sg = asRecord(p.signer);
|
|
579
|
-
if (sg.etsi != null) {
|
|
580
|
-
out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
|
|
581
|
-
if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
|
|
582
|
-
if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
|
|
583
|
-
}
|
|
584
|
-
const v = asRecord(p.validation);
|
|
585
|
-
if (v.indication != null) {
|
|
586
|
-
out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
|
|
587
|
-
}
|
|
588
|
-
return out;
|
|
589
|
-
}
|
|
590
721
|
function parseSigParams(raw) {
|
|
591
722
|
const p = asRecord(raw);
|
|
592
723
|
const hash = str(p.hashAlgorithm);
|
|
@@ -645,160 +776,6 @@ var SessionApi = class {
|
|
|
645
776
|
}
|
|
646
777
|
};
|
|
647
778
|
|
|
648
|
-
// src/pdf.ts
|
|
649
|
-
import { createHash as createHash2 } from "crypto";
|
|
650
|
-
var DEFAULT_PDF_MAX_BYTES = 64 << 20;
|
|
651
|
-
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
652
|
-
function rec3(v) {
|
|
653
|
-
return v && typeof v === "object" ? v : {};
|
|
654
|
-
}
|
|
655
|
-
var PdfApi = class {
|
|
656
|
-
constructor(http, creds, defaults) {
|
|
657
|
-
this.http = http;
|
|
658
|
-
this.creds = creds;
|
|
659
|
-
this.defaults = defaults;
|
|
660
|
-
}
|
|
661
|
-
http;
|
|
662
|
-
creds;
|
|
663
|
-
defaults;
|
|
664
|
-
/**
|
|
665
|
-
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
666
|
-
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
667
|
-
*/
|
|
668
|
-
async prepare(input) {
|
|
669
|
-
const request = {
|
|
670
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
671
|
-
relyingPartyName: this.creds.rpName,
|
|
672
|
-
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
673
|
-
signer: input.signer,
|
|
674
|
-
flow: input.flow ?? "notification",
|
|
675
|
-
fileName: input.fileName
|
|
676
|
-
};
|
|
677
|
-
if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
678
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
679
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
680
|
-
const form = new FormData();
|
|
681
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
682
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
683
|
-
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
|
|
684
|
-
const receivedAt = Date.now();
|
|
685
|
-
const vc = rec3(r.vc).value;
|
|
686
|
-
const dl = rec3(r.deviceLink);
|
|
687
|
-
const digest = typeof r.digest === "string" && r.digest ? r.digest : typeof dl.digest === "string" && dl.digest ? dl.digest : void 0;
|
|
688
|
-
return {
|
|
689
|
-
sessionId: String(r.sessionID),
|
|
690
|
-
docId: String(r.docID),
|
|
691
|
-
vc: vc != null ? String(vc) : null,
|
|
692
|
-
...digest !== void 0 ? { digest } : {},
|
|
693
|
-
deviceLink: dl.sessionToken ? {
|
|
694
|
-
sessionToken: String(dl.sessionToken),
|
|
695
|
-
sessionSecret: String(dl.sessionSecret),
|
|
696
|
-
deviceLinkBase: String(dl.deviceLinkBase),
|
|
697
|
-
digest: String(dl.digest),
|
|
698
|
-
interactions: String(dl.interactions)
|
|
699
|
-
} : null,
|
|
700
|
-
receivedAt
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
705
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
706
|
-
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
707
|
-
* дараа дахин татна (`retryable`).
|
|
708
|
-
*/
|
|
709
|
-
document(sessionId) {
|
|
710
|
-
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
|
|
711
|
-
}
|
|
712
|
-
get timeout() {
|
|
713
|
-
return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
|
|
714
|
-
}
|
|
715
|
-
get maxBytes() {
|
|
716
|
-
return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
|
|
720
|
-
* (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
|
|
721
|
-
* зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
|
|
722
|
-
* LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
|
|
723
|
-
* TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
|
|
724
|
-
*/
|
|
725
|
-
async tokenPrepare(input) {
|
|
726
|
-
if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
|
|
727
|
-
throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
728
|
-
}
|
|
729
|
-
const request = {
|
|
730
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
731
|
-
relyingPartyName: this.creds.rpName,
|
|
732
|
-
signer: { etsi: input.signerEtsi.trim() },
|
|
733
|
-
certificate: Buffer.from(input.certificate).toString("base64"),
|
|
734
|
-
fileName: input.fileName
|
|
735
|
-
};
|
|
736
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
737
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
738
|
-
const form = new FormData();
|
|
739
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
740
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
741
|
-
const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
|
|
742
|
-
const r = rec3(raw);
|
|
743
|
-
const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
|
|
744
|
-
if (!r.signingID || hashToSign.length !== 32) {
|
|
745
|
-
throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
|
|
746
|
-
}
|
|
747
|
-
return {
|
|
748
|
-
signingId: String(r.signingID),
|
|
749
|
-
docId: String(r.docID ?? ""),
|
|
750
|
-
hashToSign,
|
|
751
|
-
hashAlgorithm: String(r.hashAlgorithm ?? ""),
|
|
752
|
-
signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
|
|
753
|
-
signatureFormat: String(r.signatureFormat ?? ""),
|
|
754
|
-
expiresAt: String(r.expiresAt ?? ""),
|
|
755
|
-
raw
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
|
|
760
|
-
* Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
|
|
761
|
-
* Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
|
|
762
|
-
* TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
|
|
763
|
-
*/
|
|
764
|
-
async tokenFinalize(signingId, signature) {
|
|
765
|
-
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)");
|
|
766
|
-
const r = await this.http.post(
|
|
767
|
-
routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
|
|
768
|
-
{ signature: Buffer.from(signature).toString("base64") },
|
|
769
|
-
this.timeout
|
|
770
|
-
);
|
|
771
|
-
return r;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
|
|
775
|
-
* хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
|
|
776
|
-
*/
|
|
777
|
-
tokenDocument(signingId) {
|
|
778
|
-
return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
782
|
-
* `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
|
|
783
|
-
*
|
|
784
|
-
* @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
|
|
785
|
-
*/
|
|
786
|
-
async documentVerified(sessionId, expected) {
|
|
787
|
-
let want;
|
|
788
|
-
if (typeof expected === "string") {
|
|
789
|
-
want = expected;
|
|
790
|
-
} else {
|
|
791
|
-
if (expected.documentStatus !== "READY") throw new ValidationError(`pdf.documentStatus ${expected.documentStatus} \u2260 READY`);
|
|
792
|
-
want = expected.outSha256;
|
|
793
|
-
}
|
|
794
|
-
if (!want || !/^[0-9a-fA-F]{64}$/.test(want)) throw new ValidationError("pdf.outSha256 \u0430\u043B\u0433\u0430 \u044D\u0441\u0432\u044D\u043B SHA-256 hex \u0431\u0438\u0448");
|
|
795
|
-
const bytes = await this.document(sessionId);
|
|
796
|
-
const got = createHash2("sha256").update(bytes).digest("hex");
|
|
797
|
-
if (got !== want.toLowerCase()) throw new ValidationError("\u0422\u0430\u0442\u0441\u0430\u043D PDF-\u0438\u0439\u043D SHA-256 \u043D\u044C pdf.outSha256-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
798
|
-
return bytes;
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
|
|
802
779
|
// src/organization.ts
|
|
803
780
|
var OrganizationApi = class {
|
|
804
781
|
constructor(http) {
|
|
@@ -814,7 +791,7 @@ var OrganizationApi = class {
|
|
|
814
791
|
};
|
|
815
792
|
|
|
816
793
|
// src/person.ts
|
|
817
|
-
function
|
|
794
|
+
function rec3(v) {
|
|
818
795
|
return v && typeof v === "object" ? v : {};
|
|
819
796
|
}
|
|
820
797
|
var PersonApi = class {
|
|
@@ -824,7 +801,7 @@ var PersonApi = class {
|
|
|
824
801
|
http;
|
|
825
802
|
async get(path) {
|
|
826
803
|
const raw = await this.http.get(path);
|
|
827
|
-
return { ...
|
|
804
|
+
return { ...rec3(raw), raw };
|
|
828
805
|
}
|
|
829
806
|
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
830
807
|
certificates(personEtsi) {
|
|
@@ -854,7 +831,7 @@ var PersonApi = class {
|
|
|
854
831
|
};
|
|
855
832
|
|
|
856
833
|
// src/token.ts
|
|
857
|
-
import { X509Certificate as X509Certificate2, createHash as
|
|
834
|
+
import { X509Certificate as X509Certificate2, createHash as createHash2, createPublicKey, timingSafeEqual } from "crypto";
|
|
858
835
|
|
|
859
836
|
// src/subject.ts
|
|
860
837
|
import { X509Certificate } from "crypto";
|
|
@@ -995,12 +972,12 @@ function certExtensions(der) {
|
|
|
995
972
|
}
|
|
996
973
|
|
|
997
974
|
// src/token.ts
|
|
998
|
-
function
|
|
975
|
+
function rec4(v) {
|
|
999
976
|
return v && typeof v === "object" ? v : {};
|
|
1000
977
|
}
|
|
1001
978
|
function hexSha256Equal(hex, data) {
|
|
1002
979
|
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
1003
|
-
return timingSafeEqual(
|
|
980
|
+
return timingSafeEqual(createHash2("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
1004
981
|
}
|
|
1005
982
|
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
1006
983
|
var TokenApi = class {
|
|
@@ -1029,11 +1006,11 @@ var TokenApi = class {
|
|
|
1029
1006
|
flow: "notification"
|
|
1030
1007
|
};
|
|
1031
1008
|
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
1032
|
-
const r =
|
|
1009
|
+
const r = rec4(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
1033
1010
|
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
1034
1011
|
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");
|
|
1035
1012
|
}
|
|
1036
|
-
const vc =
|
|
1013
|
+
const vc = rec4(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
1037
1014
|
return {
|
|
1038
1015
|
sessionId: String(r.sessionID ?? ""),
|
|
1039
1016
|
vc: vc != null ? String(vc) : null,
|
|
@@ -1048,7 +1025,7 @@ var TokenApi = class {
|
|
|
1048
1025
|
*/
|
|
1049
1026
|
async certificateResult(sessionId) {
|
|
1050
1027
|
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
1051
|
-
const r =
|
|
1028
|
+
const r = rec4(raw);
|
|
1052
1029
|
const item = (v) => v && typeof v === "object" ? v : null;
|
|
1053
1030
|
return {
|
|
1054
1031
|
status: String(r.status ?? ""),
|
|
@@ -1064,7 +1041,7 @@ var TokenApi = class {
|
|
|
1064
1041
|
}
|
|
1065
1042
|
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
1066
1043
|
async authChallenge() {
|
|
1067
|
-
const r =
|
|
1044
|
+
const r = rec4(
|
|
1068
1045
|
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
1069
1046
|
relyingPartyUUID: this.creds.rpUUID,
|
|
1070
1047
|
relyingPartyName: this.creds.rpName
|
|
@@ -1095,12 +1072,12 @@ var TokenApi = class {
|
|
|
1095
1072
|
certificate: Buffer.from(certDer).toString("base64"),
|
|
1096
1073
|
signature: Buffer.from(signature).toString("base64")
|
|
1097
1074
|
});
|
|
1098
|
-
const r =
|
|
1099
|
-
if (
|
|
1100
|
-
const id =
|
|
1075
|
+
const r = rec4(raw);
|
|
1076
|
+
if (rec4(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
1077
|
+
const id = rec4(r.identity);
|
|
1101
1078
|
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");
|
|
1102
|
-
const org =
|
|
1103
|
-
const cert =
|
|
1079
|
+
const org = rec4(r.organization);
|
|
1080
|
+
const cert = rec4(r.cert);
|
|
1104
1081
|
return {
|
|
1105
1082
|
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
1106
1083
|
serialHex: String(cert.serialHex ?? ""),
|
|
@@ -1148,13 +1125,13 @@ function tokenAuthDigest(rpUUID, challengeB64) {
|
|
|
1148
1125
|
}
|
|
1149
1126
|
const ch = Buffer.from(challengeB64, "base64");
|
|
1150
1127
|
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
1151
|
-
return
|
|
1128
|
+
return createHash2("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
1152
1129
|
}
|
|
1153
1130
|
function tokenLimitOf(err) {
|
|
1154
1131
|
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
1155
1132
|
let b;
|
|
1156
1133
|
try {
|
|
1157
|
-
b =
|
|
1134
|
+
b = rec4(JSON.parse(err.body));
|
|
1158
1135
|
} catch {
|
|
1159
1136
|
return void 0;
|
|
1160
1137
|
}
|
|
@@ -1167,7 +1144,7 @@ function tokenLimitOf(err) {
|
|
|
1167
1144
|
}
|
|
1168
1145
|
|
|
1169
1146
|
// src/validator.ts
|
|
1170
|
-
import { X509Certificate as X509Certificate3, constants, createHash as
|
|
1147
|
+
import { X509Certificate as X509Certificate3, constants, createHash as createHash3, publicDecrypt, verify } from "crypto";
|
|
1171
1148
|
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
1172
1149
|
var ResponseValidator = class {
|
|
1173
1150
|
anchors;
|
|
@@ -1497,7 +1474,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
1497
1474
|
return fromResponse;
|
|
1498
1475
|
}
|
|
1499
1476
|
function userChallengeOf(verifier) {
|
|
1500
|
-
return
|
|
1477
|
+
return createHash3("sha256").update(verifier, "utf8").digest("base64url");
|
|
1501
1478
|
}
|
|
1502
1479
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
1503
1480
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -1599,7 +1576,7 @@ function mgf1(hash, seed, length) {
|
|
|
1599
1576
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
1600
1577
|
const c = Buffer.alloc(4);
|
|
1601
1578
|
c.writeUInt32BE(i, 0);
|
|
1602
|
-
out.push(
|
|
1579
|
+
out.push(createHash3(nodeHash(hash)).update(seed).update(c).digest());
|
|
1603
1580
|
}
|
|
1604
1581
|
return Buffer.concat(out).subarray(0, length);
|
|
1605
1582
|
}
|
|
@@ -1623,7 +1600,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
1623
1600
|
const psLen = emLen - hLen - sLen - 2;
|
|
1624
1601
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
1625
1602
|
const salt = db.subarray(psLen + 1);
|
|
1626
|
-
const h2 =
|
|
1603
|
+
const h2 = createHash3(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
1627
1604
|
return h.equals(h2);
|
|
1628
1605
|
}
|
|
1629
1606
|
function keyUsageBits(cert) {
|
|
@@ -1757,7 +1734,6 @@ function resolveBaseUrl(baseUrl) {
|
|
|
1757
1734
|
var EidClient = class {
|
|
1758
1735
|
auth;
|
|
1759
1736
|
sign;
|
|
1760
|
-
pdf;
|
|
1761
1737
|
organization;
|
|
1762
1738
|
person;
|
|
1763
1739
|
token;
|
|
@@ -1779,11 +1755,6 @@ var EidClient = class {
|
|
|
1779
1755
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1780
1756
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1781
1757
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
|
|
1782
|
-
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1783
|
-
certificateLevel: level,
|
|
1784
|
-
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
1785
|
-
maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
|
|
1786
|
-
});
|
|
1787
1758
|
this.organization = new OrganizationApi(http);
|
|
1788
1759
|
this.person = new PersonApi(http);
|
|
1789
1760
|
this.token = new TokenApi(http, cfg.credentials);
|
|
@@ -1791,6 +1762,14 @@ var EidClient = class {
|
|
|
1791
1762
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1792
1763
|
this.rpName = cfg.credentials.rpName;
|
|
1793
1764
|
}
|
|
1765
|
+
/** {@link SignApi.pdfDigest} — клиент талын PAdES-ийн DTBS-д гарын үсгийн session (PDF CA руу очихгүй). */
|
|
1766
|
+
signPdfDigest(input) {
|
|
1767
|
+
return this.sign.pdfDigest(input);
|
|
1768
|
+
}
|
|
1769
|
+
/** {@link SignApi.registerSignature} — `pades.complete()`-ийн CMS-ийг CA-д бүртгэнэ (`POST /pdf/signatures`). */
|
|
1770
|
+
registerSignature(input) {
|
|
1771
|
+
return this.sign.registerSignature(input);
|
|
1772
|
+
}
|
|
1794
1773
|
};
|
|
1795
1774
|
|
|
1796
1775
|
// src/devicelinkv3.ts
|
|
@@ -1895,7 +1874,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1895
1874
|
}
|
|
1896
1875
|
|
|
1897
1876
|
// src/login.ts
|
|
1898
|
-
import { createHash as
|
|
1877
|
+
import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1899
1878
|
function memoryLoginStore() {
|
|
1900
1879
|
const m = /* @__PURE__ */ new Map();
|
|
1901
1880
|
return {
|
|
@@ -1920,6 +1899,51 @@ function memoryLoginStore() {
|
|
|
1920
1899
|
}
|
|
1921
1900
|
};
|
|
1922
1901
|
}
|
|
1902
|
+
var defaultRateLimit = Object.freeze({
|
|
1903
|
+
push: Object.freeze({ limit: 3, windowMs: 6e4 })
|
|
1904
|
+
});
|
|
1905
|
+
var RATE_KINDS = ["start", "push"];
|
|
1906
|
+
function windowLimiter(limit, windowMs) {
|
|
1907
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1908
|
+
return (key) => {
|
|
1909
|
+
const now = Date.now();
|
|
1910
|
+
const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
1911
|
+
if (hits.size > 1e4) hits.clear();
|
|
1912
|
+
if (h.length >= limit) {
|
|
1913
|
+
hits.set(key, h);
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
h.push(now);
|
|
1917
|
+
hits.set(key, h);
|
|
1918
|
+
return true;
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function buildRateLimit(opt) {
|
|
1922
|
+
const extra = typeof opt === "function" ? opt : void 0;
|
|
1923
|
+
const user = opt && typeof opt === "object" ? opt : {};
|
|
1924
|
+
for (const k of Object.keys(user)) {
|
|
1925
|
+
if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
|
|
1926
|
+
}
|
|
1927
|
+
const merged = { ...defaultRateLimit, ...user };
|
|
1928
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
1929
|
+
for (const k of RATE_KINDS) {
|
|
1930
|
+
const r = merged[k];
|
|
1931
|
+
if (r === void 0 || r === false) continue;
|
|
1932
|
+
if (typeof r === "function") {
|
|
1933
|
+
perKind.set(k, r);
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
|
|
1937
|
+
throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
|
|
1938
|
+
}
|
|
1939
|
+
perKind.set(k, windowLimiter(r.limit, r.windowMs));
|
|
1940
|
+
}
|
|
1941
|
+
return async (key) => {
|
|
1942
|
+
const fn = perKind.get(key.slice(0, key.indexOf(":")));
|
|
1943
|
+
if (fn && !await fn(key)) return false;
|
|
1944
|
+
return extra ? extra(key) : true;
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1923
1947
|
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1924
1948
|
function forwardedClientIp(headers, trustProxy) {
|
|
1925
1949
|
if (trustProxy === false || trustProxy === 0) return void 0;
|
|
@@ -1932,7 +1956,7 @@ function forwardedClientIp(headers, trustProxy) {
|
|
|
1932
1956
|
return header(headers, "x-real-ip").trim() || void 0;
|
|
1933
1957
|
}
|
|
1934
1958
|
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1935
|
-
var
|
|
1959
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1936
1960
|
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1937
1961
|
var IOS_BROWSERS = [
|
|
1938
1962
|
[/CriOS/i, "googlechromes"],
|
|
@@ -1976,7 +2000,7 @@ function readCookie(h, name) {
|
|
|
1976
2000
|
}
|
|
1977
2001
|
return "";
|
|
1978
2002
|
}
|
|
1979
|
-
var sha256hex = (s) =>
|
|
2003
|
+
var sha256hex = (s) => createHash4("sha256").update(s).digest("hex");
|
|
1980
2004
|
function safeEqualHex(a, b) {
|
|
1981
2005
|
const x = Buffer.from(a, "hex");
|
|
1982
2006
|
const y = Buffer.from(b, "hex");
|
|
@@ -2019,17 +2043,7 @@ function createEidLogin(opts) {
|
|
|
2019
2043
|
const successRedirect = opts.successRedirect ?? "/";
|
|
2020
2044
|
const failureRedirect = opts.failureRedirect ?? "/";
|
|
2021
2045
|
const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
|
|
2022
|
-
const
|
|
2023
|
-
const rateLimit = opts.rateLimit ?? ((key) => {
|
|
2024
|
-
if (!key.startsWith("push:")) return true;
|
|
2025
|
-
const now = Date.now();
|
|
2026
|
-
const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
|
|
2027
|
-
if (pushHits.size > 1e4) pushHits.clear();
|
|
2028
|
-
if (hits.length >= 3) return false;
|
|
2029
|
-
hits.push(now);
|
|
2030
|
-
pushHits.set(key, hits);
|
|
2031
|
-
return true;
|
|
2032
|
-
});
|
|
2046
|
+
const rateLimit = buildRateLimit(opts.rateLimit);
|
|
2033
2047
|
const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
|
|
2034
2048
|
const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
|
|
2035
2049
|
const json = (status2, body, extra = {}) => ({
|
|
@@ -2063,7 +2077,7 @@ function createEidLogin(opts) {
|
|
|
2063
2077
|
const dot = raw.indexOf(".");
|
|
2064
2078
|
const sid = raw.slice(0, dot);
|
|
2065
2079
|
const binding = raw.slice(dot + 1);
|
|
2066
|
-
if (dot < 0 || !
|
|
2080
|
+
if (dot < 0 || !UUID_RE2.test(sid) || !B64URL_RE.test(binding)) return void 0;
|
|
2067
2081
|
const tx = await store.get(sid);
|
|
2068
2082
|
if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
|
|
2069
2083
|
if (tx.expiresAt <= Date.now()) {
|
|
@@ -2288,8 +2302,6 @@ export {
|
|
|
2288
2302
|
CITIZEN_ISSUING_CA_SKI,
|
|
2289
2303
|
CertChoiceInvalidError,
|
|
2290
2304
|
DEFAULT_BASE_URL,
|
|
2291
|
-
DEFAULT_PDF_MAX_BYTES,
|
|
2292
|
-
DEFAULT_PDF_TIMEOUT_MS,
|
|
2293
2305
|
DEFAULT_SIGN_SESSION_TTL_MS,
|
|
2294
2306
|
DeviceLinkError,
|
|
2295
2307
|
EidClient,
|
|
@@ -2300,7 +2312,7 @@ export {
|
|
|
2300
2312
|
NoSigningCertificateError,
|
|
2301
2313
|
ON_BEHALF_ERROR_CODES,
|
|
2302
2314
|
OrganizationApi,
|
|
2303
|
-
|
|
2315
|
+
PDF_SIGNATURE_REGISTER_FIELDS,
|
|
2304
2316
|
PersonApi,
|
|
2305
2317
|
RETRYABLE_ERROR_CODES,
|
|
2306
2318
|
RequestInvalidError,
|
|
@@ -2314,7 +2326,9 @@ export {
|
|
|
2314
2326
|
acspV2Digest,
|
|
2315
2327
|
buildAcspV2Payload,
|
|
2316
2328
|
buildDeviceLink,
|
|
2329
|
+
buildRegisterBody,
|
|
2317
2330
|
createEidLogin,
|
|
2331
|
+
defaultRateLimit,
|
|
2318
2332
|
deviceLink,
|
|
2319
2333
|
deviceLinkInput,
|
|
2320
2334
|
forwardedClientIp,
|
|
@@ -2329,6 +2343,7 @@ export {
|
|
|
2329
2343
|
personMatchesCertificate,
|
|
2330
2344
|
qrDeviceLinkTicker,
|
|
2331
2345
|
randomChallenge,
|
|
2346
|
+
registrationFromCompleted,
|
|
2332
2347
|
resolveBaseUrl,
|
|
2333
2348
|
sessionExpiry,
|
|
2334
2349
|
sha256Base64,
|