@gerege-systems/eid-mongolia-sdk 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -198,7 +209,7 @@ try {
198
209
  // MUST: endResult OK, pdf.documentStatus READY, pdf.signer.onBehalfOf = хүссэн байгууллага,
199
210
  // татсан файлын SHA-256 = pdf.outSha256, validation.indication "valid", ltv true.
200
211
  if (r.pdf?.documentStatus !== "READY" || r.pdf.signer?.onBehalfOf !== "NTRMN-6235972") throw new Error("…");
201
- const signed = await eid.pdf.document(s.sessionId);
212
+ const signed = await eid.pdf.documentVerified(s.sessionId, r.pdf); // SHA-256 ≠ pdf.outSha256 → ValidationError
202
213
  console.log(r.pdf.signer.claimedRole); // "Гүйцэтгэх захирал, Гэрэгэ Системс ХХК (NTRMN-6235972)"
203
214
  } catch (e) {
204
215
  if ((e instanceof ForbiddenError || e instanceof ApiError) && e.code) {
@@ -212,6 +223,10 @@ try {
212
223
  // — CMS/PDF-ийг та угсардаг тул мэдүүлсэн үүргийг (signer-attributes-v2) өөрөө нэмнэ.
213
224
  ```
214
225
 
226
+ `prepare`-ийн хариу `digest` (DTBS, base64) — иргэний зурах 32 байт, push ба device-link хоёуланд (хуучин CA push-д
227
+ `undefined`). Иргэний гарын үсгийг өөрөө шалгах бол: `eid.validator.validateSign(r, s.digest!)`. Гарын үсэгтэй PDF-ийг
228
+ `pdf.document()`-ийн оронд `pdf.documentVerified(sessionId, r.pdf)`-аар тат — `READY` + `outSha256` тулгалтыг хийнэ.
229
+
215
230
  Олон талт гэрээ: А байгууллагын гаралтыг өөрчлөлтгүй Б-ийн `prepare`-д (`docID`, өөр `onBehalfOf`) илгээнэ.
216
231
  Session-ий `onBehalfOf` блок (`role`/`rightType`) нь хариу угсрах үеийн бүртгэл; `pdf.signer.onBehalfOf`/`claimedRole`
217
232
  нь баримтад бичигдсэн утга.
@@ -313,6 +328,11 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
313
328
  `""`, Web2App/App2App → таны callback) — зөвхөн `""` эсвэл
314
329
  таны илгээсэн callback байхад, Web2App/App2App-д заавал таны callback; өөр утга → татгалзана
315
330
  - **sign** → RP-ийн өгсөн digest дээр
331
+ - алгоритм нь гэрчилгээний түлхүүртэй уях: `ecdsa-with-SHA256` зөвхөн EC P-256 түлхүүрт, RSA алгоритмууд зөвхөн
332
+ RSA түлхүүрт (alg confusion хаалт). Hash нь үргэлж хүссэнтэй ЯГ тэнцүү. RP-API session-ий гарын үсэг CA дээр
333
+ SplitKey RSA (одоогийн бүх мобайл гэрчилгээ) эсвэл legacy threshold ECDSA P-256-аас гарна; ECDSA credential
334
+ зөвхөн SHA-256 digest зурдаг (өөр hash хүссэн session-д сервер гарын үсэг гаргахгүй), тиймээс SHA-384/512
335
+ хүссэн session-д `ecdsa-with-SHA256` хариу ирвэл хуурамч гэж татгалзана.
316
336
 
317
337
  Аль нэг алхам бүтэлгүйтвэл `ValidationError` шиднэ — хариунд **итгэхгүй**.
318
338
 
@@ -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,6 +742,7 @@ 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
  }
@@ -621,7 +753,7 @@ var ResponseValidator = class {
621
753
  validateSign(result, digestB64, hashAlgorithm = "SHA-256") {
622
754
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
623
755
  const digest = Buffer.from(digestB64, "base64");
624
- const { alg, hash } = this.algorithmOf(result, null, hashAlgorithm);
756
+ const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
625
757
  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
758
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
627
759
  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 +763,47 @@ var ResponseValidator = class {
631
763
  signatureValueB64: result.signatureValueB64,
632
764
  signatureAlgorithm: result.signatureAlgorithm,
633
765
  certificate: leaf,
634
- subject: leaf.subject
766
+ subject: leaf.subject,
767
+ parsedSubject: parseSubject(leaf)
635
768
  };
636
769
  }
637
770
  /**
638
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
639
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
771
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
772
+ * (downgrade / alg confusion хаалт).
773
+ *
774
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
775
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
776
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
777
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
778
+ * Иймд:
779
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
780
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
781
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
640
782
  */
641
- algorithmOf(result, wantAlg, wantHash) {
783
+ algorithmOf(result, leaf, wantAlg, wantHash) {
642
784
  const alg = result.signatureAlgorithm ?? "";
785
+ const keyType = leaf.publicKey.asymmetricKeyType;
643
786
  let hash;
644
787
  if (alg === "rsassa-pss") {
645
788
  hash = String(result.signatureAlgorithmParameters?.hashAlgorithm ?? "");
789
+ if (!HASHES.has(hash)) throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 hashAlgorithm: ${hash}`);
646
790
  const mgf = result.signatureAlgorithmParameters?.maskGenAlgorithm?.parameters?.hashAlgorithm;
647
791
  if (mgf && mgf !== hash) throw new ValidationError("MGF1 hash \u043D\u044C hashAlgorithm-\u0430\u0430\u0441 \u04E9\u04E9\u0440");
648
792
  if (result.signatureAlgorithmParameters?.saltLength != null && result.signatureAlgorithmParameters.saltLength !== hashLen(hash)) {
649
793
  throw new ValidationError("saltLength \u2260 hLen");
650
794
  }
795
+ 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
796
  } else if (/^sha(256|384|512)WithRSAEncryption$/.test(alg)) {
652
797
  hash = "SHA-" + alg.slice(3, 6);
798
+ 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
799
  } else if (alg === "ecdsa-with-SHA256") {
654
800
  hash = "SHA-256";
801
+ if (keyType !== "ec" || leaf.publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
802
+ 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`);
803
+ }
804
+ if (wantHash !== hash) {
805
+ 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)`);
806
+ }
655
807
  } else {
656
808
  throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 signatureAlgorithm: ${alg}`);
657
809
  }
@@ -672,7 +824,7 @@ var ResponseValidator = class {
672
824
  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
825
  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
826
  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"));
827
+ const leaf = new import_node_crypto4.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
676
828
  this.checkValidity(leaf);
677
829
  this.checkLevel(String(result.certificateLevel ?? ""));
678
830
  if (this.anchors.length === 0) {
@@ -750,16 +902,16 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
750
902
  return fromResponse;
751
903
  }
752
904
  function userChallengeOf(verifier) {
753
- return (0, import_node_crypto2.createHash)("sha256").update(verifier, "utf8").digest("base64url");
905
+ return (0, import_node_crypto4.createHash)("sha256").update(verifier, "utf8").digest("base64url");
754
906
  }
755
907
  function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
756
908
  const sig = Buffer.from(sigB64, "base64");
757
909
  const h = nodeHash(hash);
758
910
  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);
911
+ return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
760
912
  }
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);
913
+ if (alg === "ecdsa-with-SHA256") return (0, import_node_crypto4.verify)(h, payload, { key, dsaEncoding: "der" }, sig);
914
+ return (0, import_node_crypto4.verify)(h, payload, { key, padding: import_node_crypto4.constants.RSA_PKCS1_PADDING }, sig);
763
915
  }
764
916
  function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
765
917
  const sig = Buffer.from(sigB64, "base64");
@@ -768,7 +920,7 @@ function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
768
920
  if (!modBits || sig.length !== Math.ceil(modBits / 8)) return false;
769
921
  let em;
770
922
  try {
771
- em = (0, import_node_crypto2.publicDecrypt)({ key, padding: import_node_crypto2.constants.RSA_NO_PADDING }, sig);
923
+ em = (0, import_node_crypto4.publicDecrypt)({ key, padding: import_node_crypto4.constants.RSA_NO_PADDING }, sig);
772
924
  } catch {
773
925
  return false;
774
926
  }
@@ -841,6 +993,7 @@ function ecdsaP256VerifyPrehashed(digest, sig, key) {
841
993
  const R = ptAdd(ptMul(mod(z * w, n), [P256.gx, P256.gy]), ptMul(mod(r * w, n), Q));
842
994
  return R !== null && mod(R[0], n) === r;
843
995
  }
996
+ var HASHES = /* @__PURE__ */ new Set(["SHA-256", "SHA-384", "SHA-512"]);
844
997
  var DIGEST_INFO = {
845
998
  "SHA-256": Buffer.from("3031300d060960864801650304020105000420", "hex"),
846
999
  "SHA-384": Buffer.from("3041300d060960864801650304020205000430", "hex"),
@@ -851,7 +1004,7 @@ function mgf1(hash, seed, length) {
851
1004
  for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
852
1005
  const c = Buffer.alloc(4);
853
1006
  c.writeUInt32BE(i, 0);
854
- out.push((0, import_node_crypto2.createHash)(nodeHash(hash)).update(seed).update(c).digest());
1007
+ out.push((0, import_node_crypto4.createHash)(nodeHash(hash)).update(seed).update(c).digest());
855
1008
  }
856
1009
  return Buffer.concat(out).subarray(0, length);
857
1010
  }
@@ -875,7 +1028,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
875
1028
  const psLen = emLen - hLen - sLen - 2;
876
1029
  if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
877
1030
  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();
1031
+ const h2 = (0, import_node_crypto4.createHash)(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
879
1032
  return h.equals(h2);
880
1033
  }
881
1034
  function isCa(cert) {
@@ -970,7 +1123,7 @@ function openOrShowQR(session, opts) {
970
1123
  }
971
1124
 
972
1125
  // src/devicelinkv3.ts
973
- var import_node_crypto3 = require("crypto");
1126
+ var import_node_crypto5 = require("crypto");
974
1127
  var TYPES = ["QR", "Web2App", "App2App"];
975
1128
  var PROTOCOL = { auth: "ACSP_V2", sign: "RAW_DIGEST_SIGNATURE", cert: "" };
976
1129
  var TOKEN_RE = /^[A-Za-z0-9_-]+$/;
@@ -1027,7 +1180,7 @@ function buildDeviceLink(p) {
1027
1180
  callback,
1028
1181
  unprotected
1029
1182
  ].join("|");
1030
- const authCode = (0, import_node_crypto3.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1183
+ const authCode = (0, import_node_crypto5.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
1031
1184
  return `${unprotected}&authCode=${authCode}`;
1032
1185
  }
1033
1186
  function qrDeviceLinkTicker(input, onLink, opts) {
@@ -1040,7 +1193,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
1040
1193
  }
1041
1194
 
1042
1195
  // src/oidc.ts
1043
- var import_node_crypto4 = require("crypto");
1196
+ var import_node_crypto6 = require("crypto");
1044
1197
  var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
1045
1198
  var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
1046
1199
  var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
@@ -1297,7 +1450,7 @@ function formEncode(s) {
1297
1450
  function timingSafeEqualStr(a, b) {
1298
1451
  const x = Buffer.from(a, "utf8");
1299
1452
  const y = Buffer.from(b, "utf8");
1300
- return x.length === y.length && (0, import_node_crypto4.timingSafeEqual)(x, y);
1453
+ return x.length === y.length && (0, import_node_crypto6.timingSafeEqual)(x, y);
1301
1454
  }
1302
1455
  function decodeJsonPart(s) {
1303
1456
  try {
@@ -1348,6 +1501,7 @@ function callbackParams(input) {
1348
1501
  isMobileUserAgent,
1349
1502
  openOrShowQR,
1350
1503
  parseSessionResult,
1504
+ parseSubject,
1351
1505
  pkceChallenge,
1352
1506
  qrDeviceLinkTicker,
1353
1507
  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 хаалт).
@@ -271,6 +314,8 @@ interface VerifiedIdentity {
271
314
  certificate: X509Certificate;
272
315
  /** Cert subject DN (түүхий). Ж: "C=MN, CN=..., SERIALNUMBER=PNOMN-...". */
273
316
  subject: string;
317
+ /** Subject-ийн задалсан талбарууд (нэр, ETSI `serialNumber`, улс) — {@link parseSubject}. */
318
+ parsedSubject: CertSubject;
274
319
  certificateLevel: string;
275
320
  }
276
321
  /** Баталгаажсан гарын үсэг (sign). */
@@ -280,6 +325,8 @@ interface VerifiedSignature {
280
325
  signatureAlgorithm: string | null;
281
326
  certificate: X509Certificate;
282
327
  subject: string;
328
+ /** Subject-ийн задалсан талбарууд — {@link parseSubject}. */
329
+ parsedSubject: CertSubject;
283
330
  }
284
331
  declare class ResponseValidator {
285
332
  private readonly anchors;
@@ -301,8 +348,17 @@ declare class ResponseValidator {
301
348
  */
302
349
  validateSign(result: SessionResult, digestB64: string, hashAlgorithm?: string): VerifiedSignature;
303
350
  /**
304
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
305
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
351
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
352
+ * (downgrade / alg confusion хаалт).
353
+ *
354
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
355
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
356
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
357
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
358
+ * Иймд:
359
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
360
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
361
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
306
362
  */
307
363
  private algorithmOf;
308
364
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
@@ -593,4 +649,4 @@ declare class EidOidcClient {
593
649
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
650
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
651
 
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 };
652
+ 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 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 хаалт).
@@ -271,6 +314,8 @@ interface VerifiedIdentity {
271
314
  certificate: X509Certificate;
272
315
  /** Cert subject DN (түүхий). Ж: "C=MN, CN=..., SERIALNUMBER=PNOMN-...". */
273
316
  subject: string;
317
+ /** Subject-ийн задалсан талбарууд (нэр, ETSI `serialNumber`, улс) — {@link parseSubject}. */
318
+ parsedSubject: CertSubject;
274
319
  certificateLevel: string;
275
320
  }
276
321
  /** Баталгаажсан гарын үсэг (sign). */
@@ -280,6 +325,8 @@ interface VerifiedSignature {
280
325
  signatureAlgorithm: string | null;
281
326
  certificate: X509Certificate;
282
327
  subject: string;
328
+ /** Subject-ийн задалсан талбарууд — {@link parseSubject}. */
329
+ parsedSubject: CertSubject;
283
330
  }
284
331
  declare class ResponseValidator {
285
332
  private readonly anchors;
@@ -301,8 +348,17 @@ declare class ResponseValidator {
301
348
  */
302
349
  validateSign(result: SessionResult, digestB64: string, hashAlgorithm?: string): VerifiedSignature;
303
350
  /**
304
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
305
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
351
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
352
+ * (downgrade / alg confusion хаалт).
353
+ *
354
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
355
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
356
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
357
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
358
+ * Иймд:
359
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
360
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
361
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
306
362
  */
307
363
  private algorithmOf;
308
364
  /** state/endResult, cert байгаа эсэх, chain, validity, certLevel — нийтлэг шалгалт. */
@@ -593,4 +649,4 @@ declare class EidOidcClient {
593
649
  /** PKCE S256: `BASE64URL(SHA-256(ASCII(code_verifier)))` (RFC 7636 §4.2). */
594
650
  declare function pkceChallenge(codeVerifier: string): Promise<string>;
595
651
 
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 };
652
+ 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 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,6 +642,7 @@ 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
  }
@@ -522,7 +653,7 @@ var ResponseValidator = class {
522
653
  validateSign(result, digestB64, hashAlgorithm = "SHA-256") {
523
654
  const leaf = this.precheck(result, "RAW_DIGEST_SIGNATURE");
524
655
  const digest = Buffer.from(digestB64, "base64");
525
- const { alg, hash } = this.algorithmOf(result, null, hashAlgorithm);
656
+ const { alg, hash } = this.algorithmOf(result, leaf, null, hashAlgorithm);
526
657
  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
658
  if (!verifyPrehashedSignature(digest, result.signatureValueB64, leaf.publicKey, alg, hash)) {
528
659
  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 +663,47 @@ var ResponseValidator = class {
532
663
  signatureValueB64: result.signatureValueB64,
533
664
  signatureAlgorithm: result.signatureAlgorithm,
534
665
  certificate: leaf,
535
- subject: leaf.subject
666
+ subject: leaf.subject,
667
+ parsedSubject: parseSubject(leaf)
536
668
  };
537
669
  }
538
670
  /**
539
- * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй тулгана (downgrade хаалт).
540
- * ecdsa-with-SHA256 (хуучин cert) хүссэн rsassa-pss-ийн оронд ирэхийг зөвшөөрнө — SHA-256-аар л.
671
+ * Хариуны signatureAlgorithm + hashAlgorithm-ийг RP-ийн хүссэнтэй ба иргэний түлхүүрийн төрөлтэй тулгана
672
+ * (downgrade / alg confusion хаалт).
673
+ *
674
+ * CA-ийн бодит зан (ca-eidmongolia-mn `service/memory/crypto_wired.go` `messageHash`, `domain/enums.go`):
675
+ * RP-API session-ий гарын үсэг хоёр замаас л гарна — SplitKey RSA (`rsassa-pss` / `shaNNNWithRSAEncryption`,
676
+ * hash = хүсэлтийн `hashAlgorithm`) ба legacy threshold ECDSA P-256 (ҮРГЭЛЖ `ecdsa-with-SHA256`). ECDSA credential
677
+ * зөвхөн 32 байт digest зурдаг: session-ий hash SHA-256 биш бол сервер гарын үсэг гаргахгүй (session OK болохгүй).
678
+ * Иймд:
679
+ * - hash нь ҮРГЭЛЖ хүссэнтэй яг тэнцүү — `ecdsa-with-SHA256` хариу SHA-384/512 хүссэн session-д ирвэл хуурамч;
680
+ * - `ecdsa-with-SHA256` нь хүссэн `rsassa-pss`-ийн оронд ирж болно (credential-ийн төрөл), ГЭХДЭЭ зөвхөн
681
+ * EC P-256 түлхүүртэй гэрчилгээнд; RSA алгоритмууд зөвхөн RSA түлхүүрт (alg confusion үгүй).
541
682
  */
542
- algorithmOf(result, wantAlg, wantHash) {
683
+ algorithmOf(result, leaf, wantAlg, wantHash) {
543
684
  const alg = result.signatureAlgorithm ?? "";
685
+ const keyType = leaf.publicKey.asymmetricKeyType;
544
686
  let hash;
545
687
  if (alg === "rsassa-pss") {
546
688
  hash = String(result.signatureAlgorithmParameters?.hashAlgorithm ?? "");
689
+ if (!HASHES.has(hash)) throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 hashAlgorithm: ${hash}`);
547
690
  const mgf = result.signatureAlgorithmParameters?.maskGenAlgorithm?.parameters?.hashAlgorithm;
