@gerege-systems/eid-mongolia-sdk 0.14.1 → 0.15.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 +140 -46
- package/dist/{browser-CuMH5IVG.d.cts → browser-8k_VDSP8.d.cts} +19 -169
- package/dist/{browser-CuMH5IVG.d.ts → browser-BZ3hvv6-.d.ts} +19 -169
- package/dist/browser.d.cts +2 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +5 -3
- package/dist/chunk-REWNE7PO.js +51 -0
- package/dist/{chunk-QPJDZLHR.js → chunk-TB2KQH3W.js} +10 -46
- package/dist/errors-Z7SgLVTr.d.cts +149 -0
- package/dist/errors-Z7SgLVTr.d.ts +149 -0
- package/dist/index.cjs +268 -256
- package/dist/index.d.cts +147 -156
- package/dist/index.d.ts +147 -156
- package/dist/index.js +247 -241
- package/dist/pades.cjs +3520 -0
- package/dist/pades.d.cts +128 -0
- package/dist/pades.d.ts +128 -0
- package/dist/pades.js +3484 -0
- package/package.json +16 -6
package/dist/index.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deviceLink,
|
|
3
|
+
isMobileUserAgent,
|
|
4
|
+
openOrShowQR
|
|
5
|
+
} from "./chunk-REWNE7PO.js";
|
|
1
6
|
import {
|
|
2
7
|
ApiError,
|
|
3
8
|
AuthenticationError,
|
|
9
|
+
CertChoiceInvalidError,
|
|
4
10
|
DeviceLinkError,
|
|
5
11
|
EidError,
|
|
6
12
|
ForbiddenError,
|
|
7
13
|
NetworkError,
|
|
14
|
+
NoSigningCertificateError,
|
|
8
15
|
ON_BEHALF_ERROR_CODES,
|
|
9
16
|
RETRYABLE_ERROR_CODES,
|
|
17
|
+
RequestInvalidError,
|
|
10
18
|
ResponseTooLargeError,
|
|
11
19
|
SessionFailedError,
|
|
12
20
|
ValidationError,
|
|
13
|
-
deviceLink,
|
|
14
|
-
isMobileUserAgent,
|
|
15
21
|
maskPath,
|
|
16
|
-
openOrShowQR,
|
|
17
22
|
parseRetryAfter
|
|
18
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TB2KQH3W.js";
|
|
19
24
|
|
|
20
25
|
// src/http.ts
|
|
21
26
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -31,15 +36,6 @@ var Http = class {
|
|
|
31
36
|
get(path, timeoutMs) {
|
|
32
37
|
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
33
38
|
}
|
|
34
|
-
/** multipart/form-data POST (Content-Type-ийг boundary-тай нь fetch тавина). JSON хариу. */
|
|
35
|
-
postMultipart(path, form, timeoutMs) {
|
|
36
|
-
return this.json("POST", path, form, void 0, timeoutMs);
|
|
37
|
-
}
|
|
38
|
-
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
|
|
39
|
-
async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
|
|
40
|
-
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
41
|
-
return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
|
|
42
|
-
}
|
|
43
39
|
async json(method, path, body, contentType, timeoutMs) {
|
|
44
40
|
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
45
41
|
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
|
|
@@ -85,7 +81,8 @@ var Http = class {
|
|
|
85
81
|
{ headers, retryAfterMs }
|
|
86
82
|
);
|
|
87
83
|
}
|
|
88
|
-
|
|
84
|
+
const Typed = code === "NO_SIGNING_CERTIFICATE" ? NoSigningCertificateError : code === "CERT_CHOICE_INVALID" ? CertChoiceInvalidError : code === "REQUEST_INVALID" ? RequestInvalidError : ApiError;
|
|
85
|
+
throw new Typed(res.status, text, code, { headers, retryAfterMs });
|
|
89
86
|
}
|
|
90
87
|
};
|
|
91
88
|
async function readLimited(res, limit, op) {
|
|
@@ -143,8 +140,6 @@ var RP_ROUTES = {
|
|
|
143
140
|
signDeviceLinkDocument: { method: "POST", path: "/signature/device-link/document/{documentNumber}" },
|
|
144
141
|
certChoiceDeviceLink: { method: "POST", path: "/signature/certificate-choice/device-link/anonymous" },
|
|
145
142
|
certChoiceEtsi: { method: "POST", path: "/signature/certificate-choice/notification/etsi/{semanticsIdentifier}" },
|
|
146
|
-
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
147
|
-
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
148
143
|
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
149
144
|
session: { method: "GET", path: "/session/{sessionId}" },
|
|
150
145
|
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
@@ -153,15 +148,29 @@ var RP_ROUTES = {
|
|
|
153
148
|
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
154
149
|
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
155
150
|
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
156
|
-
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
151
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" },
|
|
152
|
+
pdfSignatures: { method: "POST", path: "/pdf/signatures" }
|
|
157
153
|
};
|
|
158
154
|
var RP_ROUTES_UNDISCOVERED = {
|
|
159
155
|
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
160
|
-
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
161
|
-
pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
|
|
162
|
-
pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
|
|
163
|
-
pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
|
|
156
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" }
|
|
164
157
|
};
|
|
158
|
+
var PDF_SIGNATURE_REGISTER_FIELDS = [
|
|
159
|
+
"docID",
|
|
160
|
+
"revIndex",
|
|
161
|
+
"medium",
|
|
162
|
+
"sessionID",
|
|
163
|
+
"cms",
|
|
164
|
+
"fileName",
|
|
165
|
+
"inSha256",
|
|
166
|
+
"pageSha256",
|
|
167
|
+
"sigRevSha256",
|
|
168
|
+
"outSha256",
|
|
169
|
+
"priorCmsSha256",
|
|
170
|
+
"priorRevisionSha256",
|
|
171
|
+
"onBehalfOf",
|
|
172
|
+
"signerEtsi"
|
|
173
|
+
];
|
|
165
174
|
function routePath(route, ...args) {
|
|
166
175
|
let i = 0;
|
|
167
176
|
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
@@ -211,6 +220,21 @@ function acspV2Digest(p, hashAlgorithm) {
|
|
|
211
220
|
return createHash(nodeHash(hashAlgorithm)).update(buildAcspV2Payload(p)).digest();
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
// src/types.ts
|
|
224
|
+
var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
|
|
225
|
+
function sessionExpiry(sentAtMs, ttlMs) {
|
|
226
|
+
return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
|
|
227
|
+
}
|
|
228
|
+
function parseExpiresAt(v) {
|
|
229
|
+
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(v)) return void 0;
|
|
230
|
+
const t = Date.parse(v);
|
|
231
|
+
return Number.isFinite(t) ? t : void 0;
|
|
232
|
+
}
|
|
233
|
+
function startExpiry(raw, sentAtMs, ttlMs) {
|
|
234
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
235
|
+
return parseExpiresAt(r.expiresAt) ?? sessionExpiry(sentAtMs, ttlMs);
|
|
236
|
+
}
|
|
237
|
+
|
|
214
238
|
// src/auth.ts
|
|
215
239
|
function rec(v) {
|
|
216
240
|
return v && typeof v === "object" ? v : {};
|
|
@@ -256,11 +280,19 @@ var AuthApi = class {
|
|
|
256
280
|
parseNotification(raw, acsp) {
|
|
257
281
|
const r = rec(raw);
|
|
258
282
|
const vc = rec(r.vc).value;
|
|
259
|
-
|
|
283
|
+
const expiresAt = parseExpiresAt(r.expiresAt);
|
|
284
|
+
return {
|
|
285
|
+
sessionId: String(r.sessionID),
|
|
286
|
+
vc: vc != null ? String(vc) : null,
|
|
287
|
+
rpChallenge: acsp.rpChallenge,
|
|
288
|
+
acsp,
|
|
289
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
290
|
+
};
|
|
260
291
|
}
|
|
261
292
|
parseDeviceLink(raw, acsp) {
|
|
262
293
|
const r = rec(raw);
|
|
263
294
|
const vc = typeof r.vc === "object" && r.vc !== null ? rec(r.vc).value : r.vc;
|
|
295
|
+
const expiresAt = parseExpiresAt(r.expiresAt);
|
|
264
296
|
return {
|
|
265
297
|
sessionId: String(r.sessionID),
|
|
266
298
|
sessionToken: r.sessionToken != null ? String(r.sessionToken) : null,
|
|
@@ -270,7 +302,8 @@ var AuthApi = class {
|
|
|
270
302
|
rpChallenge: acsp.rpChallenge,
|
|
271
303
|
acsp,
|
|
272
304
|
receivedAt: Date.now(),
|
|
273
|
-
sessionType: "auth"
|
|
305
|
+
sessionType: "auth",
|
|
306
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
274
307
|
};
|
|
275
308
|
}
|
|
276
309
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
@@ -299,13 +332,85 @@ var AuthApi = class {
|
|
|
299
332
|
}
|
|
300
333
|
};
|
|
301
334
|
|
|
302
|
-
// src/types.ts
|
|
303
|
-
var DEFAULT_SIGN_SESSION_TTL_MS = 6e5;
|
|
304
|
-
function sessionExpiry(sentAtMs, ttlMs) {
|
|
305
|
-
return sentAtMs + (ttlMs && ttlMs > 0 ? ttlMs : DEFAULT_SIGN_SESSION_TTL_MS);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
335
|
// src/sign.ts
|
|
336
|
+
function registrationFromCompleted(done, extra) {
|
|
337
|
+
const out = {
|
|
338
|
+
docID: done.docId,
|
|
339
|
+
revIndex: done.revIndex,
|
|
340
|
+
cms: done.cms,
|
|
341
|
+
fileName: extra.fileName,
|
|
342
|
+
inSha256: done.inSha256,
|
|
343
|
+
sigRevSha256: done.sigRevSha256,
|
|
344
|
+
outSha256: done.outSha256
|
|
345
|
+
};
|
|
346
|
+
if (extra.medium) out.medium = extra.medium;
|
|
347
|
+
if (extra.sessionID) out.sessionID = extra.sessionID;
|
|
348
|
+
if (done.hasPage && done.pageSha256) out.pageSha256 = done.pageSha256;
|
|
349
|
+
if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
|
|
350
|
+
if (extra.priorRevisionSha256?.length) out.priorRevisionSha256 = extra.priorRevisionSha256;
|
|
351
|
+
if (extra.onBehalfOf) out.onBehalfOf = extra.onBehalfOf;
|
|
352
|
+
if (extra.signerEtsi) out.signerEtsi = extra.signerEtsi;
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
356
|
+
var MAX_CMS_BYTES = 28672;
|
|
357
|
+
function buildRegisterBody(input) {
|
|
358
|
+
const fail2 = (m) => {
|
|
359
|
+
throw new ValidationError(`registerSignature: ${m}`);
|
|
360
|
+
};
|
|
361
|
+
if (!UUID_RE.test(input.docID ?? "")) fail2("docID UUID \u0431\u0438\u0448");
|
|
362
|
+
if (!Number.isInteger(input.revIndex) || input.revIndex < 1 || input.revIndex > 16) fail2("revIndex 1\u201316");
|
|
363
|
+
const hex = (name, v) => {
|
|
364
|
+
const x = v?.trim().toLowerCase() ?? "";
|
|
365
|
+
if (!HEX64.test(x)) fail2(`${name} SHA-256 hex (64) \u0431\u0438\u0448`);
|
|
366
|
+
return x;
|
|
367
|
+
};
|
|
368
|
+
let cmsB64;
|
|
369
|
+
if (typeof input.cms === "string") {
|
|
370
|
+
const raw = Buffer.from(input.cms, "base64");
|
|
371
|
+
if (raw.length === 0 || raw.toString("base64") !== input.cms) fail2("cms \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442 base64 \u0431\u0438\u0448");
|
|
372
|
+
cmsB64 = input.cms;
|
|
373
|
+
if (raw.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
374
|
+
} else {
|
|
375
|
+
if (!input.cms?.length) fail2("cms \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
376
|
+
if (input.cms.length > MAX_CMS_BYTES) fail2(`cms > ${MAX_CMS_BYTES} \u0431\u0430\u0439\u0442`);
|
|
377
|
+
cmsB64 = Buffer.from(input.cms).toString("base64");
|
|
378
|
+
}
|
|
379
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
380
|
+
if (!fileName || Array.from(fileName).length > PDF_MAX_FILE_NAME) fail2(`fileName \u0437\u0430\u0430\u0432\u0430\u043B, \u2264 ${PDF_MAX_FILE_NAME} \u0442\u044D\u043C\u0434\u044D\u0433\u0442`);
|
|
381
|
+
const sessionID = input.sessionID?.trim() ?? "";
|
|
382
|
+
const medium = input.medium ?? (sessionID ? "phone" : "token");
|
|
383
|
+
if (medium !== "phone" && medium !== "token") fail2("medium: phone | token");
|
|
384
|
+
if (medium === "phone" && !sessionID) fail2("phone-\u0434 sessionID \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
385
|
+
const priorCms = (input.priorCmsSha256 ?? []).map((h, i) => hex(`priorCmsSha256[${i}]`, h));
|
|
386
|
+
const priorRev = (input.priorRevisionSha256 ?? []).map((h, i) => hex(`priorRevisionSha256[${i}]`, h));
|
|
387
|
+
if (priorCms.length + priorRev.length !== input.revIndex - 1) fail2("priorCmsSha256 + priorRevisionSha256-\u0438\u0439\u043D \u0442\u043E\u043E = revIndex \u2212 1");
|
|
388
|
+
if (input.revIndex === 1 && !input.pageSha256) fail2("revIndex 1-\u0434 pageSha256 \u0437\u0430\u0430\u0432\u0430\u043B");
|
|
389
|
+
const body = { docID: input.docID, revIndex: input.revIndex, medium };
|
|
390
|
+
if (sessionID) body.sessionID = sessionID;
|
|
391
|
+
body.cms = cmsB64;
|
|
392
|
+
body.fileName = fileName;
|
|
393
|
+
body.inSha256 = hex("inSha256", input.inSha256);
|
|
394
|
+
if (input.pageSha256) body.pageSha256 = hex("pageSha256", input.pageSha256);
|
|
395
|
+
body.sigRevSha256 = hex("sigRevSha256", input.sigRevSha256);
|
|
396
|
+
body.outSha256 = hex("outSha256", input.outSha256);
|
|
397
|
+
if (priorCms.length) body.priorCmsSha256 = priorCms;
|
|
398
|
+
if (priorRev.length) body.priorRevisionSha256 = priorRev;
|
|
399
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
400
|
+
if (input.signerEtsi?.trim()) body.signerEtsi = input.signerEtsi.trim();
|
|
401
|
+
for (const k of Object.keys(body)) if (!PDF_SIGNATURE_REGISTER_FIELDS.includes(k)) fail2(`\u0434\u043E\u0442\u043E\u043E\u0434: \u04AF\u043B \u043C\u044D\u0434\u044D\u0433\u0434\u044D\u0445 \u0442\u0430\u043B\u0431\u0430\u0440 ${k}`);
|
|
402
|
+
return body;
|
|
403
|
+
}
|
|
404
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
405
|
+
var PDF_DISPLAY_TEXT = "\u0411\u0430\u0440\u0438\u043C\u0442\u0430\u0434 \u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0437\u0443\u0440\u0430\u0445: ";
|
|
406
|
+
var PDF_MAX_FILE_NAME = 120;
|
|
407
|
+
function dtbsBase64(v) {
|
|
408
|
+
const raw = typeof v === "string" ? Buffer.from(v, "base64") : Buffer.from(v);
|
|
409
|
+
if (raw.length !== 32 || typeof v === "string" && raw.toString("base64") !== v) {
|
|
410
|
+
throw new ValidationError("pdfDigest: dtbs \u043D\u044C 32 \u0431\u0430\u0439\u0442 (SHA-256) \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
411
|
+
}
|
|
412
|
+
return raw.toString("base64");
|
|
413
|
+
}
|
|
309
414
|
function rec2(v) {
|
|
310
415
|
return v && typeof v === "object" ? v : {};
|
|
311
416
|
}
|
|
@@ -356,7 +461,7 @@ var SignApi = class {
|
|
|
356
461
|
vc: vcOf(r),
|
|
357
462
|
rpChallenge: "",
|
|
358
463
|
...sign ? { sign } : {},
|
|
359
|
-
expiresAt:
|
|
464
|
+
expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
|
|
360
465
|
};
|
|
361
466
|
}
|
|
362
467
|
async deviceLink(path, body, extra) {
|
|
@@ -371,7 +476,7 @@ var SignApi = class {
|
|
|
371
476
|
rpChallenge: "",
|
|
372
477
|
receivedAt: Date.now(),
|
|
373
478
|
...extra,
|
|
374
|
-
expiresAt:
|
|
479
|
+
expiresAt: startExpiry(r, sent, this.defaults.sessionTtlMs)
|
|
375
480
|
};
|
|
376
481
|
}
|
|
377
482
|
/**
|
|
@@ -390,6 +495,77 @@ var SignApi = class {
|
|
|
390
495
|
const { body, sign } = this.body(digestB64, interactions, opts, "rsassa-pss");
|
|
391
496
|
return this.notification(routePath(RP_ROUTES.signNotificationDocument, documentNumber), body, sign);
|
|
392
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Клиент талын PAdES (`@gerege-systems/eid-mongolia-sdk/pades`): `prepare()`-ийн DTBS-д иргэнээр гарын үсэг зуруулна.
|
|
500
|
+
* PDF CA руу ОЧИХГҮЙ — зөвхөн 32 байт. CA-ийн өөрийн PAdES session-тэй ижил: `RAW_DIGEST_SIGNATURE`,
|
|
501
|
+
* `sha256WithRSAEncryption` (CMS-д PKCS#1 v1.5; legacy ECDSA иргэнд сервер `ecdsa-with-SHA256`), `displayTextAndPIN`
|
|
502
|
+
* «Баримтад гарын үсэг зурах: <fileName>» (60 тэмдэгтэд тайрна).
|
|
503
|
+
*
|
|
504
|
+
* Дараа нь `session.waitForResult(sessionId)` → `validator.validateSign(result, s.digest)` → `signature.value`-г
|
|
505
|
+
* (base64 → байт) `complete(state, signature)`-д.
|
|
506
|
+
*/
|
|
507
|
+
async pdfDigest(input) {
|
|
508
|
+
const digest = dtbsBase64(input.dtbs);
|
|
509
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
510
|
+
if (!fileName || Array.from(fileName).length > PDF_MAX_FILE_NAME) {
|
|
511
|
+
throw new ValidationError(`pdfDigest: fileName \u0437\u0430\u0430\u0432\u0430\u043B, \u2264 ${PDF_MAX_FILE_NAME} \u0442\u044D\u043C\u0434\u044D\u0433\u0442`);
|
|
512
|
+
}
|
|
513
|
+
const signer = input.signer;
|
|
514
|
+
const etsi = signer?.etsi?.trim();
|
|
515
|
+
const doc = signer?.documentNumber?.trim();
|
|
516
|
+
if (!etsi === !doc) throw new ValidationError("pdfDigest: signer \u2014 etsi \u044D\u0441\u0432\u044D\u043B documentNumber-\u0438\u0439\u043D \u044F\u0433 \u043D\u044D\u0433 \u043D\u044C");
|
|
517
|
+
const deviceLink2 = input.flow === "device-link";
|
|
518
|
+
const display = Array.from(PDF_DISPLAY_TEXT + fileName).slice(0, 60).join("");
|
|
519
|
+
const interactions = interactionsToBase64([{ type: "displayTextAndPIN", displayText60: display }]);
|
|
520
|
+
const body = {
|
|
521
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
522
|
+
relyingPartyName: this.creds.rpName,
|
|
523
|
+
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
524
|
+
signatureProtocol: "RAW_DIGEST_SIGNATURE",
|
|
525
|
+
signatureProtocolParameters: {
|
|
526
|
+
digest,
|
|
527
|
+
signatureAlgorithm: "sha256WithRSAEncryption",
|
|
528
|
+
signatureAlgorithmParameters: { hashAlgorithm: "SHA-256" }
|
|
529
|
+
},
|
|
530
|
+
interactions,
|
|
531
|
+
fileName
|
|
532
|
+
};
|
|
533
|
+
if (input.initialCallbackUrl?.trim()) body.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
534
|
+
if (input.onBehalfOf?.trim()) body.onBehalfOf = input.onBehalfOf.trim();
|
|
535
|
+
const route = deviceLink2 ? etsi ? RP_ROUTES.signDeviceLinkEtsi : RP_ROUTES.signDeviceLinkDocument : etsi ? RP_ROUTES.signNotificationEtsi : RP_ROUTES.signNotificationDocument;
|
|
536
|
+
const r = rec2(await this.http.post(routePath(route, etsi ?? doc), body));
|
|
537
|
+
const receivedAt = Date.now();
|
|
538
|
+
const vc = typeof r.vc === "object" && r.vc !== null ? rec2(r.vc).value : r.vc;
|
|
539
|
+
let dl = null;
|
|
540
|
+
if (deviceLink2) {
|
|
541
|
+
if (!r.sessionToken || !r.sessionSecret || !r.deviceLinkBase) {
|
|
542
|
+
throw new ValidationError("pdfDigest: device-link \u0445\u0430\u0440\u0438\u0443\u043D\u0434 sessionToken/sessionSecret/deviceLinkBase \u0430\u043B\u0433\u0430");
|
|
543
|
+
}
|
|
544
|
+
dl = { sessionToken: String(r.sessionToken), sessionSecret: String(r.sessionSecret), deviceLinkBase: String(r.deviceLinkBase) };
|
|
545
|
+
}
|
|
546
|
+
return { sessionId: String(r.sessionID), vc: vc != null ? String(vc) : null, digest, interactions, deviceLink: dl, receivedAt };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* `POST /pdf/signatures` — `pades.complete()`-ийн дараа CMS-ийг CA-д бүртгэнэ (`/verify/{docID}`, иргэний түүх). PDF
|
|
550
|
+
* илгээгдэхгүй. SIGN эрх. CA CMS-ийг шалгана: 04-ийн SIGN cert, session digest = SHA-256(signedAttrs), claimedRole ↔
|
|
551
|
+
* төлөөлөл. Алдаа: 403 `REPRESENTATION_REVOKED`, 409 `CLAIMED_ROLE_MISMATCH` / `REGISTRY_CONFLICT` /
|
|
552
|
+
* `DOC_ID_CONFLICT` / `PDF_FOREIGN_SIGNATURE` (`ApiError.code`). Бүртгэлгүй файл ч Adobe/EU DSS-д хүчинтэй — зөвхөн
|
|
553
|
+
* `/verify` «бүртгэлгүй» гэнэ.
|
|
554
|
+
*/
|
|
555
|
+
async registerSignature(input) {
|
|
556
|
+
return this.registerBody(buildRegisterBody(input));
|
|
557
|
+
}
|
|
558
|
+
async registerBody(body) {
|
|
559
|
+
const raw = await this.http.post(routePath(RP_ROUTES.pdfSignatures), body);
|
|
560
|
+
const r = rec2(raw);
|
|
561
|
+
return {
|
|
562
|
+
docID: String(r.docID ?? body.docID),
|
|
563
|
+
revIndex: Number(r.revIndex ?? 0),
|
|
564
|
+
registeredAt: typeof r.registeredAt === "string" ? r.registeredAt : "",
|
|
565
|
+
verifyUrl: String(r.verifyUrl ?? ""),
|
|
566
|
+
raw
|
|
567
|
+
};
|
|
568
|
+
}
|
|
393
569
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
394
570
|
async textByEtsi(id, text, interactions, opts) {
|
|
395
571
|
return this.digestByEtsi(id, sha256Base64(text), interactions, opts);
|
|
@@ -505,7 +681,6 @@ function parseSessionResult(raw) {
|
|
|
505
681
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
506
682
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
507
683
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
508
|
-
pdf: parsePdf(r.pdf),
|
|
509
684
|
person: parsePerson(r.person),
|
|
510
685
|
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
511
686
|
raw
|
|
@@ -543,28 +718,6 @@ function personMatchesCertificate(person, subject) {
|
|
|
543
718
|
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
544
719
|
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
545
720
|
}
|
|
546
|
-
function parsePdf(raw) {
|
|
547
|
-
const p = asRecord(raw);
|
|
548
|
-
const docID = str(p.docID);
|
|
549
|
-
if (!docID) return null;
|
|
550
|
-
const out = { docID, documentStatus: String(p.documentStatus ?? "") };
|
|
551
|
-
if (p.errorCode != null) out.errorCode = String(p.errorCode);
|
|
552
|
-
if (p.outSha256 != null) out.outSha256 = String(p.outSha256);
|
|
553
|
-
if (p.size != null) out.size = Number(p.size);
|
|
554
|
-
if (p.expiresAt != null) out.expiresAt = String(p.expiresAt);
|
|
555
|
-
if (p.signatureLevel != null) out.signatureLevel = String(p.signatureLevel);
|
|
556
|
-
const sg = asRecord(p.signer);
|
|
557
|
-
if (sg.etsi != null) {
|
|
558
|
-
out.signer = { etsi: String(sg.etsi), certSerial: String(sg.certSerial ?? "") };
|
|
559
|
-
if (sg.onBehalfOf) out.signer.onBehalfOf = String(sg.onBehalfOf);
|
|
560
|
-
if (sg.claimedRole) out.signer.claimedRole = String(sg.claimedRole);
|
|
561
|
-
}
|
|
562
|
-
const v = asRecord(p.validation);
|
|
563
|
-
if (v.indication != null) {
|
|
564
|
-
out.validation = { indication: String(v.indication), ltv: v.ltv === true, signatures: Number(v.signatures ?? 0) };
|
|
565
|
-
}
|
|
566
|
-
return out;
|
|
567
|
-
}
|
|
568
721
|
function parseSigParams(raw) {
|
|
569
722
|
const p = asRecord(raw);
|
|
570
723
|
const hash = str(p.hashAlgorithm);
|
|
@@ -623,160 +776,6 @@ var SessionApi = class {
|
|
|
623
776
|
}
|
|
624
777
|
};
|
|
625
778
|
|
|
626
|
-
// src/pdf.ts
|
|
627
|
-
import { createHash as createHash2 } from "crypto";
|
|
628
|
-
var DEFAULT_PDF_MAX_BYTES = 64 << 20;
|
|
629
|
-
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
630
|
-
function rec3(v) {
|
|
631
|
-
return v && typeof v === "object" ? v : {};
|
|
632
|
-
}
|
|
633
|
-
var PdfApi = class {
|
|
634
|
-
constructor(http, creds, defaults) {
|
|
635
|
-
this.http = http;
|
|
636
|
-
this.creds = creds;
|
|
637
|
-
this.defaults = defaults;
|
|
638
|
-
}
|
|
639
|
-
http;
|
|
640
|
-
creds;
|
|
641
|
-
defaults;
|
|
642
|
-
/**
|
|
643
|
-
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
644
|
-
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
645
|
-
*/
|
|
646
|
-
async prepare(input) {
|
|
647
|
-
const request = {
|
|
648
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
649
|
-
relyingPartyName: this.creds.rpName,
|
|
650
|
-
certificateLevel: input.certificateLevel ?? this.defaults.certificateLevel,
|
|
651
|
-
signer: input.signer,
|
|
652
|
-
flow: input.flow ?? "notification",
|
|
653
|
-
fileName: input.fileName
|
|
654
|
-
};
|
|
655
|
-
if (input.initialCallbackUrl?.trim()) request.initialCallbackUrl = input.initialCallbackUrl.trim();
|
|
656
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
657
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
658
|
-
const form = new FormData();
|
|
659
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
660
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
661
|
-
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
|
|
662
|
-
const receivedAt = Date.now();
|
|
663
|
-
const vc = rec3(r.vc).value;
|
|
664
|
-
const dl = rec3(r.deviceLink);
|
|
665
|
-
const digest = typeof r.digest === "string" && r.digest ? r.digest : typeof dl.digest === "string" && dl.digest ? dl.digest : void 0;
|
|
666
|
-
return {
|
|
667
|
-
sessionId: String(r.sessionID),
|
|
668
|
-
docId: String(r.docID),
|
|
669
|
-
vc: vc != null ? String(vc) : null,
|
|
670
|
-
...digest !== void 0 ? { digest } : {},
|
|
671
|
-
deviceLink: dl.sessionToken ? {
|
|
672
|
-
sessionToken: String(dl.sessionToken),
|
|
673
|
-
sessionSecret: String(dl.sessionSecret),
|
|
674
|
-
deviceLinkBase: String(dl.deviceLinkBase),
|
|
675
|
-
digest: String(dl.digest),
|
|
676
|
-
interactions: String(dl.interactions)
|
|
677
|
-
} : null,
|
|
678
|
-
receivedAt
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
683
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
684
|
-
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
685
|
-
* дараа дахин татна (`retryable`).
|
|
686
|
-
*/
|
|
687
|
-
document(sessionId) {
|
|
688
|
-
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
|
|
689
|
-
}
|
|
690
|
-
get timeout() {
|
|
691
|
-
return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
|
|
692
|
-
}
|
|
693
|
-
get maxBytes() {
|
|
694
|
-
return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
|
|
698
|
-
* (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
|
|
699
|
-
* зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
|
|
700
|
-
* LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
|
|
701
|
-
* TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
|
|
702
|
-
*/
|
|
703
|
-
async tokenPrepare(input) {
|
|
704
|
-
if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
|
|
705
|
-
throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
706
|
-
}
|
|
707
|
-
const request = {
|
|
708
|
-
relyingPartyUUID: this.creds.rpUUID,
|
|
709
|
-
relyingPartyName: this.creds.rpName,
|
|
710
|
-
signer: { etsi: input.signerEtsi.trim() },
|
|
711
|
-
certificate: Buffer.from(input.certificate).toString("base64"),
|
|
712
|
-
fileName: input.fileName
|
|
713
|
-
};
|
|
714
|
-
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
715
|
-
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
716
|
-
const form = new FormData();
|
|
717
|
-
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
718
|
-
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
719
|
-
const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
|
|
720
|
-
const r = rec3(raw);
|
|
721
|
-
const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
|
|
722
|
-
if (!r.signingID || hashToSign.length !== 32) {
|
|
723
|
-
throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
|
|
724
|
-
}
|
|
725
|
-
return {
|
|
726
|
-
signingId: String(r.signingID),
|
|
727
|
-
docId: String(r.docID ?? ""),
|
|
728
|
-
hashToSign,
|
|
729
|
-
hashAlgorithm: String(r.hashAlgorithm ?? ""),
|
|
730
|
-
signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
|
|
731
|
-
signatureFormat: String(r.signatureFormat ?? ""),
|
|
732
|
-
expiresAt: String(r.expiresAt ?? ""),
|
|
733
|
-
raw
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
|
|
738
|
-
* Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
|
|
739
|
-
* Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
|
|
740
|
-
* TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
|
|
741
|
-
*/
|
|
742
|
-
async tokenFinalize(signingId, signature) {
|
|
743
|
-
if (signature?.length !== 64) throw new Error("eid: pdf token: signature \u043D\u044C \u0442\u04AF\u04AF\u0445\u0438\u0439 r||s 64 \u0431\u0430\u0439\u0442 \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439 (P-256, DER \u0431\u0438\u0448)");
|
|
744
|
-
const r = await this.http.post(
|
|
745
|
-
routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
|
|
746
|
-
{ signature: Buffer.from(signature).toString("base64") },
|
|
747
|
-
this.timeout
|
|
748
|
-
);
|
|
749
|
-
return r;
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
|
|
753
|
-
* хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
|
|
754
|
-
*/
|
|
755
|
-
tokenDocument(signingId) {
|
|
756
|
-
return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
760
|
-
* `expected` — poll-ийн `result.pdf` блок (`documentStatus` READY + `outSha256` заавал) эсвэл SHA-256 hex.
|
|
761
|
-
*
|
|
762
|
-
* @throws {ValidationError} блок READY биш / `outSha256` алга / hash таарахгүй (файлд бүү итгэ).
|
|
763
|
-
*/
|
|
764
|
-
async documentVerified(sessionId, expected) {
|
|
765
|
-
let want;
|
|
766
|
-
if (typeof expected === "string") {
|
|
767
|
-
want = expected;
|
|
768
|
-
} else {
|
|
769
|
-
if (expected.documentStatus !== "READY") throw new ValidationError(`pdf.documentStatus ${expected.documentStatus} \u2260 READY`);
|
|
770
|
-
want = expected.outSha256;
|
|
771
|
-
}
|
|
772
|
-
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");
|
|
773
|
-
const bytes = await this.document(sessionId);
|
|
774
|
-
const got = createHash2("sha256").update(bytes).digest("hex");
|
|
775
|
-
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");
|
|
776
|
-
return bytes;
|
|
777
|
-
}
|
|
778
|
-
};
|
|
779
|
-
|
|
780
779
|
// src/organization.ts
|
|
781
780
|
var OrganizationApi = class {
|
|
782
781
|
constructor(http) {
|
|
@@ -792,7 +791,7 @@ var OrganizationApi = class {
|
|
|
792
791
|
};
|
|
793
792
|
|
|
794
793
|
// src/person.ts
|
|
795
|
-
function
|
|
794
|
+
function rec3(v) {
|
|
796
795
|
return v && typeof v === "object" ? v : {};
|
|
797
796
|
}
|
|
798
797
|
var PersonApi = class {
|
|
@@ -802,7 +801,7 @@ var PersonApi = class {
|
|
|
802
801
|
http;
|
|
803
802
|
async get(path) {
|
|
804
803
|
const raw = await this.http.get(path);
|
|
805
|
-
return { ...
|
|
804
|
+
return { ...rec3(raw), raw };
|
|
806
805
|
}
|
|
807
806
|
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
808
807
|
certificates(personEtsi) {
|
|
@@ -832,7 +831,7 @@ var PersonApi = class {
|
|
|
832
831
|
};
|
|
833
832
|
|
|
834
833
|
// src/token.ts
|
|
835
|
-
import { X509Certificate as X509Certificate2, createHash as
|
|
834
|
+
import { X509Certificate as X509Certificate2, createHash as createHash2, createPublicKey, timingSafeEqual } from "crypto";
|
|
836
835
|
|
|
837
836
|
// src/subject.ts
|
|
838
837
|
import { X509Certificate } from "crypto";
|
|
@@ -973,12 +972,12 @@ function certExtensions(der) {
|
|
|
973
972
|
}
|
|
974
973
|
|
|
975
974
|
// src/token.ts
|
|
976
|
-
function
|
|
975
|
+
function rec4(v) {
|
|
977
976
|
return v && typeof v === "object" ? v : {};
|
|
978
977
|
}
|
|
979
978
|
function hexSha256Equal(hex, data) {
|
|
980
979
|
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
981
|
-
return timingSafeEqual(
|
|
980
|
+
return timingSafeEqual(createHash2("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
982
981
|
}
|
|
983
982
|
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
984
983
|
var TokenApi = class {
|
|
@@ -1007,11 +1006,11 @@ var TokenApi = class {
|
|
|
1007
1006
|
flow: "notification"
|
|
1008
1007
|
};
|
|
1009
1008
|
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
1010
|
-
const r =
|
|
1009
|
+
const r = rec4(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
1011
1010
|
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
1012
1011
|
throw new ValidationError("token: CA-\u0438\u0439\u043D csrAuthSha256/csrSignSha256 \u0438\u043B\u0433\u044D\u044D\u0441\u044D\u043D CSR-\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1013
1012
|
}
|
|
1014
|
-
const vc =
|
|
1013
|
+
const vc = rec4(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
1015
1014
|
return {
|
|
1016
1015
|
sessionId: String(r.sessionID ?? ""),
|
|
1017
1016
|
vc: vc != null ? String(vc) : null,
|
|
@@ -1026,7 +1025,7 @@ var TokenApi = class {
|
|
|
1026
1025
|
*/
|
|
1027
1026
|
async certificateResult(sessionId) {
|
|
1028
1027
|
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
1029
|
-
const r =
|
|
1028
|
+
const r = rec4(raw);
|
|
1030
1029
|
const item = (v) => v && typeof v === "object" ? v : null;
|
|
1031
1030
|
return {
|
|
1032
1031
|
status: String(r.status ?? ""),
|
|
@@ -1042,7 +1041,7 @@ var TokenApi = class {
|
|
|
1042
1041
|
}
|
|
1043
1042
|
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
1044
1043
|
async authChallenge() {
|
|
1045
|
-
const r =
|
|
1044
|
+
const r = rec4(
|
|
1046
1045
|
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
1047
1046
|
relyingPartyUUID: this.creds.rpUUID,
|
|
1048
1047
|
relyingPartyName: this.creds.rpName
|
|
@@ -1073,12 +1072,12 @@ var TokenApi = class {
|
|
|
1073
1072
|
certificate: Buffer.from(certDer).toString("base64"),
|
|
1074
1073
|
signature: Buffer.from(signature).toString("base64")
|
|
1075
1074
|
});
|
|
1076
|
-
const r =
|
|
1077
|
-
if (
|
|
1078
|
-
const id =
|
|
1075
|
+
const r = rec4(raw);
|
|
1076
|
+
if (rec4(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
1077
|
+
const id = rec4(r.identity);
|
|
1079
1078
|
if (!id.etsi || id.etsi !== serial) throw new ValidationError("token auth: identity.etsi \u043D\u044C \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 SERIALNUMBER \u0431\u0438\u0448");
|
|
1080
|
-
const org =
|
|
1081
|
-
const cert =
|
|
1079
|
+
const org = rec4(r.organization);
|
|
1080
|
+
const cert = rec4(r.cert);
|
|
1082
1081
|
return {
|
|
1083
1082
|
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
1084
1083
|
serialHex: String(cert.serialHex ?? ""),
|
|
@@ -1126,13 +1125,13 @@ function tokenAuthDigest(rpUUID, challengeB64) {
|
|
|
1126
1125
|
}
|
|
1127
1126
|
const ch = Buffer.from(challengeB64, "base64");
|
|
1128
1127
|
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
1129
|
-
return
|
|
1128
|
+
return createHash2("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
1130
1129
|
}
|
|
1131
1130
|
function tokenLimitOf(err) {
|
|
1132
1131
|
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
1133
1132
|
let b;
|
|
1134
1133
|
try {
|
|
1135
|
-
b =
|
|
1134
|
+
b = rec4(JSON.parse(err.body));
|
|
1136
1135
|
} catch {
|
|
1137
1136
|
return void 0;
|
|
1138
1137
|
}
|
|
@@ -1145,7 +1144,7 @@ function tokenLimitOf(err) {
|
|
|
1145
1144
|
}
|
|
1146
1145
|
|
|
1147
1146
|
// src/validator.ts
|
|
1148
|
-
import { X509Certificate as X509Certificate3, constants, createHash as
|
|
1147
|
+
import { X509Certificate as X509Certificate3, constants, createHash as createHash3, publicDecrypt, verify } from "crypto";
|
|
1149
1148
|
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
1150
1149
|
var ResponseValidator = class {
|
|
1151
1150
|
anchors;
|
|
@@ -1475,7 +1474,7 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
1475
1474
|
return fromResponse;
|
|
1476
1475
|
}
|
|
1477
1476
|
function userChallengeOf(verifier) {
|
|
1478
|
-
return
|
|
1477
|
+
return createHash3("sha256").update(verifier, "utf8").digest("base64url");
|
|
1479
1478
|
}
|
|
1480
1479
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
1481
1480
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -1577,7 +1576,7 @@ function mgf1(hash, seed, length) {
|
|
|
1577
1576
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
1578
1577
|
const c = Buffer.alloc(4);
|
|
1579
1578
|
c.writeUInt32BE(i, 0);
|
|
1580
|
-
out.push(
|
|
1579
|
+
out.push(createHash3(nodeHash(hash)).update(seed).update(c).digest());
|
|
1581
1580
|
}
|
|
1582
1581
|
return Buffer.concat(out).subarray(0, length);
|
|
1583
1582
|
}
|
|
@@ -1601,7 +1600,7 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
1601
1600
|
const psLen = emLen - hLen - sLen - 2;
|
|
1602
1601
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
1603
1602
|
const salt = db.subarray(psLen + 1);
|
|
1604
|
-
const h2 =
|
|
1603
|
+
const h2 = createHash3(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
1605
1604
|
return h.equals(h2);
|
|
1606
1605
|
}
|
|
1607
1606
|
function keyUsageBits(cert) {
|
|
@@ -1735,7 +1734,6 @@ function resolveBaseUrl(baseUrl) {
|
|
|
1735
1734
|
var EidClient = class {
|
|
1736
1735
|
auth;
|
|
1737
1736
|
sign;
|
|
1738
|
-
pdf;
|
|
1739
1737
|
organization;
|
|
1740
1738
|
person;
|
|
1741
1739
|
token;
|
|
@@ -1757,11 +1755,6 @@ var EidClient = class {
|
|
|
1757
1755
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1758
1756
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1759
1757
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level, sessionTtlMs: cfg.signSessionTtlMs });
|
|
1760
|
-
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1761
|
-
certificateLevel: level,
|
|
1762
|
-
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
1763
|
-
maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
|
|
1764
|
-
});
|
|
1765
1758
|
this.organization = new OrganizationApi(http);
|
|
1766
1759
|
this.person = new PersonApi(http);
|
|
1767
1760
|
this.token = new TokenApi(http, cfg.credentials);
|
|
@@ -1769,6 +1762,14 @@ var EidClient = class {
|
|
|
1769
1762
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1770
1763
|
this.rpName = cfg.credentials.rpName;
|
|
1771
1764
|
}
|
|
1765
|
+
/** {@link SignApi.pdfDigest} — клиент талын PAdES-ийн DTBS-д гарын үсгийн session (PDF CA руу очихгүй). */
|
|
1766
|
+
signPdfDigest(input) {
|
|
1767
|
+
return this.sign.pdfDigest(input);
|
|
1768
|
+
}
|
|
1769
|
+
/** {@link SignApi.registerSignature} — `pades.complete()`-ийн CMS-ийг CA-д бүртгэнэ (`POST /pdf/signatures`). */
|
|
1770
|
+
registerSignature(input) {
|
|
1771
|
+
return this.sign.registerSignature(input);
|
|
1772
|
+
}
|
|
1772
1773
|
};
|
|
1773
1774
|
|
|
1774
1775
|
// src/devicelinkv3.ts
|
|
@@ -1873,7 +1874,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1873
1874
|
}
|
|
1874
1875
|
|
|
1875
1876
|
// src/login.ts
|
|
1876
|
-
import { createHash as
|
|
1877
|
+
import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1877
1878
|
function memoryLoginStore() {
|
|
1878
1879
|
const m = /* @__PURE__ */ new Map();
|
|
1879
1880
|
return {
|
|
@@ -1910,7 +1911,7 @@ function forwardedClientIp(headers, trustProxy) {
|
|
|
1910
1911
|
return header(headers, "x-real-ip").trim() || void 0;
|
|
1911
1912
|
}
|
|
1912
1913
|
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1913
|
-
var
|
|
1914
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1914
1915
|
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1915
1916
|
var IOS_BROWSERS = [
|
|
1916
1917
|
[/CriOS/i, "googlechromes"],
|
|
@@ -1954,7 +1955,7 @@ function readCookie(h, name) {
|
|
|
1954
1955
|
}
|
|
1955
1956
|
return "";
|
|
1956
1957
|
}
|
|
1957
|
-
var sha256hex = (s) =>
|
|
1958
|
+
var sha256hex = (s) => createHash4("sha256").update(s).digest("hex");
|
|
1958
1959
|
function safeEqualHex(a, b) {
|
|
1959
1960
|
const x = Buffer.from(a, "hex");
|
|
1960
1961
|
const y = Buffer.from(b, "hex");
|
|
@@ -2041,7 +2042,7 @@ function createEidLogin(opts) {
|
|
|
2041
2042
|
const dot = raw.indexOf(".");
|
|
2042
2043
|
const sid = raw.slice(0, dot);
|
|
2043
2044
|
const binding = raw.slice(dot + 1);
|
|
2044
|
-
if (dot < 0 || !
|
|
2045
|
+
if (dot < 0 || !UUID_RE2.test(sid) || !B64URL_RE.test(binding)) return void 0;
|
|
2045
2046
|
const tx = await store.get(sid);
|
|
2046
2047
|
if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
|
|
2047
2048
|
if (tx.expiresAt <= Date.now()) {
|
|
@@ -2264,9 +2265,8 @@ export {
|
|
|
2264
2265
|
AuthApi,
|
|
2265
2266
|
AuthenticationError,
|
|
2266
2267
|
CITIZEN_ISSUING_CA_SKI,
|
|
2268
|
+
CertChoiceInvalidError,
|
|
2267
2269
|
DEFAULT_BASE_URL,
|
|
2268
|
-
DEFAULT_PDF_MAX_BYTES,
|
|
2269
|
-
DEFAULT_PDF_TIMEOUT_MS,
|
|
2270
2270
|
DEFAULT_SIGN_SESSION_TTL_MS,
|
|
2271
2271
|
DeviceLinkError,
|
|
2272
2272
|
EidClient,
|
|
@@ -2274,11 +2274,13 @@ export {
|
|
|
2274
2274
|
ForbiddenError,
|
|
2275
2275
|
Http,
|
|
2276
2276
|
NetworkError,
|
|
2277
|
+
NoSigningCertificateError,
|
|
2277
2278
|
ON_BEHALF_ERROR_CODES,
|
|
2278
2279
|
OrganizationApi,
|
|
2279
|
-
|
|
2280
|
+
PDF_SIGNATURE_REGISTER_FIELDS,
|
|
2280
2281
|
PersonApi,
|
|
2281
2282
|
RETRYABLE_ERROR_CODES,
|
|
2283
|
+
RequestInvalidError,
|
|
2282
2284
|
ResponseTooLargeError,
|
|
2283
2285
|
ResponseValidator,
|
|
2284
2286
|
SessionApi,
|
|
@@ -2289,6 +2291,7 @@ export {
|
|
|
2289
2291
|
acspV2Digest,
|
|
2290
2292
|
buildAcspV2Payload,
|
|
2291
2293
|
buildDeviceLink,
|
|
2294
|
+
buildRegisterBody,
|
|
2292
2295
|
createEidLogin,
|
|
2293
2296
|
deviceLink,
|
|
2294
2297
|
deviceLinkInput,
|
|
@@ -2297,15 +2300,18 @@ export {
|
|
|
2297
2300
|
isMobileUserAgent,
|
|
2298
2301
|
memoryLoginStore,
|
|
2299
2302
|
openOrShowQR,
|
|
2303
|
+
parseExpiresAt,
|
|
2300
2304
|
parseRetryAfter,
|
|
2301
2305
|
parseSessionResult,
|
|
2302
2306
|
parseSubject,
|
|
2303
2307
|
personMatchesCertificate,
|
|
2304
2308
|
qrDeviceLinkTicker,
|
|
2305
2309
|
randomChallenge,
|
|
2310
|
+
registrationFromCompleted,
|
|
2306
2311
|
resolveBaseUrl,
|
|
2307
2312
|
sessionExpiry,
|
|
2308
2313
|
sha256Base64,
|
|
2314
|
+
startExpiry,
|
|
2309
2315
|
tokenAuthDigest,
|
|
2310
2316
|
tokenLimitOf,
|
|
2311
2317
|
userChallengeOf,
|