@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 +21 -1
- package/dist/{browser-DqKDj2nB.d.cts → browser-B_XtyuFG.d.cts} +1 -1
- package/dist/{browser-DqKDj2nB.d.ts → browser-B_XtyuFG.d.ts} +1 -1
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/index.cjs +175 -21
- package/dist/index.d.cts +61 -5
- package/dist/index.d.ts +61 -5
- package/dist/index.js +166 -13
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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 };
|
package/dist/browser.d.cts
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-
|
|
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-
|
|
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
|
|
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
|
|
564
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
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-ийн хүссэнтэй
|
|
639
|
-
*
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
762
|
-
return (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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-
|
|
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,
|
|
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-ийн хүссэнтэй
|
|
305
|
-
*
|
|
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-
|
|
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,
|
|
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-ийн хүссэнтэй
|
|
305
|
-
*
|
|
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
|
|
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
|
|
465
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
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-ийн хүссэнтэй
|
|
540
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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.
|
|
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",
|