@gerege-systems/eid-mongolia-sdk 0.9.1 → 0.11.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 +82 -5
- package/dist/{browser-B_XtyuFG.d.cts → browser-BVR6gtrx.d.cts} +115 -5
- package/dist/{browser-B_XtyuFG.d.ts → browser-BVR6gtrx.d.ts} +115 -5
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/{chunk-NXDKQZIW.js → chunk-QPJDZLHR.js} +81 -2
- package/dist/index.cjs +785 -63
- package/dist/index.d.cts +402 -8
- package/dist/index.d.ts +402 -8
- package/dist/index.js +696 -57
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -23,7 +23,10 @@ __export(index_exports, {
|
|
|
23
23
|
ApiError: () => ApiError,
|
|
24
24
|
AuthApi: () => AuthApi,
|
|
25
25
|
AuthenticationError: () => AuthenticationError,
|
|
26
|
+
CITIZEN_ISSUING_CA_SKI: () => CITIZEN_ISSUING_CA_SKI,
|
|
26
27
|
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
28
|
+
DEFAULT_PDF_MAX_BYTES: () => DEFAULT_PDF_MAX_BYTES,
|
|
29
|
+
DEFAULT_PDF_TIMEOUT_MS: () => DEFAULT_PDF_TIMEOUT_MS,
|
|
27
30
|
DeviceLinkError: () => DeviceLinkError,
|
|
28
31
|
EID_ACR_ADVANCED: () => EID_ACR_ADVANCED,
|
|
29
32
|
EID_ACR_QUALIFIED: () => EID_ACR_QUALIFIED,
|
|
@@ -38,10 +41,14 @@ __export(index_exports, {
|
|
|
38
41
|
ON_BEHALF_ERROR_CODES: () => ON_BEHALF_ERROR_CODES,
|
|
39
42
|
OrganizationApi: () => OrganizationApi,
|
|
40
43
|
PdfApi: () => PdfApi,
|
|
44
|
+
PersonApi: () => PersonApi,
|
|
45
|
+
RETRYABLE_ERROR_CODES: () => RETRYABLE_ERROR_CODES,
|
|
46
|
+
ResponseTooLargeError: () => ResponseTooLargeError,
|
|
41
47
|
ResponseValidator: () => ResponseValidator,
|
|
42
48
|
SessionApi: () => SessionApi,
|
|
43
49
|
SessionFailedError: () => SessionFailedError,
|
|
44
50
|
SignApi: () => SignApi,
|
|
51
|
+
TokenApi: () => TokenApi,
|
|
45
52
|
ValidationError: () => ValidationError,
|
|
46
53
|
acspV2Digest: () => acspV2Digest,
|
|
47
54
|
buildAcspV2Payload: () => buildAcspV2Payload,
|
|
@@ -50,16 +57,21 @@ __export(index_exports, {
|
|
|
50
57
|
interactionsToBase64: () => interactionsToBase64,
|
|
51
58
|
isMobileUserAgent: () => isMobileUserAgent,
|
|
52
59
|
openOrShowQR: () => openOrShowQR,
|
|
60
|
+
parseRetryAfter: () => parseRetryAfter,
|
|
53
61
|
parseSessionResult: () => parseSessionResult,
|
|
54
62
|
parseSubject: () => parseSubject,
|
|
63
|
+
personMatchesCertificate: () => personMatchesCertificate,
|
|
55
64
|
pkceChallenge: () => pkceChallenge,
|
|
56
65
|
qrDeviceLinkTicker: () => qrDeviceLinkTicker,
|
|
57
66
|
randomChallenge: () => randomChallenge,
|
|
58
67
|
resolveBaseUrl: () => resolveBaseUrl,
|
|
59
68
|
sha256Base64: () => sha256Base64,
|
|
69
|
+
tokenAuthDigest: () => tokenAuthDigest,
|
|
70
|
+
tokenLimitOf: () => tokenLimitOf,
|
|
60
71
|
userChallengeOf: () => userChallengeOf,
|
|
61
72
|
verifyPayloadSignature: () => verifyPayloadSignature,
|
|
62
|
-
verifyPrehashedSignature: () => verifyPrehashedSignature
|
|
73
|
+
verifyPrehashedSignature: () => verifyPrehashedSignature,
|
|
74
|
+
verifyTokenCertificates: () => verifyTokenCertificates
|
|
63
75
|
});
|
|
64
76
|
module.exports = __toCommonJS(index_exports);
|
|
65
77
|
|
|
@@ -71,25 +83,79 @@ var EidError = class extends Error {
|
|
|
71
83
|
}
|
|
72
84
|
};
|
|
73
85
|
var AuthenticationError = class extends EidError {
|
|
86
|
+
/** Серверийн `{"code"}`; байхгүй / JSON биш (proxy) бол undefined. */
|
|
87
|
+
code;
|
|
88
|
+
/** Серверийн `{"error"}` текст (логт); JSON биш (proxy) бол undefined. */
|
|
89
|
+
serverMessage;
|
|
90
|
+
/** Хариуны бие (таслагдаагүй текст). */
|
|
91
|
+
body;
|
|
92
|
+
headers;
|
|
93
|
+
constructor(message, init) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.code = init?.code;
|
|
96
|
+
this.serverMessage = init?.serverMessage;
|
|
97
|
+
this.body = init?.body;
|
|
98
|
+
this.headers = init?.headers;
|
|
99
|
+
}
|
|
74
100
|
};
|
|
75
101
|
var ForbiddenError = class extends EidError {
|
|
76
|
-
constructor(message, code) {
|
|
102
|
+
constructor(message, code, init) {
|
|
77
103
|
super(message);
|
|
78
104
|
this.code = code;
|
|
105
|
+
this.headers = init?.headers;
|
|
106
|
+
this.retryAfterMs = init?.retryAfterMs;
|
|
79
107
|
}
|
|
80
108
|
code;
|
|
109
|
+
headers;
|
|
110
|
+
/** Retry-After (мс), байхгүй бол undefined. */
|
|
111
|
+
retryAfterMs;
|
|
81
112
|
};
|
|
82
113
|
var ApiError = class extends EidError {
|
|
83
|
-
constructor(status, body, code) {
|
|
114
|
+
constructor(status, body, code, init) {
|
|
84
115
|
super(`RP-API ${status}: ${body}`);
|
|
85
116
|
this.status = status;
|
|
86
117
|
this.body = body;
|
|
87
118
|
this.code = code;
|
|
119
|
+
this.headers = init?.headers;
|
|
120
|
+
this.retryAfterMs = init?.retryAfterMs;
|
|
88
121
|
}
|
|
89
122
|
status;
|
|
90
123
|
body;
|
|
91
124
|
code;
|
|
125
|
+
headers;
|
|
126
|
+
/**
|
|
127
|
+
* Retry-After (мс, секунд эсвэл HTTP-огноо задалсан); байхгүй/буруу бол undefined. CA түр алдаанд (429
|
|
128
|
+
* PDF_PREPARE_BUSY / RATE_LIMITED, 503 DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE г.м.) тавина.
|
|
129
|
+
*/
|
|
130
|
+
retryAfterMs;
|
|
131
|
+
/**
|
|
132
|
+
* CA-ийн түр алдаа ({@link RETRYABLE_ERROR_CODES}-ийн нэг, эсвэл 429/503 + Retry-After): `retryAfterMs`-ийн дараа
|
|
133
|
+
* ижил хүсэлтийг давтаж болно.
|
|
134
|
+
*/
|
|
135
|
+
get retryable() {
|
|
136
|
+
if (this.code && RETRYABLE_ERROR_CODES.includes(this.code)) return true;
|
|
137
|
+
return (this.status === 429 || this.status === 503) && (this.retryAfterMs ?? 0) > 0;
|
|
138
|
+
}
|
|
92
139
|
};
|
|
140
|
+
var RETRYABLE_ERROR_CODES = [
|
|
141
|
+
"PDF_PREPARE_BUSY",
|
|
142
|
+
"DOCUMENT_PENDING",
|
|
143
|
+
"TSA_UNAVAILABLE",
|
|
144
|
+
"LTV_UNAVAILABLE",
|
|
145
|
+
"RATE_LIMITED",
|
|
146
|
+
"STATE_UNAVAILABLE",
|
|
147
|
+
"ISSUANCE_PENDING"
|
|
148
|
+
];
|
|
149
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
150
|
+
const v = (value ?? "").trim();
|
|
151
|
+
if (!v) return void 0;
|
|
152
|
+
if (/^-?\d+$/.test(v)) {
|
|
153
|
+
const n = Number(v);
|
|
154
|
+
return n > 0 && n <= 86400 ? n * 1e3 : void 0;
|
|
155
|
+
}
|
|
156
|
+
const t = Date.parse(v);
|
|
157
|
+
return Number.isNaN(t) || t <= now ? void 0 : t - now;
|
|
158
|
+
}
|
|
93
159
|
var ON_BEHALF_ERROR_CODES = [
|
|
94
160
|
"REPRESENTATION_DENIED",
|
|
95
161
|
"REPRESENTATION_PENDING",
|
|
@@ -100,7 +166,28 @@ var ON_BEHALF_ERROR_CODES = [
|
|
|
100
166
|
"REPRESENTATION_REVOKED"
|
|
101
167
|
];
|
|
102
168
|
var NetworkError = class extends EidError {
|
|
169
|
+
/** "GET /session/…" (таних тэмдэг хасагдсан зам). */
|
|
170
|
+
op;
|
|
171
|
+
/** Хүсэлт хугацаа хэтэрсэн. */
|
|
172
|
+
timeout;
|
|
173
|
+
constructor(message, init) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.op = init?.op;
|
|
176
|
+
this.timeout = init?.timeout;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var ResponseTooLargeError = class extends EidError {
|
|
180
|
+
constructor(op, limit) {
|
|
181
|
+
super(`${op}: \u0445\u0430\u0440\u0438\u0443 ${limit} \u0431\u0430\u0439\u0442\u0430\u0430\u0441 \u0438\u0445`);
|
|
182
|
+
this.op = op;
|
|
183
|
+
this.limit = limit;
|
|
184
|
+
}
|
|
185
|
+
op;
|
|
186
|
+
limit;
|
|
103
187
|
};
|
|
188
|
+
function maskPath(path) {
|
|
189
|
+
return path.split(/[?#]/)[0].replace(/\/(etsi|document)\/[^/?#\s]+/g, "/$1/\u2026");
|
|
190
|
+
}
|
|
104
191
|
var SessionFailedError = class extends EidError {
|
|
105
192
|
constructor(endResult) {
|
|
106
193
|
super(`Session \u0430\u043C\u0436\u0438\u043B\u0442\u0433\u04AF\u0439: ${endResult}`);
|
|
@@ -115,6 +202,7 @@ var DeviceLinkError = class extends EidError {
|
|
|
115
202
|
|
|
116
203
|
// src/http.ts
|
|
117
204
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
205
|
+
var MAX_JSON_BYTES = 4 << 20;
|
|
118
206
|
var Http = class {
|
|
119
207
|
constructor(cfg) {
|
|
120
208
|
this.cfg = cfg;
|
|
@@ -130,14 +218,14 @@ var Http = class {
|
|
|
130
218
|
postMultipart(path, form, timeoutMs) {
|
|
131
219
|
return this.json("POST", path, form, void 0, timeoutMs);
|
|
132
220
|
}
|
|
133
|
-
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF). */
|
|
134
|
-
async getBytes(path, timeoutMs) {
|
|
221
|
+
/** GET — хариуг байтаар (ж: гарын үсэгтэй PDF), `maxBytes` хүртэл; хэтэрвэл {@link ResponseTooLargeError}. */
|
|
222
|
+
async getBytes(path, timeoutMs, maxBytes = 64 << 20) {
|
|
135
223
|
const res = await this.send("GET", path, void 0, void 0, timeoutMs);
|
|
136
|
-
return
|
|
224
|
+
return readLimited(res, maxBytes, `GET ${maskPath(path)}`);
|
|
137
225
|
}
|
|
138
226
|
async json(method, path, body, contentType, timeoutMs) {
|
|
139
227
|
const res = await this.send(method, path, body, contentType, timeoutMs);
|
|
140
|
-
const text = await res
|
|
228
|
+
const text = new TextDecoder().decode(await readLimited(res, MAX_JSON_BYTES, `${method} ${maskPath(path)}`));
|
|
141
229
|
return text ? JSON.parse(text) : {};
|
|
142
230
|
}
|
|
143
231
|
async send(method, path, body, contentType, timeoutMs) {
|
|
@@ -154,28 +242,62 @@ var Http = class {
|
|
|
154
242
|
};
|
|
155
243
|
if (this.cfg.dispatcher) init.dispatcher = this.cfg.dispatcher;
|
|
156
244
|
let res;
|
|
245
|
+
const op = `${method} ${maskPath(path)}`;
|
|
157
246
|
try {
|
|
158
247
|
res = await f(url, init);
|
|
159
248
|
} catch (e) {
|
|
160
249
|
const err = e;
|
|
161
250
|
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
162
|
-
throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${
|
|
251
|
+
throw new NetworkError(`\u0425\u04AF\u0441\u044D\u043B\u0442 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0445\u044D\u0442\u044D\u0440\u043B\u044D\u044D: ${op}`, { op, timeout: true });
|
|
163
252
|
}
|
|
164
|
-
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${
|
|
253
|
+
throw new NetworkError(`\u0421\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${op}): ${err.message.split(url).join(op)}`, { op, timeout: false });
|
|
165
254
|
}
|
|
166
255
|
if (res.status < 300) return res;
|
|
167
256
|
const text = await res.text();
|
|
168
257
|
const { error, code } = errorBody(text);
|
|
169
|
-
|
|
258
|
+
const headers = res.headers;
|
|
259
|
+
const retryAfterMs = parseRetryAfter(headers.get("Retry-After"));
|
|
260
|
+
if (res.status === 401) {
|
|
261
|
+
throw new AuthenticationError(`API secret \u0431\u0443\u0440\u0443\u0443 \u044D\u0441\u0432\u044D\u043B \u0431\u0430\u0439\u0445\u0433\u04AF\u0439 (401${code ? ` ${code}` : ""})`, { code, serverMessage: error, body: text, headers });
|
|
262
|
+
}
|
|
170
263
|
if (res.status === 403) {
|
|
171
264
|
throw new ForbiddenError(
|
|
172
265
|
error ? `RP-API 403${code ? ` ${code}` : ""}: ${error}` : "IP allowlist \u044D\u0441\u0432\u044D\u043B mTLS \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u04E9\u0433\u04AF\u0439 (403)",
|
|
173
|
-
code
|
|
266
|
+
code,
|
|
267
|
+
{ headers, retryAfterMs }
|
|
174
268
|
);
|
|
175
269
|
}
|
|
176
|
-
throw new ApiError(res.status, text, code);
|
|
270
|
+
throw new ApiError(res.status, text, code, { headers, retryAfterMs });
|
|
177
271
|
}
|
|
178
272
|
};
|
|
273
|
+
async function readLimited(res, limit, op) {
|
|
274
|
+
const declared = Number(res.headers.get("content-length") ?? NaN);
|
|
275
|
+
if (declared > limit) {
|
|
276
|
+
await res.body?.cancel().catch(() => void 0);
|
|
277
|
+
throw new ResponseTooLargeError(op, limit);
|
|
278
|
+
}
|
|
279
|
+
if (!res.body) return new Uint8Array(0);
|
|
280
|
+
const reader = res.body.getReader();
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let total = 0;
|
|
283
|
+
for (; ; ) {
|
|
284
|
+
const { done, value } = await reader.read();
|
|
285
|
+
if (done) break;
|
|
286
|
+
total += value.byteLength;
|
|
287
|
+
if (total > limit) {
|
|
288
|
+
await reader.cancel().catch(() => void 0);
|
|
289
|
+
throw new ResponseTooLargeError(op, limit);
|
|
290
|
+
}
|
|
291
|
+
chunks.push(value);
|
|
292
|
+
}
|
|
293
|
+
const out = new Uint8Array(total);
|
|
294
|
+
let off = 0;
|
|
295
|
+
for (const c of chunks) {
|
|
296
|
+
out.set(c, off);
|
|
297
|
+
off += c.byteLength;
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
179
301
|
function errorBody(text) {
|
|
180
302
|
try {
|
|
181
303
|
const v = JSON.parse(text);
|
|
@@ -190,6 +312,51 @@ function errorBody(text) {
|
|
|
190
312
|
}
|
|
191
313
|
}
|
|
192
314
|
|
|
315
|
+
// src/contract.ts
|
|
316
|
+
var RP_ROUTES = {
|
|
317
|
+
authNotificationEtsi: { method: "POST", path: "/authentication/notification/etsi/{semanticsIdentifier}" },
|
|
318
|
+
authNotificationDocument: { method: "POST", path: "/authentication/notification/document/{documentNumber}" },
|
|
319
|
+
authDeviceLinkAnonymous: { method: "POST", path: "/authentication/device-link/anonymous" },
|
|
320
|
+
authDeviceLinkEtsi: { method: "POST", path: "/authentication/device-link/etsi/{semanticsIdentifier}" },
|
|
321
|
+
signNotificationEtsi: { method: "POST", path: "/signature/notification/etsi/{semanticsIdentifier}" },
|
|
322
|
+
signNotificationDocument: { method: "POST", path: "/signature/notification/document/{documentNumber}" },
|
|
323
|
+
pdfPrepare: { method: "POST", path: "/pdf/sign/prepare" },
|
|
324
|
+
pdfDocument: { method: "GET", path: "/pdf/sign/{sessionID}/document" },
|
|
325
|
+
orgRepresentations: { method: "GET", path: "/organization/representations/etsi/{personEtsi}" },
|
|
326
|
+
session: { method: "GET", path: "/session/{sessionId}" },
|
|
327
|
+
personCertificates: { method: "GET", path: "/certificates/etsi/{personEtsi}" },
|
|
328
|
+
personDevices: { method: "GET", path: "/devices/etsi/{personEtsi}" },
|
|
329
|
+
personActivity: { method: "GET", path: "/rp/activity/etsi/{personEtsi}" },
|
|
330
|
+
personSummary: { method: "GET", path: "/person/summary/etsi/{personEtsi}" },
|
|
331
|
+
personChildren: { method: "GET", path: "/person/children/etsi/{personEtsi}" },
|
|
332
|
+
tokenCertificate: { method: "POST", path: "/token/certificate" },
|
|
333
|
+
tokenCertificateResult: { method: "GET", path: "/token/certificate/{sessionID}" }
|
|
334
|
+
};
|
|
335
|
+
var RP_ROUTES_UNDISCOVERED = {
|
|
336
|
+
tokenAuthChallenge: { method: "POST", path: "/token/auth/challenge" },
|
|
337
|
+
tokenAuthVerify: { method: "POST", path: "/token/auth/verify" },
|
|
338
|
+
pdfTokenPrepare: { method: "POST", path: "/pdf/token/prepare" },
|
|
339
|
+
pdfTokenFinalize: { method: "POST", path: "/pdf/token/{signingID}/finalize" },
|
|
340
|
+
pdfTokenDocument: { method: "GET", path: "/pdf/token/{signingID}/document" }
|
|
341
|
+
};
|
|
342
|
+
function routePath(route, ...args) {
|
|
343
|
+
let i = 0;
|
|
344
|
+
const out = route.path.replace(/\{[^}]*\}/g, () => {
|
|
345
|
+
const a = args[i++];
|
|
346
|
+
if (a === void 0) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0434\u0443\u0442\u0443\u0443`);
|
|
347
|
+
return encodeURIComponent(a);
|
|
348
|
+
});
|
|
349
|
+
if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
var OIDC_EXPECT = {
|
|
353
|
+
defaultScope: "openid",
|
|
354
|
+
pkceMethod: "S256",
|
|
355
|
+
idTokenAlg: "ES256",
|
|
356
|
+
/** `Authorization: Basic` (RFC 6749 §2.3.1). */
|
|
357
|
+
tokenEndpointAuthMethod: "client_secret_basic"
|
|
358
|
+
};
|
|
359
|
+
|
|
193
360
|
// src/crypto.ts
|
|
194
361
|
var import_node_crypto = require("crypto");
|
|
195
362
|
function sha256Base64(data) {
|
|
@@ -292,25 +459,25 @@ var AuthApi = class {
|
|
|
292
459
|
/** Push — РД/иргэний дугаар/ETSI-ээр (сервер төрлийг таьна). Иргэний утас руу мэдэгдэл очно. */
|
|
293
460
|
async notificationByEtsi(id, interactions, opts) {
|
|
294
461
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
295
|
-
const raw = await this.http.post(
|
|
462
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationEtsi, id), body);
|
|
296
463
|
return this.parseNotification(raw, acsp);
|
|
297
464
|
}
|
|
298
465
|
/** Push — баримтын дугаараар (тодорхой төхөөрөмж рүү). */
|
|
299
466
|
async notificationByDocument(documentNumber, interactions, opts) {
|
|
300
467
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["Notification"], opts);
|
|
301
|
-
const raw = await this.http.post(
|
|
468
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authNotificationDocument, documentNumber), body);
|
|
302
469
|
return this.parseNotification(raw, acsp);
|
|
303
470
|
}
|
|
304
471
|
/** QR/App2App — нэр томьёололгүй (иргэн хэн болохыг урьдчилж мэдэхгүй). QR код үүсгэхэд. */
|
|
305
472
|
async deviceLinkAnonymous(interactions, opts) {
|
|
306
473
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
307
|
-
const raw = await this.http.post(
|
|
474
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkAnonymous), body);
|
|
308
475
|
return this.parseDeviceLink(raw, acsp);
|
|
309
476
|
}
|
|
310
477
|
/** QR/App2App — тодорхой иргэн (РД/civil_id/ETSI). */
|
|
311
478
|
async deviceLinkByEtsi(id, interactions, opts) {
|
|
312
479
|
const { body, acsp } = this.baseBody(randomChallenge(), interactions, ["QR"], opts);
|
|
313
|
-
const raw = await this.http.post(
|
|
480
|
+
const raw = await this.http.post(routePath(RP_ROUTES.authDeviceLinkEtsi, id), body);
|
|
314
481
|
return this.parseDeviceLink(raw, acsp);
|
|
315
482
|
}
|
|
316
483
|
};
|
|
@@ -343,6 +510,7 @@ var SignApi = class {
|
|
|
343
510
|
};
|
|
344
511
|
if (opts?.callbackUrl?.trim()) body.initialCallbackUrl = opts.callbackUrl.trim();
|
|
345
512
|
if (opts?.onBehalfOf?.trim()) body.onBehalfOf = opts.onBehalfOf.trim();
|
|
513
|
+
if (opts?.fileName?.trim()) body.fileName = opts.fileName.trim();
|
|
346
514
|
return body;
|
|
347
515
|
}
|
|
348
516
|
parse(raw) {
|
|
@@ -355,12 +523,12 @@ var SignApi = class {
|
|
|
355
523
|
* digest-ийг гаднаас бэлдэж өгнө — текст бус бинар баримтад тохиромжтой.
|
|
356
524
|
*/
|
|
357
525
|
async digestByEtsi(id, digestB64, interactions, opts) {
|
|
358
|
-
const raw = await this.http.post(
|
|
526
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationEtsi, id), this.body(digestB64, interactions, opts));
|
|
359
527
|
return this.parse(raw);
|
|
360
528
|
}
|
|
361
529
|
/** Бэлэн digest-ээр push sign — баримтын дугаараар (auth-аас гарсан documentNumber дээр). */
|
|
362
530
|
async digestByDocument(documentNumber, digestB64, interactions, opts) {
|
|
363
|
-
const raw = await this.http.post(
|
|
531
|
+
const raw = await this.http.post(routePath(RP_ROUTES.signNotificationDocument, documentNumber), this.body(digestB64, interactions, opts));
|
|
364
532
|
return this.parse(raw);
|
|
365
533
|
}
|
|
366
534
|
/** Текстийн SHA-256 digest-ийг дотроо тооцоод push sign (богино текст баримтад тохиромжтой). */
|
|
@@ -401,9 +569,44 @@ function parseSessionResult(raw) {
|
|
|
401
569
|
signatureAlgorithmParameters: parseSigParams(sig.signatureAlgorithmParameters),
|
|
402
570
|
interactionTypeUsed: str(r.interactionTypeUsed),
|
|
403
571
|
onBehalfOf: parseOrg(r.onBehalfOf),
|
|
404
|
-
pdf: parsePdf(r.pdf)
|
|
572
|
+
pdf: parsePdf(r.pdf),
|
|
573
|
+
person: parsePerson(r.person),
|
|
574
|
+
tokenCertificate: parseTokenCert(r.tokenCertificate),
|
|
575
|
+
raw
|
|
405
576
|
};
|
|
406
577
|
}
|
|
578
|
+
var PERSON_STRINGS = [
|
|
579
|
+
"firstName",
|
|
580
|
+
"lastName",
|
|
581
|
+
"familyName",
|
|
582
|
+
"firstNameEn",
|
|
583
|
+
"lastNameEn",
|
|
584
|
+
"civilId",
|
|
585
|
+
"regNo",
|
|
586
|
+
"birthDate",
|
|
587
|
+
"gender"
|
|
588
|
+
];
|
|
589
|
+
function parsePerson(raw) {
|
|
590
|
+
const p = asRecord(raw);
|
|
591
|
+
const out = {};
|
|
592
|
+
for (const k of PERSON_STRINGS) if (typeof p[k] === "string" && p[k]) out[k] = p[k];
|
|
593
|
+
if (typeof p.geID === "number" && p.geID !== 0) out.geID = p.geID;
|
|
594
|
+
return Object.keys(out).length ? out : null;
|
|
595
|
+
}
|
|
596
|
+
function parseTokenCert(raw) {
|
|
597
|
+
const t = asRecord(raw);
|
|
598
|
+
const status = str(t.status);
|
|
599
|
+
if (!status) return null;
|
|
600
|
+
const out = { status };
|
|
601
|
+
if (t.errorCode) out.errorCode = String(t.errorCode);
|
|
602
|
+
if (t.authSerialHex) out.authSerialHex = String(t.authSerialHex);
|
|
603
|
+
if (t.signSerialHex) out.signSerialHex = String(t.signSerialHex);
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
function personMatchesCertificate(person, subject) {
|
|
607
|
+
const serial = typeof subject === "string" ? subject : subject?.serialNumber;
|
|
608
|
+
return !!person?.civilId && !!serial && serial === `PNOMN-${person.civilId}`;
|
|
609
|
+
}
|
|
407
610
|
function parsePdf(raw) {
|
|
408
611
|
const p = asRecord(raw);
|
|
409
612
|
const docID = str(p.docID);
|
|
@@ -457,7 +660,7 @@ var SessionApi = class {
|
|
|
457
660
|
/** Нэг удаагийн long-poll. serverPollMs хүртэл хүлээнэ. */
|
|
458
661
|
async poll(sessionId, serverPollMs = SERVER_POLL_MS) {
|
|
459
662
|
const raw = await this.http.get(
|
|
460
|
-
|
|
663
|
+
`${routePath(RP_ROUTES.session, sessionId)}?timeoutMs=${serverPollMs}`,
|
|
461
664
|
serverPollMs + 1e4
|
|
462
665
|
// HTTP timeout нь серверийн poll-оос урт
|
|
463
666
|
);
|
|
@@ -482,6 +685,8 @@ var SessionApi = class {
|
|
|
482
685
|
|
|
483
686
|
// src/pdf.ts
|
|
484
687
|
var import_node_crypto2 = require("crypto");
|
|
688
|
+
var DEFAULT_PDF_MAX_BYTES = 64 << 20;
|
|
689
|
+
var DEFAULT_PDF_TIMEOUT_MS = 8e4;
|
|
485
690
|
function rec3(v) {
|
|
486
691
|
return v && typeof v === "object" ? v : {};
|
|
487
692
|
}
|
|
@@ -494,7 +699,10 @@ var PdfApi = class {
|
|
|
494
699
|
http;
|
|
495
700
|
creds;
|
|
496
701
|
defaults;
|
|
497
|
-
/**
|
|
702
|
+
/**
|
|
703
|
+
* POST /pdf/sign/prepare — multipart (`request` JSON + `pdf`). SIGN эрх шаардана. Түр алдаа: `ApiError` 429
|
|
704
|
+
* PDF_PREPARE_BUSY, 503 LTV_UNAVAILABLE / TSA_UNAVAILABLE — `retryAfterMs`-ийн дараа дахин (`retryable`).
|
|
705
|
+
*/
|
|
498
706
|
async prepare(input) {
|
|
499
707
|
const request = {
|
|
500
708
|
relyingPartyUUID: this.creds.rpUUID,
|
|
@@ -510,7 +718,7 @@ var PdfApi = class {
|
|
|
510
718
|
const form = new FormData();
|
|
511
719
|
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
512
720
|
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
513
|
-
const r = rec3(await this.http.postMultipart(
|
|
721
|
+
const r = rec3(await this.http.postMultipart(routePath(RP_ROUTES.pdfPrepare), form, this.timeout));
|
|
514
722
|
const receivedAt = Date.now();
|
|
515
723
|
const vc = rec3(r.vc).value;
|
|
516
724
|
const dl = rec3(r.deviceLink);
|
|
@@ -532,10 +740,80 @@ var PdfApi = class {
|
|
|
532
740
|
}
|
|
533
741
|
/**
|
|
534
742
|
* GET /pdf/sign/{sessionID}/document — гарын үсэгтэй PDF (READY-ээс хойш 30 мин). Татсан байтын SHA-256-ийг
|
|
535
|
-
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`.
|
|
743
|
+
* session-ий `pdf.outSha256`-тай тулга. Эрх хасагдсан бол `ForbiddenError` `.code = "REPRESENTATION_REVOKED"`. Баримт
|
|
744
|
+
* бэлэн болоогүй бол `ApiError` 503 `.code` DOCUMENT_PENDING / TSA_UNAVAILABLE / LTV_UNAVAILABLE — `retryAfterMs`-ийн
|
|
745
|
+
* дараа дахин татна (`retryable`).
|
|
536
746
|
*/
|
|
537
747
|
document(sessionId) {
|
|
538
|
-
return this.http.getBytes(
|
|
748
|
+
return this.http.getBytes(routePath(RP_ROUTES.pdfDocument, sessionId), this.timeout, this.maxBytes);
|
|
749
|
+
}
|
|
750
|
+
get timeout() {
|
|
751
|
+
return this.defaults.timeoutMs ?? DEFAULT_PDF_TIMEOUT_MS;
|
|
752
|
+
}
|
|
753
|
+
get maxBytes() {
|
|
754
|
+
return this.defaults.maxBytes ?? DEFAULT_PDF_MAX_BYTES;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* USB токены PAdES (CA docs/PADES_SIGNING.md §16.5, TOKEN_CERT.md) — session / утас оролцохгүй. POST /pdf/token/prepare
|
|
758
|
+
* (multipart: `request` JSON + `pdf`), SIGN эрх. Дараа нь токеноор `hashToSign`-ийг CKM_ECDSA (prehashed, P-256)-аар
|
|
759
|
+
* зурж {@link tokenFinalize}. Түр алдаа: `ApiError` 429 PDF_PREPARE_BUSY / RATE_LIMITED, 503 TSA_UNAVAILABLE /
|
|
760
|
+
* LTV_UNAVAILABLE / STATE_UNAVAILABLE (`retryable`). Гэрчилгээ токенд холбогдоогүй / цуцлагдсан → 409
|
|
761
|
+
* TOKEN_CERT_INVALID. Зам нь CA-д бий ч `/.well-known/eid`-д хараахан алга (гэрээний тест WARN).
|
|
762
|
+
*/
|
|
763
|
+
async tokenPrepare(input) {
|
|
764
|
+
if (!input.pdf?.length || !input.certificate?.length || !input.signerEtsi?.trim() || !input.fileName?.trim()) {
|
|
765
|
+
throw new Error("eid: pdf token: pdf, certificate, signerEtsi, fileName \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
766
|
+
}
|
|
767
|
+
const request = {
|
|
768
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
769
|
+
relyingPartyName: this.creds.rpName,
|
|
770
|
+
signer: { etsi: input.signerEtsi.trim() },
|
|
771
|
+
certificate: Buffer.from(input.certificate).toString("base64"),
|
|
772
|
+
fileName: input.fileName
|
|
773
|
+
};
|
|
774
|
+
if (input.docID?.trim()) request.docID = input.docID.trim();
|
|
775
|
+
if (input.onBehalfOf?.trim()) request.onBehalfOf = input.onBehalfOf.trim();
|
|
776
|
+
const form = new FormData();
|
|
777
|
+
form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
|
|
778
|
+
form.append("pdf", new Blob([input.pdf], { type: "application/pdf" }), input.fileName);
|
|
779
|
+
const raw = await this.http.postMultipart(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenPrepare), form, this.timeout);
|
|
780
|
+
const r = rec3(raw);
|
|
781
|
+
const hashToSign = new Uint8Array(Buffer.from(String(r.hashToSign ?? ""), "base64"));
|
|
782
|
+
if (!r.signingID || hashToSign.length !== 32) {
|
|
783
|
+
throw new ValidationError("pdf token prepare: signingID / hashToSign (32 \u0431\u0430\u0439\u0442) \u0431\u0443\u0440\u0443\u0443");
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
signingId: String(r.signingID),
|
|
787
|
+
docId: String(r.docID ?? ""),
|
|
788
|
+
hashToSign,
|
|
789
|
+
hashAlgorithm: String(r.hashAlgorithm ?? ""),
|
|
790
|
+
signatureAlgorithm: String(r.signatureAlgorithm ?? ""),
|
|
791
|
+
signatureFormat: String(r.signatureFormat ?? ""),
|
|
792
|
+
expiresAt: String(r.expiresAt ?? ""),
|
|
793
|
+
raw
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* POST /pdf/token/{signingID}/finalize `{signature: base64 түүхий r||s}` — `signature` 64 байт (P-256 r||s, DER БИШ).
|
|
798
|
+
* Амжвал READY баримтын блок (`outSha256` — {@link tokenDocument}-оор татсан файлтай тулга). READY-ээс хойш идэмпотент.
|
|
799
|
+
* Алдаа: 400 SIGNATURE_INVALID (3 удаа → FAILED), 409 DOCUMENT_FAILED, 410 DOCUMENT_EXPIRED, 503 DOCUMENT_PENDING /
|
|
800
|
+
* TSA_UNAVAILABLE / LTV_UNAVAILABLE — ижил гарын үсгийг `retryAfterMs`-ийн дараа дахин илгээнэ.
|
|
801
|
+
*/
|
|
802
|
+
async tokenFinalize(signingId, signature) {
|
|
803
|
+
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)");
|
|
804
|
+
const r = await this.http.post(
|
|
805
|
+
routePath(RP_ROUTES_UNDISCOVERED.pdfTokenFinalize, signingId),
|
|
806
|
+
{ signature: Buffer.from(signature).toString("base64") },
|
|
807
|
+
this.timeout
|
|
808
|
+
);
|
|
809
|
+
return r;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* GET /pdf/token/{signingID}/document — гарын үсэгтэй PDF (зөвхөн үүсгэсэн RP, expiresAt хүртэл), `pdfMaxBytes`
|
|
813
|
+
* хүртэл. SHA-256-ийг {@link tokenFinalize}-ийн `outSha256`-тай заавал тулга.
|
|
814
|
+
*/
|
|
815
|
+
tokenDocument(signingId) {
|
|
816
|
+
return this.http.getBytes(routePath(RP_ROUTES_UNDISCOVERED.pdfTokenDocument, signingId), this.timeout, this.maxBytes);
|
|
539
817
|
}
|
|
540
818
|
/**
|
|
541
819
|
* {@link document} + тулгалт: татсан байтын SHA-256 нь session poll-ийн `pdf.outSha256`-тай тэнцүү байх ёстой.
|
|
@@ -567,13 +845,53 @@ var OrganizationApi = class {
|
|
|
567
845
|
http;
|
|
568
846
|
/** GET /organization/representations/etsi/{personEtsi}. Сонголт нь итгэх эх биш — сервер дахин шалгана. */
|
|
569
847
|
async getRepresentations(personEtsi) {
|
|
570
|
-
const raw = await this.http.get(
|
|
848
|
+
const raw = await this.http.get(routePath(RP_ROUTES.orgRepresentations, personEtsi));
|
|
571
849
|
const list = Array.isArray(raw.representations) ? raw.representations : [];
|
|
572
|
-
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list };
|
|
850
|
+
return { personEtsi: String(raw.personEtsi ?? personEtsi), representations: list, raw };
|
|
573
851
|
}
|
|
574
852
|
};
|
|
575
853
|
|
|
576
|
-
// src/
|
|
854
|
+
// src/person.ts
|
|
855
|
+
function rec4(v) {
|
|
856
|
+
return v && typeof v === "object" ? v : {};
|
|
857
|
+
}
|
|
858
|
+
var PersonApi = class {
|
|
859
|
+
constructor(http) {
|
|
860
|
+
this.http = http;
|
|
861
|
+
}
|
|
862
|
+
http;
|
|
863
|
+
async get(path) {
|
|
864
|
+
const raw = await this.http.get(path);
|
|
865
|
+
return { ...rec4(raw), raw };
|
|
866
|
+
}
|
|
867
|
+
/** Иргэний бүх гэрчилгээ + статусын тоолол. */
|
|
868
|
+
certificates(personEtsi) {
|
|
869
|
+
return this.get(routePath(RP_ROUTES.personCertificates, personEtsi));
|
|
870
|
+
}
|
|
871
|
+
/** Иргэний холбоотой төхөөрөмжүүд (идэвхтэй + идэвхгүй). */
|
|
872
|
+
devices(personEtsi) {
|
|
873
|
+
return this.get(routePath(RP_ROUTES.personDevices, personEtsi));
|
|
874
|
+
}
|
|
875
|
+
/** Иргэний үйл ажиллагааны түүх. */
|
|
876
|
+
activity(personEtsi, query) {
|
|
877
|
+
const q = new URLSearchParams();
|
|
878
|
+
if (query?.flow) q.set("flow", query.flow);
|
|
879
|
+
if (query?.limit && query.limit > 0) q.set("limit", String(query.limit));
|
|
880
|
+
if (query?.offset && query.offset > 0) q.set("offset", String(query.offset));
|
|
881
|
+
const qs = q.toString();
|
|
882
|
+
return this.get(routePath(RP_ROUTES.personActivity, personEtsi) + (qs ? `?${qs}` : ""));
|
|
883
|
+
}
|
|
884
|
+
/** Самбарын нэгдсэн тоо. */
|
|
885
|
+
summary(personEtsi) {
|
|
886
|
+
return this.get(routePath(RP_ROUTES.personSummary, personEtsi));
|
|
887
|
+
}
|
|
888
|
+
/** Асран хамгаалж буй хүүхдүүд (зөвхөн унших). */
|
|
889
|
+
children(personEtsi) {
|
|
890
|
+
return this.get(routePath(RP_ROUTES.personChildren, personEtsi));
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// src/token.ts
|
|
577
895
|
var import_node_crypto4 = require("crypto");
|
|
578
896
|
|
|
579
897
|
// src/subject.ts
|
|
@@ -680,8 +998,215 @@ function parseSubject(cert) {
|
|
|
680
998
|
out.name = full || out.commonName || null;
|
|
681
999
|
return out;
|
|
682
1000
|
}
|
|
1001
|
+
function certExtension(der, oid) {
|
|
1002
|
+
return certExtensions(der).find((e) => e.oid === oid)?.value;
|
|
1003
|
+
}
|
|
1004
|
+
function certExtensions(der) {
|
|
1005
|
+
const out = [];
|
|
1006
|
+
const top = tlv(der, 0, der.length);
|
|
1007
|
+
const tbs = tlv(der, top.start, top.end);
|
|
1008
|
+
let f = tlv(der, tbs.start, tbs.end);
|
|
1009
|
+
if (f.tag === 160) f = tlv(der, f.end, tbs.end);
|
|
1010
|
+
for (let k = 0; k < 5; k++) f = tlv(der, f.end, tbs.end);
|
|
1011
|
+
for (let pos = f.end; pos < tbs.end; ) {
|
|
1012
|
+
const t = tlv(der, pos, tbs.end);
|
|
1013
|
+
if (t.tag === 163) {
|
|
1014
|
+
const seq = tlv(der, t.start, t.end);
|
|
1015
|
+
for (let ep = seq.start; ep < seq.end; ) {
|
|
1016
|
+
const ext = tlv(der, ep, seq.end);
|
|
1017
|
+
const id = tlv(der, ext.start, ext.end);
|
|
1018
|
+
let v = tlv(der, id.end, ext.end);
|
|
1019
|
+
let critical = false;
|
|
1020
|
+
if (v.tag === 1) {
|
|
1021
|
+
critical = v.end > v.start && der[v.start] !== 0;
|
|
1022
|
+
v = tlv(der, v.end, ext.end);
|
|
1023
|
+
}
|
|
1024
|
+
if (v.tag !== 4) throw new ValidationError("extension: extnValue OCTET STRING \u0431\u0438\u0448");
|
|
1025
|
+
out.push({ oid: oidOf(der, id), critical, value: der.subarray(v.start, v.end) });
|
|
1026
|
+
ep = ext.end;
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
pos = t.end;
|
|
1031
|
+
}
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/token.ts
|
|
1036
|
+
function rec5(v) {
|
|
1037
|
+
return v && typeof v === "object" ? v : {};
|
|
1038
|
+
}
|
|
1039
|
+
function hexSha256Equal(hex, data) {
|
|
1040
|
+
if (typeof hex !== "string" || !/^[0-9a-fA-F]{64}$/.test(hex)) return false;
|
|
1041
|
+
return (0, import_node_crypto4.timingSafeEqual)((0, import_node_crypto4.createHash)("sha256").update(data).digest(), Buffer.from(hex, "hex"));
|
|
1042
|
+
}
|
|
1043
|
+
var TOKEN_RESULT_TIMEOUT_MS = 3e4;
|
|
1044
|
+
var TokenApi = class {
|
|
1045
|
+
constructor(http, creds) {
|
|
1046
|
+
this.http = http;
|
|
1047
|
+
this.creds = creds;
|
|
1048
|
+
}
|
|
1049
|
+
http;
|
|
1050
|
+
creds;
|
|
1051
|
+
/**
|
|
1052
|
+
* POST /token/certificate — токены AUTH + SIGN гэрчилгээ олголт эхлүүлнэ (иргэн утсаараа батална). CA-ийн буцаасан
|
|
1053
|
+
* CSR hash-ууд илгээсэнтэй таарахгүй бол ValidationError.
|
|
1054
|
+
*/
|
|
1055
|
+
async issueCertificate(input) {
|
|
1056
|
+
if (!input.signerEtsi?.trim() || !input.csrAuth?.length || !input.csrSign?.length || !input.tokenSerial?.trim()) {
|
|
1057
|
+
throw new Error("token: signerEtsi, csrAuth, csrSign, tokenSerial \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
1058
|
+
}
|
|
1059
|
+
const body = {
|
|
1060
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
1061
|
+
relyingPartyName: this.creds.rpName,
|
|
1062
|
+
signer: { etsi: input.signerEtsi.trim() },
|
|
1063
|
+
csrAuth: Buffer.from(input.csrAuth).toString("base64"),
|
|
1064
|
+
csrSign: Buffer.from(input.csrSign).toString("base64"),
|
|
1065
|
+
tokenSerial: input.tokenSerial.trim(),
|
|
1066
|
+
onBehalfOf: input.onBehalfOf?.trim() ?? "",
|
|
1067
|
+
flow: "notification"
|
|
1068
|
+
};
|
|
1069
|
+
if (input.tokenLabel) body.tokenLabel = input.tokenLabel;
|
|
1070
|
+
const r = rec5(await this.http.post(routePath(RP_ROUTES.tokenCertificate), body));
|
|
1071
|
+
if (!hexSha256Equal(r.csrAuthSha256, input.csrAuth) || !hexSha256Equal(r.csrSignSha256, input.csrSign)) {
|
|
1072
|
+
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");
|
|
1073
|
+
}
|
|
1074
|
+
const vc = rec5(r.vc).value ?? (typeof r.vc === "string" ? r.vc : void 0);
|
|
1075
|
+
return {
|
|
1076
|
+
sessionId: String(r.sessionID ?? ""),
|
|
1077
|
+
vc: vc != null ? String(vc) : null,
|
|
1078
|
+
csrAuthSha256: String(r.csrAuthSha256).toLowerCase(),
|
|
1079
|
+
csrSignSha256: String(r.csrSignSha256).toLowerCase(),
|
|
1080
|
+
...typeof r.expiresAt === "string" ? { expiresAt: r.expiresAt } : {}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* GET /token/certificate/{sessionID} — олгосон AUTH + SIGN гэрчилгээ + гинж. Токенд бичихээс өмнө
|
|
1085
|
+
* {@link verifyTokenCertificates}-ээр CSR-ийн түлхүүртэй тулга.
|
|
1086
|
+
*/
|
|
1087
|
+
async certificateResult(sessionId) {
|
|
1088
|
+
const raw = await this.http.get(routePath(RP_ROUTES.tokenCertificateResult, sessionId), TOKEN_RESULT_TIMEOUT_MS);
|
|
1089
|
+
const r = rec5(raw);
|
|
1090
|
+
const item = (v) => v && typeof v === "object" ? v : null;
|
|
1091
|
+
return {
|
|
1092
|
+
status: String(r.status ?? ""),
|
|
1093
|
+
auth: item(r.auth),
|
|
1094
|
+
sign: item(r.sign),
|
|
1095
|
+
chain: Array.isArray(r.chain) ? r.chain.map(String) : [],
|
|
1096
|
+
chainPem: String(r.chainPem ?? ""),
|
|
1097
|
+
organization: r.organization && typeof r.organization === "object" ? r.organization : null,
|
|
1098
|
+
keyMedium: String(r.keyMedium ?? ""),
|
|
1099
|
+
certificateLevel: String(r.certificateLevel ?? ""),
|
|
1100
|
+
raw
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/** POST /token/auth/challenge — энэ RP-д холбогдсон нэг удаагийн challenge (2 мин). */
|
|
1104
|
+
async authChallenge() {
|
|
1105
|
+
const r = rec5(
|
|
1106
|
+
await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthChallenge), {
|
|
1107
|
+
relyingPartyUUID: this.creds.rpUUID,
|
|
1108
|
+
relyingPartyName: this.creds.rpName
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
return {
|
|
1112
|
+
challengeId: String(r.challengeId ?? ""),
|
|
1113
|
+
challenge: String(r.challenge ?? ""),
|
|
1114
|
+
rpUUID: String(r.rpUUID ?? ""),
|
|
1115
|
+
expiresAt: String(r.expiresAt ?? "")
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* POST /token/auth/verify — токены AUTH гэрчилгээ (DER) + challenge дээрх түүхий r||s. CA гинж, төлөв, цуцлалт, гарын
|
|
1120
|
+
* үсгийг шалгана; SDK нэмж endResult === OK ба identity.etsi === гэрчилгээний SERIALNUMBER-ийг тулгана (эс бөгөөс
|
|
1121
|
+
* ValidationError). Challenge нэг удаагийн — алдааны дараа шинээр авна.
|
|
1122
|
+
*/
|
|
1123
|
+
async authVerify(challengeId, certDer, signature) {
|
|
1124
|
+
let serial;
|
|
1125
|
+
try {
|
|
1126
|
+
serial = parseSubject(new import_node_crypto4.X509Certificate(certDer)).serialNumber;
|
|
1127
|
+
} catch {
|
|
1128
|
+
throw new Error("token auth: \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1129
|
+
}
|
|
1130
|
+
if (!challengeId || !signature?.length) throw new Error("token auth: challengeId \u0431\u0430 signature \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
1131
|
+
const raw = await this.http.post(routePath(RP_ROUTES_UNDISCOVERED.tokenAuthVerify), {
|
|
1132
|
+
challengeId,
|
|
1133
|
+
certificate: Buffer.from(certDer).toString("base64"),
|
|
1134
|
+
signature: Buffer.from(signature).toString("base64")
|
|
1135
|
+
});
|
|
1136
|
+
const r = rec5(raw);
|
|
1137
|
+
if (rec5(r.result).endResult !== "OK") throw new ValidationError("token auth: endResult OK \u0431\u0438\u0448");
|
|
1138
|
+
const id = rec5(r.identity);
|
|
1139
|
+
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");
|
|
1140
|
+
const org = rec5(r.organization);
|
|
1141
|
+
const cert = rec5(r.cert);
|
|
1142
|
+
return {
|
|
1143
|
+
identity: { etsi: String(id.etsi), givenName: String(id.givenName ?? ""), surname: String(id.surname ?? "") },
|
|
1144
|
+
serialHex: String(cert.serialHex ?? ""),
|
|
1145
|
+
notAfter: String(cert.notAfter ?? ""),
|
|
1146
|
+
organization: org.etsi ? { etsi: String(org.etsi), name: String(org.name ?? "") } : null,
|
|
1147
|
+
raw
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function csrSpki(csr) {
|
|
1152
|
+
const top = tlv(csr, 0, csr.length);
|
|
1153
|
+
const info = tlv(csr, top.start, top.end);
|
|
1154
|
+
const version = tlv(csr, info.start, info.end);
|
|
1155
|
+
const subject = tlv(csr, version.end, info.end);
|
|
1156
|
+
const spki = tlv(csr, subject.end, info.end);
|
|
1157
|
+
return (0, import_node_crypto4.createPublicKey)({ key: Buffer.from(csr.subarray(subject.end, spki.end)), format: "der", type: "spki" }).export({
|
|
1158
|
+
format: "der",
|
|
1159
|
+
type: "spki"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
function verifyTokenCertificates(result, csrAuth, csrSign) {
|
|
1163
|
+
if (!result?.auth || !result.sign) throw new ValidationError("token: auth/sign \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0430\u043B\u0433\u0430");
|
|
1164
|
+
for (const [name, item, csr] of [
|
|
1165
|
+
["auth", result.auth, csrAuth],
|
|
1166
|
+
["sign", result.sign, csrSign]
|
|
1167
|
+
]) {
|
|
1168
|
+
let want;
|
|
1169
|
+
try {
|
|
1170
|
+
want = csrSpki(csr);
|
|
1171
|
+
} catch {
|
|
1172
|
+
throw new Error(`token: ${name} CSR \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
1173
|
+
}
|
|
1174
|
+
let got;
|
|
1175
|
+
try {
|
|
1176
|
+
got = new import_node_crypto4.X509Certificate(Buffer.from(item.certificate, "base64")).publicKey.export({ format: "der", type: "spki" });
|
|
1177
|
+
} catch {
|
|
1178
|
+
throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0437\u0430\u0434\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
1179
|
+
}
|
|
1180
|
+
if (!got.equals(want)) throw new ValidationError(`token: ${name} \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u043B\u0445\u04AF\u04AF\u0440 CSR-\u0438\u0439\u043D\u0445\u0442\u044D\u0439 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function tokenAuthDigest(rpUUID, challengeB64) {
|
|
1184
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(rpUUID)) {
|
|
1185
|
+
throw new Error("token auth: rpUUID \u0436\u0438\u0436\u0438\u0433 \u04AF\u0441\u0433\u0438\u0439\u043D 36 \u0442\u044D\u043C\u0434\u044D\u0433\u0442 UUID \u0431\u0438\u0448");
|
|
1186
|
+
}
|
|
1187
|
+
const ch = Buffer.from(challengeB64, "base64");
|
|
1188
|
+
if (ch.length !== 32 || ch.toString("base64") !== challengeB64) throw new Error("token auth: challenge 32 \u0431\u0430\u0439\u0442 base64 \u0431\u0438\u0448");
|
|
1189
|
+
return (0, import_node_crypto4.createHash)("sha256").update("eID-TOKEN-AUTH", "ascii").update(rpUUID, "ascii").update(ch).digest();
|
|
1190
|
+
}
|
|
1191
|
+
function tokenLimitOf(err) {
|
|
1192
|
+
if (!(err instanceof ApiError) || err.code !== "TOKEN_LIMIT_REACHED") return void 0;
|
|
1193
|
+
let b;
|
|
1194
|
+
try {
|
|
1195
|
+
b = rec5(JSON.parse(err.body));
|
|
1196
|
+
} catch {
|
|
1197
|
+
return void 0;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof b.limit !== "number" || !Number.isInteger(b.limit) || b.limit < 0) return void 0;
|
|
1200
|
+
if (b.kind === "PERSONAL") return { kind: "PERSONAL", limit: b.limit };
|
|
1201
|
+
if (b.kind === "ORGANIZATION" && typeof b.organization === "string" && b.organization.startsWith("NTRMN-")) {
|
|
1202
|
+
return { kind: "ORGANIZATION", limit: b.limit, organization: b.organization };
|
|
1203
|
+
}
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
683
1206
|
|
|
684
1207
|
// src/validator.ts
|
|
1208
|
+
var import_node_crypto5 = require("crypto");
|
|
1209
|
+
var CITIZEN_ISSUING_CA_SKI = "b8a37f2c64ad677912db7d9e34f246358a2fb909";
|
|
685
1210
|
var ResponseValidator = class {
|
|
686
1211
|
anchors;
|
|
687
1212
|
intermediates;
|
|
@@ -690,14 +1215,28 @@ var ResponseValidator = class {
|
|
|
690
1215
|
allowUntrusted;
|
|
691
1216
|
revocation;
|
|
692
1217
|
revocationMode;
|
|
1218
|
+
allowedIssuers;
|
|
1219
|
+
checkKeyUsage;
|
|
1220
|
+
schemeName;
|
|
693
1221
|
constructor(trust = {}) {
|
|
694
|
-
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new
|
|
695
|
-
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new
|
|
1222
|
+
this.anchors = (trust.trustAnchorsPem ?? []).map((p) => new import_node_crypto5.X509Certificate(p));
|
|
1223
|
+
this.intermediates = (trust.intermediatesPem ?? []).map((p) => new import_node_crypto5.X509Certificate(p));
|
|
696
1224
|
this.requiredLevel = trust.requiredLevel ?? "QUALIFIED";
|
|
697
1225
|
this.skew = trust.clockSkewMs ?? 0;
|
|
698
1226
|
this.allowUntrusted = trust.allowUntrusted ?? false;
|
|
699
1227
|
this.revocation = trust.revocation;
|
|
700
1228
|
this.revocationMode = trust.revocationMode ?? "hard-fail";
|
|
1229
|
+
if (trust.allowedIssuerSkis?.length) {
|
|
1230
|
+
this.allowedIssuers = new Set(
|
|
1231
|
+
trust.allowedIssuerSkis.map((s) => {
|
|
1232
|
+
const ski = s.trim().replace(/:/g, "").toLowerCase();
|
|
1233
|
+
if (!/^(?:[0-9a-f]{2})+$/.test(ski)) throw new Error(`ResponseValidator: allowedIssuerSkis: "${s}" hex key identifier \u0431\u0438\u0448`);
|
|
1234
|
+
return ski;
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
this.checkKeyUsage = trust.checkKeyUsage ?? false;
|
|
1239
|
+
this.schemeName = trust.schemeName;
|
|
701
1240
|
}
|
|
702
1241
|
// ── нийтийн API ──
|
|
703
1242
|
/**
|
|
@@ -725,6 +1264,7 @@ var ResponseValidator = class {
|
|
|
725
1264
|
if (!interactionTypeUsed) throw new ValidationError("interactionTypeUsed \u0430\u043B\u0433\u0430");
|
|
726
1265
|
const { alg, hash } = this.algorithmOf(result, leaf, acsp.signatureAlgorithm, acsp.hashAlgorithm);
|
|
727
1266
|
const payload = buildAcspV2Payload({
|
|
1267
|
+
...this.schemeName ? { schemeName: this.schemeName } : {},
|
|
728
1268
|
serverRandom,
|
|
729
1269
|
rpChallenge: acsp.rpChallenge,
|
|
730
1270
|
userChallenge,
|
|
@@ -828,9 +1368,10 @@ var ResponseValidator = class {
|
|
|
828
1368
|
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");
|
|
829
1369
|
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");
|
|
830
1370
|
if (!result.documentNumber) throw new ValidationError("\u0425\u0430\u0440\u0438\u0443\u043D\u0434 documentNumber \u0430\u043B\u0433\u0430");
|
|
831
|
-
const leaf = new
|
|
1371
|
+
const leaf = new import_node_crypto5.X509Certificate(Buffer.from(result.certificateDerB64, "base64"));
|
|
832
1372
|
this.checkValidity(leaf);
|
|
833
1373
|
this.checkLevel(String(result.certificateLevel ?? ""));
|
|
1374
|
+
this.checkPurpose(leaf, protocol);
|
|
834
1375
|
if (this.anchors.length === 0) {
|
|
835
1376
|
if (!this.allowUntrusted) {
|
|
836
1377
|
throw new ValidationError(
|
|
@@ -838,10 +1379,37 @@ var ResponseValidator = class {
|
|
|
838
1379
|
);
|
|
839
1380
|
}
|
|
840
1381
|
} else {
|
|
841
|
-
this.verifyChain(leaf);
|
|
1382
|
+
this.verifyChain(leaf, protocol === "ACSP_V2" ? EKU_CLIENT_AUTH : EKU_EMAIL_PROTECTION);
|
|
842
1383
|
}
|
|
1384
|
+
this.checkIssuer(leaf);
|
|
843
1385
|
return leaf;
|
|
844
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* checkKeyUsage үед гэрчилгээний зориулалт протоколд тохирох эсэх (CA crypto/ca_profile.go): ACSP_V2 (auth) → KU
|
|
1389
|
+
* digitalSignature + EKU clientAuth (эсвэл anyExtendedKeyUsage); RAW_DIGEST_SIGNATURE (sign) → KU contentCommitment.
|
|
1390
|
+
*/
|
|
1391
|
+
checkPurpose(leaf, protocol) {
|
|
1392
|
+
if (!this.checkKeyUsage) return;
|
|
1393
|
+
const ku = keyUsageBits(leaf);
|
|
1394
|
+
if (protocol === "ACSP_V2") {
|
|
1395
|
+
if (!(ku & 128)) throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage digitalSignature \u0430\u043B\u0433\u0430");
|
|
1396
|
+
const eku = extKeyUsage(leaf);
|
|
1397
|
+
if (!eku.includes("1.3.6.1.5.5.7.3.2") && !eku.includes("2.5.29.37.0")) {
|
|
1398
|
+
throw new ValidationError("\u041D\u044D\u0432\u0442\u0440\u044D\u043B\u0442\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 ExtKeyUsage clientAuth \u0430\u043B\u0433\u0430 (\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0431\u0430\u0439\u0436 \u0431\u043E\u043B\u0437\u043E\u0448\u0433\u04AF\u0439)");
|
|
1399
|
+
}
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (!(ku & 64)) throw new ValidationError("\u0413\u0430\u0440\u044B\u043D \u04AF\u0441\u0433\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0434 KeyUsage nonRepudiation (contentCommitment) \u0430\u043B\u0433\u0430");
|
|
1403
|
+
}
|
|
1404
|
+
/** allowedIssuerSkis үед leaf-ийн шууд issuer-ийн SKI (issuer-т алга бол leaf-ийн AKI) жагсаалтад байх ёстой. */
|
|
1405
|
+
checkIssuer(leaf) {
|
|
1406
|
+
if (!this.allowedIssuers) return;
|
|
1407
|
+
const issuer = this.findIssuer(leaf);
|
|
1408
|
+
const ski = (issuer && subjectKeyId(issuer)) ?? authorityKeyId(leaf);
|
|
1409
|
+
if (!ski || !this.allowedIssuers.has(ski)) {
|
|
1410
|
+
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u0433 \u0433\u0430\u0440\u0433\u0430\u0441\u0430\u043D CA \u0437\u04E9\u0432\u0448\u04E9\u04E9\u0440\u04E9\u0433\u0434\u0441\u04E9\u043D \u0438\u0440\u0433\u044D\u043D\u0438\u0439 issuer \u0431\u0438\u0448 (allowedIssuerSkis)");
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
845
1413
|
checkValidity(cert) {
|
|
846
1414
|
const now = Date.now();
|
|
847
1415
|
const from = Date.parse(cert.validFrom);
|
|
@@ -855,21 +1423,40 @@ var ResponseValidator = class {
|
|
|
855
1423
|
const need = rank[this.requiredLevel] ?? 2;
|
|
856
1424
|
if (got < need) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0442\u04AF\u0432\u0448\u0438\u043D \u0445\u04AF\u0440\u044D\u043B\u0446\u044D\u0445\u0433\u04AF\u0439: ${level} < ${this.requiredLevel}`);
|
|
857
1425
|
}
|
|
858
|
-
/**
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* RFC 5280 замын шалгалт (Go SDK-ийн `x509.Certificate.Verify`-тэй ижил дүрэм): leaf-ээс trust anchor хүртэлх
|
|
1428
|
+
* замыг intermediates/anchors-оос (backtracking-тай) угсарна. Issuer бүр: subject = child.issuer, AKI/SKI таарах,
|
|
1429
|
+
* гарын үсэг, CA (basicConstraints CA:TRUE), KeyUsage тавьсан бол keyCertSign, **pathLenConstraint** (доорх
|
|
1430
|
+
* завсрын CA-ийн тоо), хүчинтэй хугацаа, EKU тавьсан бол `usage` / anyExtendedKeyUsage агуулах. Leaf болон CA
|
|
1431
|
+
* бүрийн үл таних **critical extension** → татгалзана. **Name constraints**-ийг SDK үнэлдэггүй — гинжинд байвал
|
|
1432
|
+
* fail-closed (CA-ийн бодит гинж 01–04-т алга). Цаг = now, leaf-ийн хүчинтэй хугацаанд хавчсан (`clockSkewMs`-ийн
|
|
1433
|
+
* хүрээнд; leaf-ийн хугацааг checkValidity шалгасан). certificatePolicies шалгахгүй (Go-той ижил).
|
|
1434
|
+
*/
|
|
1435
|
+
verifyChain(leaf, usage) {
|
|
1436
|
+
const now = Math.min(Math.max(Date.now(), Date.parse(leaf.validFrom)), Date.parse(leaf.validTo));
|
|
1437
|
+
const leafReason = criticalReason(leaf) ?? ekuReason(leaf, usage);
|
|
1438
|
+
if (leafReason) throw new ValidationError(`\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${leafReason}`);
|
|
1439
|
+
let reason;
|
|
1440
|
+
const extend = (cur, cas) => {
|
|
1441
|
+
if (cas.length >= 8) return false;
|
|
1442
|
+
for (const [cand, isAnchor] of [
|
|
1443
|
+
...this.anchors.map((c) => [c, true]),
|
|
1444
|
+
...this.intermediates.map((c) => [c, false])
|
|
1445
|
+
]) {
|
|
1446
|
+
if (cas.includes(cand) || !signedBy(cur, cand)) continue;
|
|
1447
|
+
const bad = caReason(cand, cas.length, now, usage);
|
|
1448
|
+
if (bad) {
|
|
1449
|
+
reason ??= bad;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (isAnchor || extend(cand, [...cas, cand])) return true;
|
|
865
1453
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439");
|
|
1454
|
+
return false;
|
|
1455
|
+
};
|
|
1456
|
+
if (extend(leaf, [])) return;
|
|
1457
|
+
throw new ValidationError(
|
|
1458
|
+
reason ? `\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0433\u0438\u043D\u0436 \u0445\u04AF\u0447\u0438\u043D\u0433\u04AF\u0439: ${reason}` : "\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0438\u0442\u0433\u044D\u043B\u0446\u043B\u0438\u0439\u043D \u0433\u0438\u043D\u0436 trust anchor (Root CA) \u0445\u04AF\u0440\u0441\u044D\u043D\u0433\u04AF\u0439"
|
|
1459
|
+
);
|
|
873
1460
|
}
|
|
874
1461
|
/** leaf-ийн шууд issuer-ийг trust pool-оос буцаана (revocation шалгагчид issuer хэрэгтэй). */
|
|
875
1462
|
findIssuer(leaf) {
|
|
@@ -884,14 +1471,23 @@ var ResponseValidator = class {
|
|
|
884
1471
|
* дуудна (тэдгээр нь sync; revocation нь сүлжээ I/O тул async).
|
|
885
1472
|
*/
|
|
886
1473
|
async checkRevocation(cert) {
|
|
887
|
-
|
|
1474
|
+
await this.checkRevocationStatus(cert);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* {@link checkRevocation} + юу шийдсэнийг буцаана (лог/аудитад): "good" | "unchecked" (unknown, soft-fail) |
|
|
1478
|
+
* "skipped" (hook алга). "revoked", мөн hard-fail горимд "unknown" → ValidationError; hook-ийн алдаа дамжина.
|
|
1479
|
+
*/
|
|
1480
|
+
async checkRevocationStatus(cert) {
|
|
1481
|
+
if (!this.revocation) return "skipped";
|
|
888
1482
|
const status = await this.revocation(cert, this.findIssuer(cert));
|
|
1483
|
+
if (status === "good") return "good";
|
|
889
1484
|
if (status === "revoked") {
|
|
890
1485
|
throw new ValidationError("\u0413\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D \u0446\u0443\u0446\u043B\u0430\u0433\u0434\u0441\u0430\u043D (revoked)");
|
|
891
1486
|
}
|
|
892
|
-
if (
|
|
1487
|
+
if (this.revocationMode === "hard-fail") {
|
|
893
1488
|
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)");
|
|
894
1489
|
}
|
|
1490
|
+
return "unchecked";
|
|
895
1491
|
}
|
|
896
1492
|
};
|
|
897
1493
|
function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
@@ -906,16 +1502,16 @@ function signedCallbackUrl(fromResponse, rpCallback, flowType) {
|
|
|
906
1502
|
return fromResponse;
|
|
907
1503
|
}
|
|
908
1504
|
function userChallengeOf(verifier) {
|
|
909
|
-
return (0,
|
|
1505
|
+
return (0, import_node_crypto5.createHash)("sha256").update(verifier, "utf8").digest("base64url");
|
|
910
1506
|
}
|
|
911
1507
|
function verifyPayloadSignature(payload, sigB64, key, alg, hash) {
|
|
912
1508
|
const sig = Buffer.from(sigB64, "base64");
|
|
913
1509
|
const h = nodeHash(hash);
|
|
914
1510
|
if (alg === "rsassa-pss") {
|
|
915
|
-
return (0,
|
|
1511
|
+
return (0, import_node_crypto5.verify)(h, payload, { key, padding: import_node_crypto5.constants.RSA_PKCS1_PSS_PADDING, saltLength: hashLen(hash) }, sig);
|
|
916
1512
|
}
|
|
917
|
-
if (alg === "ecdsa-with-SHA256") return (0,
|
|
918
|
-
return (0,
|
|
1513
|
+
if (alg === "ecdsa-with-SHA256") return (0, import_node_crypto5.verify)(h, payload, { key, dsaEncoding: "der" }, sig);
|
|
1514
|
+
return (0, import_node_crypto5.verify)(h, payload, { key, padding: import_node_crypto5.constants.RSA_PKCS1_PADDING }, sig);
|
|
919
1515
|
}
|
|
920
1516
|
function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
|
|
921
1517
|
const sig = Buffer.from(sigB64, "base64");
|
|
@@ -924,7 +1520,7 @@ function verifyPrehashedSignature(digest, sigB64, key, alg, hash) {
|
|
|
924
1520
|
if (!modBits || sig.length !== Math.ceil(modBits / 8)) return false;
|
|
925
1521
|
let em;
|
|
926
1522
|
try {
|
|
927
|
-
em = (0,
|
|
1523
|
+
em = (0, import_node_crypto5.publicDecrypt)({ key, padding: import_node_crypto5.constants.RSA_NO_PADDING }, sig);
|
|
928
1524
|
} catch {
|
|
929
1525
|
return false;
|
|
930
1526
|
}
|
|
@@ -1008,7 +1604,7 @@ function mgf1(hash, seed, length) {
|
|
|
1008
1604
|
for (let i = 0, n = 0; n < length; i++, n += hashLen(hash)) {
|
|
1009
1605
|
const c = Buffer.alloc(4);
|
|
1010
1606
|
c.writeUInt32BE(i, 0);
|
|
1011
|
-
out.push((0,
|
|
1607
|
+
out.push((0, import_node_crypto5.createHash)(nodeHash(hash)).update(seed).update(c).digest());
|
|
1012
1608
|
}
|
|
1013
1609
|
return Buffer.concat(out).subarray(0, length);
|
|
1014
1610
|
}
|
|
@@ -1032,12 +1628,118 @@ function emsaPssVerify(mHash, em, emBits, hash) {
|
|
|
1032
1628
|
const psLen = emLen - hLen - sLen - 2;
|
|
1033
1629
|
if (!db.subarray(0, psLen).every((b) => b === 0) || db[psLen] !== 1) return false;
|
|
1034
1630
|
const salt = db.subarray(psLen + 1);
|
|
1035
|
-
const h2 = (0,
|
|
1631
|
+
const h2 = (0, import_node_crypto5.createHash)(nodeHash(hash)).update(Buffer.alloc(8)).update(mHash).update(salt).digest();
|
|
1036
1632
|
return h.equals(h2);
|
|
1037
1633
|
}
|
|
1634
|
+
function keyUsageBits(cert) {
|
|
1635
|
+
const v = certExtension(cert.raw, "2.5.29.15");
|
|
1636
|
+
if (!v) return 0;
|
|
1637
|
+
const bs = tlv(v, 0, v.length);
|
|
1638
|
+
if (bs.tag !== 3 || bs.end - bs.start < 2) return 0;
|
|
1639
|
+
return v[bs.start + 1];
|
|
1640
|
+
}
|
|
1641
|
+
function extKeyUsage(cert) {
|
|
1642
|
+
const v = certExtension(cert.raw, "2.5.29.37");
|
|
1643
|
+
if (!v) return [];
|
|
1644
|
+
const seq = tlv(v, 0, v.length);
|
|
1645
|
+
const out = [];
|
|
1646
|
+
for (let p = seq.start; p < seq.end; ) {
|
|
1647
|
+
const t = tlv(v, p, seq.end);
|
|
1648
|
+
out.push(oidOf(v, t));
|
|
1649
|
+
p = t.end;
|
|
1650
|
+
}
|
|
1651
|
+
return out;
|
|
1652
|
+
}
|
|
1653
|
+
function subjectKeyId(cert) {
|
|
1654
|
+
const v = certExtension(cert.raw, "2.5.29.14");
|
|
1655
|
+
if (!v) return void 0;
|
|
1656
|
+
const o = tlv(v, 0, v.length);
|
|
1657
|
+
return o.tag === 4 && o.end > o.start ? Buffer.from(v.subarray(o.start, o.end)).toString("hex") : void 0;
|
|
1658
|
+
}
|
|
1659
|
+
function authorityKeyId(cert) {
|
|
1660
|
+
const v = certExtension(cert.raw, "2.5.29.35");
|
|
1661
|
+
if (!v) return void 0;
|
|
1662
|
+
const seq = tlv(v, 0, v.length);
|
|
1663
|
+
if (seq.start >= seq.end) return void 0;
|
|
1664
|
+
const k = tlv(v, seq.start, seq.end);
|
|
1665
|
+
return k.tag === 128 && k.end > k.start ? Buffer.from(v.subarray(k.start, k.end)).toString("hex") : void 0;
|
|
1666
|
+
}
|
|
1038
1667
|
function isCa(cert) {
|
|
1039
1668
|
return cert.ca === true;
|
|
1040
1669
|
}
|
|
1670
|
+
var EKU_ANY = "2.5.29.37.0";
|
|
1671
|
+
var EKU_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
|
|
1672
|
+
var EKU_EMAIL_PROTECTION = "1.3.6.1.5.5.7.3.4";
|
|
1673
|
+
var OID_NAME_CONSTRAINTS = "2.5.29.30";
|
|
1674
|
+
var HANDLED_CRITICAL = /* @__PURE__ */ new Set([
|
|
1675
|
+
"2.5.29.14",
|
|
1676
|
+
// subjectKeyIdentifier
|
|
1677
|
+
"2.5.29.15",
|
|
1678
|
+
// keyUsage
|
|
1679
|
+
"2.5.29.17",
|
|
1680
|
+
// subjectAltName
|
|
1681
|
+
"2.5.29.19",
|
|
1682
|
+
// basicConstraints
|
|
1683
|
+
"2.5.29.30",
|
|
1684
|
+
// nameConstraints (CA-д — caReason fail-closed)
|
|
1685
|
+
"2.5.29.31",
|
|
1686
|
+
// cRLDistributionPoints
|
|
1687
|
+
"2.5.29.32",
|
|
1688
|
+
// certificatePolicies
|
|
1689
|
+
"2.5.29.35",
|
|
1690
|
+
// authorityKeyIdentifier
|
|
1691
|
+
"2.5.29.37",
|
|
1692
|
+
// extKeyUsage
|
|
1693
|
+
"1.3.6.1.5.5.7.1.1"
|
|
1694
|
+
// authorityInfoAccess
|
|
1695
|
+
]);
|
|
1696
|
+
function signedBy(child, cand) {
|
|
1697
|
+
if (child.issuer !== cand.subject) return false;
|
|
1698
|
+
const aki = authorityKeyId(child);
|
|
1699
|
+
const ski = subjectKeyId(cand);
|
|
1700
|
+
if (aki && ski && aki !== ski) return false;
|
|
1701
|
+
try {
|
|
1702
|
+
return child.verify(cand.publicKey);
|
|
1703
|
+
} catch {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function criticalReason(cert) {
|
|
1708
|
+
return certExtensions(cert.raw).some((e) => e.critical && !HANDLED_CRITICAL.has(e.oid)) ? "\u04AF\u043B \u0442\u0430\u043D\u0438\u0445 critical extension" : void 0;
|
|
1709
|
+
}
|
|
1710
|
+
function ekuReason(cert, usage) {
|
|
1711
|
+
const eku = extKeyUsage(cert);
|
|
1712
|
+
return eku.length && !eku.includes(usage) && !eku.includes(EKU_ANY) ? "\u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 ExtKeyUsage \u044D\u043D\u044D \u04AF\u0439\u043B\u0434\u044D\u043B\u0434 \u0442\u043E\u0445\u0438\u0440\u043E\u0445\u0433\u04AF\u0439 (\u0433\u0438\u043D\u0436\u0438\u0439\u043D EKU)" : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
function caReason(ca, below, now, usage) {
|
|
1715
|
+
if (!isCa(ca)) return "\u0437\u0430\u0432\u0441\u0440\u044B\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D CA \u0431\u0438\u0448 \u044D\u0441\u0432\u044D\u043B keyCertSign \u044D\u0440\u0445\u0433\u04AF\u0439";
|
|
1716
|
+
if (certExtension(ca.raw, "2.5.29.15") && !(keyUsageBits(ca) & 4)) return "CA-\u0438\u0439\u043D KeyUsage-\u0434 keyCertSign \u0430\u043B\u0433\u0430";
|
|
1717
|
+
const from = Date.parse(ca.validFrom);
|
|
1718
|
+
const to = Date.parse(ca.validTo);
|
|
1719
|
+
if (Number.isNaN(from) || Number.isNaN(to) || now < from || now > to) {
|
|
1720
|
+
return "\u0433\u0438\u043D\u0436\u0438\u0439\u043D \u0433\u044D\u0440\u0447\u0438\u043B\u0433\u044D\u044D\u043D\u0438\u0439 \u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D \u044D\u0441\u0432\u044D\u043B \u0445\u04AF\u0447\u0438\u043D\u0442\u044D\u0439 \u0431\u043E\u043B\u043E\u043E\u0433\u04AF\u0439";
|
|
1721
|
+
}
|
|
1722
|
+
const pathLen = basicConstraintsPathLen(ca);
|
|
1723
|
+
if (pathLen !== void 0 && below > pathLen) return "pathLenConstraint \u0437\u04E9\u0440\u0447\u0441\u04E9\u043D (\u0437\u0430\u0432\u0441\u0440\u044B\u043D CA \u0445\u044D\u0442 \u043E\u043B\u043E\u043D)";
|
|
1724
|
+
if (certExtension(ca.raw, OID_NAME_CONSTRAINTS)) return "CA-\u0434 name constraints \u0431\u0430\u0439\u043D\u0430 (SDK \u04AF\u043D\u044D\u043B\u0434\u044D\u0433\u0433\u04AF\u0439 \u2014 fail-closed)";
|
|
1725
|
+
return criticalReason(ca) ?? ekuReason(ca, usage);
|
|
1726
|
+
}
|
|
1727
|
+
function basicConstraintsPathLen(cert) {
|
|
1728
|
+
const v = certExtension(cert.raw, "2.5.29.19");
|
|
1729
|
+
if (!v) return void 0;
|
|
1730
|
+
const seq = tlv(v, 0, v.length);
|
|
1731
|
+
for (let p = seq.start; p < seq.end; ) {
|
|
1732
|
+
const t = tlv(v, p, seq.end);
|
|
1733
|
+
if (t.tag === 2) {
|
|
1734
|
+
if (t.end - t.start > 4 || (v[t.start] & 128) !== 0) throw new ValidationError("basicConstraints: pathLen \u0431\u0443\u0440\u0443\u0443");
|
|
1735
|
+
let n = 0;
|
|
1736
|
+
for (let i = t.start; i < t.end; i++) n = n * 256 + v[i];
|
|
1737
|
+
return n;
|
|
1738
|
+
}
|
|
1739
|
+
p = t.end;
|
|
1740
|
+
}
|
|
1741
|
+
return void 0;
|
|
1742
|
+
}
|
|
1041
1743
|
|
|
1042
1744
|
// src/client.ts
|
|
1043
1745
|
var DEFAULT_BASE_URL = "https://rp.eidmongolia.mn";
|
|
@@ -1062,6 +1764,8 @@ var EidClient = class {
|
|
|
1062
1764
|
sign;
|
|
1063
1765
|
pdf;
|
|
1064
1766
|
organization;
|
|
1767
|
+
person;
|
|
1768
|
+
token;
|
|
1065
1769
|
session;
|
|
1066
1770
|
validator;
|
|
1067
1771
|
constructor(cfg) {
|
|
@@ -1077,8 +1781,14 @@ var EidClient = class {
|
|
|
1077
1781
|
const level = cfg.defaultCertificateLevel ?? "QUALIFIED";
|
|
1078
1782
|
this.auth = new AuthApi(http, cfg.credentials, { certificateLevel: level });
|
|
1079
1783
|
this.sign = new SignApi(http, cfg.credentials, { certificateLevel: level });
|
|
1080
|
-
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1784
|
+
this.pdf = new PdfApi(http, cfg.credentials, {
|
|
1785
|
+
certificateLevel: level,
|
|
1786
|
+
timeoutMs: cfg.pdfTimeoutMs ?? DEFAULT_PDF_TIMEOUT_MS,
|
|
1787
|
+
maxBytes: cfg.pdfMaxBytes && cfg.pdfMaxBytes > 0 ? cfg.pdfMaxBytes : DEFAULT_PDF_MAX_BYTES
|
|
1788
|
+
});
|
|
1081
1789
|
this.organization = new OrganizationApi(http);
|
|
1790
|
+
this.person = new PersonApi(http);
|
|
1791
|
+
this.token = new TokenApi(http, cfg.credentials);
|
|
1082
1792
|
this.session = new SessionApi(http);
|
|
1083
1793
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1084
1794
|
}
|
|
@@ -1127,7 +1837,7 @@ function openOrShowQR(session, opts) {
|
|
|
1127
1837
|
}
|
|
1128
1838
|
|
|
1129
1839
|
// src/devicelinkv3.ts
|
|
1130
|
-
var
|
|
1840
|
+
var import_node_crypto6 = require("crypto");
|
|
1131
1841
|
var TYPES = ["QR", "Web2App", "App2App"];
|
|
1132
1842
|
var PROTOCOL = { auth: "ACSP_V2", sign: "RAW_DIGEST_SIGNATURE", cert: "" };
|
|
1133
1843
|
var TOKEN_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -1184,7 +1894,7 @@ function buildDeviceLink(p) {
|
|
|
1184
1894
|
callback,
|
|
1185
1895
|
unprotected
|
|
1186
1896
|
].join("|");
|
|
1187
|
-
const authCode = (0,
|
|
1897
|
+
const authCode = (0, import_node_crypto6.createHmac)("sha256", Buffer.from(p.sessionSecret, "base64")).update(payload, "utf8").digest("base64url");
|
|
1188
1898
|
return `${unprotected}&authCode=${authCode}`;
|
|
1189
1899
|
}
|
|
1190
1900
|
function qrDeviceLinkTicker(input, onLink, opts) {
|
|
@@ -1197,7 +1907,7 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1197
1907
|
}
|
|
1198
1908
|
|
|
1199
1909
|
// src/oidc.ts
|
|
1200
|
-
var
|
|
1910
|
+
var import_node_crypto7 = require("crypto");
|
|
1201
1911
|
var EID_OIDC_ISSUER = "https://login.eidmongolia.mn";
|
|
1202
1912
|
var EID_ACR_QUALIFIED = "urn:eidmongolia:loa:qualified";
|
|
1203
1913
|
var EID_ACR_ADVANCED = "urn:eidmongolia:loa:advanced";
|
|
@@ -1234,7 +1944,7 @@ var EidOidcClient = class {
|
|
|
1234
1944
|
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1235
1945
|
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1236
1946
|
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1237
|
-
this.scope = cfg.scope ??
|
|
1947
|
+
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1238
1948
|
}
|
|
1239
1949
|
cfg;
|
|
1240
1950
|
issuer;
|
|
@@ -1282,7 +1992,7 @@ var EidOidcClient = class {
|
|
|
1282
1992
|
state,
|
|
1283
1993
|
nonce,
|
|
1284
1994
|
code_challenge: await pkceChallenge(codeVerifier),
|
|
1285
|
-
code_challenge_method:
|
|
1995
|
+
code_challenge_method: OIDC_EXPECT.pkceMethod
|
|
1286
1996
|
});
|
|
1287
1997
|
if (opts.display) p.set("display", opts.display);
|
|
1288
1998
|
u.search = p.toString();
|
|
@@ -1361,7 +2071,7 @@ var EidOidcClient = class {
|
|
|
1361
2071
|
const header = decodeJsonPart(parts[0]);
|
|
1362
2072
|
const payload = decodeJsonPart(parts[1]);
|
|
1363
2073
|
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1364
|
-
if (header.alg !==
|
|
2074
|
+
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1365
2075
|
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1366
2076
|
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1367
2077
|
const pick = (ks) => ks.find(
|
|
@@ -1454,7 +2164,7 @@ function formEncode(s) {
|
|
|
1454
2164
|
function timingSafeEqualStr(a, b) {
|
|
1455
2165
|
const x = Buffer.from(a, "utf8");
|
|
1456
2166
|
const y = Buffer.from(b, "utf8");
|
|
1457
|
-
return x.length === y.length && (0,
|
|
2167
|
+
return x.length === y.length && (0, import_node_crypto7.timingSafeEqual)(x, y);
|
|
1458
2168
|
}
|
|
1459
2169
|
function decodeJsonPart(s) {
|
|
1460
2170
|
try {
|
|
@@ -1477,7 +2187,10 @@ function callbackParams(input) {
|
|
|
1477
2187
|
ApiError,
|
|
1478
2188
|
AuthApi,
|
|
1479
2189
|
AuthenticationError,
|
|
2190
|
+
CITIZEN_ISSUING_CA_SKI,
|
|
1480
2191
|
DEFAULT_BASE_URL,
|
|
2192
|
+
DEFAULT_PDF_MAX_BYTES,
|
|
2193
|
+
DEFAULT_PDF_TIMEOUT_MS,
|
|
1481
2194
|
DeviceLinkError,
|
|
1482
2195
|
EID_ACR_ADVANCED,
|
|
1483
2196
|
EID_ACR_QUALIFIED,
|
|
@@ -1492,10 +2205,14 @@ function callbackParams(input) {
|
|
|
1492
2205
|
ON_BEHALF_ERROR_CODES,
|
|
1493
2206
|
OrganizationApi,
|
|
1494
2207
|
PdfApi,
|
|
2208
|
+
PersonApi,
|
|
2209
|
+
RETRYABLE_ERROR_CODES,
|
|
2210
|
+
ResponseTooLargeError,
|
|
1495
2211
|
ResponseValidator,
|
|
1496
2212
|
SessionApi,
|
|
1497
2213
|
SessionFailedError,
|
|
1498
2214
|
SignApi,
|
|
2215
|
+
TokenApi,
|
|
1499
2216
|
ValidationError,
|
|
1500
2217
|
acspV2Digest,
|
|
1501
2218
|
buildAcspV2Payload,
|
|
@@ -1504,14 +2221,19 @@ function callbackParams(input) {
|
|
|
1504
2221
|
interactionsToBase64,
|
|
1505
2222
|
isMobileUserAgent,
|
|
1506
2223
|
openOrShowQR,
|
|
2224
|
+
parseRetryAfter,
|
|
1507
2225
|
parseSessionResult,
|
|
1508
2226
|
parseSubject,
|
|
2227
|
+
personMatchesCertificate,
|
|
1509
2228
|
pkceChallenge,
|
|
1510
2229
|
qrDeviceLinkTicker,
|
|
1511
2230
|
randomChallenge,
|
|
1512
2231
|
resolveBaseUrl,
|
|
1513
2232
|
sha256Base64,
|
|
2233
|
+
tokenAuthDigest,
|
|
2234
|
+
tokenLimitOf,
|
|
1514
2235
|
userChallengeOf,
|
|
1515
2236
|
verifyPayloadSignature,
|
|
1516
|
-
verifyPrehashedSignature
|
|
2237
|
+
verifyPrehashedSignature,
|
|
2238
|
+
verifyTokenCertificates
|
|
1517
2239
|
});
|