edockit 0.1.1-beta.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,3 +1,7 @@
1
+ /*!
2
+ * MIT License
3
+ * Copyright (c) 2025 Edgars Jēkabsons, ZenomyTech SIA
4
+ */
1
5
  import { unzipSync } from 'fflate';
2
6
  import { X509Certificate } from '@peculiar/x509';
3
7
 
@@ -1242,6 +1246,7 @@ function parseSignatureElement(signatureElement, xmlDoc) {
1242
1246
  // Get references and checksums
1243
1247
  const references = [];
1244
1248
  const signedChecksums = {};
1249
+ const digestAlgorithms = {};
1245
1250
  const referenceElements = querySelectorAll(signedInfo, "ds\\:Reference, Reference");
1246
1251
  for (const reference of referenceElements) {
1247
1252
  const uri = reference.getAttribute("URI") || "";
@@ -1261,6 +1266,14 @@ function parseSignatureElement(signatureElement, xmlDoc) {
1261
1266
  // Clean up URI
1262
1267
  const cleanUri = decodedUri.startsWith("./") ? decodedUri.substring(2) : decodedUri;
1263
1268
  references.push(cleanUri);
1269
+ // Find DigestMethod algorithm
1270
+ const digestMethodEl = querySelector(reference, "ds\\:DigestMethod, DigestMethod");
1271
+ if (digestMethodEl) {
1272
+ const algo = digestMethodEl.getAttribute("Algorithm");
1273
+ if (algo) {
1274
+ digestAlgorithms[cleanUri] = algo;
1275
+ }
1276
+ }
1264
1277
  // Find DigestValue
1265
1278
  const digestValueEl = querySelector(reference, "ds\\:DigestValue, DigestValue");
1266
1279
  if (digestValueEl && digestValueEl.textContent) {
@@ -1275,6 +1288,7 @@ function parseSignatureElement(signatureElement, xmlDoc) {
1275
1288
  publicKey,
1276
1289
  signerInfo,
1277
1290
  signedChecksums,
1291
+ digestAlgorithms,
1278
1292
  references,
1279
1293
  algorithm: signatureAlgorithm,
1280
1294
  signatureValue,
@@ -1403,6 +1417,206 @@ function parseEdoc(edocBuffer) {
1403
1417
  }
1404
1418
  }
1405
1419
 
1420
+ /**
1421
+ * Fixes ASN.1 DER encoding of RSA keys to be compatible with browser Web Crypto
1422
+ *
1423
+ * This specifically targets the issue with modulus padding in SPKI format RSA keys.
1424
+ * In DER encoding, when the high bit of an INTEGER is set, a 0x00 byte must be
1425
+ * prepended to distinguish it from a negative number. Some libraries omit this padding,
1426
+ * which works in Node.js but causes browsers to reject the key.
1427
+ */
1428
+ /**
1429
+ * Fixes ASN.1 DER encoding of RSA modulus to ensure proper padding
1430
+ *
1431
+ * @param publicKeyData The original public key data as ArrayBuffer
1432
+ * @returns Fixed key with proper modulus padding
1433
+ */
1434
+ function fixRSAModulusPadding(publicKeyData) {
1435
+ const log = () => { };
1436
+ const keyBytes = new Uint8Array(publicKeyData);
1437
+ // Check if we have a valid SPKI format RSA key
1438
+ // It should start with SEQUENCE tag (0x30)
1439
+ if (keyBytes[0] !== 0x30) {
1440
+ return publicKeyData;
1441
+ }
1442
+ // Look for RSA OID to confirm it's an RSA key
1443
+ const RSA_OID = [0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
1444
+ let oidPosition = -1;
1445
+ for (let i = 0; i <= keyBytes.length - RSA_OID.length; i++) {
1446
+ let match = true;
1447
+ for (let j = 0; j < RSA_OID.length; j++) {
1448
+ if (keyBytes[i + j] !== RSA_OID[j]) {
1449
+ match = false;
1450
+ break;
1451
+ }
1452
+ }
1453
+ if (match) {
1454
+ oidPosition = i;
1455
+ break;
1456
+ }
1457
+ }
1458
+ if (oidPosition === -1) {
1459
+ return publicKeyData;
1460
+ }
1461
+ // Find the BitString that contains the key
1462
+ let bitStringPosition = -1;
1463
+ for (let i = oidPosition + RSA_OID.length; i < keyBytes.length; i++) {
1464
+ if (keyBytes[i] === 0x03) {
1465
+ // BIT STRING tag
1466
+ bitStringPosition = i;
1467
+ break;
1468
+ }
1469
+ }
1470
+ if (bitStringPosition === -1) {
1471
+ return publicKeyData;
1472
+ }
1473
+ // Skip BIT STRING tag and length bytes to find unused bits byte
1474
+ let bitStringLengthBytes = 0;
1475
+ if ((keyBytes[bitStringPosition + 1] & 0x80) === 0) {
1476
+ // Short form length
1477
+ bitStringLengthBytes = 1;
1478
+ }
1479
+ else {
1480
+ // Long form length
1481
+ bitStringLengthBytes = 1 + (keyBytes[bitStringPosition + 1] & 0x7f);
1482
+ }
1483
+ // The unused bits byte follows the length bytes
1484
+ const unusedBitsPosition = bitStringPosition + 1 + bitStringLengthBytes;
1485
+ if (unusedBitsPosition >= keyBytes.length) {
1486
+ return publicKeyData;
1487
+ }
1488
+ // The inner SEQUENCE (RSA key) should follow the unused bits byte
1489
+ const innerSequencePosition = unusedBitsPosition + 1;
1490
+ if (innerSequencePosition >= keyBytes.length || keyBytes[innerSequencePosition] !== 0x30) {
1491
+ return publicKeyData;
1492
+ }
1493
+ // Skip the inner SEQUENCE tag and length bytes to find the modulus
1494
+ let innerSequenceLengthBytes = 0;
1495
+ if ((keyBytes[innerSequencePosition + 1] & 0x80) === 0) {
1496
+ // Short form length
1497
+ innerSequenceLengthBytes = 1;
1498
+ }
1499
+ else {
1500
+ // Long form length
1501
+ innerSequenceLengthBytes = 1 + (keyBytes[innerSequencePosition + 1] & 0x7f);
1502
+ }
1503
+ // The modulus should be an INTEGER (tag 0x02) after the inner SEQUENCE
1504
+ const modulusPosition = innerSequencePosition + 1 + innerSequenceLengthBytes;
1505
+ if (modulusPosition >= keyBytes.length || keyBytes[modulusPosition] !== 0x02) {
1506
+ return publicKeyData;
1507
+ }
1508
+ // Skip the INTEGER tag and parse its length to find the modulus value
1509
+ let modulusLengthBytes = 0;
1510
+ let modulusLength = 0;
1511
+ if ((keyBytes[modulusPosition + 1] & 0x80) === 0) {
1512
+ // Short form length
1513
+ modulusLength = keyBytes[modulusPosition + 1];
1514
+ modulusLengthBytes = 1;
1515
+ }
1516
+ else {
1517
+ // Long form length
1518
+ const numLengthBytes = keyBytes[modulusPosition + 1] & 0x7f;
1519
+ modulusLengthBytes = 1 + numLengthBytes;
1520
+ // Calculate multi-byte length
1521
+ modulusLength = 0;
1522
+ for (let i = 0; i < numLengthBytes; i++) {
1523
+ modulusLength = (modulusLength << 8) | keyBytes[modulusPosition + 2 + i];
1524
+ }
1525
+ }
1526
+ // The first byte of the modulus value
1527
+ const modulusValuePosition = modulusPosition + 1 + modulusLengthBytes;
1528
+ if (modulusValuePosition >= keyBytes.length) {
1529
+ return publicKeyData;
1530
+ }
1531
+ // Check if the high bit is set and padding is needed
1532
+ if ((keyBytes[modulusValuePosition] & 0x80) !== 0) {
1533
+ // Create a new key buffer with room for the padding byte
1534
+ const fixedKey = new Uint8Array(keyBytes.length + 1);
1535
+ // Copy bytes up to the modulus value
1536
+ fixedKey.set(keyBytes.slice(0, modulusValuePosition));
1537
+ // Add the padding byte
1538
+ fixedKey[modulusValuePosition] = 0x00;
1539
+ // Copy the rest of the original key after the padding
1540
+ fixedKey.set(keyBytes.slice(modulusValuePosition), modulusValuePosition + 1);
1541
+ // Now fix all the length fields that need to be incremented
1542
+ // 1. Fix modulus length field
1543
+ if ((keyBytes[modulusPosition + 1] & 0x80) === 0) {
1544
+ // Short form
1545
+ fixedKey[modulusPosition + 1] = keyBytes[modulusPosition + 1] + 1;
1546
+ }
1547
+ else {
1548
+ // Long form
1549
+ const numLengthBytes = keyBytes[modulusPosition + 1] & 0x7f;
1550
+ let lengthValue = 0;
1551
+ for (let i = 0; i < numLengthBytes; i++) {
1552
+ lengthValue = (lengthValue << 8) | keyBytes[modulusPosition + 2 + i];
1553
+ }
1554
+ lengthValue += 1;
1555
+ for (let i = numLengthBytes - 1; i >= 0; i--) {
1556
+ fixedKey[modulusPosition + 2 + i] = lengthValue & 0xff;
1557
+ lengthValue >>= 8;
1558
+ }
1559
+ }
1560
+ // 2. Fix inner SEQUENCE length field
1561
+ if ((keyBytes[innerSequencePosition + 1] & 0x80) === 0) {
1562
+ // Short form
1563
+ fixedKey[innerSequencePosition + 1] = keyBytes[innerSequencePosition + 1] + 1;
1564
+ }
1565
+ else {
1566
+ // Long form
1567
+ const numLengthBytes = keyBytes[innerSequencePosition + 1] & 0x7f;
1568
+ let lengthValue = 0;
1569
+ for (let i = 0; i < numLengthBytes; i++) {
1570
+ lengthValue = (lengthValue << 8) | keyBytes[innerSequencePosition + 2 + i];
1571
+ }
1572
+ lengthValue += 1;
1573
+ for (let i = numLengthBytes - 1; i >= 0; i--) {
1574
+ fixedKey[innerSequencePosition + 2 + i] = lengthValue & 0xff;
1575
+ lengthValue >>= 8;
1576
+ }
1577
+ }
1578
+ // 3. Fix BIT STRING length field
1579
+ if ((keyBytes[bitStringPosition + 1] & 0x80) === 0) {
1580
+ // Short form
1581
+ fixedKey[bitStringPosition + 1] = keyBytes[bitStringPosition + 1] + 1;
1582
+ }
1583
+ else {
1584
+ // Long form
1585
+ const numLengthBytes = keyBytes[bitStringPosition + 1] & 0x7f;
1586
+ let lengthValue = 0;
1587
+ for (let i = 0; i < numLengthBytes; i++) {
1588
+ lengthValue = (lengthValue << 8) | keyBytes[bitStringPosition + 2 + i];
1589
+ }
1590
+ lengthValue += 1;
1591
+ for (let i = numLengthBytes - 1; i >= 0; i--) {
1592
+ fixedKey[bitStringPosition + 2 + i] = lengthValue & 0xff;
1593
+ lengthValue >>= 8;
1594
+ }
1595
+ }
1596
+ // 4. Fix outer SEQUENCE length field
1597
+ if ((keyBytes[1] & 0x80) === 0) {
1598
+ // Short form
1599
+ fixedKey[1] = keyBytes[1] + 1;
1600
+ }
1601
+ else {
1602
+ // Long form
1603
+ const numLengthBytes = keyBytes[1] & 0x7f;
1604
+ let lengthValue = 0;
1605
+ for (let i = 0; i < numLengthBytes; i++) {
1606
+ lengthValue = (lengthValue << 8) | keyBytes[1 + 1 + i];
1607
+ }
1608
+ lengthValue += 1;
1609
+ for (let i = numLengthBytes - 1; i >= 0; i--) {
1610
+ fixedKey[1 + 1 + i] = lengthValue & 0xff;
1611
+ lengthValue >>= 8;
1612
+ }
1613
+ }
1614
+ log("Fixed key length: " + fixedKey.length);
1615
+ return fixedKey.buffer;
1616
+ }
1617
+ return publicKeyData;
1618
+ }
1619
+
1406
1620
  /**
1407
1621
  * Detects if code is running in a browser environment
1408
1622
  * @returns true if in browser, false otherwise
@@ -1489,6 +1703,23 @@ function nodeDigest(fileContent, hashAlgo) {
1489
1703
  }
1490
1704
  });
1491
1705
  }
1706
+ /**
1707
+ * Parse digest algorithm URI to normalized algorithm name
1708
+ * @param algorithmUri The algorithm URI (e.g., http://www.w3.org/2001/04/xmlenc#sha256)
1709
+ * @returns Normalized algorithm name (e.g., SHA-256)
1710
+ */
1711
+ function parseDigestAlgorithmUri(algorithmUri) {
1712
+ const uri = algorithmUri.toLowerCase();
1713
+ if (uri.includes("sha512"))
1714
+ return "SHA-512";
1715
+ if (uri.includes("sha384"))
1716
+ return "SHA-384";
1717
+ if (uri.includes("sha256"))
1718
+ return "SHA-256";
1719
+ if (uri.includes("sha1"))
1720
+ return "SHA-1";
1721
+ return "SHA-256"; // Default fallback
1722
+ }
1492
1723
  /**
1493
1724
  * Verify checksums of files against signature
1494
1725
  * @param signature The signature information
@@ -1498,20 +1729,24 @@ function nodeDigest(fileContent, hashAlgo) {
1498
1729
  async function verifyChecksums(signature, files) {
1499
1730
  const results = {};
1500
1731
  let allValid = true;
1501
- // Determine hash algorithm from signature algorithm or use default
1502
- let digestAlgorithm = "SHA-256";
1732
+ // Default digest algorithm from signature algorithm (fallback if per-file not available)
1733
+ let defaultDigestAlgorithm = "SHA-256";
1503
1734
  if (signature.algorithm) {
1504
1735
  if (signature.algorithm.includes("sha1")) {
1505
- digestAlgorithm = "SHA-1";
1736
+ defaultDigestAlgorithm = "SHA-1";
1506
1737
  }
1507
1738
  else if (signature.algorithm.includes("sha384")) {
1508
- digestAlgorithm = "SHA-384";
1739
+ defaultDigestAlgorithm = "SHA-384";
1509
1740
  }
1510
1741
  else if (signature.algorithm.includes("sha512")) {
1511
- digestAlgorithm = "SHA-512";
1742
+ defaultDigestAlgorithm = "SHA-512";
1512
1743
  }
1513
1744
  }
1514
1745
  const checksumPromises = Object.entries(signature.signedChecksums).map(async ([filename, expectedChecksum]) => {
1746
+ // Get the per-file digest algorithm, or fall back to default
1747
+ const digestAlgorithm = signature.digestAlgorithms?.[filename]
1748
+ ? parseDigestAlgorithmUri(signature.digestAlgorithms[filename])
1749
+ : defaultDigestAlgorithm;
1515
1750
  // Check if file exists in the container
1516
1751
  const fileContent = files.get(filename);
1517
1752
  if (!fileContent) {
@@ -1671,12 +1906,50 @@ async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, alg
1671
1906
  let publicKey;
1672
1907
  try {
1673
1908
  const subtle = getCryptoSubtle();
1909
+ if (isBrowser() && algorithm.name === "RSASSA-PKCS1-v1_5") {
1910
+ // console.log("Browser environment detected, applying RSA key fix");
1911
+ publicKeyData = fixRSAModulusPadding(publicKeyData);
1912
+ }
1674
1913
  publicKey = await subtle.importKey("spki", publicKeyData, algorithm, false, ["verify"]);
1675
1914
  }
1676
- catch (error) {
1915
+ catch (unknownError) {
1916
+ // First cast to Error type if applicable
1917
+ const error = unknownError instanceof Error ? unknownError : new Error(String(unknownError));
1918
+ // Determine detailed error reason
1919
+ let detailedReason = "Unknown reason";
1920
+ let errorCategory = "KEY_IMPORT_ERROR";
1921
+ // Categorize the error
1922
+ if (error.name === "DataError") {
1923
+ detailedReason = "Key data format is invalid or incompatible";
1924
+ errorCategory = "INVALID_KEY_FORMAT";
1925
+ }
1926
+ else if (error.name === "NotSupportedError") {
1927
+ detailedReason = "Algorithm or parameters not supported";
1928
+ errorCategory = "UNSUPPORTED_ALGORITHM";
1929
+ }
1930
+ else if (error.message.includes("namedCurve")) {
1931
+ detailedReason = "Missing or invalid namedCurve parameter";
1932
+ errorCategory = "INVALID_CURVE";
1933
+ }
1934
+ else if (error.message.includes("hash")) {
1935
+ detailedReason = "Incompatible or unsupported hash algorithm";
1936
+ errorCategory = "INVALID_HASH";
1937
+ }
1938
+ // Add ECDSA-specific diagnostics
1939
+ if (algorithm.name === "ECDSA") {
1940
+ const keyLength = publicKeyData.byteLength;
1941
+ detailedReason += ` (Key length: ${keyLength})`;
1942
+ }
1677
1943
  return {
1678
1944
  isValid: false,
1679
- reason: `Failed to import public key: ${error instanceof Error ? error.message : String(error)}`,
1945
+ reason: `Failed to import public key: ${detailedReason}`,
1946
+ errorDetails: {
1947
+ category: errorCategory,
1948
+ originalMessage: error.message,
1949
+ algorithm: { ...algorithm },
1950
+ environment: isBrowser() ? "browser" : "node",
1951
+ keyLength: publicKeyData.byteLength,
1952
+ },
1680
1953
  };
1681
1954
  }
1682
1955
  // Verify the signature
@@ -1714,10 +1987,23 @@ async function verifySignature(signatureInfo, files, options = {}) {
1714
1987
  const errors = [];
1715
1988
  // Verify certificate
1716
1989
  const certResult = await verifyCertificate(signatureInfo.certificatePEM, options.verifyTime || signatureInfo.signingTime);
1990
+ // If certificate validation failed, add detailed error
1991
+ if (!certResult.isValid) {
1992
+ const certErrorMsg = `Certificate validation error: ${certResult.reason || "Unknown reason"}`;
1993
+ errors.push(certErrorMsg);
1994
+ }
1717
1995
  // Verify checksums
1718
1996
  const checksumResult = options.verifyChecksums !== false
1719
1997
  ? await verifyChecksums(signatureInfo, files)
1720
1998
  : { isValid: true, details: {} };
1999
+ // If checksum validation failed, add detailed error
2000
+ if (!checksumResult.isValid) {
2001
+ const failedChecksums = Object.entries(checksumResult.details)
2002
+ .filter(([_, details]) => !details.matches)
2003
+ .map(([filename]) => filename)
2004
+ .join(", ");
2005
+ errors.push(`Checksum validation failed for files: ${failedChecksums}`);
2006
+ }
1721
2007
  // Verify XML signature if we have the necessary components
1722
2008
  let signatureResult = { isValid: true };
1723
2009
  if (options.verifySignatures !== false &&
@@ -1730,19 +2016,64 @@ async function verifySignature(signatureInfo, files, options = {}) {
1730
2016
  name: "RSASSA-PKCS1-v1_5",
1731
2017
  hash: "SHA-256",
1732
2018
  };
1733
- if (algorithm.includes("ecdsa-sha256")) {
2019
+ if (algorithm.includes("ecdsa") && signatureInfo.publicKey.namedCurve) {
2020
+ keyAlgorithm.namedCurve = signatureInfo.publicKey.namedCurve;
1734
2021
  keyAlgorithm.name = "ECDSA";
2022
+ }
2023
+ if (algorithm.includes("ecdsa-sha256")) {
1735
2024
  keyAlgorithm.hash = "SHA-256";
1736
- if (signatureInfo.publicKey.namedCurve) {
1737
- keyAlgorithm.namedCurve = signatureInfo.publicKey.namedCurve;
1738
- }
2025
+ }
2026
+ else if (algorithm.includes("ecdsa-sha384")) {
2027
+ keyAlgorithm.hash = "SHA-384";
2028
+ }
2029
+ else if (algorithm.includes("ecdsa-sha512")) {
2030
+ keyAlgorithm.hash = "SHA-512";
1739
2031
  }
1740
2032
  else if (algorithm.includes("rsa-sha1")) {
1741
2033
  keyAlgorithm.hash = "SHA-1";
1742
2034
  }
2035
+ else if (algorithm.includes("rsa-pss")) {
2036
+ keyAlgorithm.name = "RSA-PSS";
2037
+ keyAlgorithm.saltLength = 32; // Default salt length (adjust based on hash size)
2038
+ if (algorithm.includes("sha384")) {
2039
+ keyAlgorithm.hash = "SHA-384";
2040
+ keyAlgorithm.saltLength = 48;
2041
+ }
2042
+ else if (algorithm.includes("sha512")) {
2043
+ keyAlgorithm.hash = "SHA-512";
2044
+ keyAlgorithm.saltLength = 64;
2045
+ }
2046
+ else {
2047
+ keyAlgorithm.hash = "SHA-256"; // Default
2048
+ }
2049
+ }
2050
+ else if (algorithm.includes("rsa-sha384")) {
2051
+ keyAlgorithm.hash = "SHA-384";
2052
+ }
2053
+ else if (algorithm.includes("rsa-sha512")) {
2054
+ keyAlgorithm.hash = "SHA-512";
2055
+ }
1743
2056
  signatureResult = await verifySignedInfo(signatureInfo.rawXml, signatureInfo.signatureValue, signatureInfo.publicKey.rawData, keyAlgorithm, signatureInfo.canonicalizationMethod);
2057
+ // If signature validation failed, add detailed error
1744
2058
  if (!signatureResult.isValid) {
1745
- errors.push(signatureResult.reason || "XML signature verification failed");
2059
+ // Format a detailed error message with the error details
2060
+ let detailedErrorMessage = signatureResult.reason || "XML signature verification failed";
2061
+ // Add error details if available
2062
+ if (signatureResult.errorDetails) {
2063
+ const details = signatureResult.errorDetails;
2064
+ detailedErrorMessage += ` [Category: ${details.category}, Environment: ${details.environment}`;
2065
+ if (details.algorithm) {
2066
+ detailedErrorMessage += `, Algorithm: ${details.algorithm.name}`;
2067
+ if (details.algorithm.namedCurve) {
2068
+ detailedErrorMessage += `, Curve: ${details.algorithm.namedCurve}`;
2069
+ }
2070
+ }
2071
+ if (details.keyLength) {
2072
+ detailedErrorMessage += `, Key length: ${details.keyLength} bytes`;
2073
+ }
2074
+ detailedErrorMessage += `]`;
2075
+ }
2076
+ errors.push(detailedErrorMessage);
1746
2077
  }
1747
2078
  }
1748
2079
  else if (options.verifySignatures !== false) {