edockit 0.1.1-beta.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/core/canonicalization/XMLCanonicalizer.d.ts +51 -0
- package/dist/core/certificate.d.ts +98 -0
- package/dist/core/parser/certificateUtils.d.ts +6 -0
- package/dist/core/parser/signatureParser.d.ts +33 -0
- package/dist/core/parser/types.d.ts +38 -0
- package/dist/core/parser.d.ts +8 -0
- package/dist/core/verification.d.ts +94 -0
- package/dist/index.cjs.js +1787 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +1776 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.umd.js +12 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/utils/xmlParser.d.ts +75 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1787 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var fflate = require('fflate');
|
|
6
|
+
var x509 = require('@peculiar/x509');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursive DOM traversal to find elements with a given tag name
|
|
10
|
+
* (Fallback method when XPath is not available or fails)
|
|
11
|
+
*
|
|
12
|
+
* @param parent The parent element to search within
|
|
13
|
+
* @param selector CSS-like selector with namespace support (e.g., "ds:SignedInfo, SignedInfo")
|
|
14
|
+
* @returns Array of matching elements
|
|
15
|
+
*/
|
|
16
|
+
function findElementsByTagNameRecursive(parent, selector) {
|
|
17
|
+
const results = [];
|
|
18
|
+
const selectors = selector.split(",").map((s) => s.trim());
|
|
19
|
+
// Parse each selector part to extract namespace and local name
|
|
20
|
+
const parsedSelectors = [];
|
|
21
|
+
for (const sel of selectors) {
|
|
22
|
+
const parts = sel.split(/\\:|:/).filter(Boolean);
|
|
23
|
+
if (parts.length === 1) {
|
|
24
|
+
parsedSelectors.push({ name: parts[0] });
|
|
25
|
+
}
|
|
26
|
+
else if (parts.length === 2) {
|
|
27
|
+
parsedSelectors.push({ ns: parts[0], name: parts[1] });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Recursive search function - keep the original node references
|
|
31
|
+
function searchNode(node) {
|
|
32
|
+
if (!node)
|
|
33
|
+
return;
|
|
34
|
+
if (node.nodeType === 1) {
|
|
35
|
+
// Element node - make sure we're working with an actual DOM Element
|
|
36
|
+
const element = node;
|
|
37
|
+
const nodeName = element.nodeName;
|
|
38
|
+
const localName = element.localName;
|
|
39
|
+
// Check if this element matches any of our selectors
|
|
40
|
+
for (const sel of parsedSelectors) {
|
|
41
|
+
// Match by full nodeName (which might include namespace prefix)
|
|
42
|
+
if (sel.ns && nodeName === `${sel.ns}:${sel.name}`) {
|
|
43
|
+
results.push(element); // Store the actual DOM element reference
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
// Match by local name only
|
|
47
|
+
if (localName === sel.name || nodeName === sel.name) {
|
|
48
|
+
results.push(element); // Store the actual DOM element reference
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
// Match by checking if nodeName ends with the local name
|
|
52
|
+
if (nodeName.endsWith(`:${sel.name}`)) {
|
|
53
|
+
results.push(element); // Store the actual DOM element reference
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Search all child nodes
|
|
59
|
+
if (node.childNodes) {
|
|
60
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
61
|
+
searchNode(node.childNodes[i]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
searchNode(parent);
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
// Known XML namespaces used in XML Signatures and related standards
|
|
69
|
+
const NAMESPACES = {
|
|
70
|
+
ds: "http://www.w3.org/2000/09/xmldsig#",
|
|
71
|
+
dsig11: "http://www.w3.org/2009/xmldsig11#",
|
|
72
|
+
dsig2: "http://www.w3.org/2010/xmldsig2#",
|
|
73
|
+
ec: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
74
|
+
dsig_more: "http://www.w3.org/2001/04/xmldsig-more#",
|
|
75
|
+
xenc: "http://www.w3.org/2001/04/xmlenc#",
|
|
76
|
+
xenc11: "http://www.w3.org/2009/xmlenc11#",
|
|
77
|
+
xades: "http://uri.etsi.org/01903/v1.3.2#",
|
|
78
|
+
xades141: "http://uri.etsi.org/01903/v1.4.1#",
|
|
79
|
+
asic: "http://uri.etsi.org/02918/v1.2.1#",
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Create an XML parser that works in both browser and Node environments
|
|
83
|
+
*/
|
|
84
|
+
function createXMLParser() {
|
|
85
|
+
// Check if we're in a browser environment with native DOM support
|
|
86
|
+
if (typeof window !== "undefined" && window.DOMParser) {
|
|
87
|
+
return new window.DOMParser();
|
|
88
|
+
}
|
|
89
|
+
// We're in Node.js, so use xmldom
|
|
90
|
+
try {
|
|
91
|
+
// Import dynamically to avoid bundling issues
|
|
92
|
+
const { DOMParser } = require("@xmldom/xmldom");
|
|
93
|
+
return new DOMParser();
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
throw new Error("XML DOM parser not available. In Node.js environments, please install @xmldom/xmldom package.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Uses XPath to find a single element in an XML document
|
|
101
|
+
*
|
|
102
|
+
* @param parent The parent element or document to search within
|
|
103
|
+
* @param xpathExpression The XPath expression to evaluate
|
|
104
|
+
* @param namespaces Optional namespace mapping (defaults to common XML signature namespaces)
|
|
105
|
+
* @returns The found element or null
|
|
106
|
+
*/
|
|
107
|
+
function queryByXPath(parent, xpathExpression, namespaces = NAMESPACES) {
|
|
108
|
+
try {
|
|
109
|
+
// Browser environment with native XPath
|
|
110
|
+
if (typeof document !== "undefined" && document.evaluate) {
|
|
111
|
+
const nsResolver = createNsResolverForBrowser(namespaces);
|
|
112
|
+
const result = document.evaluate(xpathExpression, parent, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
113
|
+
return result.singleNodeValue;
|
|
114
|
+
}
|
|
115
|
+
// Node.js environment with xpath module
|
|
116
|
+
else {
|
|
117
|
+
const xpath = require("xpath");
|
|
118
|
+
const nsResolver = createNsResolverForNode(namespaces);
|
|
119
|
+
// Use a try-catch here to handle specific XPath issues
|
|
120
|
+
try {
|
|
121
|
+
const nodes = xpath.select(xpathExpression, parent, nsResolver);
|
|
122
|
+
return nodes.length > 0 ? nodes[0] : null;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// If we get a namespace error, try a simpler XPath with just local-name()
|
|
126
|
+
if (typeof err === "object" &&
|
|
127
|
+
err !== null &&
|
|
128
|
+
"message" in err &&
|
|
129
|
+
typeof err.message === "string" &&
|
|
130
|
+
err.message.includes("Cannot resolve QName")) {
|
|
131
|
+
// Extract the element name we're looking for from the XPath
|
|
132
|
+
const match = xpathExpression.match(/local-name\(\)='([^']+)'/);
|
|
133
|
+
if (match && match[1]) {
|
|
134
|
+
const elementName = match[1];
|
|
135
|
+
const simplifiedXPath = `.//*[local-name()='${elementName}']`;
|
|
136
|
+
const nodes = xpath.select(simplifiedXPath, parent);
|
|
137
|
+
return nodes.length > 0 ? nodes[0] : null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw err; // Re-throw if we couldn't handle it
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.error(`XPath evaluation failed for "${xpathExpression}":`, e);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Uses XPath to find all matching elements in an XML document
|
|
151
|
+
*
|
|
152
|
+
* @param parent The parent element or document to search within
|
|
153
|
+
* @param xpathExpression The XPath expression to evaluate
|
|
154
|
+
* @param namespaces Optional namespace mapping (defaults to common XML signature namespaces)
|
|
155
|
+
* @returns Array of matching elements
|
|
156
|
+
*/
|
|
157
|
+
function queryAllByXPath(parent, xpathExpression, namespaces = NAMESPACES) {
|
|
158
|
+
try {
|
|
159
|
+
// Browser environment with native XPath
|
|
160
|
+
if (typeof document !== "undefined" && document.evaluate) {
|
|
161
|
+
const nsResolver = createNsResolverForBrowser(namespaces);
|
|
162
|
+
const result = document.evaluate(xpathExpression, parent, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
163
|
+
const elements = [];
|
|
164
|
+
for (let i = 0; i < result.snapshotLength; i++) {
|
|
165
|
+
elements.push(result.snapshotItem(i));
|
|
166
|
+
}
|
|
167
|
+
return elements;
|
|
168
|
+
}
|
|
169
|
+
// Node.js environment with xpath module
|
|
170
|
+
else {
|
|
171
|
+
const xpath = require("xpath");
|
|
172
|
+
const nsResolver = createNsResolverForNode(namespaces);
|
|
173
|
+
// Use a try-catch here to handle specific XPath issues
|
|
174
|
+
try {
|
|
175
|
+
const nodes = xpath.select(xpathExpression, parent, nsResolver);
|
|
176
|
+
return nodes;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
// If we get a namespace error, try a simpler XPath with just local-name()
|
|
180
|
+
if (typeof err === "object" &&
|
|
181
|
+
err !== null &&
|
|
182
|
+
"message" in err &&
|
|
183
|
+
typeof err.message === "string" &&
|
|
184
|
+
err.message.includes("Cannot resolve QName")) {
|
|
185
|
+
// Extract the element name we're looking for from the XPath
|
|
186
|
+
const match = xpathExpression.match(/local-name\(\)='([^']+)'/);
|
|
187
|
+
if (match && match[1]) {
|
|
188
|
+
const elementName = match[1];
|
|
189
|
+
const simplifiedXPath = `.//*[local-name()='${elementName}']`;
|
|
190
|
+
const nodes = xpath.select(simplifiedXPath, parent);
|
|
191
|
+
return nodes;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw err; // Re-throw if we couldn't handle it
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
console.error(`XPath evaluation failed for "${xpathExpression}":`, e);
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Helper function to create a namespace resolver for browser environments
|
|
205
|
+
*/
|
|
206
|
+
function createNsResolverForBrowser(namespaces) {
|
|
207
|
+
return function (prefix) {
|
|
208
|
+
if (prefix === null)
|
|
209
|
+
return null;
|
|
210
|
+
return namespaces[prefix] || null;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Helper function to create a namespace resolver for Node.js environments
|
|
215
|
+
*/
|
|
216
|
+
function createNsResolverForNode(namespaces) {
|
|
217
|
+
return namespaces;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Converts a CSS-like selector (with namespace support) to an XPath expression
|
|
221
|
+
*
|
|
222
|
+
* @param selector CSS-like selector (e.g., "ds:SignedInfo, SignedInfo")
|
|
223
|
+
* @returns Equivalent XPath expression
|
|
224
|
+
*/
|
|
225
|
+
function selectorToXPath(selector) {
|
|
226
|
+
// Split by comma to handle alternative selectors
|
|
227
|
+
const parts = selector.split(",").map((s) => s.trim());
|
|
228
|
+
const xpathParts = [];
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
// Handle namespaced selectors (both prefix:name and prefix\\:name formats)
|
|
231
|
+
const segments = part.split(/\\:|:/).filter(Boolean);
|
|
232
|
+
if (segments.length === 1) {
|
|
233
|
+
// Simple element name without namespace
|
|
234
|
+
// Match any element with the right local name
|
|
235
|
+
xpathParts.push(`.//*[local-name()='${segments[0]}']`);
|
|
236
|
+
}
|
|
237
|
+
else if (segments.length === 2) {
|
|
238
|
+
// Element with namespace prefix - only use local-name() or specific namespace prefix
|
|
239
|
+
// that we know is registered, avoiding the generic 'ns:' prefix
|
|
240
|
+
xpathParts.push(`.//${segments[0]}:${segments[1]} | .//*[local-name()='${segments[1]}']`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Join with | operator (XPath's OR)
|
|
244
|
+
return xpathParts.join(" | ");
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Enhanced querySelector that uses XPath for better namespace handling
|
|
248
|
+
* (Drop-in replacement for the original querySelector function)
|
|
249
|
+
*
|
|
250
|
+
* @param parent The parent element or document to search within
|
|
251
|
+
* @param selector A CSS-like selector (with namespace handling)
|
|
252
|
+
* @returns The found element or null
|
|
253
|
+
*/
|
|
254
|
+
function querySelector(parent, selector) {
|
|
255
|
+
// First try native querySelector if we're in a browser
|
|
256
|
+
if (typeof parent.querySelector === "function") {
|
|
257
|
+
try {
|
|
258
|
+
const result = parent.querySelector(selector);
|
|
259
|
+
if (result)
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
// Fallback to XPath if querySelector fails (e.g., due to namespace issues)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// First try with our enhanced DOM traversal methods (more reliable in some cases)
|
|
267
|
+
const elements = findElementsByTagNameRecursive(parent, selector);
|
|
268
|
+
if (elements.length > 0) {
|
|
269
|
+
return elements[0];
|
|
270
|
+
}
|
|
271
|
+
// Then try XPath as a fallback
|
|
272
|
+
try {
|
|
273
|
+
const xpath = selectorToXPath(selector);
|
|
274
|
+
return queryByXPath(parent, xpath);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
console.warn("XPath query failed, using direct DOM traversal as fallback");
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Enhanced querySelectorAll that uses XPath for better namespace handling
|
|
283
|
+
* (Drop-in replacement for the original querySelectorAll function)
|
|
284
|
+
*
|
|
285
|
+
* @param parent The parent element or document to search within
|
|
286
|
+
* @param selector A CSS-like selector (with namespace handling)
|
|
287
|
+
* @returns Array of matching elements
|
|
288
|
+
*/
|
|
289
|
+
function querySelectorAll(parent, selector) {
|
|
290
|
+
// First try native querySelectorAll if we're in a browser
|
|
291
|
+
if (typeof parent.querySelectorAll === "function") {
|
|
292
|
+
try {
|
|
293
|
+
const results = parent.querySelectorAll(selector);
|
|
294
|
+
if (results.length > 0) {
|
|
295
|
+
const elements = [];
|
|
296
|
+
for (let i = 0; i < results.length; i++) {
|
|
297
|
+
elements.push(results[i]);
|
|
298
|
+
}
|
|
299
|
+
return elements;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
// Fallback to XPath if querySelectorAll fails (e.g., due to namespace issues)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// First try with our enhanced DOM traversal methods (more reliable in some cases)
|
|
307
|
+
const elements = findElementsByTagNameRecursive(parent, selector);
|
|
308
|
+
if (elements.length > 0) {
|
|
309
|
+
return elements;
|
|
310
|
+
}
|
|
311
|
+
// Then try XPath as a fallback
|
|
312
|
+
try {
|
|
313
|
+
const xpath = selectorToXPath(selector);
|
|
314
|
+
return queryAllByXPath(parent, xpath);
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
console.warn("XPath query failed, using direct DOM traversal as fallback");
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Serialize a DOM node to XML string
|
|
323
|
+
*/
|
|
324
|
+
function serializeToXML(node) {
|
|
325
|
+
// Check if we're in a browser environment with native XMLSerializer
|
|
326
|
+
if (typeof window !== "undefined" && window.XMLSerializer) {
|
|
327
|
+
return new window.XMLSerializer().serializeToString(node);
|
|
328
|
+
}
|
|
329
|
+
// If we're using xmldom
|
|
330
|
+
try {
|
|
331
|
+
const { XMLSerializer } = require("@xmldom/xmldom");
|
|
332
|
+
return new XMLSerializer().serializeToString(node);
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
throw new Error("XML Serializer not available. In Node.js environments, please install @xmldom/xmldom package.");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Canonicalization method URIs
|
|
340
|
+
const CANONICALIZATION_METHODS = {
|
|
341
|
+
default: "c14n",
|
|
342
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315": "c14n",
|
|
343
|
+
"http://www.w3.org/2006/12/xml-c14n11": "c14n11",
|
|
344
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#": "c14n_exc",
|
|
345
|
+
};
|
|
346
|
+
// Internal method implementations
|
|
347
|
+
const methods = {
|
|
348
|
+
c14n: {
|
|
349
|
+
beforeChildren: () => "",
|
|
350
|
+
afterChildren: () => "",
|
|
351
|
+
betweenChildren: () => "",
|
|
352
|
+
afterElement: () => "",
|
|
353
|
+
isCanonicalizationMethod: "c14n",
|
|
354
|
+
},
|
|
355
|
+
c14n11: {
|
|
356
|
+
beforeChildren: (hasElementChildren, hasMixedContent) => {
|
|
357
|
+
// If it's mixed content, don't add newlines
|
|
358
|
+
if (hasMixedContent)
|
|
359
|
+
return "";
|
|
360
|
+
return hasElementChildren ? "\n" : "";
|
|
361
|
+
},
|
|
362
|
+
afterChildren: (hasElementChildren, hasMixedContent) => {
|
|
363
|
+
// If it's mixed content, don't add newlines
|
|
364
|
+
if (hasMixedContent)
|
|
365
|
+
return "";
|
|
366
|
+
return hasElementChildren ? "\n" : "";
|
|
367
|
+
},
|
|
368
|
+
betweenChildren: (prevIsElement, nextIsElement, hasMixedContent) => {
|
|
369
|
+
// If it's mixed content, don't add newlines between elements
|
|
370
|
+
if (hasMixedContent)
|
|
371
|
+
return "";
|
|
372
|
+
// Only add newline between elements
|
|
373
|
+
return prevIsElement && nextIsElement ? "\n" : "";
|
|
374
|
+
},
|
|
375
|
+
afterElement: () => "",
|
|
376
|
+
isCanonicalizationMethod: "c14n11",
|
|
377
|
+
},
|
|
378
|
+
c14n_exc: {
|
|
379
|
+
beforeChildren: () => "",
|
|
380
|
+
afterChildren: () => "",
|
|
381
|
+
betweenChildren: () => "",
|
|
382
|
+
afterElement: () => "",
|
|
383
|
+
isCanonicalizationMethod: "c14n_exc",
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
// Define these constants as they're used in the code
|
|
387
|
+
const NODE_TYPES = {
|
|
388
|
+
ELEMENT_NODE: 1,
|
|
389
|
+
TEXT_NODE: 3,
|
|
390
|
+
};
|
|
391
|
+
class XMLCanonicalizer {
|
|
392
|
+
constructor(method = methods.c14n) {
|
|
393
|
+
this.method = method;
|
|
394
|
+
}
|
|
395
|
+
// Static method to get canonicalizer by URI
|
|
396
|
+
static fromMethod(methodUri) {
|
|
397
|
+
const methodKey = CANONICALIZATION_METHODS[methodUri];
|
|
398
|
+
if (!methodKey) {
|
|
399
|
+
throw new Error(`Unsupported canonicalization method: ${methodUri}`);
|
|
400
|
+
}
|
|
401
|
+
return new XMLCanonicalizer(methods[methodKey]);
|
|
402
|
+
}
|
|
403
|
+
setMethod(method) {
|
|
404
|
+
this.method = method;
|
|
405
|
+
}
|
|
406
|
+
static escapeXml(text) {
|
|
407
|
+
return text
|
|
408
|
+
.replace(/&/g, "&")
|
|
409
|
+
.replace(/</g, "<")
|
|
410
|
+
.replace(/>/g, ">")
|
|
411
|
+
.replace(/"/g, """)
|
|
412
|
+
.replace(/'/g, "'");
|
|
413
|
+
}
|
|
414
|
+
// Helper method to collect namespaces from ancestors
|
|
415
|
+
static collectNamespaces(node, visibleNamespaces = new Map()) {
|
|
416
|
+
let current = node;
|
|
417
|
+
while (current && current.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
418
|
+
const element = current;
|
|
419
|
+
// Handle default namespace
|
|
420
|
+
const xmlnsAttr = element.getAttribute("xmlns");
|
|
421
|
+
if (xmlnsAttr !== null && !visibleNamespaces.has("")) {
|
|
422
|
+
visibleNamespaces.set("", xmlnsAttr);
|
|
423
|
+
}
|
|
424
|
+
// Handle prefixed namespaces
|
|
425
|
+
const attrs = element.attributes;
|
|
426
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
427
|
+
const attr = attrs[i];
|
|
428
|
+
if (attr.name.startsWith("xmlns:")) {
|
|
429
|
+
const prefix = attr.name.substring(6);
|
|
430
|
+
if (!visibleNamespaces.has(prefix)) {
|
|
431
|
+
visibleNamespaces.set(prefix, attr.value);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
current = current.parentNode;
|
|
436
|
+
}
|
|
437
|
+
return visibleNamespaces;
|
|
438
|
+
}
|
|
439
|
+
// Helper method to collect namespaces used in the specific element and its descendants
|
|
440
|
+
static collectUsedNamespaces(node, allVisibleNamespaces = new Map(), inclusivePrefixList = []) {
|
|
441
|
+
const usedNamespaces = new Map();
|
|
442
|
+
const visitedPrefixes = new Set(); // Track prefixes we've already processed
|
|
443
|
+
// Recursive function to check for namespace usage
|
|
444
|
+
function processNode(currentNode, isRoot = false) {
|
|
445
|
+
if (currentNode.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
446
|
+
const element = currentNode;
|
|
447
|
+
// Check element's namespace
|
|
448
|
+
const elementNs = element.namespaceURI;
|
|
449
|
+
const elementPrefix = element.prefix || "";
|
|
450
|
+
if (elementPrefix && elementNs) {
|
|
451
|
+
// If this is the root element or a prefix we haven't seen yet
|
|
452
|
+
if (isRoot || !visitedPrefixes.has(elementPrefix)) {
|
|
453
|
+
visitedPrefixes.add(elementPrefix);
|
|
454
|
+
// If the namespace URI matches what we have in allVisibleNamespaces for this prefix
|
|
455
|
+
const nsUri = allVisibleNamespaces.get(elementPrefix);
|
|
456
|
+
if (nsUri && nsUri === elementNs && !usedNamespaces.has(elementPrefix)) {
|
|
457
|
+
usedNamespaces.set(elementPrefix, nsUri);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Check attributes for namespaces
|
|
462
|
+
const attrs = element.attributes;
|
|
463
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
464
|
+
const attr = attrs[i];
|
|
465
|
+
if (attr.name.includes(":") && !attr.name.startsWith("xmlns:")) {
|
|
466
|
+
const attrPrefix = attr.name.split(":")[0];
|
|
467
|
+
// Only process this prefix if we haven't seen it before or it's the root element
|
|
468
|
+
if (isRoot || !visitedPrefixes.has(attrPrefix)) {
|
|
469
|
+
visitedPrefixes.add(attrPrefix);
|
|
470
|
+
const nsUri = allVisibleNamespaces.get(attrPrefix);
|
|
471
|
+
if (nsUri && !usedNamespaces.has(attrPrefix)) {
|
|
472
|
+
usedNamespaces.set(attrPrefix, nsUri);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Include namespaces from inclusivePrefixList
|
|
478
|
+
for (const prefix of inclusivePrefixList) {
|
|
479
|
+
const nsUri = allVisibleNamespaces.get(prefix);
|
|
480
|
+
if (nsUri && !usedNamespaces.has(prefix)) {
|
|
481
|
+
usedNamespaces.set(prefix, nsUri);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Process child nodes
|
|
485
|
+
for (let i = 0; i < currentNode.childNodes.length; i++) {
|
|
486
|
+
processNode(currentNode.childNodes[i], false);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
processNode(node, true); // Start with root = true
|
|
491
|
+
return usedNamespaces;
|
|
492
|
+
}
|
|
493
|
+
static isBase64Element(node) {
|
|
494
|
+
if (node.nodeType !== NODE_TYPES.ELEMENT_NODE)
|
|
495
|
+
return false;
|
|
496
|
+
const element = node;
|
|
497
|
+
const localName = element.localName || element.nodeName.split(":").pop() || "";
|
|
498
|
+
return this.base64Elements.has(localName);
|
|
499
|
+
}
|
|
500
|
+
// Method to analyze whitespace in document
|
|
501
|
+
static analyzeWhitespace(node) {
|
|
502
|
+
// If node is a document, use the document element
|
|
503
|
+
const rootNode = node.nodeType === NODE_TYPES.ELEMENT_NODE ? node : node.documentElement;
|
|
504
|
+
function analyzeNode(node) {
|
|
505
|
+
if (node.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
506
|
+
// Initialize whitespace info
|
|
507
|
+
node._whitespace = {
|
|
508
|
+
hasMixedContent: false,
|
|
509
|
+
hasExistingLinebreaks: false,
|
|
510
|
+
originalContent: {},
|
|
511
|
+
};
|
|
512
|
+
const children = Array.from(node.childNodes);
|
|
513
|
+
let hasTextContent = false;
|
|
514
|
+
let hasElementContent = false;
|
|
515
|
+
let hasLinebreaks = false;
|
|
516
|
+
// First, check if there's any non-whitespace text content
|
|
517
|
+
for (const child of children) {
|
|
518
|
+
if (child.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
519
|
+
const text = child.nodeValue || "";
|
|
520
|
+
if (text.trim().length > 0) {
|
|
521
|
+
hasTextContent = true;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Second, check if there are any element children
|
|
527
|
+
for (const child of children) {
|
|
528
|
+
if (child.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
529
|
+
hasElementContent = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Now process all children and analyze recursively
|
|
534
|
+
for (let i = 0; i < children.length; i++) {
|
|
535
|
+
const child = children[i];
|
|
536
|
+
if (child.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
537
|
+
const text = child.nodeValue || "";
|
|
538
|
+
// Store original text
|
|
539
|
+
child._originalText = text;
|
|
540
|
+
// Check for linebreaks in text
|
|
541
|
+
if (text.includes("\n")) {
|
|
542
|
+
hasLinebreaks = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (child.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
546
|
+
// Recursively analyze child elements
|
|
547
|
+
analyzeNode(child);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Set mixed content flag - true if there's both text content and element children
|
|
551
|
+
node._whitespace.hasMixedContent = hasTextContent && hasElementContent;
|
|
552
|
+
node._whitespace.hasExistingLinebreaks = hasLinebreaks;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
analyzeNode(rootNode);
|
|
556
|
+
}
|
|
557
|
+
// Standard canonicalization method
|
|
558
|
+
canonicalize(node, visibleNamespaces = new Map(), options = { isStartingNode: true }) {
|
|
559
|
+
if (!node)
|
|
560
|
+
return "";
|
|
561
|
+
let result = "";
|
|
562
|
+
if (node.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
563
|
+
// Create a new map for this element's visible namespaces
|
|
564
|
+
const elementVisibleNamespaces = new Map(visibleNamespaces);
|
|
565
|
+
const element = node;
|
|
566
|
+
// Collect namespaces declared on this element
|
|
567
|
+
// Handle default namespace
|
|
568
|
+
const xmlnsAttr = element.getAttribute("xmlns");
|
|
569
|
+
if (xmlnsAttr !== null) {
|
|
570
|
+
elementVisibleNamespaces.set("", xmlnsAttr);
|
|
571
|
+
}
|
|
572
|
+
// Handle prefixed namespaces
|
|
573
|
+
const nsAttrs = element.attributes;
|
|
574
|
+
for (let i = 0; i < nsAttrs.length; i++) {
|
|
575
|
+
const attr = nsAttrs[i];
|
|
576
|
+
if (attr.name.startsWith("xmlns:")) {
|
|
577
|
+
const prefix = attr.name.substring(6);
|
|
578
|
+
elementVisibleNamespaces.set(prefix, attr.value);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Prepare the element's start tag
|
|
582
|
+
const prefix = element.prefix || "";
|
|
583
|
+
const localName = element.localName || element.nodeName.split(":").pop() || "";
|
|
584
|
+
const qName = prefix ? `${prefix}:${localName}` : localName;
|
|
585
|
+
result += "<" + qName;
|
|
586
|
+
// Handle namespaces based on whether it's the starting node
|
|
587
|
+
if (options.isStartingNode) {
|
|
588
|
+
// Collect all namespaces in scope for this element
|
|
589
|
+
const allNamespaces = XMLCanonicalizer.collectNamespaces(node);
|
|
590
|
+
// Include all namespaces that are in scope, sorted appropriately
|
|
591
|
+
const nsEntries = Array.from(allNamespaces.entries()).sort((a, b) => {
|
|
592
|
+
if (a[0] === "")
|
|
593
|
+
return -1;
|
|
594
|
+
if (b[0] === "")
|
|
595
|
+
return 1;
|
|
596
|
+
return a[0].localeCompare(b[0]);
|
|
597
|
+
});
|
|
598
|
+
for (const [prefix, uri] of nsEntries) {
|
|
599
|
+
if (prefix === "") {
|
|
600
|
+
result += ` xmlns="${uri}"`;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
result += ` xmlns:${prefix}="${uri}"`;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// For non-starting nodes, only include newly declared namespaces
|
|
609
|
+
const nsEntries = Array.from(elementVisibleNamespaces.entries())
|
|
610
|
+
.filter(([p, uri]) => {
|
|
611
|
+
// Include if:
|
|
612
|
+
// 1. It's not in the parent's visible namespaces, or
|
|
613
|
+
// 2. The URI is different from parent's
|
|
614
|
+
return !visibleNamespaces.has(p) || visibleNamespaces.get(p) !== uri;
|
|
615
|
+
})
|
|
616
|
+
.sort((a, b) => {
|
|
617
|
+
if (a[0] === "")
|
|
618
|
+
return -1;
|
|
619
|
+
if (b[0] === "")
|
|
620
|
+
return 1;
|
|
621
|
+
return a[0].localeCompare(b[0]);
|
|
622
|
+
});
|
|
623
|
+
for (const [prefix, uri] of nsEntries) {
|
|
624
|
+
if (prefix === "") {
|
|
625
|
+
result += ` xmlns="${uri}"`;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
result += ` xmlns:${prefix}="${uri}"`;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Handle attributes (sorted lexicographically)
|
|
633
|
+
const elementAttrs = element.attributes;
|
|
634
|
+
const attrArray = [];
|
|
635
|
+
for (let i = 0; i < elementAttrs.length; i++) {
|
|
636
|
+
const attr = elementAttrs[i];
|
|
637
|
+
if (!attr.name.startsWith("xmlns")) {
|
|
638
|
+
attrArray.push(attr);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
attrArray.sort((a, b) => a.name.localeCompare(b.name));
|
|
642
|
+
for (const attr of attrArray) {
|
|
643
|
+
result += ` ${attr.name}="${XMLCanonicalizer.escapeXml(attr.value)}"`;
|
|
644
|
+
}
|
|
645
|
+
result += ">";
|
|
646
|
+
// Process children
|
|
647
|
+
const children = Array.from(node.childNodes);
|
|
648
|
+
let hasElementChildren = false;
|
|
649
|
+
let lastWasElement = false;
|
|
650
|
+
const hasMixedContent = node._whitespace?.hasMixedContent || false;
|
|
651
|
+
// First pass to determine if we have element children
|
|
652
|
+
for (const child of children) {
|
|
653
|
+
if (child.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
654
|
+
hasElementChildren = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Check if we need to add a newline for c14n11
|
|
659
|
+
// Don't add newlines for mixed content
|
|
660
|
+
const needsInitialNewline = this.method.isCanonicalizationMethod === "c14n11" &&
|
|
661
|
+
hasElementChildren &&
|
|
662
|
+
!node._whitespace?.hasExistingLinebreaks &&
|
|
663
|
+
!hasMixedContent;
|
|
664
|
+
// Add newline for c14n11 if needed
|
|
665
|
+
if (needsInitialNewline) {
|
|
666
|
+
result += this.method.beforeChildren(hasElementChildren, hasMixedContent);
|
|
667
|
+
}
|
|
668
|
+
// Process each child
|
|
669
|
+
for (let i = 0; i < children.length; i++) {
|
|
670
|
+
const child = children[i];
|
|
671
|
+
const isElement = child.nodeType === NODE_TYPES.ELEMENT_NODE;
|
|
672
|
+
const nextChild = i < children.length - 1 ? children[i + 1] : null;
|
|
673
|
+
nextChild && nextChild.nodeType === NODE_TYPES.ELEMENT_NODE;
|
|
674
|
+
// Handle text node
|
|
675
|
+
if (child.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
676
|
+
const text = child.nodeValue || "";
|
|
677
|
+
if (XMLCanonicalizer.isBase64Element(node)) {
|
|
678
|
+
// Special handling for base64 content
|
|
679
|
+
result += text.replace(/\r/g, "
");
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
// Use the original text exactly as it was
|
|
683
|
+
result += child._originalText || text;
|
|
684
|
+
}
|
|
685
|
+
lastWasElement = false;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
// Handle element node
|
|
689
|
+
if (isElement) {
|
|
690
|
+
// Add newline between elements if needed for c14n11
|
|
691
|
+
// Don't add newlines for mixed content
|
|
692
|
+
if (lastWasElement &&
|
|
693
|
+
this.method.isCanonicalizationMethod === "c14n11" &&
|
|
694
|
+
!node._whitespace?.hasExistingLinebreaks &&
|
|
695
|
+
!hasMixedContent) {
|
|
696
|
+
result += this.method.betweenChildren(true, true, hasMixedContent);
|
|
697
|
+
}
|
|
698
|
+
// Recursively canonicalize the child element
|
|
699
|
+
result += this.canonicalize(child, elementVisibleNamespaces, {
|
|
700
|
+
isStartingNode: false,
|
|
701
|
+
});
|
|
702
|
+
lastWasElement = true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Add final newline for c14n11 if needed
|
|
706
|
+
// Don't add newlines for mixed content
|
|
707
|
+
if (needsInitialNewline) {
|
|
708
|
+
result += this.method.afterChildren(hasElementChildren, hasMixedContent);
|
|
709
|
+
}
|
|
710
|
+
result += "</" + qName + ">";
|
|
711
|
+
}
|
|
712
|
+
else if (node.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
713
|
+
// For standalone text nodes
|
|
714
|
+
const text = node._originalText || node.nodeValue || "";
|
|
715
|
+
result += XMLCanonicalizer.escapeXml(text);
|
|
716
|
+
}
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
// Exclusive canonicalization implementation
|
|
720
|
+
canonicalizeExclusive(node, visibleNamespaces = new Map(), options = {}) {
|
|
721
|
+
if (!node)
|
|
722
|
+
return "";
|
|
723
|
+
const { inclusiveNamespacePrefixList = [], isStartingNode = true } = options;
|
|
724
|
+
let result = "";
|
|
725
|
+
if (node.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
726
|
+
const element = node;
|
|
727
|
+
// First, collect all namespaces that are visible at this point
|
|
728
|
+
const allVisibleNamespaces = XMLCanonicalizer.collectNamespaces(element);
|
|
729
|
+
// Then, determine which namespaces are actually used in this subtree
|
|
730
|
+
const usedNamespaces = isStartingNode
|
|
731
|
+
? XMLCanonicalizer.collectUsedNamespaces(element, allVisibleNamespaces, inclusiveNamespacePrefixList)
|
|
732
|
+
: new Map(); // For child elements, don't add any more namespaces
|
|
733
|
+
// Start the element opening tag
|
|
734
|
+
const prefix = element.prefix || "";
|
|
735
|
+
const localName = element.localName || element.nodeName.split(":").pop() || "";
|
|
736
|
+
const qName = prefix ? `${prefix}:${localName}` : localName;
|
|
737
|
+
result += "<" + qName;
|
|
738
|
+
// Add namespace declarations for used namespaces (only at the top level)
|
|
739
|
+
if (isStartingNode) {
|
|
740
|
+
const nsEntries = Array.from(usedNamespaces.entries()).sort((a, b) => {
|
|
741
|
+
if (a[0] === "")
|
|
742
|
+
return -1;
|
|
743
|
+
if (b[0] === "")
|
|
744
|
+
return 1;
|
|
745
|
+
return a[0].localeCompare(b[0]);
|
|
746
|
+
});
|
|
747
|
+
for (const [prefix, uri] of nsEntries) {
|
|
748
|
+
if (prefix === "") {
|
|
749
|
+
result += ` xmlns="${uri}"`;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
result += ` xmlns:${prefix}="${uri}"`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Add attributes (sorted lexicographically)
|
|
757
|
+
const elementAttrs = element.attributes;
|
|
758
|
+
const attrArray = [];
|
|
759
|
+
for (let i = 0; i < elementAttrs.length; i++) {
|
|
760
|
+
const attr = elementAttrs[i];
|
|
761
|
+
if (!attr.name.startsWith("xmlns")) {
|
|
762
|
+
attrArray.push(attr);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
attrArray.sort((a, b) => a.name.localeCompare(b.name));
|
|
766
|
+
for (const attr of attrArray) {
|
|
767
|
+
result += ` ${attr.name}="${XMLCanonicalizer.escapeXml(attr.value)}"`;
|
|
768
|
+
}
|
|
769
|
+
result += ">";
|
|
770
|
+
// Process child nodes
|
|
771
|
+
const children = Array.from(node.childNodes);
|
|
772
|
+
for (let i = 0; i < children.length; i++) {
|
|
773
|
+
const child = children[i];
|
|
774
|
+
if (child.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
775
|
+
const text = child.nodeValue || "";
|
|
776
|
+
if (XMLCanonicalizer.isBase64Element(node)) {
|
|
777
|
+
// Special handling for base64 content
|
|
778
|
+
result += text.replace(/\r/g, "
");
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
// Regular text handling
|
|
782
|
+
result += XMLCanonicalizer.escapeXml(text);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else if (child.nodeType === NODE_TYPES.ELEMENT_NODE) {
|
|
786
|
+
// Recursively process child elements
|
|
787
|
+
// For child elements, we pass the namespaces from the parent but mark as non-root
|
|
788
|
+
result += this.canonicalizeExclusive(child, new Map([...visibleNamespaces, ...usedNamespaces]), // Pass all namespaces to children
|
|
789
|
+
{
|
|
790
|
+
inclusiveNamespacePrefixList,
|
|
791
|
+
isStartingNode: false, // Mark as non-starting node
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Close the element
|
|
796
|
+
result += "</" + qName + ">";
|
|
797
|
+
}
|
|
798
|
+
else if (node.nodeType === NODE_TYPES.TEXT_NODE) {
|
|
799
|
+
// Handle standalone text node
|
|
800
|
+
const text = node.nodeValue || "";
|
|
801
|
+
result += XMLCanonicalizer.escapeXml(text);
|
|
802
|
+
}
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
// Static methods for canonicalization
|
|
806
|
+
static c14n(node) {
|
|
807
|
+
// First analyze document whitespace
|
|
808
|
+
this.analyzeWhitespace(node);
|
|
809
|
+
// Then create canonicalizer and process the node
|
|
810
|
+
const canonicalizer = new XMLCanonicalizer(methods.c14n);
|
|
811
|
+
return canonicalizer.canonicalize(node);
|
|
812
|
+
}
|
|
813
|
+
static c14n11(node) {
|
|
814
|
+
// First analyze document whitespace
|
|
815
|
+
this.analyzeWhitespace(node);
|
|
816
|
+
// Then create canonicalizer and process the node
|
|
817
|
+
const canonicalizer = new XMLCanonicalizer(methods.c14n11);
|
|
818
|
+
return canonicalizer.canonicalize(node);
|
|
819
|
+
}
|
|
820
|
+
static c14n_exc(node, inclusiveNamespacePrefixList = []) {
|
|
821
|
+
// First analyze document whitespace
|
|
822
|
+
this.analyzeWhitespace(node);
|
|
823
|
+
// Create canonicalizer and process the node with exclusive canonicalization
|
|
824
|
+
const canonicalizer = new XMLCanonicalizer(methods.c14n_exc);
|
|
825
|
+
return canonicalizer.canonicalizeExclusive(node, new Map(), {
|
|
826
|
+
inclusiveNamespacePrefixList,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
// Method that takes URI directly
|
|
830
|
+
static canonicalize(node, methodUri, options = {}) {
|
|
831
|
+
// Get the method from the URI
|
|
832
|
+
const methodKey = CANONICALIZATION_METHODS[methodUri] ||
|
|
833
|
+
CANONICALIZATION_METHODS.default;
|
|
834
|
+
switch (methodKey) {
|
|
835
|
+
case "c14n":
|
|
836
|
+
return this.c14n(node);
|
|
837
|
+
case "c14n11":
|
|
838
|
+
return this.c14n11(node);
|
|
839
|
+
case "c14n_exc":
|
|
840
|
+
return this.c14n_exc(node, options.inclusiveNamespacePrefixList || []);
|
|
841
|
+
default:
|
|
842
|
+
throw new Error(`Unsupported canonicalization method: ${methodUri}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
XMLCanonicalizer.base64Elements = new Set([
|
|
847
|
+
"DigestValue",
|
|
848
|
+
"X509Certificate",
|
|
849
|
+
"EncapsulatedTimeStamp",
|
|
850
|
+
"EncapsulatedOCSPValue",
|
|
851
|
+
"IssuerSerialV2",
|
|
852
|
+
]);
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Format a certificate string as a proper PEM certificate
|
|
856
|
+
* @param certBase64 Base64-encoded certificate
|
|
857
|
+
* @returns Formatted PEM certificate
|
|
858
|
+
*/
|
|
859
|
+
function formatPEM$1(certBase64) {
|
|
860
|
+
if (!certBase64)
|
|
861
|
+
return "";
|
|
862
|
+
// Remove any whitespace from the base64 string
|
|
863
|
+
const cleanBase64 = certBase64.replace(/\s+/g, "");
|
|
864
|
+
// Split the base64 into lines of 64 characters
|
|
865
|
+
const lines = [];
|
|
866
|
+
for (let i = 0; i < cleanBase64.length; i += 64) {
|
|
867
|
+
lines.push(cleanBase64.substring(i, i + 64));
|
|
868
|
+
}
|
|
869
|
+
// Format as PEM certificate
|
|
870
|
+
return `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----`;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Extract subject information from an X.509 certificate
|
|
874
|
+
* @param certificate X509Certificate instance
|
|
875
|
+
* @returns Signer information object
|
|
876
|
+
*/
|
|
877
|
+
function extractSignerInfo(certificate) {
|
|
878
|
+
const result = {
|
|
879
|
+
validFrom: certificate.notBefore,
|
|
880
|
+
validTo: certificate.notAfter,
|
|
881
|
+
issuer: {},
|
|
882
|
+
};
|
|
883
|
+
// Try to extract fields using various approaches
|
|
884
|
+
// Approach 1: Try direct access to typed subject properties
|
|
885
|
+
try {
|
|
886
|
+
if (typeof certificate.subject === "object" && certificate.subject !== null) {
|
|
887
|
+
// Handle subject properties
|
|
888
|
+
const subject = certificate.subject;
|
|
889
|
+
result.commonName = subject.commonName;
|
|
890
|
+
result.organization = subject.organizationName;
|
|
891
|
+
result.country = subject.countryName;
|
|
892
|
+
}
|
|
893
|
+
// Handle issuer properties
|
|
894
|
+
if (typeof certificate.issuer === "object" && certificate.issuer !== null) {
|
|
895
|
+
const issuer = certificate.issuer;
|
|
896
|
+
result.issuer.commonName = issuer.commonName;
|
|
897
|
+
result.issuer.organization = issuer.organizationName;
|
|
898
|
+
result.issuer.country = issuer.countryName;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
catch (e) {
|
|
902
|
+
console.warn("Could not extract subject/issuer as objects:", e);
|
|
903
|
+
}
|
|
904
|
+
// Approach 2: Parse subject/issuer as strings if they are strings
|
|
905
|
+
try {
|
|
906
|
+
if (typeof certificate.subject === "string") {
|
|
907
|
+
const subjectStr = certificate.subject;
|
|
908
|
+
// Parse the string format (usually CN=name,O=org,C=country)
|
|
909
|
+
const subjectParts = subjectStr.split(",");
|
|
910
|
+
for (const part of subjectParts) {
|
|
911
|
+
const [key, value] = part.trim().split("=");
|
|
912
|
+
if (key === "CN")
|
|
913
|
+
result.commonName = result.commonName || value;
|
|
914
|
+
if (key === "O")
|
|
915
|
+
result.organization = result.organization || value;
|
|
916
|
+
if (key === "C")
|
|
917
|
+
result.country = result.country || value;
|
|
918
|
+
if (key === "SN")
|
|
919
|
+
result.surname = value;
|
|
920
|
+
if (key === "G" || key === "GN")
|
|
921
|
+
result.givenName = value;
|
|
922
|
+
if (key === "SERIALNUMBER" || key === "2.5.4.5")
|
|
923
|
+
result.serialNumber = value?.replace("PNOLV-", "");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (typeof certificate.issuer === "string") {
|
|
927
|
+
const issuerStr = certificate.issuer;
|
|
928
|
+
// Parse the string format
|
|
929
|
+
const issuerParts = issuerStr.split(",");
|
|
930
|
+
for (const part of issuerParts) {
|
|
931
|
+
const [key, value] = part.trim().split("=");
|
|
932
|
+
if (key === "CN")
|
|
933
|
+
result.issuer.commonName = result.issuer.commonName || value;
|
|
934
|
+
if (key === "O")
|
|
935
|
+
result.issuer.organization = result.issuer.organization || value;
|
|
936
|
+
if (key === "C")
|
|
937
|
+
result.issuer.country = result.issuer.country || value;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (e) {
|
|
942
|
+
console.warn("Could not extract subject/issuer as strings:", e);
|
|
943
|
+
}
|
|
944
|
+
// Approach 3: Try to use getField method if available
|
|
945
|
+
try {
|
|
946
|
+
if ("subjectName" in certificate && certificate.subjectName?.getField) {
|
|
947
|
+
const subjectName = certificate.subjectName;
|
|
948
|
+
// Only set if not already set from previous approaches
|
|
949
|
+
result.commonName = result.commonName || subjectName.getField("CN")?.[0];
|
|
950
|
+
result.surname = result.surname || subjectName.getField("SN")?.[0];
|
|
951
|
+
result.givenName = result.givenName || subjectName.getField("G")?.[0];
|
|
952
|
+
result.serialNumber =
|
|
953
|
+
result.serialNumber || subjectName.getField("2.5.4.5")?.[0]?.replace("PNOLV-", "");
|
|
954
|
+
result.country = result.country || subjectName.getField("C")?.[0];
|
|
955
|
+
result.organization = result.organization || subjectName.getField("O")?.[0];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
catch (e) {
|
|
959
|
+
console.warn("Could not extract fields using getField method:", e);
|
|
960
|
+
}
|
|
961
|
+
// Get the serial number from the certificate if not found in subject
|
|
962
|
+
if (!result.serialNumber && certificate.serialNumber) {
|
|
963
|
+
result.serialNumber = certificate.serialNumber;
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Parse a certificate from base64 data
|
|
969
|
+
* @param certData Base64-encoded certificate data
|
|
970
|
+
* @returns Parsed certificate information
|
|
971
|
+
*/
|
|
972
|
+
async function parseCertificate(certData) {
|
|
973
|
+
try {
|
|
974
|
+
let pemCert = certData;
|
|
975
|
+
// Check if it's already in PEM format, if not, convert it
|
|
976
|
+
if (!certData.includes("-----BEGIN CERTIFICATE-----")) {
|
|
977
|
+
// Only clean non-PEM format data before conversion
|
|
978
|
+
const cleanedCertData = certData.replace(/[\r\n\s]/g, "");
|
|
979
|
+
pemCert = formatPEM$1(cleanedCertData);
|
|
980
|
+
}
|
|
981
|
+
const cert = new x509.X509Certificate(pemCert);
|
|
982
|
+
const signerInfo = extractSignerInfo(cert);
|
|
983
|
+
return {
|
|
984
|
+
subject: {
|
|
985
|
+
commonName: signerInfo.commonName,
|
|
986
|
+
organization: signerInfo.organization,
|
|
987
|
+
country: signerInfo.country,
|
|
988
|
+
surname: signerInfo.surname,
|
|
989
|
+
givenName: signerInfo.givenName,
|
|
990
|
+
serialNumber: signerInfo.serialNumber,
|
|
991
|
+
},
|
|
992
|
+
validFrom: signerInfo.validFrom,
|
|
993
|
+
validTo: signerInfo.validTo,
|
|
994
|
+
issuer: signerInfo.issuer,
|
|
995
|
+
serialNumber: cert.serialNumber,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
console.error("Certificate parsing error:", error);
|
|
1000
|
+
throw new Error("Failed to parse certificate: " + (error instanceof Error ? error.message : String(error)));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Check if a certificate was valid at a specific time
|
|
1005
|
+
* @param cert Certificate object or info
|
|
1006
|
+
* @param checkTime The time to check validity against (defaults to current time)
|
|
1007
|
+
* @returns Validity check result
|
|
1008
|
+
*/
|
|
1009
|
+
function checkCertificateValidity(cert, checkTime = new Date()) {
|
|
1010
|
+
// Extract validity dates based on input type
|
|
1011
|
+
const validFrom = "notBefore" in cert ? cert.notBefore : cert.validFrom;
|
|
1012
|
+
const validTo = "notAfter" in cert ? cert.notAfter : cert.validTo;
|
|
1013
|
+
// Check if certificate is valid at the specified time
|
|
1014
|
+
if (checkTime < validFrom) {
|
|
1015
|
+
return {
|
|
1016
|
+
isValid: false,
|
|
1017
|
+
reason: `Certificate not yet valid. Valid from ${validFrom.toISOString()}`,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (checkTime > validTo) {
|
|
1021
|
+
return {
|
|
1022
|
+
isValid: false,
|
|
1023
|
+
reason: `Certificate expired. Valid until ${validTo.toISOString()}`,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
return { isValid: true };
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Helper function to get signer display name from certificate
|
|
1030
|
+
* @param certInfo Certificate information
|
|
1031
|
+
* @returns Formatted display name
|
|
1032
|
+
*/
|
|
1033
|
+
function getSignerDisplayName(certInfo) {
|
|
1034
|
+
const { subject } = certInfo;
|
|
1035
|
+
if (subject.givenName && subject.surname) {
|
|
1036
|
+
return `${subject.givenName} ${subject.surname}`;
|
|
1037
|
+
}
|
|
1038
|
+
if (subject.commonName) {
|
|
1039
|
+
return subject.commonName;
|
|
1040
|
+
}
|
|
1041
|
+
// Fallback to serial number if available
|
|
1042
|
+
return subject.serialNumber || "Unknown Signer";
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Helper function to format certificate validity period in a human-readable format
|
|
1046
|
+
* @param certInfo Certificate information
|
|
1047
|
+
* @returns Formatted validity period
|
|
1048
|
+
*/
|
|
1049
|
+
function formatValidityPeriod(certInfo) {
|
|
1050
|
+
const { validFrom, validTo } = certInfo;
|
|
1051
|
+
const formatDate = (date) => {
|
|
1052
|
+
return date.toLocaleDateString(undefined, {
|
|
1053
|
+
year: "numeric",
|
|
1054
|
+
month: "long",
|
|
1055
|
+
day: "numeric",
|
|
1056
|
+
});
|
|
1057
|
+
};
|
|
1058
|
+
return `${formatDate(validFrom)} to ${formatDate(validTo)}`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/core/parser/certificateUtils.ts
|
|
1062
|
+
/**
|
|
1063
|
+
* Format a certificate string as a proper PEM certificate
|
|
1064
|
+
* @param certBase64 Base64-encoded certificate
|
|
1065
|
+
* @returns Formatted PEM certificate
|
|
1066
|
+
*/
|
|
1067
|
+
function formatPEM(certBase64) {
|
|
1068
|
+
if (!certBase64)
|
|
1069
|
+
return "";
|
|
1070
|
+
// Remove any whitespace from the base64 string
|
|
1071
|
+
const cleanBase64 = certBase64.replace(/\s+/g, "");
|
|
1072
|
+
// Split the base64 into lines of 64 characters
|
|
1073
|
+
const lines = [];
|
|
1074
|
+
for (let i = 0; i < cleanBase64.length; i += 64) {
|
|
1075
|
+
lines.push(cleanBase64.substring(i, i + 64));
|
|
1076
|
+
}
|
|
1077
|
+
// Format as PEM certificate
|
|
1078
|
+
return `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/core/parser/signatureParser.ts
|
|
1082
|
+
/**
|
|
1083
|
+
* Find signature files in the eDoc container
|
|
1084
|
+
* @param files Map of filenames to file contents
|
|
1085
|
+
* @returns Array of signature filenames
|
|
1086
|
+
*/
|
|
1087
|
+
function findSignatureFiles(files) {
|
|
1088
|
+
// Signature files are typically named with patterns like:
|
|
1089
|
+
// - signatures0.xml
|
|
1090
|
+
// - META-INF/signatures*.xml
|
|
1091
|
+
return Array.from(files.keys()).filter((filename) => filename.match(/META-INF\/signatures\d*\.xml$/) ||
|
|
1092
|
+
filename.match(/META-INF\/.*signatures.*\.xml$/i));
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Parse a signature file that contains a single signature
|
|
1096
|
+
* @param xmlContent The XML file content
|
|
1097
|
+
* @param filename The filename (for reference)
|
|
1098
|
+
* @returns The parsed signature with raw XML content
|
|
1099
|
+
*/
|
|
1100
|
+
function parseSignatureFile(xmlContent, filename) {
|
|
1101
|
+
const text = new TextDecoder().decode(xmlContent);
|
|
1102
|
+
const parser = createXMLParser();
|
|
1103
|
+
const xmlDoc = parser.parseFromString(text, "application/xml");
|
|
1104
|
+
// Using our querySelector helper to find the signature element
|
|
1105
|
+
const signatureElements = querySelectorAll(xmlDoc, "ds\\:Signature, Signature");
|
|
1106
|
+
if (signatureElements.length === 0) {
|
|
1107
|
+
console.warn(`No Signature elements found in ${filename}`);
|
|
1108
|
+
// If we have ASiC-XAdES format, try to find signatures differently
|
|
1109
|
+
if (text.includes("XAdESSignatures")) {
|
|
1110
|
+
const rootElement = xmlDoc.documentElement;
|
|
1111
|
+
// Try direct DOM traversal
|
|
1112
|
+
if (rootElement) {
|
|
1113
|
+
// Look for Signature elements as direct children
|
|
1114
|
+
const directSignature = querySelector(rootElement, "ds\\:Signature, Signature");
|
|
1115
|
+
if (directSignature) {
|
|
1116
|
+
let signatureInfo = parseSignatureElement(directSignature, xmlDoc);
|
|
1117
|
+
signatureInfo.rawXml = text;
|
|
1118
|
+
return signatureInfo;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// Fallback: parse as text
|
|
1122
|
+
const mockSignature = parseBasicSignatureFromText(text);
|
|
1123
|
+
if (mockSignature) {
|
|
1124
|
+
return {
|
|
1125
|
+
...mockSignature,
|
|
1126
|
+
rawXml: text,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
// Parse the signature and add the raw XML
|
|
1133
|
+
let signatureInfo = parseSignatureElement(signatureElements[0], xmlDoc);
|
|
1134
|
+
signatureInfo.rawXml = text;
|
|
1135
|
+
return signatureInfo;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Parse a single signature element using a browser-like approach
|
|
1139
|
+
* @param signatureElement The signature element to parse
|
|
1140
|
+
* @param xmlDoc The parent XML document
|
|
1141
|
+
* @returns Parsed signature information
|
|
1142
|
+
*/
|
|
1143
|
+
function parseSignatureElement(signatureElement, xmlDoc) {
|
|
1144
|
+
// Get signature ID
|
|
1145
|
+
const signatureId = signatureElement.getAttribute("Id") || "unknown";
|
|
1146
|
+
// Find SignedInfo just like in browser code
|
|
1147
|
+
const signedInfo = querySelector(signatureElement, "ds\\:SignedInfo, SignedInfo");
|
|
1148
|
+
//const signedInfo = queryByXPath(signatureElement, ".//*[local-name()='SignedInfo']");
|
|
1149
|
+
if (!signedInfo) {
|
|
1150
|
+
throw new Error("SignedInfo element not found");
|
|
1151
|
+
}
|
|
1152
|
+
// Get the canonicalization method
|
|
1153
|
+
const c14nMethodEl = querySelector(signedInfo, "ds\\:CanonicalizationMethod, CanonicalizationMethod");
|
|
1154
|
+
let canonicalizationMethod = CANONICALIZATION_METHODS.default;
|
|
1155
|
+
if (c14nMethodEl) {
|
|
1156
|
+
canonicalizationMethod = c14nMethodEl.getAttribute("Algorithm") || canonicalizationMethod;
|
|
1157
|
+
}
|
|
1158
|
+
// // Serialize the SignedInfo element to XML string
|
|
1159
|
+
// let signedInfoXml = "";
|
|
1160
|
+
// try {
|
|
1161
|
+
// // Use the serializeToXML utility function which handles browser/Node environments
|
|
1162
|
+
// signedInfoXml = serializeToXML(signedInfo);
|
|
1163
|
+
// } catch (e) {
|
|
1164
|
+
// console.warn("Could not serialize SignedInfo element:", e);
|
|
1165
|
+
// }
|
|
1166
|
+
// Serialize the SignedInfo element to XML string
|
|
1167
|
+
let signedInfoXml = "";
|
|
1168
|
+
signedInfoXml = serializeToXML(signedInfo);
|
|
1169
|
+
// try {
|
|
1170
|
+
// // First check if signedInfo is a valid node
|
|
1171
|
+
// if (!signedInfo) {
|
|
1172
|
+
// console.warn("SignedInfo element is undefined or null");
|
|
1173
|
+
// signedInfoXml = "";
|
|
1174
|
+
// } else if (signedInfo.nodeType === undefined) {
|
|
1175
|
+
// console.warn("SignedInfo doesn't appear to be a valid XML node:", signedInfo);
|
|
1176
|
+
// signedInfoXml = "";
|
|
1177
|
+
// } else {
|
|
1178
|
+
// // Try to use XMLSerializer if available in browser
|
|
1179
|
+
// if (typeof window !== "undefined" && window.XMLSerializer) {
|
|
1180
|
+
// signedInfoXml = new window.XMLSerializer().serializeToString(signedInfo);
|
|
1181
|
+
// } else {
|
|
1182
|
+
// // Node.js environment - use xmldom only
|
|
1183
|
+
// const xmldom = require("@xmldom/xmldom");
|
|
1184
|
+
// const serializer = new xmldom.XMLSerializer();
|
|
1185
|
+
// signedInfoXml = serializer.serializeToString(signedInfo);
|
|
1186
|
+
// }
|
|
1187
|
+
// }
|
|
1188
|
+
// } catch (e) {
|
|
1189
|
+
// console.warn("Could not serialize SignedInfo element:", e);
|
|
1190
|
+
// }
|
|
1191
|
+
// Get signature method
|
|
1192
|
+
const signatureMethod = querySelector(signedInfo, "ds\\:SignatureMethod, SignatureMethod");
|
|
1193
|
+
const signatureAlgorithm = signatureMethod?.getAttribute("Algorithm") || "";
|
|
1194
|
+
// Get signature value
|
|
1195
|
+
const signatureValueEl = querySelector(signatureElement, "ds\\:SignatureValue, SignatureValue");
|
|
1196
|
+
const signatureValue = signatureValueEl?.textContent?.replace(/\s+/g, "") || "";
|
|
1197
|
+
// Get certificate
|
|
1198
|
+
const certElement = querySelector(signatureElement, "ds\\:X509Certificate, X509Certificate");
|
|
1199
|
+
let certificate = "";
|
|
1200
|
+
let certificatePEM = "";
|
|
1201
|
+
let signerInfo = undefined;
|
|
1202
|
+
let publicKey = undefined;
|
|
1203
|
+
if (!certElement) {
|
|
1204
|
+
// Try to find via KeyInfo path
|
|
1205
|
+
const keyInfo = querySelector(signatureElement, "ds\\:KeyInfo, KeyInfo");
|
|
1206
|
+
if (keyInfo) {
|
|
1207
|
+
const x509Data = querySelector(keyInfo, "ds\\:X509Data, X509Data");
|
|
1208
|
+
if (x509Data) {
|
|
1209
|
+
const nestedCert = querySelector(x509Data, "ds\\:X509Certificate, X509Certificate");
|
|
1210
|
+
if (nestedCert) {
|
|
1211
|
+
certificate = nestedCert.textContent?.replace(/\s+/g, "") || "";
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
certificate = certElement.textContent?.replace(/\s+/g, "") || "";
|
|
1218
|
+
}
|
|
1219
|
+
if (certificate) {
|
|
1220
|
+
certificatePEM = formatPEM(certificate);
|
|
1221
|
+
// Extract public key and signer info
|
|
1222
|
+
try {
|
|
1223
|
+
const x509$1 = new x509.X509Certificate(certificatePEM);
|
|
1224
|
+
const algorithm = x509$1.publicKey.algorithm;
|
|
1225
|
+
publicKey = {
|
|
1226
|
+
algorithm: algorithm.name,
|
|
1227
|
+
...("namedCurve" in algorithm
|
|
1228
|
+
? {
|
|
1229
|
+
namedCurve: algorithm.namedCurve,
|
|
1230
|
+
}
|
|
1231
|
+
: {}),
|
|
1232
|
+
rawData: x509$1.publicKey.rawData,
|
|
1233
|
+
};
|
|
1234
|
+
// Extract signer information from certificate
|
|
1235
|
+
signerInfo = extractSignerInfo(x509$1);
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
console.error("Failed to extract certificate information:", error);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// Get signing time
|
|
1242
|
+
const signingTimeElement = querySelector(xmlDoc, "xades\\:SigningTime, SigningTime");
|
|
1243
|
+
const signingTime = signingTimeElement && signingTimeElement.textContent
|
|
1244
|
+
? new Date(signingTimeElement.textContent.trim())
|
|
1245
|
+
: new Date();
|
|
1246
|
+
// Get references and checksums
|
|
1247
|
+
const references = [];
|
|
1248
|
+
const signedChecksums = {};
|
|
1249
|
+
const referenceElements = querySelectorAll(signedInfo, "ds\\:Reference, Reference");
|
|
1250
|
+
for (const reference of referenceElements) {
|
|
1251
|
+
const uri = reference.getAttribute("URI") || "";
|
|
1252
|
+
const type = reference.getAttribute("Type") || "";
|
|
1253
|
+
// Skip references that don't point to files or are SignedProperties
|
|
1254
|
+
if (!uri || uri.startsWith("#") || type.includes("SignedProperties")) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
// Decode URI if needed (handle URL encoding like Sample%20File.pdf)
|
|
1258
|
+
let decodedUri = uri;
|
|
1259
|
+
try {
|
|
1260
|
+
decodedUri = decodeURIComponent(uri);
|
|
1261
|
+
}
|
|
1262
|
+
catch (e) {
|
|
1263
|
+
console.error(`Failed to decode URI: ${uri}`, e);
|
|
1264
|
+
}
|
|
1265
|
+
// Clean up URI
|
|
1266
|
+
const cleanUri = decodedUri.startsWith("./") ? decodedUri.substring(2) : decodedUri;
|
|
1267
|
+
references.push(cleanUri);
|
|
1268
|
+
// Find DigestValue
|
|
1269
|
+
const digestValueEl = querySelector(reference, "ds\\:DigestValue, DigestValue");
|
|
1270
|
+
if (digestValueEl && digestValueEl.textContent) {
|
|
1271
|
+
signedChecksums[cleanUri] = digestValueEl.textContent.replace(/\s+/g, "");
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
id: signatureId,
|
|
1276
|
+
signingTime,
|
|
1277
|
+
certificate,
|
|
1278
|
+
certificatePEM,
|
|
1279
|
+
publicKey,
|
|
1280
|
+
signerInfo,
|
|
1281
|
+
signedChecksums,
|
|
1282
|
+
references,
|
|
1283
|
+
algorithm: signatureAlgorithm,
|
|
1284
|
+
signatureValue,
|
|
1285
|
+
signedInfoXml,
|
|
1286
|
+
canonicalizationMethod,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Fallback for creating a basic signature from text when DOM parsing fails
|
|
1291
|
+
* @param xmlText The full XML text
|
|
1292
|
+
* @returns A basic signature or null if parsing fails
|
|
1293
|
+
*/
|
|
1294
|
+
function parseBasicSignatureFromText(xmlText) {
|
|
1295
|
+
try {
|
|
1296
|
+
// Extract signature ID
|
|
1297
|
+
const idMatch = xmlText.match(/<ds:Signature[^>]*Id=["']([^"']*)["']/);
|
|
1298
|
+
const id = idMatch && idMatch[1] ? idMatch[1] : "unknown";
|
|
1299
|
+
// Extract signature value
|
|
1300
|
+
const sigValueMatch = xmlText.match(/<ds:SignatureValue[^>]*>([\s\S]*?)<\/ds:SignatureValue>/);
|
|
1301
|
+
const signatureValue = sigValueMatch && sigValueMatch[1] ? sigValueMatch[1].replace(/\s+/g, "") : "";
|
|
1302
|
+
// Extract certificate
|
|
1303
|
+
const certMatch = xmlText.match(/<ds:X509Certificate>([\s\S]*?)<\/ds:X509Certificate>/);
|
|
1304
|
+
const certificate = certMatch && certMatch[1] ? certMatch[1].replace(/\s+/g, "") : "";
|
|
1305
|
+
// Extract algorithm
|
|
1306
|
+
const algoMatch = xmlText.match(/<ds:SignatureMethod[^>]*Algorithm=["']([^"']*)["']/);
|
|
1307
|
+
const algorithm = algoMatch && algoMatch[1] ? algoMatch[1] : "";
|
|
1308
|
+
// Extract signing time
|
|
1309
|
+
const timeMatch = xmlText.match(/<xades:SigningTime>([\s\S]*?)<\/xades:SigningTime>/);
|
|
1310
|
+
const signingTime = timeMatch && timeMatch[1] ? new Date(timeMatch[1].trim()) : new Date();
|
|
1311
|
+
// Extract references
|
|
1312
|
+
const references = [];
|
|
1313
|
+
const signedChecksums = {};
|
|
1314
|
+
// Use regex to find all references
|
|
1315
|
+
const refRegex = /<ds:Reference[^>]*URI=["']([^#][^"']*)["'][^>]*>[\s\S]*?<ds:DigestValue>([\s\S]*?)<\/ds:DigestValue>/g;
|
|
1316
|
+
let refMatch;
|
|
1317
|
+
while ((refMatch = refRegex.exec(xmlText)) !== null) {
|
|
1318
|
+
if (refMatch[1] && !refMatch[1].startsWith("#")) {
|
|
1319
|
+
const uri = decodeURIComponent(refMatch[1]);
|
|
1320
|
+
references.push(uri);
|
|
1321
|
+
if (refMatch[2]) {
|
|
1322
|
+
signedChecksums[uri] = refMatch[2].replace(/\s+/g, "");
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// Format the PEM certificate
|
|
1327
|
+
const certificatePEM = formatPEM(certificate);
|
|
1328
|
+
return {
|
|
1329
|
+
id,
|
|
1330
|
+
signingTime,
|
|
1331
|
+
certificate,
|
|
1332
|
+
certificatePEM,
|
|
1333
|
+
signedChecksums,
|
|
1334
|
+
references,
|
|
1335
|
+
algorithm,
|
|
1336
|
+
signatureValue,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
console.error("Error in fallback text parsing:", error);
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Parse an eDoc container from a buffer
|
|
1347
|
+
* @param edocBuffer The raw eDoc file content
|
|
1348
|
+
* @returns Parsed container with files, document file list, metadata file list, signed file list, and signatures
|
|
1349
|
+
*/
|
|
1350
|
+
function parseEdoc(edocBuffer) {
|
|
1351
|
+
try {
|
|
1352
|
+
// Unzip the eDoc container
|
|
1353
|
+
const unzipped = fflate.unzipSync(edocBuffer);
|
|
1354
|
+
// Convert to Maps for easier access
|
|
1355
|
+
const files = new Map();
|
|
1356
|
+
const documentFileList = [];
|
|
1357
|
+
const metadataFileList = [];
|
|
1358
|
+
Object.entries(unzipped).forEach(([filename, content]) => {
|
|
1359
|
+
files.set(filename, content);
|
|
1360
|
+
// Separate files into document and metadata categories
|
|
1361
|
+
if (filename.startsWith("META-INF/") || filename === "mimetype") {
|
|
1362
|
+
metadataFileList.push(filename);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
documentFileList.push(filename);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
// Find and parse signatures
|
|
1369
|
+
const signatures = [];
|
|
1370
|
+
const signatureFiles = findSignatureFiles(files);
|
|
1371
|
+
const signedFileSet = new Set();
|
|
1372
|
+
for (const sigFile of signatureFiles) {
|
|
1373
|
+
const sigContent = files.get(sigFile);
|
|
1374
|
+
if (sigContent) {
|
|
1375
|
+
try {
|
|
1376
|
+
// Parse signatures from the file - could contain multiple
|
|
1377
|
+
const fileSignature = parseSignatureFile(sigContent, sigFile);
|
|
1378
|
+
if (fileSignature) {
|
|
1379
|
+
signatures.push(fileSignature);
|
|
1380
|
+
// Add referenced files to the set of signed files
|
|
1381
|
+
if (fileSignature.references && fileSignature.references.length > 0) {
|
|
1382
|
+
fileSignature.references.forEach((ref) => {
|
|
1383
|
+
// Only add files that actually exist in the container
|
|
1384
|
+
if (files.has(ref)) {
|
|
1385
|
+
signedFileSet.add(ref);
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
console.error(`Error parsing signature ${sigFile}:`, error);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const signedFileList = Array.from(signedFileSet);
|
|
1397
|
+
return {
|
|
1398
|
+
files,
|
|
1399
|
+
documentFileList,
|
|
1400
|
+
metadataFileList,
|
|
1401
|
+
signedFileList,
|
|
1402
|
+
signatures,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
catch (error) {
|
|
1406
|
+
throw new Error(`Failed to parse eDoc container: ${error instanceof Error ? error.message : String(error)}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Detects if code is running in a browser environment
|
|
1412
|
+
* @returns true if in browser, false otherwise
|
|
1413
|
+
*/
|
|
1414
|
+
function isBrowser() {
|
|
1415
|
+
return (typeof window !== "undefined" &&
|
|
1416
|
+
typeof window.crypto !== "undefined" &&
|
|
1417
|
+
typeof window.crypto.subtle !== "undefined");
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Compute a digest (hash) of file content with browser/node compatibility
|
|
1421
|
+
* @param fileContent The file content as Uint8Array
|
|
1422
|
+
* @param algorithm The digest algorithm to use (e.g., 'SHA-256')
|
|
1423
|
+
* @returns Promise with Base64-encoded digest
|
|
1424
|
+
*/
|
|
1425
|
+
async function computeDigest(fileContent, algorithm) {
|
|
1426
|
+
// Normalize algorithm name
|
|
1427
|
+
const normalizedAlgo = algorithm.replace(/-/g, "").toLowerCase();
|
|
1428
|
+
let hashAlgo;
|
|
1429
|
+
// Map algorithm URIs to crypto algorithm names
|
|
1430
|
+
if (normalizedAlgo.includes("sha256")) {
|
|
1431
|
+
hashAlgo = "sha256";
|
|
1432
|
+
}
|
|
1433
|
+
else if (normalizedAlgo.includes("sha1")) {
|
|
1434
|
+
hashAlgo = "sha1";
|
|
1435
|
+
}
|
|
1436
|
+
else if (normalizedAlgo.includes("sha384")) {
|
|
1437
|
+
hashAlgo = "sha384";
|
|
1438
|
+
}
|
|
1439
|
+
else if (normalizedAlgo.includes("sha512")) {
|
|
1440
|
+
hashAlgo = "sha512";
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
throw new Error(`Unsupported digest algorithm: ${algorithm}`);
|
|
1444
|
+
}
|
|
1445
|
+
if (isBrowser()) {
|
|
1446
|
+
return browserDigest(fileContent, hashAlgo);
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
return nodeDigest(fileContent, hashAlgo);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Compute digest using Web Crypto API in browser
|
|
1454
|
+
* @param fileContent Uint8Array of file content
|
|
1455
|
+
* @param hashAlgo Normalized hash algorithm name
|
|
1456
|
+
* @returns Promise with Base64-encoded digest
|
|
1457
|
+
*/
|
|
1458
|
+
async function browserDigest(fileContent, hashAlgo) {
|
|
1459
|
+
// Map to Web Crypto API algorithm names
|
|
1460
|
+
const browserAlgoMap = {
|
|
1461
|
+
sha1: "SHA-1",
|
|
1462
|
+
sha256: "SHA-256",
|
|
1463
|
+
sha384: "SHA-384",
|
|
1464
|
+
sha512: "SHA-512",
|
|
1465
|
+
};
|
|
1466
|
+
const browserAlgo = browserAlgoMap[hashAlgo];
|
|
1467
|
+
if (!browserAlgo) {
|
|
1468
|
+
throw new Error(`Unsupported browser digest algorithm: ${hashAlgo}`);
|
|
1469
|
+
}
|
|
1470
|
+
const hashBuffer = await window.crypto.subtle.digest(browserAlgo, fileContent);
|
|
1471
|
+
// Convert ArrayBuffer to Base64
|
|
1472
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1473
|
+
const hashBase64 = btoa(String.fromCharCode.apply(null, hashArray));
|
|
1474
|
+
return hashBase64;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Compute digest using Node.js crypto module
|
|
1478
|
+
* @param fileContent Uint8Array of file content
|
|
1479
|
+
* @param hashAlgo Normalized hash algorithm name
|
|
1480
|
+
* @returns Promise with Base64-encoded digest
|
|
1481
|
+
*/
|
|
1482
|
+
function nodeDigest(fileContent, hashAlgo) {
|
|
1483
|
+
return new Promise((resolve, reject) => {
|
|
1484
|
+
try {
|
|
1485
|
+
// Dynamically import Node.js crypto
|
|
1486
|
+
const crypto = require("crypto");
|
|
1487
|
+
const hash = crypto.createHash(hashAlgo);
|
|
1488
|
+
hash.update(Buffer.from(fileContent));
|
|
1489
|
+
resolve(hash.digest("base64"));
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
reject(new Error(`Node digest computation failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Verify checksums of files against signature
|
|
1498
|
+
* @param signature The signature information
|
|
1499
|
+
* @param files Map of filenames to file contents
|
|
1500
|
+
* @returns Promise with verification results for each file
|
|
1501
|
+
*/
|
|
1502
|
+
async function verifyChecksums(signature, files) {
|
|
1503
|
+
const results = {};
|
|
1504
|
+
let allValid = true;
|
|
1505
|
+
// Determine hash algorithm from signature algorithm or use default
|
|
1506
|
+
let digestAlgorithm = "SHA-256";
|
|
1507
|
+
if (signature.algorithm) {
|
|
1508
|
+
if (signature.algorithm.includes("sha1")) {
|
|
1509
|
+
digestAlgorithm = "SHA-1";
|
|
1510
|
+
}
|
|
1511
|
+
else if (signature.algorithm.includes("sha384")) {
|
|
1512
|
+
digestAlgorithm = "SHA-384";
|
|
1513
|
+
}
|
|
1514
|
+
else if (signature.algorithm.includes("sha512")) {
|
|
1515
|
+
digestAlgorithm = "SHA-512";
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
const checksumPromises = Object.entries(signature.signedChecksums).map(async ([filename, expectedChecksum]) => {
|
|
1519
|
+
// Check if file exists in the container
|
|
1520
|
+
const fileContent = files.get(filename);
|
|
1521
|
+
if (!fileContent) {
|
|
1522
|
+
// File not found - this could be due to URI encoding or path format
|
|
1523
|
+
// Try to find by file basename
|
|
1524
|
+
const basename = filename.includes("/") ? filename.split("/").pop() : filename;
|
|
1525
|
+
let foundMatch = false;
|
|
1526
|
+
if (basename) {
|
|
1527
|
+
for (const [containerFilename, content] of files.entries()) {
|
|
1528
|
+
if (containerFilename.endsWith(basename)) {
|
|
1529
|
+
// Found a match by basename
|
|
1530
|
+
const actualChecksum = await computeDigest(content, digestAlgorithm);
|
|
1531
|
+
const matches = expectedChecksum === actualChecksum;
|
|
1532
|
+
results[filename] = {
|
|
1533
|
+
expected: expectedChecksum,
|
|
1534
|
+
actual: actualChecksum,
|
|
1535
|
+
matches,
|
|
1536
|
+
fileFound: true,
|
|
1537
|
+
};
|
|
1538
|
+
if (!matches)
|
|
1539
|
+
allValid = false;
|
|
1540
|
+
foundMatch = true;
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (!foundMatch) {
|
|
1546
|
+
// Really not found
|
|
1547
|
+
results[filename] = {
|
|
1548
|
+
expected: expectedChecksum,
|
|
1549
|
+
actual: "",
|
|
1550
|
+
matches: false,
|
|
1551
|
+
fileFound: false,
|
|
1552
|
+
};
|
|
1553
|
+
allValid = false;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
// File found directly - verify checksum
|
|
1558
|
+
const actualChecksum = await computeDigest(fileContent, digestAlgorithm);
|
|
1559
|
+
const matches = expectedChecksum === actualChecksum;
|
|
1560
|
+
results[filename] = {
|
|
1561
|
+
expected: expectedChecksum,
|
|
1562
|
+
actual: actualChecksum,
|
|
1563
|
+
matches,
|
|
1564
|
+
fileFound: true,
|
|
1565
|
+
};
|
|
1566
|
+
if (!matches)
|
|
1567
|
+
allValid = false;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
// Wait for all checksums to be verified
|
|
1571
|
+
await Promise.all(checksumPromises);
|
|
1572
|
+
return {
|
|
1573
|
+
isValid: allValid,
|
|
1574
|
+
details: results,
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Verify certificate validity
|
|
1579
|
+
* @param certificatePEM PEM-formatted certificate
|
|
1580
|
+
* @param verifyTime Time to check validity against
|
|
1581
|
+
* @returns Certificate verification result
|
|
1582
|
+
*/
|
|
1583
|
+
async function verifyCertificate(certificatePEM, verifyTime = new Date()) {
|
|
1584
|
+
try {
|
|
1585
|
+
const cert = new x509.X509Certificate(certificatePEM);
|
|
1586
|
+
const validityResult = checkCertificateValidity(cert, verifyTime);
|
|
1587
|
+
// Parse certificate info
|
|
1588
|
+
const certInfo = await parseCertificate(certificatePEM);
|
|
1589
|
+
return {
|
|
1590
|
+
isValid: validityResult.isValid,
|
|
1591
|
+
reason: validityResult.reason,
|
|
1592
|
+
info: certInfo,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
catch (error) {
|
|
1596
|
+
return {
|
|
1597
|
+
isValid: false,
|
|
1598
|
+
reason: `Certificate parsing error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Safely get the crypto.subtle implementation in either browser or Node.js
|
|
1604
|
+
* @returns The SubtleCrypto interface
|
|
1605
|
+
*/
|
|
1606
|
+
function getCryptoSubtle() {
|
|
1607
|
+
if (isBrowser()) {
|
|
1608
|
+
return window.crypto.subtle;
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
// In Node.js environment
|
|
1612
|
+
return crypto.subtle;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Base64 decode a string in a cross-platform way
|
|
1617
|
+
* @param base64 Base64 encoded string
|
|
1618
|
+
* @returns Uint8Array of decoded bytes
|
|
1619
|
+
*/
|
|
1620
|
+
function base64ToUint8Array(base64) {
|
|
1621
|
+
if (typeof atob === "function") {
|
|
1622
|
+
// Browser approach
|
|
1623
|
+
const binaryString = atob(base64);
|
|
1624
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1625
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1626
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1627
|
+
}
|
|
1628
|
+
return bytes;
|
|
1629
|
+
}
|
|
1630
|
+
else {
|
|
1631
|
+
// Node.js approach
|
|
1632
|
+
const buffer = Buffer.from(base64, "base64");
|
|
1633
|
+
return new Uint8Array(buffer);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Verify the XML signature specifically using SignedInfo and SignatureValue
|
|
1638
|
+
* @param signatureXml The XML string of the SignedInfo element
|
|
1639
|
+
* @param signatureValue The base64-encoded signature value
|
|
1640
|
+
* @param publicKeyData The public key raw data
|
|
1641
|
+
* @param algorithm Key algorithm details
|
|
1642
|
+
* @param canonicalizationMethod The canonicalization method used
|
|
1643
|
+
* @returns Signature verification result
|
|
1644
|
+
*/
|
|
1645
|
+
async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, algorithm, canonicalizationMethod) {
|
|
1646
|
+
try {
|
|
1647
|
+
// Parse the SignedInfo XML
|
|
1648
|
+
const parser = createXMLParser();
|
|
1649
|
+
const xmlDoc = parser.parseFromString(signatureXml, "application/xml");
|
|
1650
|
+
const signedInfo = querySelector(xmlDoc, "ds:SignedInfo");
|
|
1651
|
+
if (!signedInfo) {
|
|
1652
|
+
return {
|
|
1653
|
+
isValid: false,
|
|
1654
|
+
reason: "SignedInfo element not found in provided XML",
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
// Determine canonicalization method
|
|
1658
|
+
const c14nMethod = canonicalizationMethod || CANONICALIZATION_METHODS.default;
|
|
1659
|
+
// Canonicalize the SignedInfo element
|
|
1660
|
+
const canonicalizedSignedInfo = XMLCanonicalizer.canonicalize(signedInfo, c14nMethod);
|
|
1661
|
+
// Clean up signature value (remove whitespace)
|
|
1662
|
+
const cleanSignatureValue = signatureValue.replace(/\s+/g, "");
|
|
1663
|
+
// Convert base64 signature to binary
|
|
1664
|
+
let signatureBytes;
|
|
1665
|
+
try {
|
|
1666
|
+
signatureBytes = base64ToUint8Array(cleanSignatureValue);
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
return {
|
|
1670
|
+
isValid: false,
|
|
1671
|
+
reason: `Failed to decode signature value: ${error instanceof Error ? error.message : String(error)}`,
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
// Import the public key
|
|
1675
|
+
let publicKey;
|
|
1676
|
+
try {
|
|
1677
|
+
const subtle = getCryptoSubtle();
|
|
1678
|
+
publicKey = await subtle.importKey("spki", publicKeyData, algorithm, false, ["verify"]);
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
return {
|
|
1682
|
+
isValid: false,
|
|
1683
|
+
reason: `Failed to import public key: ${error instanceof Error ? error.message : String(error)}`,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
// Verify the signature
|
|
1687
|
+
const signedData = new TextEncoder().encode(canonicalizedSignedInfo);
|
|
1688
|
+
try {
|
|
1689
|
+
const subtle = getCryptoSubtle();
|
|
1690
|
+
const result = await subtle.verify(algorithm, publicKey, signatureBytes, signedData);
|
|
1691
|
+
return {
|
|
1692
|
+
isValid: result,
|
|
1693
|
+
reason: result ? undefined : "Signature verification failed",
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
return {
|
|
1698
|
+
isValid: false,
|
|
1699
|
+
reason: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
catch (error) {
|
|
1704
|
+
return {
|
|
1705
|
+
isValid: false,
|
|
1706
|
+
reason: `SignedInfo verification error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Verify a complete signature (certificate, checksums, and signature)
|
|
1712
|
+
* @param signatureInfo Signature information
|
|
1713
|
+
* @param files File contents
|
|
1714
|
+
* @param options Verification options
|
|
1715
|
+
* @returns Complete verification result
|
|
1716
|
+
*/
|
|
1717
|
+
async function verifySignature(signatureInfo, files, options = {}) {
|
|
1718
|
+
const errors = [];
|
|
1719
|
+
// Verify certificate
|
|
1720
|
+
const certResult = await verifyCertificate(signatureInfo.certificatePEM, options.verifyTime || signatureInfo.signingTime);
|
|
1721
|
+
// Verify checksums
|
|
1722
|
+
const checksumResult = options.verifyChecksums !== false
|
|
1723
|
+
? await verifyChecksums(signatureInfo, files)
|
|
1724
|
+
: { isValid: true, details: {} };
|
|
1725
|
+
// Verify XML signature if we have the necessary components
|
|
1726
|
+
let signatureResult = { isValid: true };
|
|
1727
|
+
if (options.verifySignatures !== false &&
|
|
1728
|
+
signatureInfo.rawXml &&
|
|
1729
|
+
signatureInfo.signatureValue &&
|
|
1730
|
+
signatureInfo.publicKey) {
|
|
1731
|
+
// Determine algorithm
|
|
1732
|
+
const algorithm = signatureInfo.algorithm || "";
|
|
1733
|
+
const keyAlgorithm = {
|
|
1734
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1735
|
+
hash: "SHA-256",
|
|
1736
|
+
};
|
|
1737
|
+
if (algorithm.includes("ecdsa-sha256")) {
|
|
1738
|
+
keyAlgorithm.name = "ECDSA";
|
|
1739
|
+
keyAlgorithm.hash = "SHA-256";
|
|
1740
|
+
if (signatureInfo.publicKey.namedCurve) {
|
|
1741
|
+
keyAlgorithm.namedCurve = signatureInfo.publicKey.namedCurve;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
else if (algorithm.includes("rsa-sha1")) {
|
|
1745
|
+
keyAlgorithm.hash = "SHA-1";
|
|
1746
|
+
}
|
|
1747
|
+
signatureResult = await verifySignedInfo(signatureInfo.rawXml, signatureInfo.signatureValue, signatureInfo.publicKey.rawData, keyAlgorithm, signatureInfo.canonicalizationMethod);
|
|
1748
|
+
if (!signatureResult.isValid) {
|
|
1749
|
+
errors.push(signatureResult.reason || "XML signature verification failed");
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
else if (options.verifySignatures !== false) {
|
|
1753
|
+
// Missing information for signature verification
|
|
1754
|
+
const missingComponents = [];
|
|
1755
|
+
if (!signatureInfo.rawXml)
|
|
1756
|
+
missingComponents.push("Signature XML");
|
|
1757
|
+
if (!signatureInfo.signatureValue)
|
|
1758
|
+
missingComponents.push("SignatureValue");
|
|
1759
|
+
if (!signatureInfo.publicKey)
|
|
1760
|
+
missingComponents.push("Public Key");
|
|
1761
|
+
errors.push(`Cannot verify XML signature: missing ${missingComponents.join(", ")}`);
|
|
1762
|
+
signatureResult = {
|
|
1763
|
+
isValid: false,
|
|
1764
|
+
reason: `Missing required components: ${missingComponents.join(", ")}`,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
// Determine overall validity
|
|
1768
|
+
const isValid = certResult.isValid && checksumResult.isValid && signatureResult.isValid;
|
|
1769
|
+
// Return the complete result
|
|
1770
|
+
return {
|
|
1771
|
+
isValid,
|
|
1772
|
+
certificate: certResult,
|
|
1773
|
+
checksums: checksumResult,
|
|
1774
|
+
signature: options.verifySignatures !== false ? signatureResult : undefined,
|
|
1775
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
exports.CANONICALIZATION_METHODS = CANONICALIZATION_METHODS;
|
|
1780
|
+
exports.XMLCanonicalizer = XMLCanonicalizer;
|
|
1781
|
+
exports.formatValidityPeriod = formatValidityPeriod;
|
|
1782
|
+
exports.getSignerDisplayName = getSignerDisplayName;
|
|
1783
|
+
exports.parseCertificate = parseCertificate;
|
|
1784
|
+
exports.parseEdoc = parseEdoc;
|
|
1785
|
+
exports.verifyChecksums = verifyChecksums;
|
|
1786
|
+
exports.verifySignature = verifySignature;
|
|
1787
|
+
//# sourceMappingURL=index.cjs.js.map
|