@gerege-systems/eid-mongolia-sdk 0.8.1 → 0.9.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 CHANGED
@@ -151,8 +151,19 @@ const result = await eid.session.waitForResult(s.sessionId); // long-poll
151
151
  const who = eid.validator.validateAuth(result, s.acsp); // ← гинж + ACSP_V2 крипто-баталгаажуулна
152
152
 
153
153
  console.log(who.documentNumber, who.subject); // баталгаажсан иргэн
154
+ const { name, serialNumber } = who.parsedSubject; // "Болд Бат", "PNOMN-12345678" (givenName/surname/commonName/country…)
154
155
  ```
155
156
 
157
+ Subject-ийн талбарууд (`parsedSubject`, `validateSign`-д ч мөн) нь `parseSubject(cert)`-ээс — OID 2.5.4.42/4/3/5/6/10/97,
158
+ UTF8String/PrintableString/BMPString. `parseSubject` нь зөвхөн задлалт: баталгаажаагүй гэрчилгээний нэрэнд бүү итгэ.
159
+
160
+ > **Ижил утсан дээр push хэрэглэхгүй.** Push (`notificationBy*`)-ийн хүсэлт eID аппад мэдэгдэл / «хүлээгдэж буй»
161
+ > жагсаалтаар ирдэг; Smart-ID ба eID-д Notification төрлийн device link **байхгүй** (CA `docs/DEVICE_LINK_V3.md` §3).
162
+ > Хуучин `/dl?sessionId&vc` холбоос (`deviceLink`) push session-д ч юу ч өгөхгүй (сервер `DEVICE_LINK_REQUIRED`).
163
+ > Иргэн RP-ийг eID апптай ижил утсан дээр нээсэн бол push-ийн оронд **device-link Web2App** урсгал хэрэглэ
164
+ > (`auth.deviceLink*` / `pdf.prepare({ flow: "device-link" })` + `buildDeviceLink(…, "Web2App")`, §4) — товч аппыг шууд
165
+ > нээж, callback-аар буцна. Push-д «eID аппаа нээнэ үү» гэсэн текст л хангалттай.
166
+
156
167
  ## 3. Гарын үсэг (qualified signature)
157
168
 
158
169
  ```ts
@@ -171,6 +182,16 @@ const sig = eid.validator.validateSign(result, digest); // digest-ийн эср
171
182
  console.log(sig.signatureValueB64, sig.subject);
172
183
  ```
173
184
 
185
+ Device-link / push-оор эхлүүлсэн sign session-д эхлүүлсэн урсгалаа `expectedFlowType`-оор тулга (`validateAuth`-тай
186
+ ижил: зөрвөл `ValidationError`). Гуравдах параметр нь `hashAlgorithm` мөр (хуучин хэлбэр) эсвэл объект:
187
+
188
+ ```ts
189
+ eid.validator.validateSign(result, digest, { expectedFlowType: "QR" }); // эсвэл "Web2App" / "App2App" / "Notification"
190
+ eid.validator.validateSign(result, digest384, { hashAlgorithm: "SHA-384", expectedFlowType: "Web2App" });
191
+ ```
192
+
193
+ RAW_DIGEST_SIGNATURE-д `flowType` гарын үсэгт ордоггүй (зөвхөн digest зурагдана) — энэ нь RP-API хариуны тулгалт.
194
+
174
195
  ## 3a. Байгууллагын нэрийн өмнөөс гарын үсэг (onBehalfOf)
175
196
 
176
197
  Иргэн **өөрийн PIN2 гэрчилгээгээр** зурна; тухайн мөчид байгууллагыг төлөөлөх эрхтэйг eID **өөрийн бүртгэлээр**
@@ -198,7 +219,7 @@ try {
198
219
  // MUST: endResult OK, pdf.documentStatus READY, pdf.signer.onBehalfOf = хүссэн байгууллага,
199
220
  // татсан файлын SHA-256 = pdf.outSha256, validation.indication "valid", ltv true.
200
221
  if (r.pdf?.documentStatus !== "READY" || r.pdf.signer?.onBehalfOf !== "NTRMN-6235972") throw new Error("…");
201
- const signed = await eid.pdf.document(s.sessionId);
222
+ const signed = await eid.pdf.documentVerified(s.sessionId, r.pdf); // SHA-256 ≠ pdf.outSha256 → ValidationError
202
223
  console.log(r.pdf.signer.claimedRole); // "Гүйцэтгэх захирал, Гэрэгэ Системс ХХК (NTRMN-6235972)"
203
224
  } catch (e) {
204
225
  if ((e instanceof ForbiddenError || e instanceof ApiError) && e.code) {
@@ -212,6 +233,11 @@ try {
212
233
  // — CMS/PDF-ийг та угсардаг тул мэдүүлсэн үүргийг (signer-attributes-v2) өөрөө нэмнэ.
213
234
  ```
214
235
 
236
+ `prepare`-ийн хариу `digest` (DTBS, base64) — иргэний зурах 32 байт, push ба device-link хоёуланд (хуучин CA push-д
237
+ `undefined`). Иргэний гарын үсгийг өөрөө шалгах бол: `eid.validator.validateSign(r, s.digest!, { expectedFlowType: "QR" })`
238
+ (харуулсан урсгалаа). Гарын үсэгтэй PDF-ийг
239
+ `pdf.document()`-ийн оронд `pdf.documentVerified(sessionId, r.pdf)`-аар тат — `READY` + `outSha256` тулгалтыг хийнэ.
240
+
215
241
  Олон талт гэрээ: А байгууллагын гаралтыг өөрчлөлтгүй Б-ийн `prepare`-д (`docID`, өөр `onBehalfOf`) илгээнэ.
216
242
  Session-ий `onBehalfOf` блок (`role`/`rightType`) нь хариу угсрах үеийн бүртгэл; `pdf.signer.onBehalfOf`/`claimedRole`
217
243
  нь баримтад бичигдсэн утга.
@@ -312,7 +338,12 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
312
338
  `initialCallbackUrl`-ийг хариуны `signature.initialCallbackUrl`-аас авна (CA урсгалаар шийднэ: QR/Notification →
313
339
  `""`, Web2App/App2App → таны callback) — зөвхөн `""` эсвэл
314
340
  таны илгээсэн callback байхад, Web2App/App2App-д заавал таны callback; өөр утга → татгалзана
315
- - **sign** → RP-ийн өгсөн digest дээр
341
+ - **sign** → RP-ийн өгсөн digest дээр; `expectedFlowType` өгвөл хариуны `flowType` ЯГ таарах (гарын үсэгт ордоггүй — хариуны тулгалт)
342
+ - алгоритм нь гэрчилгээний түлхүүртэй уях: `ecdsa-with-SHA256` зөвхөн EC P-256 түлхүүрт, RSA алгоритмууд зөвхөн
343
+ RSA түлхүүрт (alg confusion хаалт). Hash нь үргэлж хүссэнтэй ЯГ тэнцүү. RP-API session-ий гарын үсэг CA дээр
344
+ SplitKey RSA (одоогийн бүх мобайл гэрчилгээ) эсвэл legacy threshold ECDSA P-256-аас гарна; ECDSA credential
345
+ зөвхөн SHA-256 digest зурдаг (өөр hash хүссэн session-д сервер гарын үсэг гаргахгүй), тиймээс SHA-384/512
346
+ хүссэн session-д `ecdsa-with-SHA256` хариу ирвэл хуурамч гэж татгалзана.
316
347
 
317
348
  Аль нэг алхам бүтэлгүйтвэл `ValidationError` шиднэ — хариунд **итгэхгүй**.
318
349
 