548
691
  if (mgf && mgf !== hash) throw new ValidationError("MGF1 hash \u043D\u044C hashAlgorithm-\u0430\u0430\u0441 \u04E9\u04E9\u0440");
549
692
  if (result.signatureAlgorithmParameters?.saltLength != null && result.signatureAlgorithmParameters.saltLength !== hashLen(hash)) {
550
693
  throw new ValidationError("saltLength \u2260 hLen");
551
694
  }
695
+ 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
696
  } else if (/^sha(256|384|512)WithRSAEncryption$/.test(alg)) {
553
697
  hash = "SHA-" + alg.slice(3, 6);
698
+ 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
699
  } else if (alg === "ecdsa-with-SHA256") {
555
700
  hash = "SHA-256";
701
+ if (keyType !== "ec" || leaf.publicKey.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
702
+ 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`);
703
+ }
704
+ if (wantHash !== hash) {
705
+ 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)`);
706
+ }
556
707
  } else {
557
708
  throw new ValidationError(`\u0414\u044D\u043C\u0436\u0438\u0433\u0434\u044D\u044D\u0433\u04AF\u0439 signatureAlgorithm: ${alg}`);
558
709
  }
@@ -573,7 +724,7 @@ var ResponseValidator = class {
573
724
  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
725
  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
726
  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"));
727
+ const leaf = new X509Certificate2(Buffer.from(result.certificateDerB64, "base64"));
577
728
  this.checkValidity(leaf);
578
729
  this.checkLevel(String(result.certificateLevel ?? ""));
579
730
  if (this.anchors.length === 0) {
@@ -651,7 +802,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
651
802
  return fromResponse;
652
803
  }
653
804
  function userChallengeOf(verifier) {
654
- return createHash2("sha256").update(verifier, "utf8").digest("base64url");
805
+ return createHash3("sha256").update(verifier, "utf8").digest("base64url");
655
806
  }
656
807
  function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
657
808
  const sig = Buffer.from(sigB64, "base64");
@@ -742,6 +893,7 @@ function ecdsaP256VerifyPrehashed(digest, sig, key) {
742
893
  const R = ptAdd(ptMul(mod(z * w, n), [P256.gx, P256.gy]), ptMul(mod(r * w, n), Q));
743
894
  return R !== null && mod(R[0], n) === r;
744
895
  }
896
+ var HASHES = /* @__PURE__ */ new Set(["SHA-256", "SHA-384", "SHA-512"]);
745
897
  var DIGEST_INFO = {
746
898
  "SHA-256": Buffer.from("3031300d060960864801650304020105000420", "hex"),
747
899
  "SHA-384": Buffer.from("3041300d060960864801650304020205000430", "hex"),
@@ -752,7 +904,7 @@ function mgf1(hash, seed, length) {
752
904
  for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
753
905
  const c = Buffer.alloc(4);
754
906
  c.writeUInt32BE(i, 0);
755
- out.push(createHash2(nodeHash(hash)).update(seed).update(c).digest());
907
+ out.push(createHash3(nodeHash(hash)).update(seed).update(c).digest());
756
908
  }
757
909
  return Buffer.concat(out).subarray(0, length);
758
910
  }
@@ -776,7 +928,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
776
928
  const psLen = emLen - hLen - sLen - 2;
777
929
  if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
778
930
  const salt = db.subarray(psLen + 1);
779
- const h2 = createHash2(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
931
+ const h2 = createHash3(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
780
932
  return h.equals(h2);
781
933
  }
782
934
  function isCa(cert) {
@@ -1206,6 +1358,7 @@ export {
1206
1358
  isMobileUserAgent,
1207
1359
  openOrShowQR,
1208
1360
  parseSessionResult,
1361
+ parseSubject,
1209
1362
  pkceChallenge,
1210
1363
  qrDeviceLinkTicker,
1211
1364
  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.0",
4
4
  "description": "e-ID Mongolia Relying Party SDK — иргэнээр утсаар нь нэвтрэлт ба хууль ёсны цахим гарын үсэг (RP-API v3, Smart-ID нийцтэй).",
5
5
  "license": "MIT",
6
6
  "type": "module",