@gerege-systems/eid-mongolia-sdk 0.15.0 → 0.15.2
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 +83 -14
- package/dist/button.cjs +55 -45
- package/dist/button.js +55 -45
- package/dist/chunk-52O3T7DZ.js +1043 -0
- package/dist/index.cjs +885 -79
- package/dist/index.d.cts +135 -4
- package/dist/index.d.ts +135 -4
- package/dist/index.js +312 -39
- package/dist/pades.cjs +16 -3
- package/dist/pades.d.cts +5 -0
- package/dist/pades.d.ts +5 -0
- package/dist/pades.js +61 -1028
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,13 @@ import {
|
|
|
3
3
|
isMobileUserAgent,
|
|
4
4
|
openOrShowQR
|
|
5
5
|
} from "./chunk-REWNE7PO.js";
|
|
6
|
+
import {
|
|
7
|
+
OID,
|
|
8
|
+
equal,
|
|
9
|
+
ocspRequest,
|
|
10
|
+
parseCertificate,
|
|
11
|
+
parseOCSPForCert
|
|
12
|
+
} from "./chunk-52O3T7DZ.js";
|
|
6
13
|
import {
|
|
7
14
|
ApiError,
|
|
8
15
|
AuthenticationError,
|
|
@@ -33,6 +40,13 @@ var Http = class {
|
|
|
33
40
|
post(path, body, timeoutMs) {
|
|
34
41
|
return this.json("POST", path, JSON.stringify(body), "application/json", timeoutMs);
|
|
35
42
|
}
|
|
43
|
+
/** {@link post} + HTTP статус (201 / 200-г ялгах: `POST /pdf/signatures`). */
|
|
44
|
+
async postWithStatus(path, body, timeoutMs) {
|
|
45
|
+
const op = `POST ${maskPath(path)}`;
|
|
46
|
+
const res = await this.send("POST", path, JSON.stringify(body), "application/json", timeoutMs);
|
|
47
|
+
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, op));
|
|
48
|
+
return { status: res.status, body: text ? JSON.parse(text) : {} };
|
|
49
|
+
}
|
|
36
50
|
get(path, timeoutMs) {
|
|
37
51
|
return this.json("GET", path, void 0, void 0, timeoutMs);
|
|
38
52
|
}
|
|
@@ -346,12 +360,32 @@ function registrationFromCompleted(done, extra) {
|
|
|
346
360
|
if (extra.medium) out.medium = extra.medium;
|
|
347
361
|
if (extra.sessionID) out.sessionID = extra.sessionID;
|
|
348
362
|
if (done.hasPage && done.pageSha256) out.pageSha256 = done.pageSha256;
|
|
349
|
-
if (
|
|
350
|
-
|
|
363
|
+
if (extra.priorRevisionSha256?.length) {
|
|
364
|
+
if (done.priorCmsSha256.length) out.priorCmsSha256 = done.priorCmsSha256;
|
|
365
|
+
out.priorRevisionSha256 = extra.priorRevisionSha256;
|
|
366
|
+
} else if (done.priorCmsSha256.length) {
|
|
367
|
+
out.priorCmsSha256 = done.priorCmsSha256;
|
|
368
|
+
const rev = done.priorRevisionSha256 ?? [];
|
|
369
|
+
if (rev.length === done.priorCmsSha256.length) {
|
|
370
|
+
out.priorSignatures = done.priorCmsSha256.map((c, i) => ({ cmsSha256: c, revisionSha256: rev[i] }));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
351
373
|
if (extra.onBehalfOf) out.onBehalfOf = extra.onBehalfOf;
|
|
352
374
|
if (extra.signerEtsi) out.signerEtsi = extra.signerEtsi;
|
|
353
375
|
return out;
|
|
354
376
|
}
|
|
377
|
+
function priorSplits(n) {
|
|
378
|
+
const out = [];
|
|
379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
380
|
+
for (let k = 1; k <= n; k++) {
|
|
381
|
+
seen.add((1 << k) - 1);
|
|
382
|
+
out.push(Array.from({ length: n }, (_, i) => i < k));
|
|
383
|
+
}
|
|
384
|
+
if (n <= 4) {
|
|
385
|
+
for (let m = 1; m < 1 << n; m++) if (!seen.has(m)) out.push(Array.from({ length: n }, (_, i) => (m & 1 << i) !== 0));
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
355
389
|
var HEX64 = /^[0-9a-f]{64}$/;
|
|
356
390
|
var MAX_CMS_BYTES = 28672;
|
|
357
391
|
function buildRegisterBody(input) {
|
|
@@ -553,16 +587,38 @@ var SignApi = class {
|
|
|
553
587
|
* `/verify` «бүртгэлгүй» гэнэ.
|
|
554
588
|
*/
|
|
555
589
|
async registerSignature(input) {
|
|
556
|
-
|
|
590
|
+
const body = buildRegisterBody(input);
|
|
591
|
+
const prior = input.priorSignatures ?? [];
|
|
592
|
+
if (input.priorRevisionSha256?.length || prior.length === 0 || prior.length !== input.revIndex - 1) return this.registerBody(body);
|
|
593
|
+
try {
|
|
594
|
+
return await this.registerBody(body);
|
|
595
|
+
} catch (first) {
|
|
596
|
+
if (!(first instanceof ApiError && first.code === "PRIOR_MISMATCH")) throw first;
|
|
597
|
+
for (const asRevision of priorSplits(prior.length)) {
|
|
598
|
+
const next = {
|
|
599
|
+
...input,
|
|
600
|
+
priorSignatures: void 0,
|
|
601
|
+
priorCmsSha256: prior.filter((_, i) => !asRevision[i]).map((p) => p.cmsSha256),
|
|
602
|
+
priorRevisionSha256: prior.filter((_, i) => asRevision[i]).map((p) => p.revisionSha256)
|
|
603
|
+
};
|
|
604
|
+
try {
|
|
605
|
+
return await this.registerBody(buildRegisterBody(next));
|
|
606
|
+
} catch (e) {
|
|
607
|
+
if (!(e instanceof ApiError && e.code === "PRIOR_MISMATCH")) throw e;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
throw first;
|
|
611
|
+
}
|
|
557
612
|
}
|
|
558
613
|
async registerBody(body) {
|
|
559
|
-
const raw = await this.http.
|
|
614
|
+
const { status, body: raw } = await this.http.postWithStatus(routePath(RP_ROUTES.pdfSignatures), body);
|
|
560
615
|
const r = rec2(raw);
|
|
561
616
|
return {
|
|
562
617
|
docID: String(r.docID ?? body.docID),
|
|
563
618
|
revIndex: Number(r.revIndex ?? 0),
|
|
564
619
|
registeredAt: typeof r.registeredAt === "string" ? r.registeredAt : "",
|
|
565
620
|
verifyUrl: String(r.verifyUrl ?? ""),
|
|
621
|
+
created: status === 201,
|
|
566
622
|
raw
|
|
567
623
|
};
|
|
568
624
|
}
|
|
@@ -1450,13 +1506,20 @@ var ResponseValidator = class {
|
|
|
1450
1506
|
* "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
|
|
1451
1507
|
*/
|
|
1452
1508
|
async checkRevocationStatus(cert) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1509
|
+
return this.checkRevocationWith(cert, this.revocation, this.revocationMode);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* {@link checkRevocationStatus}-тэй ижил, гэхдээ TrustConfig-ийн биш өгсөн checker ба горимоор (issuer-ийг trust
|
|
1513
|
+
* pool-оос олно). checker алга бол "skipped"; горим анхдагч "hard-fail".
|
|
1514
|
+
*/
|
|
1515
|
+
async checkRevocationWith(cert, checker, mode = "hard-fail") {
|
|
1516
|
+
if (!checker) return "skipped";
|
|
1517
|
+
const status = await checker(cert, this.findIssuer(cert));
|
|
1455
1518
|
if (status === "good") return "good";
|
|
1456
1519
|
if (status === "revoked") {
|
|
1457
1520
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
|
|
1458
1521
|
}
|
|
1459
|
-
if (
|
|
1522
|
+
if (mode === "hard-fail") {
|
|
1460
1523
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 revocation \u0442\u04E9\u043B\u04E9\u0432 \u0442\u043E\u0433\u0442\u043E\u043E\u0433\u0434\u0441\u043E\u043D\u0433\u04AF\u0439 (hard-fail)");
|
|
1461
1524
|
}
|
|
1462
1525
|
return "unchecked";
|
|
@@ -1772,6 +1835,76 @@ var EidClient = class {
|
|
|
1772
1835
|
}
|
|
1773
1836
|
};
|
|
1774
1837
|
|
|
1838
|
+
// src/ocsp.ts
|
|
1839
|
+
import { createHash as createHash4 } from "crypto";
|
|
1840
|
+
var CACHE_TTL_MS = 5 * 6e4;
|
|
1841
|
+
var CACHE_MAX = 1024;
|
|
1842
|
+
var MAX_BYTES = 16 << 10;
|
|
1843
|
+
var CLOCK_SKEW_MS = 5 * 6e4;
|
|
1844
|
+
var MAX_AGE_MS = 48 * 36e5;
|
|
1845
|
+
function ocspRevocationChecker(opts = {}) {
|
|
1846
|
+
const doFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
1847
|
+
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
1848
|
+
const totalMs = Math.max(timeoutMs, 3e3);
|
|
1849
|
+
const now = opts.now ?? Date.now;
|
|
1850
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1851
|
+
async function ask(leaf, issuer) {
|
|
1852
|
+
const body = await ocspRequest(leaf, issuer);
|
|
1853
|
+
const deadline = Date.now() + totalMs;
|
|
1854
|
+
for (const raw of leaf.ocspServers) {
|
|
1855
|
+
const left = deadline - Date.now();
|
|
1856
|
+
if (left <= 0) break;
|
|
1857
|
+
try {
|
|
1858
|
+
const u = new URL(raw);
|
|
1859
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") continue;
|
|
1860
|
+
const res = await doFetch(u.toString(), {
|
|
1861
|
+
method: "POST",
|
|
1862
|
+
headers: { "Content-Type": "application/ocsp-request", Accept: "application/ocsp-response" },
|
|
1863
|
+
body,
|
|
1864
|
+
redirect: "manual",
|
|
1865
|
+
signal: AbortSignal.timeout(Math.min(timeoutMs, left))
|
|
1866
|
+
});
|
|
1867
|
+
if (res.status !== 200) continue;
|
|
1868
|
+
const der = new Uint8Array(await res.arrayBuffer());
|
|
1869
|
+
if (der.length > MAX_BYTES) continue;
|
|
1870
|
+
const resp = await parseOCSPForCert(der, leaf, issuer);
|
|
1871
|
+
const t = now();
|
|
1872
|
+
const rc = resp.certificate;
|
|
1873
|
+
if (rc && !equal(rc.raw, issuer.raw)) {
|
|
1874
|
+
if (!rc.extKeyUsage.includes(OID.kpOCSPSigning)) continue;
|
|
1875
|
+
if (t < rc.notBefore.getTime() || t > rc.notAfter.getTime()) continue;
|
|
1876
|
+
}
|
|
1877
|
+
const tu = resp.thisUpdate.date.getTime();
|
|
1878
|
+
if (tu > t + CLOCK_SKEW_MS || tu < t - MAX_AGE_MS) continue;
|
|
1879
|
+
if (resp.nextUpdate && resp.nextUpdate.getTime() < t) continue;
|
|
1880
|
+
return resp.status === "good" ? "good" : "revoked";
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return void 0;
|
|
1885
|
+
}
|
|
1886
|
+
return async (cert, issuer) => {
|
|
1887
|
+
if (!issuer) return "unknown";
|
|
1888
|
+
let leaf, iss;
|
|
1889
|
+
try {
|
|
1890
|
+
leaf = parseCertificate(new Uint8Array(cert.raw));
|
|
1891
|
+
iss = parseCertificate(new Uint8Array(issuer.raw));
|
|
1892
|
+
} catch {
|
|
1893
|
+
return "unknown";
|
|
1894
|
+
}
|
|
1895
|
+
const key = createHash4("sha256").update(issuer.raw).digest("hex").slice(0, 16) + ":" + cert.serialNumber;
|
|
1896
|
+
const exp = cache.get(key);
|
|
1897
|
+
if (exp !== void 0 && now() < exp) return "good";
|
|
1898
|
+
const status = await ask(leaf, iss);
|
|
1899
|
+
if (!status) return "unknown";
|
|
1900
|
+
if (status === "good") {
|
|
1901
|
+
if (cache.size >= CACHE_MAX) cache.clear();
|
|
1902
|
+
cache.set(key, now() + CACHE_TTL_MS);
|
|
1903
|
+
}
|
|
1904
|
+
return status;
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1775
1908
|
// src/devicelinkv3.ts
|
|
1776
1909
|
import { createHmac } from "crypto";
|
|
1777
1910
|
var TYPES = ["QR", "Web2App", "App2App"];
|
|
@@ -1874,7 +2007,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1874
2007
|
}
|
|
1875
2008
|
|
|
1876
2009
|
// src/login.ts
|
|
1877
|
-
import { createHash as
|
|
2010
|
+
import { createHash as createHash5, createHmac as createHmac2, randomBytes as randomBytes2, randomInt, randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1878
2011
|
function memoryLoginStore() {
|
|
1879
2012
|
const m = /* @__PURE__ */ new Map();
|
|
1880
2013
|
return {
|
|
@@ -1899,6 +2032,51 @@ function memoryLoginStore() {
|
|
|
1899
2032
|
}
|
|
1900
2033
|
};
|
|
1901
2034
|
}
|
|
2035
|
+
var defaultRateLimit = Object.freeze({
|
|
2036
|
+
push: Object.freeze({ limit: 3, windowMs: 6e4 })
|
|
2037
|
+
});
|
|
2038
|
+
var RATE_KINDS = ["start", "push"];
|
|
2039
|
+
function windowLimiter(limit, windowMs) {
|
|
2040
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2041
|
+
return (key) => {
|
|
2042
|
+
const now = Date.now();
|
|
2043
|
+
const h = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
|
|
2044
|
+
if (hits.size > 1e4) hits.clear();
|
|
2045
|
+
if (h.length >= limit) {
|
|
2046
|
+
hits.set(key, h);
|
|
2047
|
+
return false;
|
|
2048
|
+
}
|
|
2049
|
+
h.push(now);
|
|
2050
|
+
hits.set(key, h);
|
|
2051
|
+
return true;
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function buildRateLimit(opt) {
|
|
2055
|
+
const extra = typeof opt === "function" ? opt : void 0;
|
|
2056
|
+
const user = opt && typeof opt === "object" ? opt : {};
|
|
2057
|
+
for (const k of Object.keys(user)) {
|
|
2058
|
+
if (!RATE_KINDS.includes(k)) throw new Error(`createEidLogin: rateLimit.${k} \u2014 \u0437\u04E9\u0432\u0445\u04E9\u043D start | push`);
|
|
2059
|
+
}
|
|
2060
|
+
const merged = { ...defaultRateLimit, ...user };
|
|
2061
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
2062
|
+
for (const k of RATE_KINDS) {
|
|
2063
|
+
const r = merged[k];
|
|
2064
|
+
if (r === void 0 || r === false) continue;
|
|
2065
|
+
if (typeof r === "function") {
|
|
2066
|
+
perKind.set(k, r);
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (!(Number.isInteger(r.limit) && r.limit > 0 && r.windowMs > 0)) {
|
|
2070
|
+
throw new Error(`createEidLogin: rateLimit.${k} \u2014 limit (\u0431\u04AF\u0445\u044D\u043B > 0), windowMs (> 0)`);
|
|
2071
|
+
}
|
|
2072
|
+
perKind.set(k, windowLimiter(r.limit, r.windowMs));
|
|
2073
|
+
}
|
|
2074
|
+
return async (key) => {
|
|
2075
|
+
const fn = perKind.get(key.slice(0, key.indexOf(":")));
|
|
2076
|
+
if (fn && !await fn(key)) return false;
|
|
2077
|
+
return extra ? extra(key) : true;
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
1902
2080
|
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1903
2081
|
function forwardedClientIp(headers, trustProxy) {
|
|
1904
2082
|
if (trustProxy === false || trustProxy === 0) return void 0;
|
|
@@ -1955,13 +2133,43 @@ function readCookie(h, name) {
|
|
|
1955
2133
|
}
|
|
1956
2134
|
return "";
|
|
1957
2135
|
}
|
|
1958
|
-
var sha256hex = (s) =>
|
|
2136
|
+
var sha256hex = (s) => createHash5("sha256").update(s).digest("hex");
|
|
1959
2137
|
function safeEqualHex(a, b) {
|
|
1960
2138
|
const x = Buffer.from(a, "hex");
|
|
1961
2139
|
const y = Buffer.from(b, "hex");
|
|
1962
2140
|
return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
|
|
1963
2141
|
}
|
|
1964
|
-
function
|
|
2142
|
+
function sleep(ms2, signal) {
|
|
2143
|
+
return new Promise((resolve) => {
|
|
2144
|
+
if (ms2 <= 0 || signal?.aborted) return resolve();
|
|
2145
|
+
const done = () => {
|
|
2146
|
+
clearTimeout(t);
|
|
2147
|
+
signal?.removeEventListener("abort", done);
|
|
2148
|
+
resolve();
|
|
2149
|
+
};
|
|
2150
|
+
const t = setTimeout(done, ms2);
|
|
2151
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
function isRefusal(e) {
|
|
2155
|
+
if (!(e instanceof ApiError) || e.status < 400 || e.status >= 500 || e.status === 401 || e.status === 403 || e.status === 429) {
|
|
2156
|
+
return false;
|
|
2157
|
+
}
|
|
2158
|
+
try {
|
|
2159
|
+
JSON.parse(e.body);
|
|
2160
|
+
return true;
|
|
2161
|
+
} catch {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
var RevocationFailure = class extends ValidationError {
|
|
2166
|
+
constructor(message, revoked) {
|
|
2167
|
+
super(message);
|
|
2168
|
+
this.revoked = revoked;
|
|
2169
|
+
}
|
|
2170
|
+
revoked;
|
|
2171
|
+
};
|
|
2172
|
+
function toIdentity(sessionId, r, v, flowType, revocation) {
|
|
1965
2173
|
const s = v.parsedSubject;
|
|
1966
2174
|
const etsi = s.serialNumber ?? "";
|
|
1967
2175
|
const m = /^PNOMN-([0-9]+)$/.exec(etsi);
|
|
@@ -1977,7 +2185,8 @@ function toIdentity(sessionId, r, v, flowType) {
|
|
|
1977
2185
|
certificateLevel: v.certificateLevel,
|
|
1978
2186
|
certificateDerB64: r.certificateDerB64 ?? "",
|
|
1979
2187
|
flowType,
|
|
1980
|
-
person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
|
|
2188
|
+
person: personMatchesCertificate(r.person, s) ? r.person ?? null : null,
|
|
2189
|
+
revocation
|
|
1981
2190
|
};
|
|
1982
2191
|
}
|
|
1983
2192
|
function createEidLogin(opts) {
|
|
@@ -1998,17 +2207,29 @@ function createEidLogin(opts) {
|
|
|
1998
2207
|
const successRedirect = opts.successRedirect ?? "/";
|
|
1999
2208
|
const failureRedirect = opts.failureRedirect ?? "/";
|
|
2000
2209
|
const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
|
|
2001
|
-
const
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2210
|
+
const rateLimit = buildRateLimit(opts.rateLimit);
|
|
2211
|
+
const decoyOn = opts.decoy !== false;
|
|
2212
|
+
const pushFloorMs = opts.pushFloorMs ?? 1e3;
|
|
2213
|
+
const revocation = opts.revocation === false ? void 0 : opts.revocation ?? ocspRevocationChecker();
|
|
2214
|
+
const revocationMode = opts.revocationMode ?? "soft-fail";
|
|
2215
|
+
const auditKey = opts.auditKey ?? randomBytes2(32);
|
|
2216
|
+
const pushKey = (idCode) => createHmac2("sha256", auditKey).update(`eid-push-audit:${idCode}`).digest("hex").slice(0, 32);
|
|
2217
|
+
async function emit(kind, sessionId, meta, ctx) {
|
|
2218
|
+
if (!opts.onEvent) return;
|
|
2219
|
+
try {
|
|
2220
|
+
await opts.onEvent({ kind, sessionId, meta });
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
onError(e, ctx);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function endEvent(tx, endResult, ctx) {
|
|
2226
|
+
const kind = endResult?.startsWith("USER_REFUSED") ? "refused" : endResult === "TIMEOUT" ? "expired" : "failed";
|
|
2227
|
+
return emit(kind, tx.sessionId, { mode: tx.mode, endResult: endResult ?? "UNKNOWN" }, ctx);
|
|
2228
|
+
}
|
|
2229
|
+
function unverified(tx, e, ctx) {
|
|
2230
|
+
const reason = e instanceof RevocationFailure ? e.revoked ? "revoked" : "revocation_unavailable" : "validation";
|
|
2231
|
+
return emit("unverified", tx.sessionId, { mode: tx.mode, reason }, ctx);
|
|
2232
|
+
}
|
|
2012
2233
|
const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
|
|
2013
2234
|
const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
|
|
2014
2235
|
const json = (status2, body, extra = {}) => ({
|
|
@@ -2099,11 +2320,18 @@ function createEidLogin(opts) {
|
|
|
2099
2320
|
});
|
|
2100
2321
|
}
|
|
2101
2322
|
}
|
|
2323
|
+
return { ...await save(tx), appLink };
|
|
2324
|
+
}
|
|
2325
|
+
async function save(tx) {
|
|
2102
2326
|
const binding = randomBytes2(32).toString("base64url");
|
|
2103
2327
|
const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
|
|
2104
2328
|
await store.set(full.sessionId, full, ttl);
|
|
2105
2329
|
const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
|
|
2106
|
-
return { tx: full, setCookie, appLink };
|
|
2330
|
+
return { tx: full, setCookie, appLink: void 0 };
|
|
2331
|
+
}
|
|
2332
|
+
function startDecoy() {
|
|
2333
|
+
const acsp = void 0;
|
|
2334
|
+
return save({ sessionId: randomUUID(), mode: "push", vc: String(randomInt(1e5)).padStart(5, "0"), acsp, decoy: true });
|
|
2107
2335
|
}
|
|
2108
2336
|
async function start(req) {
|
|
2109
2337
|
const url = new URL(req.url, "http://x");
|
|
@@ -2133,18 +2361,36 @@ function createEidLogin(opts) {
|
|
|
2133
2361
|
if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
|
|
2134
2362
|
const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
|
|
2135
2363
|
if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
|
|
2136
|
-
const limited = () =>
|
|
2364
|
+
const limited = async () => {
|
|
2365
|
+
if (mode === "push") await emit("push_limited", null, { mode, pushKey: pushKey(idCode) }, ctx);
|
|
2366
|
+
return json(429, { error: "rate_limited" }, { "Retry-After": "60" });
|
|
2367
|
+
};
|
|
2137
2368
|
const ip = req.ip || (opts.trustProxy ? forwardedClientIp(req.headers, opts.trustProxy) : void 0);
|
|
2138
2369
|
if (ip && !await rateLimit(`start:${ip}`)) return limited();
|
|
2139
2370
|
if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
|
|
2371
|
+
const began = Date.now();
|
|
2140
2372
|
let r;
|
|
2373
|
+
let err;
|
|
2374
|
+
let decoy = false;
|
|
2141
2375
|
try {
|
|
2142
2376
|
r = await begin(req, mode, idCode, lang);
|
|
2143
2377
|
} catch (e) {
|
|
2144
|
-
|
|
2145
|
-
const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
|
|
2146
|
-
return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
|
|
2378
|
+
err = e;
|
|
2147
2379
|
}
|
|
2380
|
+
if (err !== void 0 && mode === "push" && decoyOn && isRefusal(err)) {
|
|
2381
|
+
const ae = err;
|
|
2382
|
+
onError(new Error(`eid-login: eID push-\u0438\u0439\u0433 \u0442\u0430\u0442\u0433\u0430\u043B\u0437\u0430\u0432 (${ae.status}${ae.code ? ` ${ae.code}` : ""}); decoy session-\u043E\u043E\u0440 \u0445\u0430\u0440\u0438\u0443\u043B\u0430\u0432`), ctx);
|
|
2383
|
+
err = void 0;
|
|
2384
|
+
r = await startDecoy();
|
|
2385
|
+
decoy = true;
|
|
2386
|
+
}
|
|
2387
|
+
if (mode === "push" && pushFloorMs > 0) await sleep(began + pushFloorMs - Date.now(), req.signal);
|
|
2388
|
+
if (err !== void 0 || !r) {
|
|
2389
|
+
onError(err, ctx);
|
|
2390
|
+
if (err instanceof ApiError && err.status === 429) return limited();
|
|
2391
|
+
return json(502, { error: "eid_unavailable" });
|
|
2392
|
+
}
|
|
2393
|
+
if (mode === "push") await emit("push_sent", r.tx.sessionId, { mode, pushKey: pushKey(idCode), decoy }, ctx);
|
|
2148
2394
|
if (mode === "web2app") {
|
|
2149
2395
|
return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
|
|
2150
2396
|
}
|
|
@@ -2156,8 +2402,19 @@ function createEidLogin(opts) {
|
|
|
2156
2402
|
}
|
|
2157
2403
|
async function finish(req, tx, r, flow, verifier) {
|
|
2158
2404
|
const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
|
|
2405
|
+
const ctx = { request: req, mode: tx.mode };
|
|
2406
|
+
let rev;
|
|
2407
|
+
let status2;
|
|
2408
|
+
const checker = revocation && (async (c, i) => status2 = await revocation(c, i));
|
|
2409
|
+
try {
|
|
2410
|
+
rev = await client.validator.checkRevocationWith(v.certificate, checker, revocationMode);
|
|
2411
|
+
} catch (e) {
|
|
2412
|
+
throw new RevocationFailure(`revocation: ${e instanceof Error ? e.message : String(e)}`, status2 === "revoked");
|
|
2413
|
+
}
|
|
2414
|
+
if (rev === "unchecked") onError(new Error("eid-login: OCSP \u0445\u0430\u0440\u0438\u0443\u0433\u04AF\u0439 \u2014 \u0446\u0443\u0446\u043B\u0430\u043B\u0442 \u0448\u0430\u043B\u0433\u0430\u043B\u0433\u04AF\u0439 \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u0432 (soft-fail)"), ctx);
|
|
2159
2415
|
if (!await store.delete(tx.sessionId)) return void 0;
|
|
2160
|
-
const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow
|
|
2416
|
+
const out = await opts.onSuccess(toIdentity(tx.sessionId, r, v, flow, rev), ctx) ?? {};
|
|
2417
|
+
await emit("success", tx.sessionId, { mode: tx.mode, flow, revocation: rev }, ctx);
|
|
2161
2418
|
return out;
|
|
2162
2419
|
}
|
|
2163
2420
|
async function status(req) {
|
|
@@ -2166,20 +2423,28 @@ function createEidLogin(opts) {
|
|
|
2166
2423
|
if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2167
2424
|
const ctx = { request: req, mode: tx.mode };
|
|
2168
2425
|
let r;
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2426
|
+
if (tx.decoy) {
|
|
2427
|
+
await sleep(pollMs, req.signal);
|
|
2428
|
+
r = { state: "RUNNING" };
|
|
2429
|
+
} else {
|
|
2430
|
+
try {
|
|
2431
|
+
r = await client.session.poll(tx.sessionId, pollMs);
|
|
2432
|
+
} catch (e) {
|
|
2433
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
2434
|
+
await store.delete(tx.sessionId);
|
|
2435
|
+
await emit("expired", tx.sessionId, { mode: tx.mode }, ctx);
|
|
2436
|
+
return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2437
|
+
}
|
|
2438
|
+
onError(e, ctx);
|
|
2439
|
+
return json(502, { error: "eid_unavailable" });
|
|
2175
2440
|
}
|
|
2176
|
-
onError(e, ctx);
|
|
2177
|
-
return json(502, { error: "eid_unavailable" });
|
|
2178
2441
|
}
|
|
2179
2442
|
const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
|
|
2180
2443
|
if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
|
|
2181
2444
|
if (r.endResult !== "OK") {
|
|
2182
2445
|
await store.delete(tx.sessionId);
|
|
2446
|
+
await endEvent(tx, r.endResult, ctx);
|
|
2447
|
+
if (tx.mode === "push" && r.endResult === "TIMEOUT") return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
2183
2448
|
return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
|
|
2184
2449
|
}
|
|
2185
2450
|
if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
|
|
@@ -2191,6 +2456,7 @@ function createEidLogin(opts) {
|
|
|
2191
2456
|
onError(e, ctx);
|
|
2192
2457
|
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
|
|
2193
2458
|
await store.delete(tx.sessionId);
|
|
2459
|
+
await unverified(tx, e, ctx);
|
|
2194
2460
|
return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
|
|
2195
2461
|
}
|
|
2196
2462
|
}
|
|
@@ -2201,9 +2467,12 @@ function createEidLogin(opts) {
|
|
|
2201
2467
|
const verifier = q.get("userChallengeVerifier") ?? "";
|
|
2202
2468
|
const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
|
|
2203
2469
|
const tx = await loadTx(req);
|
|
2204
|
-
if (!tx
|
|
2205
|
-
if (!verifier) return fail2("unbound");
|
|
2470
|
+
if (!tx) return fail2("expired");
|
|
2206
2471
|
const ctx = { request: req, mode: tx.mode };
|
|
2472
|
+
if (tx.sessionId !== sid || tx.mode !== "web2app" || !verifier) {
|
|
2473
|
+
await emit("flow_refused", tx.sessionId, { mode: tx.mode, reason: "unbound" }, ctx);
|
|
2474
|
+
return fail2("unbound");
|
|
2475
|
+
}
|
|
2207
2476
|
let r;
|
|
2208
2477
|
try {
|
|
2209
2478
|
r = await client.session.poll(tx.sessionId, 1e4);
|
|
@@ -2214,6 +2483,7 @@ function createEidLogin(opts) {
|
|
|
2214
2483
|
if (r.state !== "COMPLETE") return fail2("not_complete");
|
|
2215
2484
|
if (r.endResult !== "OK") {
|
|
2216
2485
|
await store.delete(tx.sessionId);
|
|
2486
|
+
await endEvent(tx, r.endResult, ctx);
|
|
2217
2487
|
return fail2(r.endResult ?? "UNKNOWN");
|
|
2218
2488
|
}
|
|
2219
2489
|
try {
|
|
@@ -2224,6 +2494,7 @@ function createEidLogin(opts) {
|
|
|
2224
2494
|
onError(e, ctx);
|
|
2225
2495
|
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
|
|
2226
2496
|
await store.delete(tx.sessionId);
|
|
2497
|
+
await unverified(tx, e, ctx);
|
|
2227
2498
|
return fail2("VALIDATION");
|
|
2228
2499
|
}
|
|
2229
2500
|
}
|
|
@@ -2253,7 +2524,7 @@ function createEidLogin(opts) {
|
|
|
2253
2524
|
mode: "qr"
|
|
2254
2525
|
});
|
|
2255
2526
|
}
|
|
2256
|
-
const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip });
|
|
2527
|
+
const r = await handleRequest({ method: req.method, url: req.url, headers, body, ip, signal: req.signal });
|
|
2257
2528
|
const h = new Headers();
|
|
2258
2529
|
for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
|
|
2259
2530
|
return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
|
|
@@ -2293,12 +2564,14 @@ export {
|
|
|
2293
2564
|
buildDeviceLink,
|
|
2294
2565
|
buildRegisterBody,
|
|
2295
2566
|
createEidLogin,
|
|
2567
|
+
defaultRateLimit,
|
|
2296
2568
|
deviceLink,
|
|
2297
2569
|
deviceLinkInput,
|
|
2298
2570
|
forwardedClientIp,
|
|
2299
2571
|
interactionsToBase64,
|
|
2300
2572
|
isMobileUserAgent,
|
|
2301
2573
|
memoryLoginStore,
|
|
2574
|
+
ocspRevocationChecker,
|
|
2302
2575
|
openOrShowQR,
|
|
2303
2576
|
parseExpiresAt,
|
|
2304
2577
|
parseRetryAfter,
|
package/dist/pades.cjs
CHANGED
|
@@ -3173,12 +3173,20 @@ function signatureCMSList(doc) {
|
|
|
3173
3173
|
if (v.kind() !== "dict" || v.key("Contents").isNull() || v.key("ByteRange").isNull()) continue;
|
|
3174
3174
|
const t = v.key("Type").name();
|
|
3175
3175
|
if (t !== "" && t !== "Sig") continue;
|
|
3176
|
+
let cms;
|
|
3176
3177
|
try {
|
|
3177
3178
|
const n = readNode(v.key("Contents").rawString(), 0);
|
|
3178
|
-
|
|
3179
|
+
cms = n.tag === TAG.SEQUENCE ? n.raw : void 0;
|
|
3179
3180
|
} catch {
|
|
3180
|
-
|
|
3181
|
+
cms = void 0;
|
|
3181
3182
|
}
|
|
3183
|
+
let end;
|
|
3184
|
+
const br = v.key("ByteRange");
|
|
3185
|
+
if (br.len() === 4 && br.index(2).kind() === "int" && br.index(3).kind() === "int") {
|
|
3186
|
+
const e = br.index(2).int64() + br.index(3).int64();
|
|
3187
|
+
if (e > 0 && e <= doc.length) end = e;
|
|
3188
|
+
}
|
|
3189
|
+
out.push({ cms, end });
|
|
3182
3190
|
}
|
|
3183
3191
|
return out;
|
|
3184
3192
|
}
|
|
@@ -3489,9 +3497,13 @@ async function complete(state, signature, opts = {}) {
|
|
|
3489
3497
|
}
|
|
3490
3498
|
if (out.length > MAX_OUTPUT_BYTES) throw new PadesError("PDF_TOO_LARGE", "\u0433\u0430\u0440\u0430\u043B\u0442 10 MiB-\u0430\u0430\u0441 \u0438\u0445");
|
|
3491
3499
|
const prior = [];
|
|
3500
|
+
const priorRev = [];
|
|
3492
3501
|
try {
|
|
3493
3502
|
const list = signatureCMSList(signed);
|
|
3494
|
-
for (const
|
|
3503
|
+
for (const s of list.slice(0, -1)) {
|
|
3504
|
+
prior.push(s.cms ? toHex(await sha256(s.cms)) : "");
|
|
3505
|
+
priorRev.push(s.end !== void 0 ? toHex(await sha256(signed.subarray(0, s.end))) : "");
|
|
3506
|
+
}
|
|
3495
3507
|
} catch (e) {
|
|
3496
3508
|
throw new PadesError("SELF_VALIDATION_FAILED", "\u0433\u0430\u0440\u0430\u043B\u0442\u044B\u0433 \u0437\u0430\u0434\u0430\u043B\u0436 \u0447\u0430\u0434\u0441\u0430\u043D\u0433\u04AF\u0439", { cause: e });
|
|
3497
3509
|
}
|
|
@@ -3506,6 +3518,7 @@ async function complete(state, signature, opts = {}) {
|
|
|
3506
3518
|
revIndex: st.revIndex,
|
|
3507
3519
|
sigRevSha256: toHex(await sha256(signed)),
|
|
3508
3520
|
priorCmsSha256: prior,
|
|
3521
|
+
priorRevisionSha256: priorRev,
|
|
3509
3522
|
signerCert: cert.raw
|
|
3510
3523
|
};
|
|
3511
3524
|
}
|
package/dist/pades.d.cts
CHANGED
|
@@ -103,6 +103,11 @@ interface PadesCompleted {
|
|
|
103
103
|
sigRevSha256: string;
|
|
104
104
|
/** Өмнөх гарын үсгүүдийн CMS-ийн SHA-256 (hex), баримтын дарааллаар. */
|
|
105
105
|
priorCmsSha256: string[];
|
|
106
|
+
/**
|
|
107
|
+
* Өмнөх гарын үсгүүдийн revision-ий SHA-256 (hex) — `sha256(doc[0 : ByteRange[2]+ByteRange[3]])`, `priorCmsSha256`-тай
|
|
108
|
+
* ижил дарааллаар. CA горим A-ийн (CA угсарсан) мөрийг үүгээр таньдаг (`priorRevisionSha256`).
|
|
109
|
+
*/
|
|
110
|
+
priorRevisionSha256: string[];
|
|
106
111
|
/** Зурагчийн гэрчилгээ (DER). */
|
|
107
112
|
signerCert: Uint8Array;
|
|
108
113
|
}
|
package/dist/pades.d.ts
CHANGED
|
@@ -103,6 +103,11 @@ interface PadesCompleted {
|
|
|
103
103
|
sigRevSha256: string;
|
|
104
104
|
/** Өмнөх гарын үсгүүдийн CMS-ийн SHA-256 (hex), баримтын дарааллаар. */
|
|
105
105
|
priorCmsSha256: string[];
|
|
106
|
+
/**
|
|
107
|
+
* Өмнөх гарын үсгүүдийн revision-ий SHA-256 (hex) — `sha256(doc[0 : ByteRange[2]+ByteRange[3]])`, `priorCmsSha256`-тай
|
|
108
|
+
* ижил дарааллаар. CA горим A-ийн (CA угсарсан) мөрийг үүгээр таньдаг (`priorRevisionSha256`).
|
|
109
|
+
*/
|
|
110
|
+
priorRevisionSha256: string[];
|
|
106
111
|
/** Зурагчийн гэрчилгээ (DER). */
|
|
107
112
|
signerCert: Uint8Array;
|
|
108
113
|
}
|