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.
@@ -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, "&amp;")
409
+ .replace(/</g, "&lt;")
410
+ .replace(/>/g, "&gt;")
411
+ .replace(/"/g, "&quot;")
412
+ .replace(/'/g, "&apos;");
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, "&#xD;");
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, "&#xD;");
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