@@ -318,4 +318,4 @@ declare class ValidationError extends EidError {
318
318
  declare class DeviceLinkError extends EidError {
319
319
  }
320
320
 
321
- export { type AcspContext as A, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type HashAlgorithm as H, type Interaction as I, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PdfDocumentStatus as P, type RpCredentials as R, type SignatureAlgorithm as S, ValidationError as V, type SessionResult as a, type RepresentationsResponse as b, ApiError as c, AuthenticationError as d, DeviceLinkError as e, type DeviceLinkInput as f, type EndResult as g, ForbiddenError as h, type NavigatorLike as i, NetworkError as j, type OnBehalfErrorCode as k, type OpenOrShowQROptions as l, type OrgResult as m, type PdfSessionBlock as n, type PdfSigner as o, type Representation as p, SessionFailedError as q, type SessionState as r, type SignatureAlgorithmParameters as s, type SignatureProtocol as t, deviceLink as u, isMobileUserAgent as v, openOrShowQR as w };
321
+ export { type AcspContext as A, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type HashAlgorithm as H, type Interaction as I, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PdfSessionBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, ValidationError as V, type SessionResult as a, type RepresentationsResponse as b, ApiError as c, AuthenticationError as d, DeviceLinkError as e, type DeviceLinkInput as f, type EndResult as g, ForbiddenError as h, type NavigatorLike as i, NetworkError as j, type OnBehalfErrorCode as k, type OpenOrShowQROptions as l, type OrgResult as m, type PdfDocumentStatus as n, type PdfSigner as o, type Representation as p, SessionFailedError as q, type SessionState as r, type SignatureAlgorithmParameters as s, type SignatureProtocol as t, deviceLink as u, isMobileUserAgent as v, openOrShowQR as w };
@@ -318,4 +318,4 @@ declare class ValidationError extends EidError {
318
318
  declare class DeviceLinkError extends EidError {
319
319
  }
320
320
 
321
- export { type AcspContext as A, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type HashAlgorithm as H, type Interaction as I, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PdfDocumentStatus as P, type RpCredentials as R, type SignatureAlgorithm as S, ValidationError as V, type SessionResult as a, type RepresentationsResponse as b, ApiError as c, AuthenticationError as d, DeviceLinkError as e, type DeviceLinkInput as f, type EndResult as g, ForbiddenError as h, type NavigatorLike as i, NetworkError as j, type OnBehalfErrorCode as k, type OpenOrShowQROptions as l, type OrgResult as m, type PdfSessionBlock as n, type PdfSigner as o, type Representation as p, SessionFailedError as q, type SessionState as r, type SignatureAlgorithmParameters as s, type SignatureProtocol as t, deviceLink as u, isMobileUserAgent as v, openOrShowQR as w };
321
+ export { type AcspContext as A, type CertificateLevel as C, type DeviceLinkSession as D, EidError as E, type FlowType as F, type HashAlgorithm as H, type Interaction as I, type NotificationSession as N, ON_BEHALF_ERROR_CODES as O, type PdfSessionBlock as P, type RpCredentials as R, type SignatureAlgorithm as S, ValidationError as V, type SessionResult as a, type RepresentationsResponse as b, ApiError as c, AuthenticationError as d, DeviceLinkError as e, type DeviceLinkInput as f, type EndResult as g, ForbiddenError as h, type NavigatorLike as i, NetworkError as j, type OnBehalfErrorCode as k, type OpenOrShowQROptions as l, type OrgResult as m, type PdfDocumentStatus as n, type PdfSigner as o, type Representation as p, SessionFailedError as q, type SessionState as r, type SignatureAlgorithmParameters as s, type SignatureProtocol as t, deviceLink as u, isMobileUserAgent as v, openOrShowQR as w };
@@ -1 +1 @@
1
- export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.cjs';
1
+ export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-B_XtyuFG.cjs';
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.js';
1
+ export { e as DeviceLinkError, f as DeviceLinkInput, D as DeviceLinkSession, E as EidError, i as NavigatorLike, l as OpenOrShowQROptions, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-B_XtyuFG.js';
package/dist/index.cjs CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  isMobileUserAgent: () => isMobileUserAgent,
52
52
  openOrShowQR: () => openOrShowQR,
53
53
  parseSessionResult: () => parseSessionResult,
54
+ parseSubject: () => parseSubject,
54
55
  pkceChallenge: () => pkceChallenge,
55
56
  qrDeviceLinkTicker: () => qrDeviceLinkTicker,
56
57
  randomChallenge: () => randomChallenge,
@@ -480,6 +481,7 @@ var SessionApi = class {
480
481
  };
481
482
 
482
483
  // src/pdf.ts
484
+ var import_node_crypto2 = require("crypto");
483
485
  function rec3(v) {
484
486
  return v && typeof v === "object" ? v : {};
485
487
  }
@@ -512,10 +514,12 @@ var PdfApi = class {
512
514
  const receivedAt = Date.now();
513
515
  const vc = rec3(r.vc).value;
514
516
  const dl = rec3(r.deviceLink);
517
+ const digest = typeof r.digest === "string" && r.digest ? r.digest : typeof dl.digest === "string" && dl.digest ? dl.digest : void 0;
515
518
  return {
516
519
  sessionId: String(r.sessionID),
517
520
  docId: String(r.docID),
518
521
  vc: vc != null ? String(vc) : null,
522
+ ...digest !== void 0 ? { digest } : {},
519
523
  deviceLink: dl.sessionToken ? {
520
524
  sessionToken: String(dl.sessionToken),
521
525
  sessionSecret: String(dl.sessionSecret),
@@ -533,6 +537,26 @@ var PdfApi = class {
533
537
  document(sessionId) {
534
538
  return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
535
539
  }
540
+ /**
541
+ * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
542
+ * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
543
+ *
544
+ * @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
545
+ */
546
+ async documentVerified(sessionId, expected) {
547
+ let want;
548
+ if (typeof expected === "string") {
549
+ want = expected;
550
+ } else {
551
+ if (expected.documentStatus !== "READY") throw new ValidationError(`pdf.documentStatus ${expected.documentStatus} \u2260 READY`);
552
+ want = expected.outSha256;
553
+ }
554
+ 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");
555
+ const bytes = await this.document(sessionId);
556
+ const got = (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
557
+ 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");
558
+ return bytes;
559
+ }
536
560
  };
537
561
 
538
562
  // src/organization.ts
@@ -550,7 +574,114 @@ var OrganizationApi = class {
550
574
  };
551
575
 
552
576
  // src/validator.ts
553
- var import_node_crypto2 = require("crypto");
577
+ var import_node_crypto4 = require("crypto");
578
+
579
+ // src/subject.ts
580
+ var import_node_crypto3 = require("crypto");
581
+ var OIDS = {
582
+ "2.5.4.42": "givenName",
583
+ "2.5.4.4": "surname",
584
+ "2.5.4.3": "commonName",
585
+ "2.5.4.5": "serialNumber",
586
+ "2.5.4.6": "country",
587
+ "2.5.4.10": "organization",
588
+ "2.5.4.97": "organizationIdentifier"
589
+ };
590
+ function tlv(buf, pos, limit) {
591
+ if (pos + 2 > limit) throw new ValidationError("subject: DER \u0442\u043E\u043B\u0433\u043E\u0439 \u0431\u043E\u0433\u0438\u043D\u043E");
592
+ const tag = buf[pos];
593
+ if ((tag & 31) === 31) throw new ValidationError("subject: \u043E\u043B\u043E\u043D \u0431\u0430\u0439\u0442 tag \u0434\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439");
594
+ let len = buf[pos + 1];
595
+ let i = pos + 2;
596
+ if (len & 128) {
597
+ const n = len & 127;
598
+ if (n === 0 || n > 3) throw new ValidationError("subject: DER \u0443\u0440\u0442 \u0431\u0443\u0440\u0443\u0443");
599
+ if (i + n > limit) throw new ValidationError("subject: DER \u0443\u0440\u0442 \u0431\u043E\u0433\u0438\u043D\u043E");
600
+ len = 0;
601
+ for (let k = 0; k < n; k++) len = len * 256 + buf[i++];
602
+ }
603
+ if (i + len > limit) throw new ValidationError("subject: DER \u0430\u0433\u0443\u0443\u043B\u0433\u0430 \u0445\u0438\u043B\u044D\u044D\u0441 \u0433\u0430\u0440\u043D\u0430");
604
+ return { tag, start: i, end: i + len };
605
+ }
606
+ function oidOf(buf, t) {
607
+ if (t.tag !== 6 || t.end === t.start) throw new ValidationError("subject: OID \u0431\u0438\u0448");
608
+ const parts = [];
609
+ let v = 0;
610
+ for (let i = t.start; i < t.end; i++) {
611
+ const b = buf[i];
612
+ v = v * 128 + (b & 127);
613
+ if (!(b & 128)) {
614
+ if (parts.length === 0) parts.push(v < 80 ? Math.floor(v / 40) : 2, v < 80 ? v % 40 : v - 80);
615
+ else parts.push(v);
616
+ v = 0;
617
+ }
618
+ }
619
+ return parts.join(".");
620
+ }
621
+ function stringOf(buf, t) {
622
+ const bytes = buf.subarray(t.start, t.end);
623
+ switch (t.tag) {
624
+ case 12:
625
+ try {
626
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
627
+ } catch {
628
+ throw new ValidationError("subject: UTF8String \u0431\u0443\u0440\u0443\u0443 UTF-8");
629
+ }
630
+ case 19:
631
+ // PrintableString
632
+ case 22:
633
+ // IA5String
634
+ case 20:
635
+ return Buffer.from(bytes).toString("latin1");
636
+ case 30: {
637
+ if (bytes.length % 2) throw new ValidationError("subject: BMPString \u0443\u0440\u0442 \u0441\u043E\u043D\u0434\u0433\u043E\u0439");
638
+ return Buffer.from(bytes).swap16().toString("utf16le");
639
+ }
640
+ default:
641
+ return void 0;
642
+ }
643
+ }
644
+ function parseSubject(cert) {
645
+ const der = cert instanceof import_node_crypto3.X509Certificate ? new Uint8Array(cert.raw) : typeof cert === "string" ? new Uint8Array(Buffer.from(cert, "base64")) : cert;
646
+ const top = tlv(der, 0, der.length);
647
+ if (top.tag !== 48) throw new ValidationError("subject: Certificate SEQUENCE \u0431\u0438\u0448");
648
+ const tbs = tlv(der, top.start, top.end);
649
+ if (tbs.tag !== 48) throw new ValidationError("subject: tbsCertificate SEQUENCE \u0431\u0438\u0448");
650
+ let pos = tbs.start;
651
+ let first = tlv(der, pos, tbs.end);
652
+ if (first.tag === 160) {
653
+ pos = first.end;
654
+ first = tlv(der, pos, tbs.end);
655
+ }
656
+ let field = first;
657
+ for (let k = 0; k < 4; k++) field = tlv(der, field.end, tbs.end);
658
+ if (field.tag !== 48) throw new ValidationError("subject: Name SEQUENCE \u0431\u0438\u0448");
659
+ const out = { name: null, attributes: {} };
660
+ for (let sp = field.start; sp < field.end; ) {
661
+ const rdn = tlv(der, sp, field.end);
662
+ if (rdn.tag !== 49) throw new ValidationError("subject: RDN SET \u0431\u0438\u0448");
663
+ for (let ap = rdn.start; ap < rdn.end; ) {
664
+ const atv = tlv(der, ap, rdn.end);
665
+ if (atv.tag !== 48) throw new ValidationError("subject: AttributeTypeAndValue SEQUENCE \u0431\u0438\u0448");
666
+ const oidT = tlv(der, atv.start, atv.end);
667
+ const valT = tlv(der, oidT.end, atv.end);
668
+ const oid = oidOf(der, oidT);
669
+ const value = stringOf(der, valT);
670
+ if (value !== void 0 && !(oid in out.attributes)) {
671
+ out.attributes[oid] = value;
672
+ const key = OIDS[oid];
673
+ if (key) out[key] = value;
674
+ }
675
+ ap = atv.end;
676
+ }
677
+ sp = rdn.end;
678
+ }
679
+ const full = [out.givenName, out.surname].filter((s) => s).join(" ");
680
+ out.name = full || out.commonName || null;
681
+ return out;
682
+ }
683
+
684
+ // src/validator.ts
554
685
  var ResponseValidator = class {
555
686
  anchors;
556
687
  intermediates;
@@ -560,8 +691,8 @@ var ResponseValidator = class {
560
691
  revocation;
561
692
  revocationMode;
562
693
  constructor(trust = {}) {
563
- this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new import_node_crypto2.X509Certificate(p));
564
- this.intermediates = (trust.intermediatesPem ?? []).map((p) => new import_node_crypto2.X509Certificate(p));
694
+ this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new import_node_crypto4.X509Certificate(p));
695
+ this.intermediates = (trust.intermediatesPem ?? []).map((p) => new import_node_crypto4.X509Certificate(p));
565
696
  this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
566
697
  this.skew = trust.clockSkewMs ?? 0;
567
698
  this.allowUntrusted = trust.allowUntrusted ?? false;
@@ -592,7 +723,7 @@ var ResponseValidator = class {
592
723
  throw new ValidationError("userChallengeVerifier \u043D\u044C \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0437\u0443\u0440\u0430\u0433\u0434\u0441\u0430\u043D userChallenge-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
593
724
  }
594
725
  if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
595
- const { alg, hash } = this.algorithmOf(result, acsp.signatureAlgorithm, acsp.hashAlgorithm);
726
+ const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
596
727
  const payload = buildAcspV2Payload({
597
728
  serverRandom,
598
729
  rpChallenge: acsp.rpChallenge,
@@ -611,17 +742,22 @@ var ResponseValidator = class {
611
742
  documentNumber: result.documentNumber,
612
743
  certificate: leaf,
613
744
  subject: leaf.subject,
745
+ parsedSubject: parseSubject(leaf),
614
746
  certificateLevel: String(result.certificateLevel ?? "")
615
747
  };
616
748
  }
617
749
  /**
618
750
  * Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
619
- * @param hashAlgorithm — digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256).
751
+ * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
620
752
  */
621
- validateSign(result, digestB64, hashAlgorithm = "SHA-256") {
753
+ validateSign(result, digestB64, opts = {}) {
754
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
622
755
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
756
+ if (expectedFlowType && result.flowType !== expectedFlowType) {
757
+ throw new ValidationError(`flowType ${result.flowType} \u2260 \u044D\u0445\u043B\u04AF\u04AF\u043B\u0441\u044D\u043D \u0443\u0440\u0441\u0433\u0430\u043B ${expectedFlowType} (relay \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)`);
758
+ }
623
759
  const digest = Buffer.from(digestB64, "base64");
624
- const { alg, hash } = this.algorithmOf(result, null, hashAlgorithm);
760
+ const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
625
761
  if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
626
762
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
627
763
  throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u04E9\u0433\u0441\u04E9\u043D digest-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
@@ -631,27 +767,47 @@ var ResponseValidator = class {
631
767
  signatureValueB64: result.signatureValueB64,
632
768
  signatureAlgorithm: result.signatureAlgorithm,
633
769
  certificate: leaf,
634
- subject: leaf.subject
770
+ subject: leaf.subject,
771
+ parsedSubject: parseSubject(leaf)
635
772
  };
636
773
  }
637
774
  /**
638
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
639
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
775
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
776
+ * (downgrade / alg confusion хаалт).
777
+ *
778
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
779
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
780
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
781
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
782
+ * Иймд:
783
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
784
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
785
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
640
786
  */
641
- algorithmOf(result, wantAlg, wantHash) {
787
+ algorithmOf(result, leaf, wantAlg, wantHash) {
642
788
  const alg = result.signatureAlgorithm ?? "";
789
+ const keyType = leaf.publicKey.asymmetricKeyType;
643
790
  let hash;
644
791
  if (alg === "rsassa-pss") {
645
792
  hash = String(result.signatureAlgorithmParameters?.hashAlgorithm ?? "");
793
+ if (!HASHES.has(hash)) throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 hashAlgorithm: ${hash}`);
646
794
  const mgf = result.signatureAlgorithmParameters?.maskGenAlgorithm?.parameters?.hashAlgorithm;
647
795
  if (mgf && mgf !== hash) throw new ValidationError("MGF1 hash \u043D\u044C hashAlgorithm-\u0430\u0430\u0441 \u04E9\u04E9\u0440");
648
796
  if (result.signatureAlgorithmParameters?.saltLength != null && result.signatureAlgorithmParameters.saltLength !== hashLen(hash)) {
649
797
  throw new ValidationError("saltLength \u2260 hLen");
650
798
  }
799
+ if (keyType !== "rsa" && keyType !== "rsa-pss") throw new ValidationError(`${alg} \u043D\u044C ${keyType} \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
651
800
  } else if (/^sha(256|384|512)WithRSAEncryption$/.test(alg)) {
652
801
  hash = "SHA-" + alg.slice(3, 6);
802
+ if (keyType !== "rsa") throw new ValidationError(`${alg} \u043D\u044C ${keyType} \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
653
803
  } else if (alg === "ecdsa-with-SHA256") {
654
804
  hash = "SHA-256";
805
+ if (keyType !== "ec" || leaf.publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
806
+ throw new ValidationError(`${alg} \u043D\u044C EC P-256 \u0431\u0443\u0441 \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
807
+ }
808
+ if (wantHash !== hash) {
809
+ throw new ValidationError(`ecdsa-with-SHA256 \u0445\u0430\u0440\u0438\u0443 ${wantHash} \u0445\u04AF\u0441\u0441\u044D\u043D session-\u0434 \u0431\u043E\u043B\u043E\u043C\u0436\u0433\u04AF\u0439 (ECDSA credential \u0437\u04E9\u0432\u0445\u04E9\u043D SHA-256)`);
810
+ }
655
811
  } else {
656
812
  throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 signatureAlgorithm: ${alg}`);
657
813
  }
@@ -672,7 +828,7 @@ var ResponseValidator = class {
672
828
  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");
673
829
  if (!result.signatureValueB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0430\u043B\u0433\u0430");
674
830
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
675
- const leaf = new import_node_crypto2.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
831
+ const leaf = new import_node_crypto4.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
676
832
  this.checkValidity(leaf);
677
833
  this.checkLevel(String(result.certificateLevel ?? ""));
678
834
  if (this.anchors.length === 0) {
@@ -750,16 +906,16 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
750
906
  return fromResponse;
751
907
  }
752
908
  function userChallengeOf(verifier) {
753
- return (0, import_node_crypto2.createHash)("sha256").update(verifier, "utf8").digest("base64url");
909
+ return (0, import_node_crypto4.createHash)("sha256").update(verifier, "utf8").digest("base64url");
754
910
  }
755
911
  function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
756
912
  const sig = Buffer.from(sigB64, "base64");
757
913
  const h = nodeHash(hash);
758
914
  if (alg === "rsassa-pss") {
759
- return (0, import_node_crypto2.verify)(h, payload, { key, padding: import_node_crypto2.constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
915
+ return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
760
916
  }
761
- if (alg === "ecdsa-with-SHA256") return (0, import_node_crypto2.verify)(h, payload, { key, dsaEncoding: "der" }, sig);
762
- return (0, import_node_crypto2.verify)(h, payload, { key, padding: import_node_crypto2.constants.RSA_PKCS1_PADDING }, sig);
917
+ if (alg === "ecdsa-with-SHA256") return (0, import_node_crypto4.verify)(h, payload, { key, dsaEncoding: "der" }, sig);
918
+ return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PADDING }, sig);
763
919
  }
764
920
  function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
765
921
  const sig = Buffer.from(sigB64, "base64");
@@ -768,7 +924,7 @@ function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
768
924
  if (!modBits || sig.length !== Math.ceil(modBits / 8)) return false;
769
925
  let em;
770
926
  try {
771
- em = (0, import_node_crypto2.publicDecrypt)({ key, padding: import_node_crypto2.constants.RSA_NO_PADDING }, sig);
927
+ em = (0, import_node_crypto4.publicDecrypt)({ key, padding: import_node_crypto4.constants.RSA_NO_PADDING }, sig);
772
928
  } catch {
773
929
  return false;
774
930
  }
@@ -841,6 +997,7 @@ function ecdsaP256VerifyPrehashed(digest, sig, key) {
841
997
  const R = ptAdd(ptMul(mod(z * w, n), [P256.gx, P256.gy]), ptMul(mod(r * w, n), Q));
842
998
  return R !== null && mod(R[0], n) === r;
843
999
  }
1000
+ var HASHES = /* @__PURE__ */ new Set(["SHA-256", "SHA-384", "SHA-512"]);
844
1001
  var DIGEST_INFO = {
845
1002
  "SHA-256": Buffer.from("3031300d060960864801650304020105000420", "hex"),
846
1003
  "SHA-384": Buffer.from("3041300d060960864801650304020205000430", "hex"),
@@ -851,7 +1008,7 @@ function mgf1(hash, seed, length) {
851
1008
  for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
852
1009
  const c = Buffer.alloc(4);
853
1010
  c.writeUInt32BE(i, 0);
854
- out.push((0, import_node_crypto2.createHash)(nodeHash(hash)).update(seed).update(c).digest());
1011
+ out.push((0, import_node_crypto4.createHash)(nodeHash(hash)).update(seed).update(c).digest());
855
1012
  }
856
1013
  return Buffer.concat(out).subarray(0, length);
857
1014
  }
@@ -875,7 +1032,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
875
1032
  const psLen = emLen - hLen - sLen - 2;
876
1033
  if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
877
1034
  const salt = db.subarray(psLen + 1);
878
- const h2 = (0, import_node_crypto2.createHash)(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
1035
+ const h2 = (0, import_node_crypto4.createHash)(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
879
1036
  return h.equals(h2);
880
1037
  }
881
1038
  function isCa(cert) {
@@ -970,7 +1127,7 @@ function openOrShowQR(session, opts) {
970
1127
  }
971
1128
 
972
1129
  // src/devicelinkv3.ts
973
- var import_node_crypto3 = require("crypto");
1130
+ var import_node_crypto5 = require("crypto");
974
1131
  var TYPES = ["QR", "Web2App", "App2App"];
975
1132
  var PROTOCOL = { auth: "ACSP_V2", sign: "RAW_DIGEST_SIGNATURE", cert: "" };
976
1133
  var TOKEN_RE = /^[A-Za-z0-9_-]+$/;
@@ -1027,7 +1184,7 @@ function buildDeviceLink(p) {
1027
1184
  callback,
1028
1185
  unprotected
1029
1186
  ].join("|");
1030
- const authCode = (0, import_node_crypto3.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1187
+ const authCode = (0, import_node_crypto5.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1031
1188
  return `${unprotected}&authCode=${authCode}`;
1032
1189
  }
1033
1190
  function qrDeviceLinkTicker(input, onLink, opts) {
@@ -1040,7 +1197,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1040
1197
  }
1041
1198
 
1042
1199
  // src/oidc.ts
1043
- var import_node_crypto4 = require("crypto");
1200
+ var import_node_crypto6 = require("crypto");
1044
1201
  var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1045
1202
  var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1046
1203
  var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
@@ -1297,7 +1454,7 @@ function formEncode(s) {
1297
1454
  function timingSafeEqualStr(a, b) {
1298
1455
  const x = Buffer.from(a, "utf8");
1299
1456
  const y = Buffer.from(b, "utf8");
1300
- return x.length === y.length && (0, import_node_crypto4.timingSafeEqual)(x, y);
1457
+ return x.length === y.length && (0, import_node_crypto6.timingSafeEqual)(x, y);
1301
1458
  }
1302
1459
  function decodeJsonPart(s) {
1303
1460
  try {
@@ -1348,6 +1505,7 @@ function callbackParams(input) {
1348
1505
  isMobileUserAgent,
1349
1506
  openOrShowQR,
1350
1507
  parseSessionResult,
1508
+ parseSubject,
1351
1509
  pkceChallenge,
1352
1510
  qrDeviceLinkTicker,
1353
1511
  randomChallenge,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-DqKDj2nB.cjs';
2
- export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.cjs';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PdfSessionBlock, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-B_XtyuFG.cjs';
2
+ export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, n as PdfDocumentStatus, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-B_XtyuFG.cjs';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -172,6 +172,12 @@ interface PdfPrepareSession {
172
172
  docId: string;
173
173
  /** Иргэнд ХАРУУЛАХ баталгаажуулах код (ПИН биш). */
174
174
  vc: string | null;
175
+ /**
176
+ * Иргэний зурах DTBS (base64, SHA-256 = 32 байт) — урсгалаас үл хамааран. Хариуны гарын үсгийг өөрөө шалгахад:
177
+ * `validator.validateSign(result, digest)`. Хуучин CA push хариунд талбаргүй → undefined (device-link-д
178
+ * `deviceLink.digest`-ээс).
179
+ */
180
+ digest?: string;
175
181
  deviceLink: {
176
182
  sessionToken: string;
177
183
  sessionSecret: string;
@@ -194,6 +200,13 @@ declare class PdfApi {
194
200
  * session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
195
201
  */
196
202
  document(sessionId: string): Promise<Uint8Array>;
203
+ /**
204
+ * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
205
+ * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
206
+ *
207
+ * @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
208
+ */
209
+ documentVerified(sessionId: string, expected: PdfSessionBlock | string): Promise<Uint8Array>;
197
210
  }
198
211
 
199
212
  declare class OrganizationApi {
@@ -203,6 +216,36 @@ declare class OrganizationApi {
203
216
  getRepresentations(personEtsi: string): Promise<RepresentationsResponse>;
204
217
  }
205
218
 
219
+ /** Задалсан subject. Cert-д байхгүй талбар — undefined. Давхардсан OID-ийн эхнийх нь. */
220
+ interface CertSubject {
221
+ /** 2.5.4.42 GIVENNAME. */
222
+ givenName?: string;
223
+ /** 2.5.4.4 SURNAME. */
224
+ surname?: string;
225
+ /** 2.5.4.3 CN. */
226
+ commonName?: string;
227
+ /** 2.5.4.5 SERIALNUMBER — иргэнд ETSI (`PNOMN-<иргэний дугаар>`). */
228
+ serialNumber?: string;
229
+ /** 2.5.4.6 C (ж: "MN"). */
230
+ country?: string;
231
+ /** 2.5.4.10 O. */
232
+ organization?: string;
233
+ /** 2.5.4.97 organizationIdentifier (ж: `NTRMN-…`). */
234
+ organizationIdentifier?: string;
235
+ /** Харуулах нэр: «givenName surname», эс бөгөөс commonName, эс бөгөөс null. */
236
+ name: string | null;
237
+ /** Бүх атрибут, OID → утга (эхний утга). Дээрхээс бусад OID-д. */
238
+ attributes: Record<string, string>;
239
+ }
240
+ /**
241
+ * Гэрчилгээний subject-ийг задална. `cert` — `X509Certificate`, DER байт, эсвэл base64 DER
242
+ * (session хариуны `certificateDerB64`). Гэмтсэн DER → {@link ValidationError}.
243
+ *
244
+ * (!) Энэ нь зөвхөн задлалт — гэрчилгээнд итгэх эсэхийг `ResponseValidator` шийднэ. Баталгаажаагүй
245
+ * гэрчилгээний нэрэнд бүү итгэ: `validateAuth`/`validateSign`-ийн `parsedSubject`-ийг ашигла.
246
+ */
247
+ declare function parseSubject(cert: X509Certificate | Uint8Array | string): CertSubject;
248
+
206
249
  /**
207
250
  * validateAuth-ийн нэмэлт шалгалт — RP ЯМАР урсгал эхлүүлснээ мэддэг тул түүнийг тулгана
208
251
  * (device-link relay хаалт).
@@ -228,6 +271,20 @@ interface ValidateAuthOptions {
228
271
  */
229
272
  userChallengeVerifier?: string;
230
273
  }
274
+ /**
275
+ * validateSign-ийн нэмэлт шалгалт. Гуравдах параметрт мөр (`hashAlgorithm`) эсвэл энэ объектыг өгнө.
276
+ */
277
+ interface ValidateSignOptions {
278
+ /** digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256). */
279
+ hashAlgorithm?: string;
280
+ /**
281
+ * RP-ийн эхлүүлсэн урсгал (device-link: "QR" / "Web2App" / "App2App"; push → "Notification"). Өгвөл хариуны
282
+ * `flowType` ЯГ ИЖИЛ байх ёстой, эс бөгөөс `ValidationError` — validateAuth-ийн `expectedFlowType`-тэй ижил.
283
+ * ⚠️ RAW_DIGEST_SIGNATURE-д `flowType` гарын үсэгт ОРДОГГҮЙ (зөвхөн digest зурагдана) — энэ нь RP-API хариуны
284
+ * тулгалт, криптограф баталгаа биш.
285
+ */
286
+ expectedFlowType?: FlowType;
287
+ }
231
288
  /**
232
289
  * RevocationChecker — иргэний cert revoke хийгдсэн эсэхийг шалгах hook (OCSP/CRL).
233
290
  * Сүлжээний I/O шаардсан тул async. RP нь cert доторх AIA (OCSP) / CDP (CRL) URL-аар
@@ -271,6 +328,8 @@ interface VerifiedIdentity {
271
328
  certificate: X509Certificate;
272
329
  /** Cert subject DN (түүхий). Ж: "C=MN, CN=..., SERIALNUMBER=PNOMN-...". */
273
330
  subject: string;
331
+ /** Subject-ийн задалсан талбарууд (нэр, ETSI `serialNumber`, улс) — {@link parseSubject}. */
332
+ parsedSubject: CertSubject;
274
333
  certificateLevel: string;
275
334
  }
276
335
  /** Баталгаажсан гарын үсэг (sign). */
@@ -280,6 +339,8 @@ interface VerifiedSignature {
280
339
  signatureAlgorithm: string | null;
281
340
  certificate: X509Certificate;
282
341
  subject: string;
342
+ /** Subject-ийн задалсан талбарууд — {@link parseSubject}. */
343
+ parsedSubject: CertSubject;
283
344
  }
284
345
  declare class ResponseValidator {
285
346
  private readonly anchors;
@@ -297,12 +358,21 @@ declare class ResponseValidator {
297
358
  validateAuth(result: SessionResult, acsp: AcspContext, opts?: ValidateAuthOptions): VerifiedIdentity;
298
359
  /**
299
360
  * Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
300
- * @param hashAlgorithm — digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256).
361
+ * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
301
362
  */
302
- validateSign(result: SessionResult, digestB64: string, hashAlgorithm?: string): VerifiedSignature;
363
+ validateSign(result: SessionResult, digestB64: string, opts?: string | ValidateSignOptions): VerifiedSignature;
303
364
  /**
304
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
305
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
365
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
366
+ * (downgrade / alg confusion хаалт).
367
+ *
368
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
369
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
370
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
371
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
372
+ * Иймд:
373
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
374
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
375
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
306
376
  */
307
377
  private algorithmOf;
308
378
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
@@ -593,4 +663,4 @@ declare class EidOidcClient {
593
663
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
664
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
665
 
596
- export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
666
+ export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, parseSubject, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-DqKDj2nB.js';
2
- export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, P as PdfDocumentStatus, n as PdfSessionBlock, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-DqKDj2nB.js';
1
+ import { R as RpCredentials, C as CertificateLevel, I as Interaction, S as SignatureAlgorithm, H as HashAlgorithm, N as NotificationSession, D as DeviceLinkSession, a as SessionResult, P as PdfSessionBlock, b as RepresentationsResponse, A as AcspContext, F as FlowType, E as EidError } from './browser-B_XtyuFG.js';
2
+ export { c as ApiError, d as AuthenticationError, e as DeviceLinkError, f as DeviceLinkInput, g as EndResult, h as ForbiddenError, i as NavigatorLike, j as NetworkError, O as ON_BEHALF_ERROR_CODES, k as OnBehalfErrorCode, l as OpenOrShowQROptions, m as OrgResult, n as PdfDocumentStatus, o as PdfSigner, p as Representation, q as SessionFailedError, r as SessionState, s as SignatureAlgorithmParameters, t as SignatureProtocol, V as ValidationError, u as deviceLink, v as isMobileUserAgent, w as openOrShowQR } from './browser-B_XtyuFG.js';
3
3
  import { X509Certificate, KeyObject } from 'node:crypto';
4
4
 
5
5
  interface HttpConfig {
@@ -172,6 +172,12 @@ interface PdfPrepareSession {
172
172
  docId: string;
173
173
  /** Иргэнд ХАРУУЛАХ баталгаажуулах код (ПИН биш). */
174
174
  vc: string | null;
175
+ /**
176
+ * Иргэний зурах DTBS (base64, SHA-256 = 32 байт) — урсгалаас үл хамааран. Хариуны гарын үсгийг өөрөө шалгахад:
177
+ * `validator.validateSign(result, digest)`. Хуучин CA push хариунд талбаргүй → undefined (device-link-д
178
+ * `deviceLink.digest`-ээс).
179
+ */
180
+ digest?: string;
175
181
  deviceLink: {
176
182
  sessionToken: string;
177
183
  sessionSecret: string;
@@ -194,6 +200,13 @@ declare class PdfApi {
194
200
  * session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
195
201
  */
196
202
  document(sessionId: string): Promise<Uint8Array>;
203
+ /**
204
+ * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
205
+ * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
206
+ *
207
+ * @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
208
+ */
209
+ documentVerified(sessionId: string, expected: PdfSessionBlock | string): Promise<Uint8Array>;
197
210
  }
198
211
 
199
212
  declare class OrganizationApi {
@@ -203,6 +216,36 @@ declare class OrganizationApi {
203
216
  getRepresentations(personEtsi: string): Promise<RepresentationsResponse>;
204
217
  }
205
218
 
219
+ /** Задалсан subject. Cert-д байхгүй талбар — undefined. Давхардсан OID-ийн эхнийх нь. */
220
+ interface CertSubject {
221
+ /** 2.5.4.42 GIVENNAME. */
222
+ givenName?: string;
223
+ /** 2.5.4.4 SURNAME. */
224
+ surname?: string;
225
+ /** 2.5.4.3 CN. */
226
+ commonName?: string;
227
+ /** 2.5.4.5 SERIALNUMBER — иргэнд ETSI (`PNOMN-<иргэний дугаар>`). */
228
+ serialNumber?: string;
229
+ /** 2.5.4.6 C (ж: "MN"). */
230
+ country?: string;
231
+ /** 2.5.4.10 O. */
232
+ organization?: string;
233
+ /** 2.5.4.97 organizationIdentifier (ж: `NTRMN-…`). */
234
+ organizationIdentifier?: string;
235
+ /** Харуулах нэр: «givenName surname», эс бөгөөс commonName, эс бөгөөс null. */
236
+ name: string | null;
237
+ /** Бүх атрибут, OID → утга (эхний утга). Дээрхээс бусад OID-д. */
238
+ attributes: Record<string, string>;
239
+ }
240
+ /**
241
+ * Гэрчилгээний subject-ийг задална. `cert` — `X509Certificate`, DER байт, эсвэл base64 DER
242
+ * (session хариуны `certificateDerB64`). Гэмтсэн DER → {@link ValidationError}.
243
+ *
244
+ * (!) Энэ нь зөвхөн задлалт — гэрчилгээнд итгэх эсэхийг `ResponseValidator` шийднэ. Баталгаажаагүй
245
+ * гэрчилгээний нэрэнд бүү итгэ: `validateAuth`/`validateSign`-ийн `parsedSubject`-ийг ашигла.
246
+ */
247
+ declare function parseSubject(cert: X509Certificate | Uint8Array | string): CertSubject;
248
+
206
249
  /**
207
250
  * validateAuth-ийн нэмэлт шалгалт — RP ЯМАР урсгал эхлүүлснээ мэддэг тул түүнийг тулгана
208
251
  * (device-link relay хаалт).
@@ -228,6 +271,20 @@ interface ValidateAuthOptions {
228
271
  */
229
272
  userChallengeVerifier?: string;
230
273
  }
274
+ /**
275
+ * validateSign-ийн нэмэлт шалгалт. Гуравдах параметрт мөр (`hashAlgorithm`) эсвэл энэ объектыг өгнө.
276
+ */
277
+ interface ValidateSignOptions {
278
+ /** digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256). */
279
+ hashAlgorithm?: string;
280
+ /**
281
+ * RP-ийн эхлүүлсэн урсгал (device-link: "QR" / "Web2App" / "App2App"; push → "Notification"). Өгвөл хариуны
282
+ * `flowType` ЯГ ИЖИЛ байх ёстой, эс бөгөөс `ValidationError` — validateAuth-ийн `expectedFlowType`-тэй ижил.
283
+ * ⚠️ RAW_DIGEST_SIGNATURE-д `flowType` гарын үсэгт ОРДОГГҮЙ (зөвхөн digest зурагдана) — энэ нь RP-API хариуны
284
+ * тулгалт, криптограф баталгаа биш.
285
+ */
286
+ expectedFlowType?: FlowType;
287
+ }
231
288
  /**
232
289
  * RevocationChecker — иргэний cert revoke хийгдсэн эсэхийг шалгах hook (OCSP/CRL).
233
290
  * Сүлжээний I/O шаардсан тул async. RP нь cert доторх AIA (OCSP) / CDP (CRL) URL-аар
@@ -271,6 +328,8 @@ interface VerifiedIdentity {
271
328
  certificate: X509Certificate;
272
329
  /** Cert subject DN (түүхий). Ж: "C=MN, CN=..., SERIALNUMBER=PNOMN-...". */
273
330
  subject: string;
331
+ /** Subject-ийн задалсан талбарууд (нэр, ETSI `serialNumber`, улс) — {@link parseSubject}. */
332
+ parsedSubject: CertSubject;
274
333
  certificateLevel: string;
275
334
  }
276
335
  /** Баталгаажсан гарын үсэг (sign). */
@@ -280,6 +339,8 @@ interface VerifiedSignature {
280
339
  signatureAlgorithm: string | null;
281
340
  certificate: X509Certificate;
282
341
  subject: string;
342
+ /** Subject-ийн задалсан талбарууд — {@link parseSubject}. */
343
+ parsedSubject: CertSubject;
283
344
  }
284
345
  declare class ResponseValidator {
285
346
  private readonly anchors;
@@ -297,12 +358,21 @@ declare class ResponseValidator {
297
358
  validateAuth(result: SessionResult, acsp: AcspContext, opts?: ValidateAuthOptions): VerifiedIdentity;
298
359
  /**
299
360
  * Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
300
- * @param hashAlgorithm — digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256).
361
+ * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
301
362
  */
302
- validateSign(result: SessionResult, digestB64: string, hashAlgorithm?: string): VerifiedSignature;
363
+ validateSign(result: SessionResult, digestB64: string, opts?: string | ValidateSignOptions): VerifiedSignature;
303
364
  /**
304
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
305
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
365
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
366
+ * (downgrade / alg confusion хаалт).
367
+ *
368
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
369
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
370
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
371
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
372
+ * Иймд:
373
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
374
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
375
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
306
376
  */
307
377
  private algorithmOf;
308
378
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
@@ -593,4 +663,4 @@ declare class EidOidcClient {
593
663
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
664
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
665
 
596
- export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
666
+ export { AcspContext, type AcspV2Params, AuthApi, type AuthDefaults, type AuthOptions, type BuildDeviceLinkInput, type CertSubject, CertificateLevel, type ClientConfig, type CreateAuthorizationOptions, DEFAULT_BASE_URL, DeviceLinkSession, type DeviceLinkSessionType, type DeviceLinkType, EID_ACR_ADVANCED, EID_ACR_QUALIFIED, EID_OIDC_ISSUER, type EidAuthorization, type EidCallbackChecks, type EidCallbackResult, EidClient, EidError, type EidIdTokenClaims, EidOidcClient, type EidOidcClientConfig, type EidOidcDiscovery, EidOidcError, type EidOidcErrorCode, FlowType, HashAlgorithm, Http, type HttpConfig, Interaction, NotificationSession, OrganizationApi, PdfApi, type PdfDefaults, type PdfPrepareInput, type PdfPrepareSession, PdfSessionBlock, type PdfSignerRef, type QrDeviceLinkTickerOptions, RepresentationsResponse, ResponseValidator, type RevocationChecker, type RevocationStatus, RpCredentials, SessionApi, SessionResult, SignApi, type SignDefaults, type SignOptions, SignatureAlgorithm, type TrustConfig, type ValidateAuthOptions, type ValidateSignOptions, type VerifiedIdentity, type VerifiedSignature, acspV2Digest, buildAcspV2Payload, buildDeviceLink, interactionsToBase64, parseSessionResult, parseSubject, pkceChallenge, qrDeviceLinkTicker, randomChallenge, resolveBaseUrl, sha256Base64, userChallengeOf, verifyPayloadSignature, verifyPrehashedSignature };
package/dist/index.js CHANGED
@@ -381,6 +381,7 @@ var SessionApi = class {
381
381
  };
382
382
 
383
383
  // src/pdf.ts
384
+ import { createHash as createHash2 } from "crypto";
384
385
  function rec3(v) {
385
386
  return v && typeof v === "object" ? v : {};
386
387
  }
@@ -413,10 +414,12 @@ var PdfApi = class {
413
414
  const receivedAt = Date.now();
414
415
  const vc = rec3(r.vc).value;
415
416
  const dl = rec3(r.deviceLink);
417
+ const digest = typeof r.digest === "string" && r.digest ? r.digest : typeof dl.digest === "string" && dl.digest ? dl.digest : void 0;
416
418
  return {
417
419
  sessionId: String(r.sessionID),
418
420
  docId: String(r.docID),
419
421
  vc: vc != null ? String(vc) : null,
422
+ ...digest !== void 0 ? { digest } : {},
420
423
  deviceLink: dl.sessionToken ? {
421
424
  sessionToken: String(dl.sessionToken),
422
425
  sessionSecret: String(dl.sessionSecret),
@@ -434,6 +437,26 @@ var PdfApi = class {
434
437
  document(sessionId) {
435
438
  return this.http.getBytes(`/pdf/sign/${encodeURIComponent(sessionId)}/document`, 6e4);
436
439
  }
440
+ /**
441
+ * {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
442
+ * `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
443
+ *
444
+ * @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
445
+ */
446
+ async documentVerified(sessionId, expected) {
447
+ let want;
448
+ if (typeof expected === "string") {
449
+ want = expected;
450
+ } else {
451
+ if (expected.documentStatus !== "READY") throw new ValidationError(`pdf.documentStatus ${expected.documentStatus} \u2260 READY`);
452
+ want = expected.outSha256;
453
+ }
454
+ 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");
455
+ const bytes = await this.document(sessionId);
456
+ const got = createHash2("sha256").update(bytes).digest("hex");
457
+ 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");
458
+ return bytes;
459
+ }
437
460
  };
438
461
 
439
462
  // src/organization.ts
@@ -451,7 +474,114 @@ var OrganizationApi = class {
451
474
  };
452
475
 
453
476
  // src/validator.ts
454
- import { X509Certificate, constants, createHash as createHash2, publicDecrypt, verify } from "crypto";
477
+ import { X509Certificate as X509Certificate2, constants, createHash as createHash3, publicDecrypt, verify } from "crypto";
478
+
479
+ // src/subject.ts
480
+ import { X509Certificate } from "crypto";
481
+ var OIDS = {
482
+ "2.5.4.42": "givenName",
483
+ "2.5.4.4": "surname",
484
+ "2.5.4.3": "commonName",
485
+ "2.5.4.5": "serialNumber",
486
+ "2.5.4.6": "country",
487
+ "2.5.4.10": "organization",
488
+ "2.5.4.97": "organizationIdentifier"
489
+ };
490
+ function tlv(buf, pos, limit) {
491
+ if (pos + 2 > limit) throw new ValidationError("subject: DER \u0442\u043E\u043B\u0433\u043E\u0439 \u0431\u043E\u0433\u0438\u043D\u043E");
492
+ const tag = buf[pos];
493
+ if ((tag & 31) === 31) throw new ValidationError("subject: \u043E\u043B\u043E\u043D \u0431\u0430\u0439\u0442 tag \u0434\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439");
494
+ let len = buf[pos + 1];
495
+ let i = pos + 2;
496
+ if (len & 128) {
497
+ const n = len & 127;
498
+ if (n === 0 || n > 3) throw new ValidationError("subject: DER \u0443\u0440\u0442 \u0431\u0443\u0440\u0443\u0443");
499
+ if (i + n > limit) throw new ValidationError("subject: DER \u0443\u0440\u0442 \u0431\u043E\u0433\u0438\u043D\u043E");
500
+ len = 0;
501
+ for (let k = 0; k < n; k++) len = len * 256 + buf[i++];
502
+ }
503
+ if (i + len > limit) throw new ValidationError("subject: DER \u0430\u0433\u0443\u0443\u043B\u0433\u0430 \u0445\u0438\u043B\u044D\u044D\u0441 \u0433\u0430\u0440\u043D\u0430");
504
+ return { tag, start: i, end: i + len };
505
+ }
506
+ function oidOf(buf, t) {
507
+ if (t.tag !== 6 || t.end === t.start) throw new ValidationError("subject: OID \u0431\u0438\u0448");
508
+ const parts = [];
509
+ let v = 0;
510
+ for (let i = t.start; i < t.end; i++) {
511
+ const b = buf[i];
512
+ v = v * 128 + (b & 127);
513
+ if (!(b & 128)) {
514
+ if (parts.length === 0) parts.push(v < 80 ? Math.floor(v / 40) : 2, v < 80 ? v % 40 : v - 80);
515
+ else parts.push(v);
516
+ v = 0;
517
+ }
518
+ }
519
+ return parts.join(".");
520
+ }
521
+ function stringOf(buf, t) {
522
+ const bytes = buf.subarray(t.start, t.end);
523
+ switch (t.tag) {
524
+ case 12:
525
+ try {
526
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
527
+ } catch {
528
+ throw new ValidationError("subject: UTF8String \u0431\u0443\u0440\u0443\u0443 UTF-8");
529
+ }
530
+ case 19:
531
+ // PrintableString
532
+ case 22:
533
+ // IA5String
534
+ case 20:
535
+ return Buffer.from(bytes).toString("latin1");
536
+ case 30: {
537
+ if (bytes.length % 2) throw new ValidationError("subject: BMPString \u0443\u0440\u0442 \u0441\u043E\u043D\u0434\u0433\u043E\u0439");
538
+ return Buffer.from(bytes).swap16().toString("utf16le");
539
+ }
540
+ default:
541
+ return void 0;
542
+ }
543
+ }
544
+ function parseSubject(cert) {
545
+ const der = cert instanceof X509Certificate ? new Uint8Array(cert.raw) : typeof cert === "string" ? new Uint8Array(Buffer.from(cert, "base64")) : cert;
546
+ const top = tlv(der, 0, der.length);
547
+ if (top.tag !== 48) throw new ValidationError("subject: Certificate SEQUENCE \u0431\u0438\u0448");
548
+ const tbs = tlv(der, top.start, top.end);
549
+ if (tbs.tag !== 48) throw new ValidationError("subject: tbsCertificate SEQUENCE \u0431\u0438\u0448");
550
+ let pos = tbs.start;
551
+ let first = tlv(der, pos, tbs.end);
552
+ if (first.tag === 160) {
553
+ pos = first.end;
554
+ first = tlv(der, pos, tbs.end);
555
+ }
556
+ let field = first;
557
+ for (let k = 0; k < 4; k++) field = tlv(der, field.end, tbs.end);
558
+ if (field.tag !== 48) throw new ValidationError("subject: Name SEQUENCE \u0431\u0438\u0448");
559
+ const out = { name: null, attributes: {} };
560
+ for (let sp = field.start; sp < field.end; ) {
561
+ const rdn = tlv(der, sp, field.end);
562
+ if (rdn.tag !== 49) throw new ValidationError("subject: RDN SET \u0431\u0438\u0448");
563
+ for (let ap = rdn.start; ap < rdn.end; ) {
564
+ const atv = tlv(der, ap, rdn.end);
565
+ if (atv.tag !== 48) throw new ValidationError("subject: AttributeTypeAndValue SEQUENCE \u0431\u0438\u0448");
566
+ const oidT = tlv(der, atv.start, atv.end);
567
+ const valT = tlv(der, oidT.end, atv.end);
568
+ const oid = oidOf(der, oidT);
569
+ const value = stringOf(der, valT);
570
+ if (value !== void 0 && !(oid in out.attributes)) {
571
+ out.attributes[oid] = value;
572
+ const key = OIDS[oid];
573
+ if (key) out[key] = value;
574
+ }
575
+ ap = atv.end;
576
+ }
577
+ sp = rdn.end;
578
+ }
579
+ const full = [out.givenName, out.surname].filter((s) => s).join(" ");
580
+ out.name = full || out.commonName || null;
581
+ return out;
582
+ }
583
+
584
+ // src/validator.ts
455
585
  var ResponseValidator = class {
456
586
  anchors;
457
587
  intermediates;
@@ -461,8 +591,8 @@ var ResponseValidator = class {
461
591
  revocation;
462
592
  revocationMode;
463
593
  constructor(trust = {}) {
464
- this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate(p));
465
- this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate(p));
594
+ this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new X509Certificate2(p));
595
+ this.intermediates = (trust.intermediatesPem ?? []).map((p) => new X509Certificate2(p));
466
596
  this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
467
597
  this.skew = trust.clockSkewMs ?? 0;
468
598
  this.allowUntrusted = trust.allowUntrusted ?? false;
@@ -493,7 +623,7 @@ var ResponseValidator = class {
493
623
  throw new ValidationError("userChallengeVerifier \u043D\u044C \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0437\u0443\u0440\u0430\u0433\u0434\u0441\u0430\u043D userChallenge-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
494
624
  }
495
625
  if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
496
- const { alg, hash } = this.algorithmOf(result, acsp.signatureAlgorithm, acsp.hashAlgorithm);
626
+ const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
497
627
  const payload = buildAcspV2Payload({
498
628
  serverRandom,
499
629
  rpChallenge: acsp.rpChallenge,
@@ -512,17 +642,22 @@ var ResponseValidator = class {
512
642
  documentNumber: result.documentNumber,
513
643
  certificate: leaf,
514
644
  subject: leaf.subject,
645
+ parsedSubject: parseSubject(leaf),
515
646
  certificateLevel: String(result.certificateLevel ?? "")
516
647
  };
517
648
  }
518
649
  /**
519
650
  * Sign session-ийн хариуг бүрэн шалгаж, баталгаажсан гарын үсгийг буцаана.
520
- * @param hashAlgorithm — digest-ийг үүсгэсэн hash (хүсэлтэд илгээсэнтэй ижил; default SHA-256).
651
+ * @param opts — `hashAlgorithm` мөр (хуучин хэлбэр; default SHA-256) эсвэл {@link ValidateSignOptions}.
521
652
  */
522
- validateSign(result, digestB64, hashAlgorithm = "SHA-256") {
653
+ validateSign(result, digestB64, opts = {}) {
654
+ const { hashAlgorithm = "SHA-256", expectedFlowType } = typeof opts === "string" ? { hashAlgorithm: opts } : opts;
523
655
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
656
+ if (expectedFlowType && result.flowType !== expectedFlowType) {
657
+ throw new ValidationError(`flowType ${result.flowType} \u2260 \u044D\u0445\u043B\u04AF\u04AF\u043B\u0441\u044D\u043D \u0443\u0440\u0441\u0433\u0430\u043B ${expectedFlowType} (relay \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)`);
658
+ }
524
659
  const digest = Buffer.from(digestB64, "base64");
525
- const { alg, hash } = this.algorithmOf(result, null, hashAlgorithm);
660
+ const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
526
661
  if (digest.length !== hashLen(hash)) throw new ValidationError("digest \u0443\u0440\u0442 hashAlgorithm-\u0442\u0430\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
527
662
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
528
663
  throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u04E9\u0433\u0441\u04E9\u043D digest-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
@@ -532,27 +667,47 @@ var ResponseValidator = class {
532
667
  signatureValueB64: result.signatureValueB64,
533
668
  signatureAlgorithm: result.signatureAlgorithm,
534
669
  certificate: leaf,
535
- subject: leaf.subject
670
+ subject: leaf.subject,
671
+ parsedSubject: parseSubject(leaf)
536
672
  };
537
673
  }
538
674
  /**
539
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
540
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
675
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
676
+ * (downgrade / alg confusion хаалт).
677
+ *
678
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
679
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
680
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
681
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
682
+ * Иймд:
683
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
684
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
685
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
541
686
  */
542
- algorithmOf(result, wantAlg, wantHash) {
687
+ algorithmOf(result, leaf, wantAlg, wantHash) {
543
688
  const alg = result.signatureAlgorithm ?? "";
689
+ const keyType = leaf.publicKey.asymmetricKeyType;
544
690
  let hash;
545
691
  if (alg === "rsassa-pss") {
546
692
  hash = String(result.signatureAlgorithmParameters?.hashAlgorithm ?? "");
693
+ if (!HASHES.has(hash)) throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 hashAlgorithm: ${hash}`);
547
694
  const mgf = result.signatureAlgorithmParameters?.maskGenAlgorithm?.parameters?.hashAlgorithm;
548
695
  if (mgf && mgf !== hash) throw new ValidationError("MGF1 hash \u043D\u044C hashAlgorithm-\u0430\u0430\u0441 \u04E9\u04E9\u0440");
549
696
  if (result.signatureAlgorithmParameters?.saltLength != null && result.signatureAlgorithmParameters.saltLength !== hashLen(hash)) {
550
697
  throw new ValidationError("saltLength \u2260 hLen");
551
698
  }
699
+ if (keyType !== "rsa" && keyType !== "rsa-pss") throw new ValidationError(`${alg} \u043D\u044C ${keyType} \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
552
700
  } else if (/^sha(256|384|512)WithRSAEncryption$/.test(alg)) {
553
701
  hash = "SHA-" + alg.slice(3, 6);
702
+ if (keyType !== "rsa") throw new ValidationError(`${alg} \u043D\u044C ${keyType} \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
554
703
  } else if (alg === "ecdsa-with-SHA256") {
555
704
  hash = "SHA-256";
705
+ if (keyType !== "ec" || leaf.publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
706
+ throw new ValidationError(`${alg} \u043D\u044C EC P-256 \u0431\u0443\u0441 \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439`);
707
+ }
708
+ if (wantHash !== hash) {
709
+ throw new ValidationError(`ecdsa-with-SHA256 \u0445\u0430\u0440\u0438\u0443 ${wantHash} \u0445\u04AF\u0441\u0441\u044D\u043D session-\u0434 \u0431\u043E\u043B\u043E\u043C\u0436\u0433\u04AF\u0439 (ECDSA credential \u0437\u04E9\u0432\u0445\u04E9\u043D SHA-256)`);
710
+ }
556
711
  } else {
557
712
  throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 signatureAlgorithm: ${alg}`);
558
713
  }
@@ -573,7 +728,7 @@ var ResponseValidator = class {
573
728
  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");
574
729
  if (!result.signatureValueB64) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0430\u043B\u0433\u0430");
575
730
  if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
576
- const leaf = new X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
731
+ const leaf = new X509Certificate2(Buffer.from(result.certificateDerB64, "base64"));
577
732
  this.checkValidity(leaf);
578
733
  this.checkLevel(String(result.certificateLevel ?? ""));
579
734
  if (this.anchors.length === 0) {
@@ -651,7 +806,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
651
806
  return fromResponse;
652
807
  }
653
808
  function userChallengeOf(verifier) {
654
- return createHash2("sha256").update(verifier, "utf8").digest("base64url");
809
+ return createHash3("sha256").update(verifier, "utf8").digest("base64url");
655
810
  }
656
811
  function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
657
812
  const sig = Buffer.from(sigB64, "base64");
@@ -742,6 +897,7 @@ function ecdsaP256VerifyPrehashed(digest, sig, key) {
742
897
  const R = ptAdd(ptMul(mod(z * w, n), [P256.gx, P256.gy]), ptMul(mod(r * w, n), Q));
743
898
  return R !== null && mod(R[0], n) === r;
744
899
  }
900
+ var HASHES = /* @__PURE__ */ new Set(["SHA-256", "SHA-384", "SHA-512"]);
745
901
  var DIGEST_INFO = {
746
902
  "SHA-256": Buffer.from("3031300d060960864801650304020105000420", "hex"),
747
903
  "SHA-384": Buffer.from("3041300d060960864801650304020205000430", "hex"),
@@ -752,7 +908,7 @@ function mgf1(hash, seed, length) {
752
908
  for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
753
909
  const c = Buffer.alloc(4);
754
910
  c.writeUInt32BE(i, 0);
755
- out.push(createHash2(nodeHash(hash)).update(seed).update(c).digest());
911
+ out.push(createHash3(nodeHash(hash)).update(seed).update(c).digest());
756
912
  }
757
913
  return Buffer.concat(out).subarray(0, length);
758
914
  }
@@ -776,7 +932,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
776
932
  const psLen = emLen - hLen - sLen - 2;
777
933
  if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
778
934
  const salt = db.subarray(psLen + 1);
779
- const h2 = createHash2(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
935
+ const h2 = createHash3(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
780
936
  return h.equals(h2);
781
937
  }
782
938
  function isCa(cert) {
@@ -1206,6 +1362,7 @@ export {
1206
1362
  isMobileUserAgent,
1207
1363
  openOrShowQR,
1208
1364
  parseSessionResult,
1365
+ parseSubject,
1209
1366
  pkceChallenge,
1210
1367
  qrDeviceLinkTicker,
1211
1368
  randomChallenge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerege-systems/eid-mongolia-sdk",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",