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