@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.cjs
CHANGED
|
@@ -26,8 +26,6 @@ __export(index_exports, {
|
|
|
26
26
|
CITIZEN_ISSUING_CA_SKI: () => CITIZEN_ISSUING_CA_SKI,
|
|
27
27
|
CertChoiceInvalidError: () => CertChoiceInvalidError,
|
|
28
28
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
29
|
-
DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
|
|
30
|
-
DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
|
|
31
29
|
DEFAULT_SIGN_SESSION_TTL_MS: () => DEFAULT_SIGN_SESSION_TTL_MS,
|
|
32
30
|
DeviceLinkError: () => DeviceLinkError,
|
|
33
31
|
EidClient: () => EidClient,
|
|
@@ -38,7 +36,7 @@ __export(index_exports, {
|
|
|
38
36
|
NoSigningCertificateError: () => NoSigningCertificateError,
|
|
39
37
|
ON_BEHALF_ERROR_CODES: () => ON_BEHALF_ERROR_CODES,
|
|
40
38
|
OrganizationApi: () => OrganizationApi,
|
|
41
|
-
|
|
39
|
+
PDF_SIGNATURE_REGISTER_FIELDS: () => PDF_SIGNATURE_REGISTER_FIELDS,
|
|
42
40
|
PersonApi: () => PersonApi,
|
|
43
41
|
RETRYABLE_ERROR_CODES: () => RETRYABLE_ERROR_CODES,
|
|
44
42
|
RequestInvalidError: () => RequestInvalidError,
|
|
@@ -52,7 +50,9 @@ __export(index_exports, {
|
|
|
52
50
|
acspV2Digest: () => acspV2Digest,
|
|
53
51
|
buildAcspV2Payload: () => buildAcspV2Payload,
|
|
54
52
|
buildDeviceLink: () => buildDeviceLink,
|
|
53
|
+
buildRegisterBody: () => buildRegisterBody,
|
|
55
54
|
createEidLogin: () => createEidLogin,
|
|
55
|
+
defaultRateLimit: () => defaultRateLimit,
|
|
56
56
|
deviceLink: () => deviceLink,
|
|
57
57
|
deviceLinkInput: () => deviceLinkInput,
|
|
58
58
|
forwardedClientIp: () => forwardedClientIp,
|
|
@@ -67,6 +67,7 @@ __export(index_exports, {
|
|
|
67
67
|
personMatchesCertificate: () => personMatchesCertificate,
|
|
68
68
|
qrDeviceLinkTicker: () => qrDeviceLinkTicker,
|
|
69
69
|
randomChallenge: () => randomChallenge,
|
|
70
|
+
registrationFromCompleted: () => registrationFromCompleted,
|
|
70
71
|
resolveBaseUrl: () => resolveBaseUrl,
|
|
71
72
|
sessionExpiry: () => sessionExpiry,
|
|
72
73
|
sha256Base64: () => sha256Base64,
|
|
@@ -226,15 +227,6 @@ var Http = class {
|
|
|
226
227
|
get(path, timeoutMs) {
|
|
227
228
|
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
228
229
|
}
|
|
229
|
-
/** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
|
|
230
|
-
postMultipart(path, form, timeoutMs) {
|
|
231
|
-
return this.json("POST", path, form, void 0, timeoutMs);
|
|
232
|
-
}
|
|
233
|
-
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
|
|
234
|
-
async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
|
|
235
|
-
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
236
|
-
return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
|
|
237
|
-
}
|
|
238
230
|
async json(method, path, body, contentType, timeoutMs) {
|
|
239
231
|
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
240
232
|
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
|
|
@@ -339,8 +331,6 @@ var RP_ROUTES = {
|
|
|
339
331
|
signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
|
|
340
332
|
certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
|
|
341
333
|
certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
|
|
342
|
-
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
343
|
-
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
344
334
|
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
345
335
|
session: { method: "GET", path: "/session/{sessionId}" },
|
|
346
336
|
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
@@ -349,15 +339,29 @@ var RP_ROUTES = {
|
|
|
349
339
|
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
350
340
|
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
351
341
|
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
352
|
-
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
342
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" },
|
|
343
|
+
pdfSignatures: { method: "POST", path: "/pdf/signatures" }
|
|
353
344
|
};
|
|
354
345
|
var RP_ROUTES_UNDISCOVERED = {
|
|
355
346
|
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
356
|
-
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
357
|
-
pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
|
|
358
|
-
pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
|
|
359
|
-
pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
|
|
347
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
360
348
|
};
|
|
349
|
+
var PDF_SIGNATURE_REGISTER_FIELDS = [
|
|
350
|
+
"docID",
|
|
351
|
+
"revIndex",
|
|
352
|
+
"medium",
|
|
353
|
+
"sessionID",
|
|
354
|
+
"cms",
|
|
355
|
+
"fileName",
|
|
356
|
+
"inSha256",
|
|
357
|
+
"pageSha256",
|
|
358
|
+
"sigRevSha256",
|
|
359
|
+
"outSha256",
|
|
360
|
+
"priorCmsSha256",
|
|
361
|
+
"priorRevisionSha256",
|
|
362
|
+
"onBehalfOf",
|
|
363
|
+
"signerEtsi"
|
|
364
|
+
];
|
|
361
365
|
function routePath(route, ...args) {
|
|
362
366
|
let i = 0;
|
|
363
367
|
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
@@ -520,6 +524,84 @@ var AuthApi = class {
|
|
|
520
524
|
};
|
|
521
525
|
|
|
522
526
|
// src/sign.ts
|
|
527
|
+
function registrationFromCompleted(done, extra) {
|
|
528
|
+
const out = {
|
|
529
|
+
docID: done.docId,
|
|
530
|
+
revIndex: done.revIndex,
|
|
531
|
+
cms: done.cms,
|
|
532
|
+
fileName: extra.fileName,
|
|
533
|
+
inSha256: done.inSha256,
|
|
534
|
+
sigRevSha256: done.sigRevSha256,
|
|
535
|
+
outSha256: done.outSha256
|
|
536
|
+
};
|
|
537
|
+
if (extra.medium) out.medium = extra.medium;
|
|
538
|
+
if (extra.sessionID) out.sessionID = extra.sessionID;
|
|
539
|
+
if (done.hasPage && done.pageSha256) out.pageSha256 = done.pageSha256;
|
|
540
|
+
if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
|
|
541
|
+
if (extra.priorRevisionSha256?.length) out.priorRevisionSha256 = extra.priorRevisionSha256;
|
|
542
|
+
if (extra.onBehalfOf) out.onBehalfOf = extra.onBehalfOf;
|
|
543
|
+
if (extra.signerEtsi) out.signerEtsi = extra.signerEtsi;
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
547
|
+
var MAX_CMS_BYTES = 28672;
|
|
548
|
+
function buildRegisterBody(input) {
|
|
549
|
+
const fail2 = (m) => {
|
|
550
|
+
throw new ValidationError(`registerSignature: ${m}`);
|
|
551
|
+
};
|
|
552
|
+
if (!UUID_RE.test(input.docID ?? "")) fail2("docID UUID \u0431\u0438\u0448");
|
|
553
|
+
if (!Number.isInteger(input.revIndex) || input.revIndex < 1 || input.revIndex > 16) fail2("revIndex 1\u201316");
|
|
554
|
+
const hex = (name, v) => {
|
|
555
|
+
const x = v?.trim().toLowerCase() ?? "";
|
|
556
|
+
if (!HEX64.test(x)) fail2(`${name} SHA-256 hex (64) \u0431\u0438\u0448`);
|
|
557
|
+
return x;
|
|
558
|
+
};
|
|
559
|
+
let cmsB64;
|
|
560
|
+
if (typeof input.cms === "string") {
|
|
561
|
+
const raw = Buffer.from(input.cms, "base64");
|
|
562
|
+
if (raw.length === 0 || raw.toString("base64") !== input.cms) fail2("cms \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 base64 \u0431\u0438\u0448");
|
|
563
|
+
cmsB64 = input.cms;
|
|
564
|
+
if (raw.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
565
|
+
} else {
|
|
566
|
+
if (!input.cms?.length) fail2("cms \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
567
|
+
if (input.cms.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
568
|
+
cmsB64 = Buffer.from(input.cms).toString("base64");
|
|
569
|
+
}
|
|
570
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
571
|
+
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`);
|
|
572
|
+
const sessionID = input.sessionID?.trim() ?? "";
|
|
573
|
+
const medium = input.medium ?? (sessionID ? "phone" : "token");
|
|
574
|
+
if (medium !== "phone" && medium !== "token") fail2("medium: phone | token");
|
|
575
|
+
if (medium === "phone" && !sessionID) fail2("phone-\u0434 sessionID \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
576
|
+
const priorCms = (input.priorCmsSha256 ?? []).map((h, i) => hex(`priorCmsSha256[${i}]`, h));
|
|
577
|
+
const priorRev = (input.priorRevisionSha256 ?? []).map((h, i) => hex(`priorRevisionSha256[${i}]`, h));
|
|
578
|
+
if (priorCms.length + priorRev.length !== input.revIndex - 1) fail2("priorCmsSha256 + priorRevisionSha256-\u0438\u0439\u043D \u0442\u043E\u043E = revIndex \u2212 1");
|
|
579
|
+
if (input.revIndex === 1 && !input.pageSha256) fail2("revIndex 1-\u0434 pageSha256 \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
580
|
+
const body = { docID: input.docID, revIndex: input.revIndex, medium };
|
|
581
|
+
if (sessionID) body.sessionID = sessionID;
|
|
582
|
+
body.cms = cmsB64;
|
|
583
|
+
body.fileName = fileName;
|
|
584
|
+
body.inSha256 = hex("inSha256", input.inSha256);
|
|
585
|
+
if (input.pageSha256) body.pageSha256 = hex("pageSha256", input.pageSha256);
|
|
586
|
+
body.sigRevSha256 = hex("sigRevSha256", input.sigRevSha256);
|
|
587
|
+
body.outSha256 = hex("outSha256", input.outSha256);
|
|
588
|
+
if (priorCms.length) body.priorCmsSha256 = priorCms;
|
|
589
|
+
if (priorRev.length) body.priorRevisionSha256 = priorRev;
|
|
590
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
591
|
+
if (input.signerEtsi?.trim()) body.signerEtsi = input.signerEtsi.trim();
|
|
592
|
+
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}`);
|
|
593
|
+
return body;
|
|
594
|
+
}
|
|
595
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
596
|
+
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: ";
|
|
597
|
+
var PDF_MAX_FILE_NAME = 120;
|
|
598
|
+
function dtbsBase64(v) {
|
|
599
|
+
const raw = typeof v === "string" ? Buffer.from(v, "base64") : Buffer.from(v);
|
|
600
|
+
if (raw.length !== 32 || typeof v === "string" && raw.toString("base64") !== v) {
|
|
601
|
+
throw new ValidationError("pdfDigest: dtbs \u043D\u044C 32 \u0431\u0430\u0439\u0442 (SHA-256) \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
602
|
+
}
|
|
603
|
+
return raw.toString("base64");
|
|
604
|
+
}
|
|
523
605
|
function rec2(v) {
|
|
524
606
|
return v && typeof v === "object" ? v : {};
|
|
525
607
|
}
|
|
@@ -604,6 +686,77 @@ var SignApi = class {
|
|
|
604
686
|
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
605
687
|
return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
|
|
606
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Клиент талын PAdES (`@gerege-systems/eid-mongolia-sdk/pades`): `prepare()`-ийн DTBS-д иргэнээр гарын үсэг зуруулна.
|
|
691
|
+
* PDF CA руу ОЧИХГҮЙ — зөвхөн 32 байт. CA-ийн өөрийн PAdES session-тэй ижил: `RAW_DIGEST_SIGNATURE`,
|
|
692
|
+
* `sha256WithRSAEncryption` (CMS-д PKCS#1 v1.5; legacy ECDSA иргэнд сервер `ecdsa-with-SHA256`), `displayTextAndPIN`
|
|
693
|
+
* «Баримтад гарын үсэг зурах: <fileName>» (60 тэмдэгтэд тайрна).
|
|
694
|
+
*
|
|
695
|
+
* Дараа нь `session.waitForResult(sessionId)` → `validator.validateSign(result, s.digest)` → `signature.value`-г
|
|
696
|
+
* (base64 → байт) `complete(state, signature)`-д.
|
|
697
|
+
*/
|
|
698
|
+
async pdfDigest(input) {
|
|
699
|
+
const digest = dtbsBase64(input.dtbs);
|
|
700
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
701
|
+
if (!fileName || Array.from(fileName).length > PDF_MAX_FILE_NAME) {
|
|
702
|
+
throw new ValidationError(`pdfDigest: fileName \u0437\u0430\u0430\u0432\u0430\u043B, \u2264 ${PDF_MAX_FILE_NAME} \u0442\u044D\u043C\u0434\u044D\u0433\u0442`);
|
|
703
|
+
}
|
|
704
|
+
const signer = input.signer;
|
|
705
|
+
const etsi = signer?.etsi?.trim();
|
|
706
|
+
const doc = signer?.documentNumber?.trim();
|
|
707
|
+
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");
|
|
708
|
+
const deviceLink2 = input.flow === "device-link";
|
|
709
|
+
const display = Array.from(PDF_DISPLAY_TEXT + fileName).slice(0, 60).join("");
|
|
710
|
+
const interactions = interactionsToBase64([{ type: "displayTextAndPIN", displayText60: display }]);
|
|
711
|
+
const body = {
|
|
712
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
713
|
+
relyingPartyName: this.creds.rpName,
|
|
714
|
+
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
715
|
+
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
716
|
+
signatureProtocolParameters: {
|
|
717
|
+
digest,
|
|
718
|
+
signatureAlgorithm: "sha256WithRSAEncryption",
|
|
719
|
+
signatureAlgorithmParameters: { hashAlgorithm: "SHA-256" }
|
|
720
|
+
},
|
|
721
|
+
interactions,
|
|
722
|
+
fileName
|
|
723
|
+
};
|
|
724
|
+
if (input.initialCallbackUrl?.trim()) body.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
725
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
726
|
+
const route = deviceLink2 ? etsi ? RP_ROUTES.signDeviceLinkEtsi : RP_ROUTES.signDeviceLinkDocument : etsi ? RP_ROUTES.signNotificationEtsi : RP_ROUTES.signNotificationDocument;
|
|
727
|
+
const r = rec2(await this.http.post(routePath(route, etsi ?? doc), body));
|
|
728
|
+
const receivedAt = Date.now();
|
|
729
|
+
const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
|
|
730
|
+
let dl = null;
|
|
731
|
+
if (deviceLink2) {
|
|
732
|
+
if (!r.sessionToken || !r.sessionSecret || !r.deviceLinkBase) {
|
|
733
|
+
throw new ValidationError("pdfDigest: device-link \u0445\u0430\u0440\u0438\u0443\u043D\u0434 sessionToken/sessionSecret/deviceLinkBase \u0430\u043B\u0433\u0430");
|
|
734
|
+
}
|
|
735
|
+
dl = { sessionToken: String(r.sessionToken), sessionSecret: String(r.sessionSecret), deviceLinkBase: String(r.deviceLinkBase) };
|
|
736
|
+
}
|
|
737
|
+
return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, digest, interactions, deviceLink: dl, receivedAt };
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* `POST /pdf/signatures` — `pades.complete()`-ийн дараа CMS-ийг CA-д бүртгэнэ (`/verify/{docID}`, иргэний түүх). PDF
|
|
741
|
+
* илгээгдэхгүй. SIGN эрх. CA CMS-ийг шалгана: 04-ийн SIGN cert, session digest = SHA-256(signedAttrs), claimedRole ↔
|
|
742
|
+
* төлөөлөл. Алдаа: 403 `REPRESENTATION_REVOKED`, 409 `CLAIMED_ROLE_MISMATCH` / `REGISTRY_CONFLICT` /
|
|
743
|
+
* `DOC_ID_CONFLICT` / `PDF_FOREIGN_SIGNATURE` (`ApiError.code`). Бүртгэлгүй файл ч Adobe/EU DSS-д хүчинтэй — зөвхөн
|
|
744
|
+
* `/verify` «бүртгэлгүй» гэнэ.
|
|
745
|
+
*/
|
|
746
|
+
async registerSignature(input) {
|
|
747
|
+
return this.registerBody(buildRegisterBody(input));
|
|
748
|
+
}
|
|
749
|
+
async registerBody(body) {
|
|
750
|
+
const raw = await this.http.post(routePath(RP_ROUTES.pdfSignatures), body);
|
|
751
|
+
const r = rec2(raw);
|
|
752
|
+
return {
|
|
753
|
+
docID: String(r.docID ?? body.docID),
|
|
754
|
+
revIndex: Number(r.revIndex ?? 0),
|
|
755
|
+
registeredAt: typeof r.registeredAt === "string" ? r.registeredAt : "",
|
|
756
|
+
verifyUrl: String(r.verifyUrl ?? ""),
|
|
757
|
+
raw
|
|
758
|
+
};
|
|
759
|
+
}
|
|
607
760
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
608
761
|
async textByEtsi(id, text, interactions, opts) {
|
|
609
762
|
return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
|
|
@@ -719,7 +872,6 @@ function parseSessionResult(raw) {
|
|
|
719
872
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
720
873
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
721
874
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
722
|
-
pdf: parsePdf(r.pdf),
|
|
723
875
|
person: parsePerson(r.person),
|
|
724
876
|
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
725
877
|
raw
|
|
@@ -757,28 +909,6 @@ function personMatchesCertificate(person, subject) {
|
|
|
757
909
|
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
758
910
|
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
759
911
|
}
|
|
760
|
-
function parsePdf(raw) {
|
|
761
|
-
const p = asRecord(raw);
|
|
762
|
-
const docID = str(p.docID);
|
|
763
|
-
if (!docID) return null;
|
|
764
|
-
const out = { docID, documentStatus: String(p.documentStatus ?? "") };
|
|
765
|
-
if (p.errorCode != null) out.errorCode = String(p.errorCode);
|
|
766
|
-
if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
|
|
767
|
-
if (p.size != null) out.size = Number(p.size);
|
|
768
|
-
if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
|
|
769
|
-
if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
|
|
770
|
-
const sg = asRecord(p.signer);
|
|
771
|
-
if (sg.etsi != null) {
|
|
772
|
-
out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
|
|
773
|
-
if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
|
|
774
|
-
if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
|
|
775
|
-
}
|
|
776
|
-
const v = asRecord(p.validation);
|
|
777
|
-
if (v.indication != null) {
|
|
778
|
-
out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
|
|
779
|
-
}
|
|
780
|
-
return out;
|
|
781
|
-
}
|
|
782
912
|
function parseSigParams(raw) {
|
|
783
913
|
const p = asRecord(raw);
|
|
784
914
|
const hash = str(p.hashAlgorithm);
|
|
@@ -837,160 +967,6 @@ var SessionApi = class {
|
|
|
837
967
|
}
|
|
838
968
|
};
|
|
839
969
|
|
|
840
|
-
// src/pdf.ts
|
|
841
|
-
var import_node_crypto2 = require("crypto");
|
|
842
|
-
var DEFAULT_PDF_MAX_BYTES = 64 << 20;
|
|
843
|
-
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
844
|
-
function rec3(v) {
|
|
845
|
-
return v && typeof v === "object" ? v : {};
|
|
846
|
-
}
|
|
847
|
-
var PdfApi = class {
|
|
848
|
-
constructor(http, creds, defaults) {
|
|
849
|
-
this.http = http;
|
|
850
|
-
this.creds = creds;
|
|
851
|
-
this.defaults = defaults;
|
|
852
|
-
}
|
|
853
|
-
http;
|
|
854
|
-
creds;
|
|
855
|
-
defaults;
|
|
856
|
-
/**
|
|
857
|
-
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
858
|
-
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
859
|
-
*/
|
|
860
|
-
async prepare(input) {
|
|
861
|
-
const request = {
|
|
862
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
863
|
-
relyingPartyName: this.creds.rpName,
|
|
864
|
-
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
865
|
-
signer: input.signer,
|
|
866
|
-
flow: input.flow ?? "notification",
|
|
867
|
-
fileName: input.fileName
|
|
868
|
-
};
|
|
869
|
-
if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
870
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
871
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
872
|
-
const form = new FormData();
|
|
873
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
874
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
875
|
-
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
|
|
876
|
-
const receivedAt = Date.now();
|
|
877
|
-
const vc = rec3(r.vc).value;
|
|
878
|
-
const dl = rec3(r.deviceLink);
|
|
879
|
-
const digest = typeof r.digest === "string" && r.digest ? r.digest : typeof dl.digest === "string" && dl.digest ? dl.digest : void 0;
|
|
880
|
-
return {
|
|
881
|
-
sessionId: String(r.sessionID),
|
|
882
|
-
docId: String(r.docID),
|
|
883
|
-
vc: vc != null ? String(vc) : null,
|
|
884
|
-
...digest !== void 0 ? { digest } : {},
|
|
885
|
-
deviceLink: dl.sessionToken ? {
|
|
886
|
-
sessionToken: String(dl.sessionToken),
|
|
887
|
-
sessionSecret: String(dl.sessionSecret),
|
|
888
|
-
deviceLinkBase: String(dl.deviceLinkBase),
|
|
889
|
-
digest: String(dl.digest),
|
|
890
|
-
interactions: String(dl.interactions)
|
|
891
|
-
} : null,
|
|
892
|
-
receivedAt
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
897
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
898
|
-
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
899
|
-
* дараа дахин татна (`retryable`).
|
|
900
|
-
*/
|
|
901
|
-
document(sessionId) {
|
|
902
|
-
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
|
|
903
|
-
}
|
|
904
|
-
get timeout() {
|
|
905
|
-
return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
|
|
906
|
-
}
|
|
907
|
-
get maxBytes() {
|
|
908
|
-
return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
|
|
912
|
-
* (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
|
|
913
|
-
* зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
|
|
914
|
-
* LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
|
|
915
|
-
* TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
|
|
916
|
-
*/
|
|
917
|
-
async tokenPrepare(input) {
|
|
918
|
-
if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
|
|
919
|
-
throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
920
|
-
}
|
|
921
|
-
const request = {
|
|
922
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
923
|
-
relyingPartyName: this.creds.rpName,
|
|
924
|
-
signer: { etsi: input.signerEtsi.trim() },
|
|
925
|
-
certificate: Buffer.from(input.certificate).toString("base64"),
|
|
926
|
-
fileName: input.fileName
|
|
927
|
-
};
|
|
928
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
929
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
930
|
-
const form = new FormData();
|
|
931
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
932
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
933
|
-
const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
|
|
934
|
-
const r = rec3(raw);
|
|
935
|
-
const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
|
|
936
|
-
if (!r.signingID || hashToSign.length !== 32) {
|
|
937
|
-
throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
|
|
938
|
-
}
|
|
939
|
-
return {
|
|
940
|
-
signingId: String(r.signingID),
|
|
941
|
-
docId: String(r.docID ?? ""),
|
|
942
|
-
hashToSign,
|
|
943
|
-
hashAlgorithm: String(r.hashAlgorithm ?? ""),
|
|
944
|
-
signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
|
|
945
|
-
signatureFormat: String(r.signatureFormat ?? ""),
|
|
946
|
-
expiresAt: String(r.expiresAt ?? ""),
|
|
947
|
-
raw
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
|
|
952
|
-
* Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
|
|
953
|
-
* Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
|
|
954
|
-
* TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
|
|
955
|
-
*/
|
|
956
|
-
async tokenFinalize(signingId, signature) {
|
|
957
|
-
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)");
|
|
958
|
-
const r = await this.http.post(
|
|
959
|
-
routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
|
|
960
|
-
{ signature: Buffer.from(signature).toString("base64") },
|
|
961
|
-
this.timeout
|
|
962
|
-
);
|
|
963
|
-
return r;
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
|
|
967
|
-
* хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
|
|
968
|
-
*/
|
|
969
|
-
tokenDocument(signingId) {
|
|
970
|
-
return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
974
|
-
* `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
|
|
975
|
-
*
|
|
976
|
-
* @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
|
|
977
|
-
*/
|
|
978
|
-
async documentVerified(sessionId, expected) {
|
|
979
|
-
let want;
|
|
980
|
-
if (typeof expected === "string") {
|
|
981
|
-
want = expected;
|
|
982
|
-
} else {
|
|
983
|
-
if (expected.documentStatus !== "READY") throw new ValidationError(`pdf.documentStatus ${expected.documentStatus} \u2260 READY`);
|
|
984
|
-
want = expected.outSha256;
|
|
985
|
-
}
|
|
986
|
-
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");
|
|
987
|
-
const bytes = await this.document(sessionId);
|
|
988
|
-
const got = (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
989
|
-
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");
|
|
990
|
-
return bytes;
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
|
|
994
970
|
// src/organization.ts
|
|
995
971
|
var OrganizationApi = class {
|
|
996
972
|
constructor(http) {
|
|
@@ -1006,7 +982,7 @@ var OrganizationApi = class {
|
|
|
1006
982
|
};
|
|
1007
983
|
|
|
1008
984
|
// src/person.ts
|
|
1009
|
-
function
|
|
985
|
+
function rec3(v) {
|
|
1010
986
|
return v && typeof v === "object" ? v : {};
|
|
1011
987
|
}
|
|
1012
988
|
var PersonApi = class {
|
|
@@ -1016,7 +992,7 @@ var PersonApi = class {
|
|
|
1016
992
|
http;
|
|
1017
993
|
async get(path) {
|
|
1018
994
|
const raw = await this.http.get(path);
|
|
1019
|
-
return { ...
|
|
995
|
+
return { ...rec3(raw), raw };
|
|
1020
996
|
}
|
|
1021
997
|
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
1022
998
|
certificates(personEtsi) {
|
|
@@ -1046,10 +1022,10 @@ var PersonApi = class {
|
|
|
1046
1022
|
};
|
|
1047
1023
|
|
|
1048
1024
|
// src/token.ts
|
|
1049
|
-
var
|
|
1025
|
+
var import_node_crypto3 = require("crypto");
|
|
1050
1026
|
|
|
1051
1027
|
// src/subject.ts
|
|
1052
|
-
var
|
|
1028
|
+
var import_node_crypto2 = require("crypto");
|
|
1053
1029
|
var OIDS = {
|
|
1054
1030
|
"2.5.4.42": "givenName",
|
|
1055
1031
|
"2.5.4.4": "surname",
|
|
@@ -1114,7 +1090,7 @@ function stringOf(buf, t) {
|
|
|
1114
1090
|
}
|
|
1115
1091
|
}
|
|
1116
1092
|
function parseSubject(cert) {
|
|
1117
|
-
const der = cert instanceof
|
|
1093
|
+
const der = cert instanceof import_node_crypto2.X509Certificate ? new Uint8Array(cert.raw) : typeof cert === "string" ? new Uint8Array(Buffer.from(cert, "base64")) : cert;
|
|
1118
1094
|
const top = tlv(der, 0, der.length);
|
|
1119
1095
|
if (top.tag !== 48) throw new ValidationError("subject: Certificate SEQUENCE \u0431\u0438\u0448");
|
|
1120
1096
|
const tbs = tlv(der, top.start, top.end);
|
|
@@ -1187,12 +1163,12 @@ function certExtensions(der) {
|
|
|
1187
1163
|
}
|
|
1188
1164
|
|
|
1189
1165
|
// src/token.ts
|
|
1190
|
-
function
|
|
1166
|
+
function rec4(v) {
|
|
1191
1167
|
return v && typeof v === "object" ? v : {};
|
|
1192
1168
|
}
|
|
1193
1169
|
function hexSha256Equal(hex, data) {
|
|
1194
1170
|
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
1195
|
-
return (0,
|
|
1171
|
+
return (0, import_node_crypto3.timingSafeEqual)((0, import_node_crypto3.createHash)("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
1196
1172
|
}
|
|
1197
1173
|
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
1198
1174
|
var TokenApi = class {
|
|
@@ -1221,11 +1197,11 @@ var TokenApi = class {
|
|
|
1221
1197
|
flow: "notification"
|
|
1222
1198
|
};
|
|
1223
1199
|
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
1224
|
-
const r =
|
|
1200
|
+
const r = rec4(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
1225
1201
|
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
1226
1202
|
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");
|
|
1227
1203
|
}
|
|
1228
|
-
const vc =
|
|
1204
|
+
const vc = rec4(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
1229
1205
|
return {
|
|
1230
1206
|
sessionId: String(r.sessionID ?? ""),
|
|
1231
1207
|
vc: vc != null ? String(vc) : null,
|
|
@@ -1240,7 +1216,7 @@ var TokenApi = class {
|
|
|
1240
1216
|
*/
|
|
1241
1217
|
async certificateResult(sessionId) {
|
|
1242
1218
|
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
1243
|
-
const r =
|
|
1219
|
+
const r = rec4(raw);
|
|
1244
1220
|
const item = (v) => v && typeof v === "object" ? v : null;
|
|
1245
1221
|
return {
|
|
1246
1222
|
status: String(r.status ?? ""),
|
|
@@ -1256,7 +1232,7 @@ var TokenApi = class {
|
|
|
1256
1232
|
}
|
|
1257
1233
|
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
1258
1234
|
async authChallenge() {
|
|
1259
|
-
const r =
|
|
1235
|
+
const r = rec4(
|
|
1260
1236
|
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
1261
1237
|
relyingPartyUUID: this.creds.rpUUID,
|
|
1262
1238
|
relyingPartyName: this.creds.rpName
|
|
@@ -1277,7 +1253,7 @@ var TokenApi = class {
|
|
|
1277
1253
|
async authVerify(challengeId, certDer, signature) {
|
|
1278
1254
|
let serial;
|
|
1279
1255
|
try {
|
|
1280
|
-
serial = parseSubject(new
|
|
1256
|
+
serial = parseSubject(new import_node_crypto3.X509Certificate(certDer)).serialNumber;
|
|
1281
1257
|
} catch {
|
|
1282
1258
|
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");
|
|
1283
1259
|
}
|
|
@@ -1287,12 +1263,12 @@ var TokenApi = class {
|
|
|
1287
1263
|
certificate: Buffer.from(certDer).toString("base64"),
|
|
1288
1264
|
signature: Buffer.from(signature).toString("base64")
|
|
1289
1265
|
});
|
|
1290
|
-
const r =
|
|
1291
|
-
if (
|
|
1292
|
-
const id =
|
|
1266
|
+
const r = rec4(raw);
|
|
1267
|
+
if (rec4(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
1268
|
+
const id = rec4(r.identity);
|
|
1293
1269
|
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");
|
|
1294
|
-
const org =
|
|
1295
|
-
const cert =
|
|
1270
|
+
const org = rec4(r.organization);
|
|
1271
|
+
const cert = rec4(r.cert);
|
|
1296
1272
|
return {
|
|
1297
1273
|
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
1298
1274
|
serialHex: String(cert.serialHex ?? ""),
|
|
@@ -1308,7 +1284,7 @@ function csrSpki(csr) {
|
|
|
1308
1284
|
const version = tlv(csr, info.start, info.end);
|
|
1309
1285
|
const subject = tlv(csr, version.end, info.end);
|
|
1310
1286
|
const spki = tlv(csr, subject.end, info.end);
|
|
1311
|
-
return (0,
|
|
1287
|
+
return (0, import_node_crypto3.createPublicKey)({ key: Buffer.from(csr.subarray(subject.end, spki.end)), format: "der", type: "spki" }).export({
|
|
1312
1288
|
format: "der",
|
|
1313
1289
|
type: "spki"
|
|
1314
1290
|
});
|
|
@@ -1327,7 +1303,7 @@ function verifyTokenCertificates(result, csrAuth, csrSign) {
|
|
|
1327
1303
|
}
|
|
1328
1304
|
let got;
|
|
1329
1305
|
try {
|
|
1330
|
-
got = new
|
|
1306
|
+
got = new import_node_crypto3.X509Certificate(Buffer.from(item.certificate, "base64")).publicKey.export({ format: "der", type: "spki" });
|
|
1331
1307
|
} catch {
|
|
1332
1308
|
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`);
|
|
1333
1309
|
}
|
|
@@ -1340,13 +1316,13 @@ function tokenAuthDigest(rpUUID, challengeB64) {
|
|
|
1340
1316
|
}
|
|
1341
1317
|
const ch = Buffer.from(challengeB64, "base64");
|
|
1342
1318
|
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
1343
|
-
return (0,
|
|
1319
|
+
return (0, import_node_crypto3.createHash)("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
1344
1320
|
}
|
|
1345
1321
|
function tokenLimitOf(err) {
|
|
1346
1322
|
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
1347
1323
|
let b;
|
|
1348
1324
|
try {
|
|
1349
|
-
b =
|
|
1325
|
+
b = rec4(JSON.parse(err.body));
|
|
1350
1326
|
} catch {
|
|
1351
1327
|
return void 0;
|
|
1352
1328
|
}
|
|
@@ -1359,7 +1335,7 @@ function tokenLimitOf(err) {
|
|
|
1359
1335
|
}
|
|
1360
1336
|
|
|
1361
1337
|
// src/validator.ts
|
|
1362
|
-
var
|
|
1338
|
+
var import_node_crypto4 = require("crypto");
|
|
1363
1339
|
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
1364
1340
|
var ResponseValidator = class {
|
|
1365
1341
|
anchors;
|
|
@@ -1373,8 +1349,8 @@ var ResponseValidator = class {
|
|
|
1373
1349
|
checkKeyUsage;
|
|
1374
1350
|
schemeName;
|
|
1375
1351
|
constructor(trust = {}) {
|
|
1376
|
-
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new
|
|
1377
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
1352
|
+
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new import_node_crypto4.X509Certificate(p));
|
|
1353
|
+
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new import_node_crypto4.X509Certificate(p));
|
|
1378
1354
|
this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
|
|
1379
1355
|
this.skew = trust.clockSkewMs ?? 0;
|
|
1380
1356
|
this.allowUntrusted = trust.allowUntrusted ?? false;
|
|
@@ -1555,7 +1531,7 @@ var ResponseValidator = class {
|
|
|
1555
1531
|
leafOf(result, protocol) {
|
|
1556
1532
|
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");
|
|
1557
1533
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
1558
|
-
const leaf = new
|
|
1534
|
+
const leaf = new import_node_crypto4.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
|
|
1559
1535
|
this.checkValidity(leaf);
|
|
1560
1536
|
this.checkLevel(String(result.certificateLevel ?? ""));
|
|
1561
1537
|
this.checkPurpose(leaf, protocol);
|
|
@@ -1689,16 +1665,16 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
1689
1665
|
return fromResponse;
|
|
1690
1666
|
}
|
|
1691
1667
|
function userChallengeOf(verifier) {
|
|
1692
|
-
return (0,
|
|
1668
|
+
return (0, import_node_crypto4.createHash)("sha256").update(verifier, "utf8").digest("base64url");
|
|
1693
1669
|
}
|
|
1694
1670
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
1695
1671
|
const sig = Buffer.from(sigB64, "base64");
|
|
1696
1672
|
const h = nodeHash(hash);
|
|
1697
1673
|
if (alg === "rsassa-pss") {
|
|
1698
|
-
return (0,
|
|
1674
|
+
return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
|
|
1699
1675
|
}
|
|
1700
|
-
if (alg === "ecdsa-with-SHA256") return (0,
|
|
1701
|
-
return (0,
|
|
1676
|
+
if (alg === "ecdsa-with-SHA256") return (0, import_node_crypto4.verify)(h, payload, { key, dsaEncoding: "der" }, sig);
|
|
1677
|
+
return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PADDING }, sig);
|
|
1702
1678
|
}
|
|
1703
1679
|
function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
|
|
1704
1680
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -1707,7 +1683,7 @@ function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
|
|
|
1707
1683
|
if (!modBits || sig.length !== Math.ceil(modBits / 8)) return false;
|
|
1708
1684
|
let em;
|
|
1709
1685
|
try {
|
|
1710
|
-
em = (0,
|
|
1686
|
+
em = (0, import_node_crypto4.publicDecrypt)({ key, padding: import_node_crypto4.constants.RSA_NO_PADDING }, sig);
|
|
1711
1687
|
} catch {
|
|
1712
1688
|
return false;
|
|
1713
1689
|
}
|
|
@@ -1791,7 +1767,7 @@ function mgf1(hash, seed, length) {
|
|
|
1791
1767
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
1792
1768
|
const c = Buffer.alloc(4);
|
|
1793
1769
|
c.writeUInt32BE(i, 0);
|
|
1794
|
-
out.push((0,
|
|
1770
|
+
out.push((0, import_node_crypto4.createHash)(nodeHash(hash)).update(seed).update(c).digest());
|
|
1795
1771
|
}
|
|
1796
1772
|
return Buffer.concat(out).subarray(0, length);
|
|
1797
1773
|
}
|
|
@@ -1815,7 +1791,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
1815
1791
|
const psLen = emLen - hLen - sLen - 2;
|
|
1816
1792
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
1817
1793
|
const salt = db.subarray(psLen + 1);
|
|
1818
|
-
const h2 = (0,
|
|
1794
|
+
const h2 = (0, import_node_crypto4.createHash)(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
1819
1795
|
return h.equals(h2);
|
|
1820
1796
|
}
|
|
1821
1797
|
function keyUsageBits(cert) {
|
|
@@ -1949,7 +1925,6 @@ function resolveBaseUrl(baseUrl) {
|
|
|
1949
1925
|
var EidClient = class {
|
|
1950
1926
|
auth;
|
|
1951
1927
|
sign;
|
|
1952
|
-
pdf;
|
|
1953
1928
|
organization;
|
|
1954
1929
|
person;
|
|
1955
1930
|
token;
|
|
@@ -1971,11 +1946,6 @@ var EidClient = class {
|
|
|
1971
1946
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1972
1947
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1973
1948
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
|
|
1974
|
-
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1975
|
-
certificateLevel: level,
|
|
1976
|
-
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
1977
|
-
maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
|
|
1978
|
-
});
|
|
1979
1949
|
this.organization = new OrganizationApi(http);
|
|
1980
1950
|
this.person = new PersonApi(http);
|
|
1981
1951
|
this.token = new TokenApi(http, cfg.credentials);
|
|
@@ -1983,6 +1953,14 @@ var EidClient = class {
|
|
|
1983
1953
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1984
1954
|
this.rpName = cfg.credentials.rpName;
|
|
1985
1955
|
}
|
|
1956
|
+
/** {@link SignApi.pdfDigest} — клиент талын PAdES-ийн DTBS-д гарын үсгийн session (PDF CA руу очихгүй). */
|
|
1957
|
+
signPdfDigest(input) {
|
|
1958
|
+
return this.sign.pdfDigest(input);
|
|
1959
|
+
}
|
|
1960
|
+
/** {@link SignApi.registerSignature} — `pades.complete()`-ийн CMS-ийг CA-д бүртгэнэ (`POST /pdf/signatures`). */
|
|
1961
|
+
registerSignature(input) {
|
|
1962
|
+
return this.sign.registerSignature(input);
|
|
1963
|
+
}
|
|
1986
1964
|
};
|
|
1987
1965
|
|
|
1988
1966
|
// src/devicelink.ts
|
|
@@ -2028,7 +2006,7 @@ function openOrShowQR(session, opts) {
|
|
|
2028
2006
|
}
|
|
2029
2007
|
|
|
2030
2008
|
// src/devicelinkv3.ts
|
|
2031
|
-
var
|
|
2009
|
+
var import_node_crypto5 = require("crypto");
|
|
2032
2010
|
var TYPES = ["QR", "Web2App", "App2App"];
|
|
2033
2011
|
var PROTOCOL = { auth: "ACSP_V2", sign: "RAW_DIGEST_SIGNATURE", cert: "" };
|
|
2034
2012
|
var TOKEN_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -2085,7 +2063,7 @@ function buildDeviceLink(p) {
|
|
|
2085
2063
|
callback,
|
|
2086
2064
|
unprotected
|
|
2087
2065
|
].join("|");
|
|
2088
|
-
const authCode = (0,
|
|
2066
|
+
const authCode = (0, import_node_crypto5.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
|
|
2089
2067
|
return `${unprotected}&authCode=${authCode}`;
|
|
2090
2068
|
}
|
|
2091
2069
|
function deviceLinkInput(s, deviceLinkType, lang) {
|
|
@@ -2129,7 +2107,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
2129
2107
|
}
|
|
2130
2108
|
|
|
2131
2109
|
// src/login.ts
|
|
2132
|
-
var
|
|
2110
|
+
var import_node_crypto6 = require("crypto");
|
|
2133
2111
|
function memoryLoginStore() {
|
|
2134
2112
|
const m = /* @__PURE__ */ new Map();
|
|
2135
2113
|
return {
|
|
@@ -2154,6 +2132,51 @@ function memoryLoginStore() {
|
|
|
2154
2132
|
}
|
|
2155
2133
|
};
|
|
2156
2134
|
}
|
|
2135
|
+
var defaultRateLimit = Object.freeze({
|
|
2136
|
+
push: Object.freeze({ limit: 3, windowMs: 6e4 })
|
|
2137
|
+
});
|
|
2138
|
+
var RATE_KINDS = ["start", "push"];
|
|
2139
|
+
function windowLimiter(limit, windowMs) {
|
|
2140
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2141
|
+
return (key) => {
|
|
2142
|
+
const now = Date.now();
|
|
2143
|
+
const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
2144
|
+
if (hits.size > 1e4) hits.clear();
|
|
2145
|
+
if (h.length >= limit) {
|
|
2146
|
+
hits.set(key, h);
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
h.push(now);
|
|
2150
|
+
hits.set(key, h);
|
|
2151
|
+
return true;
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function buildRateLimit(opt) {
|
|
2155
|
+
const extra = typeof opt === "function" ? opt : void 0;
|
|
2156
|
+
const user = opt && typeof opt === "object" ? opt : {};
|
|
2157
|
+
for (const k of Object.keys(user)) {
|
|
2158
|
+
if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
|
|
2159
|
+
}
|
|
2160
|
+
const merged = { ...defaultRateLimit, ...user };
|
|
2161
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
2162
|
+
for (const k of RATE_KINDS) {
|
|
2163
|
+
const r = merged[k];
|
|
2164
|
+
if (r === void 0 || r === false) continue;
|
|
2165
|
+
if (typeof r === "function") {
|
|
2166
|
+
perKind.set(k, r);
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
|
|
2170
|
+
throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
|
|
2171
|
+
}
|
|
2172
|
+
perKind.set(k, windowLimiter(r.limit, r.windowMs));
|
|
2173
|
+
}
|
|
2174
|
+
return async (key) => {
|
|
2175
|
+
const fn = perKind.get(key.slice(0, key.indexOf(":")));
|
|
2176
|
+
if (fn && !await fn(key)) return false;
|
|
2177
|
+
return extra ? extra(key) : true;
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2157
2180
|
var CALLBACK_PATH = "/auth/eid/callback";
|
|
2158
2181
|
function forwardedClientIp(headers, trustProxy) {
|
|
2159
2182
|
if (trustProxy === false || trustProxy === 0) return void 0;
|
|
@@ -2166,7 +2189,7 @@ function forwardedClientIp(headers, trustProxy) {
|
|
|
2166
2189
|
return header(headers, "x-real-ip").trim() || void 0;
|
|
2167
2190
|
}
|
|
2168
2191
|
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
2169
|
-
var
|
|
2192
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
2170
2193
|
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
2171
2194
|
var IOS_BROWSERS = [
|
|
2172
2195
|
[/CriOS/i, "googlechromes"],
|
|
@@ -2210,11 +2233,11 @@ function readCookie(h, name) {
|
|
|
2210
2233
|
}
|
|
2211
2234
|
return "";
|
|
2212
2235
|
}
|
|
2213
|
-
var sha256hex = (s) => (0,
|
|
2236
|
+
var sha256hex = (s) => (0, import_node_crypto6.createHash)("sha256").update(s).digest("hex");
|
|
2214
2237
|
function safeEqualHex(a, b) {
|
|
2215
2238
|
const x = Buffer.from(a, "hex");
|
|
2216
2239
|
const y = Buffer.from(b, "hex");
|
|
2217
|
-
return x.length === y.length && x.length > 0 && (0,
|
|
2240
|
+
return x.length === y.length && x.length > 0 && (0, import_node_crypto6.timingSafeEqual)(x, y);
|
|
2218
2241
|
}
|
|
2219
2242
|
function toIdentity(sessionId, r, v, flowType) {
|
|
2220
2243
|
const s = v.parsedSubject;
|
|
@@ -2253,17 +2276,7 @@ function createEidLogin(opts) {
|
|
|
2253
2276
|
const successRedirect = opts.successRedirect ?? "/";
|
|
2254
2277
|
const failureRedirect = opts.failureRedirect ?? "/";
|
|
2255
2278
|
const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
|
|
2256
|
-
const
|
|
2257
|
-
const rateLimit = opts.rateLimit ?? ((key) => {
|
|
2258
|
-
if (!key.startsWith("push:")) return true;
|
|
2259
|
-
const now = Date.now();
|
|
2260
|
-
const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
|
|
2261
|
-
if (pushHits.size > 1e4) pushHits.clear();
|
|
2262
|
-
if (hits.length >= 3) return false;
|
|
2263
|
-
hits.push(now);
|
|
2264
|
-
pushHits.set(key, hits);
|
|
2265
|
-
return true;
|
|
2266
|
-
});
|
|
2279
|
+
const rateLimit = buildRateLimit(opts.rateLimit);
|
|
2267
2280
|
const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
|
|
2268
2281
|
const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
|
|
2269
2282
|
const json = (status2, body, extra = {}) => ({
|
|
@@ -2297,7 +2310,7 @@ function createEidLogin(opts) {
|
|
|
2297
2310
|
const dot = raw.indexOf(".");
|
|
2298
2311
|
const sid = raw.slice(0, dot);
|
|
2299
2312
|
const binding = raw.slice(dot + 1);
|
|
2300
|
-
if (dot < 0 || !
|
|
2313
|
+
if (dot < 0 || !UUID_RE2.test(sid) || !B64URL_RE.test(binding)) return void 0;
|
|
2301
2314
|
const tx = await store.get(sid);
|
|
2302
2315
|
if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
|
|
2303
2316
|
if (tx.expiresAt <= Date.now()) {
|
|
@@ -2354,7 +2367,7 @@ function createEidLogin(opts) {
|
|
|
2354
2367
|
});
|
|
2355
2368
|
}
|
|
2356
2369
|
}
|
|
2357
|
-
const binding = (0,
|
|
2370
|
+
const binding = (0, import_node_crypto6.randomBytes)(32).toString("base64url");
|
|
2358
2371
|
const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
|
|
2359
2372
|
await store.set(full.sessionId, full, ttl);
|
|
2360
2373
|
const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
|
|
@@ -2523,8 +2536,6 @@ function createEidLogin(opts) {
|
|
|
2523
2536
|
CITIZEN_ISSUING_CA_SKI,
|
|
2524
2537
|
CertChoiceInvalidError,
|
|
2525
2538
|
DEFAULT_BASE_URL,
|
|
2526
|
-
DEFAULT_PDF_MAX_BYTES,
|
|
2527
|
-
DEFAULT_PDF_TIMEOUT_MS,
|
|
2528
2539
|
DEFAULT_SIGN_SESSION_TTL_MS,
|
|
2529
2540
|
DeviceLinkError,
|
|
2530
2541
|
EidClient,
|
|
@@ -2535,7 +2546,7 @@ function createEidLogin(opts) {
|
|
|
2535
2546
|
NoSigningCertificateError,
|
|
2536
2547
|
ON_BEHALF_ERROR_CODES,
|
|
2537
2548
|
OrganizationApi,
|
|
2538
|
-
|
|
2549
|
+
PDF_SIGNATURE_REGISTER_FIELDS,
|
|
2539
2550
|
PersonApi,
|
|
2540
2551
|
RETRYABLE_ERROR_CODES,
|
|
2541
2552
|
RequestInvalidError,
|
|
@@ -2549,7 +2560,9 @@ function createEidLogin(opts) {
|
|
|
2549
2560
|
acspV2Digest,
|
|
2550
2561
|
buildAcspV2Payload,
|
|
2551
2562
|
buildDeviceLink,
|
|
2563
|
+
buildRegisterBody,
|
|
2552
2564
|
createEidLogin,
|
|
2565
|
+
defaultRateLimit,
|
|
2553
2566
|
deviceLink,
|
|
2554
2567
|
deviceLinkInput,
|
|
2555
2568
|
forwardedClientIp,
|
|
@@ -2564,6 +2577,7 @@ function createEidLogin(opts) {
|
|
|
2564
2577
|
personMatchesCertificate,
|
|
2565
2578
|
qrDeviceLinkTicker,
|
|
2566
2579
|
randomChallenge,
|
|
2580
|
+
registrationFromCompleted,
|
|
2567
2581
|
resolveBaseUrl,
|
|
2568
2582
|
sessionExpiry,
|
|
2569
2583
|
sha256Base64,
|