edockit 0.2.2 → 0.2.4

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
@@ -1208,6 +1208,17 @@ function parseSignatureElement(signatureElement, xmlDoc) {
1208
1208
  // Get signature value
1209
1209
  const signatureValueEl = querySelector(signatureElement, "ds\\:SignatureValue, SignatureValue");
1210
1210
  const signatureValue = signatureValueEl?.textContent?.replace(/\s+/g, "") || "";
1211
+ // Compute canonicalized SignatureValue element for timestamp verification
1212
+ let canonicalSignatureValue;
1213
+ if (signatureValueEl) {
1214
+ try {
1215
+ const canonicalizer = new XMLCanonicalizer();
1216
+ canonicalSignatureValue = canonicalizer.canonicalize(signatureValueEl);
1217
+ }
1218
+ catch {
1219
+ // Canonicalization failed - leave undefined
1220
+ }
1221
+ }
1211
1222
  // Get certificate(s)
1212
1223
  let certificate = "";
1213
1224
  let certificatePEM = "";
@@ -1339,6 +1350,7 @@ function parseSignatureElement(signatureElement, xmlDoc) {
1339
1350
  references,
1340
1351
  algorithm: signatureAlgorithm,
1341
1352
  signatureValue,
1353
+ canonicalSignatureValue,
1342
1354
  signedInfoXml,
1343
1355
  canonicalizationMethod,
1344
1356
  signatureTimestamp,
@@ -8064,11 +8076,14 @@ function arrayBufferToBase64(buffer) {
8064
8076
  }
8065
8077
  /**
8066
8078
  * Convert base64 string to ArrayBuffer
8079
+ * Handles base64 strings with whitespace (common in XML)
8067
8080
  */
8068
8081
  function base64ToArrayBuffer(base64) {
8082
+ // Strip whitespace (newlines, spaces, tabs) that may be present in XML
8083
+ const cleanBase64 = base64.replace(/\s/g, "");
8069
8084
  if (typeof atob === "function") {
8070
8085
  // Browser
8071
- const binary = atob(base64);
8086
+ const binary = atob(cleanBase64);
8072
8087
  const bytes = new Uint8Array(binary.length);
8073
8088
  for (let i = 0; i < binary.length; i++) {
8074
8089
  bytes[i] = binary.charCodeAt(i);
@@ -8076,7 +8091,7 @@ function base64ToArrayBuffer(base64) {
8076
8091
  return bytes.buffer;
8077
8092
  }
8078
8093
  // Node.js
8079
- const buffer = Buffer.from(base64, "base64");
8094
+ const buffer = Buffer.from(cleanBase64, "base64");
8080
8095
  return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
8081
8096
  }
8082
8097
  /**
@@ -8229,10 +8244,12 @@ async function fetchIssuerFromAIA(cert, timeout = 5000, proxyUrl) {
8229
8244
  */
8230
8245
  async function buildOCSPRequest(cert, issuerCert) {
8231
8246
  // Get issuer name hash (SHA-1 of issuer's DN in DER)
8232
- const issuerNameDer = AsnConvert.serialize(issuerCert.subjectName.toJSON());
8247
+ // Parse the raw certificate to get the proper ASN.1 structures for serialization
8248
+ const issuerCertAsn = AsnParser.parse(issuerCert.rawData, Certificate);
8249
+ const issuerNameDer = AsnConvert.serialize(issuerCertAsn.tbsCertificate.subject);
8233
8250
  const issuerNameHash = await computeSHA1(issuerNameDer);
8234
- // Get issuer key hash (SHA-1 of issuer's public key)
8235
- const issuerKeyHash = await computeSHA1(issuerCert.publicKey.rawData);
8251
+ // Get issuer key hash (SHA-1 of issuer's public key BIT STRING value, not the full SPKI)
8252
+ const issuerKeyHash = await computeSHA1(issuerCertAsn.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey);
8236
8253
  // Get certificate serial number
8237
8254
  const serialNumber = hexToArrayBuffer(cert.serialNumber);
8238
8255
  // Build CertID
@@ -9944,6 +9961,38 @@ function getHashAlgorithmName(oid) {
9944
9961
  };
9945
9962
  return hashAlgorithms[oid] || oid;
9946
9963
  }
9964
+ /**
9965
+ * Common OID to attribute name mappings for X.500 distinguished names
9966
+ */
9967
+ const oidToAttributeName = {
9968
+ "2.5.4.3": "CN",
9969
+ "2.5.4.6": "C",
9970
+ "2.5.4.7": "L",
9971
+ "2.5.4.8": "ST",
9972
+ "2.5.4.10": "O",
9973
+ "2.5.4.11": "OU",
9974
+ "2.5.4.5": "serialNumber",
9975
+ "1.2.840.113549.1.9.1": "emailAddress",
9976
+ };
9977
+ /**
9978
+ * Format an X.500 Name (directoryName) to a readable string
9979
+ * @param name The Name object to format
9980
+ * @returns Formatted string like "CN=Example, O=Company, C=US"
9981
+ */
9982
+ function formatDirectoryName(name) {
9983
+ const parts = [];
9984
+ for (const rdn of name) {
9985
+ for (const attr of rdn) {
9986
+ const attrName = oidToAttributeName[attr.type] || attr.type;
9987
+ // attr.value can be various ASN.1 string types
9988
+ const value = attr.value?.toString() || "";
9989
+ if (value) {
9990
+ parts.push(`${attrName}=${value}`);
9991
+ }
9992
+ }
9993
+ }
9994
+ return parts.join(", ");
9995
+ }
9947
9996
  /**
9948
9997
  * Parse RFC 3161 TimeStampToken from base64
9949
9998
  * @param timestampBase64 Base64-encoded timestamp token
@@ -9999,7 +10048,7 @@ function parseTimestamp(timestampBase64) {
9999
10048
  let tsaName;
10000
10049
  if (tstInfo.tsa) {
10001
10050
  if (tstInfo.tsa.directoryName) {
10002
- tsaName = tstInfo.tsa.directoryName.toString();
10051
+ tsaName = formatDirectoryName(tstInfo.tsa.directoryName);
10003
10052
  }
10004
10053
  else if (tstInfo.tsa.uniformResourceIdentifier) {
10005
10054
  tsaName = tstInfo.tsa.uniformResourceIdentifier;
@@ -10055,33 +10104,22 @@ async function computeHash(data, algorithm) {
10055
10104
  }
10056
10105
  /**
10057
10106
  * Verify that timestamp covers the signature value
10058
- * XAdES timestamps can cover either:
10059
- * 1. The decoded signature value bytes (standard per ETSI EN 319 132-1)
10060
- * 2. The base64-encoded string (some implementations)
10107
+ *
10108
+ * Per XAdES (ETSI EN 319 132-1), the SignatureTimeStamp covers the canonicalized
10109
+ * ds:SignatureValue XML element, not just its base64 content.
10061
10110
  *
10062
10111
  * @param timestampInfo Parsed timestamp info
10063
- * @param signatureValueBase64 Base64-encoded signature value
10112
+ * @param canonicalSignatureValue Canonicalized ds:SignatureValue XML element
10064
10113
  * @returns True if the timestamp covers the signature
10065
10114
  */
10066
- async function verifyTimestampCoversSignature(timestampInfo, signatureValueBase64) {
10115
+ async function verifyTimestampCoversSignature(timestampInfo, canonicalSignatureValue) {
10067
10116
  try {
10068
10117
  const messageImprintLower = timestampInfo.messageImprint.toLowerCase();
10069
- // Try 1: Hash of decoded signature value bytes (standard approach)
10070
- const signatureValue = base64ToArrayBuffer(signatureValueBase64);
10071
- const computedHash = await computeHash(signatureValue, timestampInfo.hashAlgorithm);
10072
- const computedHashHex = arrayBufferToHex(computedHash);
10073
- if (computedHashHex.toLowerCase() === messageImprintLower) {
10074
- return true;
10075
- }
10076
- // Try 2: Hash of base64 string (some implementations)
10077
10118
  const encoder = new TextEncoder();
10078
- const base64Bytes = encoder.encode(signatureValueBase64);
10079
- const base64Hash = await computeHash(base64Bytes.buffer, timestampInfo.hashAlgorithm);
10080
- const base64HashHex = arrayBufferToHex(base64Hash);
10081
- if (base64HashHex.toLowerCase() === messageImprintLower) {
10082
- return true;
10083
- }
10084
- return false;
10119
+ const canonicalBytes = encoder.encode(canonicalSignatureValue);
10120
+ const canonicalHash = await computeHash(canonicalBytes.buffer, timestampInfo.hashAlgorithm);
10121
+ const canonicalHashHex = arrayBufferToHex(canonicalHash);
10122
+ return canonicalHashHex.toLowerCase() === messageImprintLower;
10085
10123
  }
10086
10124
  catch (error) {
10087
10125
  console.error("Failed to verify timestamp coverage:", error instanceof Error ? error.message : String(error));
@@ -10104,15 +10142,12 @@ async function verifyTimestamp(timestampBase64, options = {}) {
10104
10142
  };
10105
10143
  }
10106
10144
  // Verify timestamp covers the signature if provided
10107
- // Note: coversSignature failure is informational - the timestamp is still valid
10108
- // and can be used for genTime. The signature value hashing varies by implementation.
10109
10145
  let coversSignature;
10110
10146
  let coversSignatureReason;
10111
- if (options.signatureValue) {
10112
- coversSignature = await verifyTimestampCoversSignature(info, options.signatureValue);
10147
+ if (options.canonicalSignatureValue) {
10148
+ coversSignature = await verifyTimestampCoversSignature(info, options.canonicalSignatureValue);
10113
10149
  if (!coversSignature) {
10114
- coversSignatureReason =
10115
- "Could not verify timestamp covers signature (implementation-specific hashing)";
10150
+ coversSignatureReason = "Could not verify timestamp covers signature (hash mismatch)";
10116
10151
  }
10117
10152
  }
10118
10153
  // Verify TSA certificate if requested
@@ -10144,7 +10179,7 @@ async function verifyTimestamp(timestampBase64, options = {}) {
10144
10179
  };
10145
10180
  }
10146
10181
  // Note: 'unknown' status is a soft fail - timestamp remains valid
10147
- // but user can check tsaRevocation.status to see if it couldn't be verified
10182
+ // but user can check tsaRevocation.status to see the actual status
10148
10183
  }
10149
10184
  catch (error) {
10150
10185
  // Revocation check failed - soft fail, add to result but don't invalidate
@@ -10399,6 +10434,115 @@ async function verifyCertificate(certificatePEM, verifyTime = new Date()) {
10399
10434
  };
10400
10435
  }
10401
10436
  }
10437
+ /**
10438
+ * Get the expected component size for an ECDSA curve
10439
+ */
10440
+ function getEcdsaComponentSize(namedCurve) {
10441
+ switch (namedCurve) {
10442
+ case "P-256":
10443
+ return 32;
10444
+ case "P-384":
10445
+ return 48;
10446
+ case "P-521":
10447
+ return 66;
10448
+ default:
10449
+ return 32; // Default to P-256
10450
+ }
10451
+ }
10452
+ /**
10453
+ * Normalize ECDSA signature to IEEE P1363 format (raw R||S) expected by Web Crypto API
10454
+ * Handles both raw format with potential padding and DER-encoded signatures
10455
+ */
10456
+ function normalizeEcdsaSignature(signatureBytes, namedCurve) {
10457
+ const componentSize = getEcdsaComponentSize(namedCurve);
10458
+ const expectedLength = componentSize * 2;
10459
+ // If already correct length, return as-is
10460
+ if (signatureBytes.length === expectedLength) {
10461
+ return signatureBytes;
10462
+ }
10463
+ // Check if it's DER-encoded (starts with SEQUENCE tag 0x30)
10464
+ if (signatureBytes[0] === 0x30) {
10465
+ return derToRawEcdsa(signatureBytes, componentSize);
10466
+ }
10467
+ // Handle raw R||S with potential leading zeros
10468
+ // Some implementations pad R and S with leading zeros
10469
+ if (signatureBytes.length > expectedLength) {
10470
+ const halfLength = signatureBytes.length / 2;
10471
+ if (Number.isInteger(halfLength)) {
10472
+ const r = signatureBytes.slice(0, halfLength);
10473
+ const s = signatureBytes.slice(halfLength);
10474
+ // Normalize R and S to exact component size
10475
+ const normalizedR = normalizeComponent(r, componentSize);
10476
+ const normalizedS = normalizeComponent(s, componentSize);
10477
+ const result = new Uint8Array(expectedLength);
10478
+ result.set(normalizedR, 0);
10479
+ result.set(normalizedS, componentSize);
10480
+ return result;
10481
+ }
10482
+ }
10483
+ // Return as-is if we can't normalize
10484
+ return signatureBytes;
10485
+ }
10486
+ /**
10487
+ * Normalize a single ECDSA component (R or S) to exact size
10488
+ * Strips leading zeros or pads with leading zeros as needed
10489
+ */
10490
+ function normalizeComponent(component, size) {
10491
+ // Strip leading zeros
10492
+ let start = 0;
10493
+ while (start < component.length - 1 && component[start] === 0) {
10494
+ start++;
10495
+ }
10496
+ const stripped = component.slice(start);
10497
+ if (stripped.length === size) {
10498
+ return stripped;
10499
+ }
10500
+ else if (stripped.length < size) {
10501
+ // Pad with leading zeros
10502
+ const padded = new Uint8Array(size);
10503
+ padded.set(stripped, size - stripped.length);
10504
+ return padded;
10505
+ }
10506
+ else {
10507
+ // Component too large - take the last 'size' bytes
10508
+ return stripped.slice(stripped.length - size);
10509
+ }
10510
+ }
10511
+ /**
10512
+ * Convert DER-encoded ECDSA signature to raw IEEE P1363 format
10513
+ */
10514
+ function derToRawEcdsa(derSignature, componentSize) {
10515
+ // DER structure: SEQUENCE { INTEGER r, INTEGER s }
10516
+ // 0x30 len 0x02 rLen r... 0x02 sLen s...
10517
+ let offset = 0;
10518
+ // Skip SEQUENCE tag
10519
+ if (derSignature[offset++] !== 0x30) {
10520
+ throw new Error("Invalid DER signature: missing SEQUENCE tag");
10521
+ }
10522
+ // Skip sequence length (may be 1 or 2 bytes)
10523
+ const seqLen = derSignature[offset++];
10524
+ if (seqLen & 0x80) {
10525
+ offset += seqLen & 0x7f; // Skip length bytes
10526
+ }
10527
+ // Parse R
10528
+ if (derSignature[offset++] !== 0x02) {
10529
+ throw new Error("Invalid DER signature: missing INTEGER tag for R");
10530
+ }
10531
+ const rLen = derSignature[offset++];
10532
+ const r = derSignature.slice(offset, offset + rLen);
10533
+ offset += rLen;
10534
+ // Parse S
10535
+ if (derSignature[offset++] !== 0x02) {
10536
+ throw new Error("Invalid DER signature: missing INTEGER tag for S");
10537
+ }
10538
+ const sLen = derSignature[offset++];
10539
+ const s = derSignature.slice(offset, offset + sLen);
10540
+ // Normalize and concatenate
10541
+ const result = new Uint8Array(componentSize * 2);
10542
+ result.set(normalizeComponent(r, componentSize), 0);
10543
+ result.set(normalizeComponent(s, componentSize), componentSize);
10544
+ return result;
10545
+ }
10402
10546
  /**
10403
10547
  * Safely get the crypto.subtle implementation in either browser or Node.js
10404
10548
  * @returns The SubtleCrypto interface
@@ -10450,6 +10594,10 @@ async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, alg
10450
10594
  reason: `Failed to decode signature value: ${error instanceof Error ? error.message : String(error)}`,
10451
10595
  };
10452
10596
  }
10597
+ // For ECDSA, normalize signature to IEEE P1363 format (raw R||S) expected by Web Crypto
10598
+ if (algorithm.name === "ECDSA") {
10599
+ signatureBytes = normalizeEcdsaSignature(signatureBytes, algorithm.namedCurve);
10600
+ }
10453
10601
  // Import the public key
10454
10602
  let publicKey;
10455
10603
  try {
@@ -10538,7 +10686,7 @@ async function verifySignature(signatureInfo, files, options = {}) {
10538
10686
  let trustedSigningTime = options.verifyTime || signatureInfo.signingTime;
10539
10687
  if (signatureInfo.signatureTimestamp && options.verifyTimestamps !== false) {
10540
10688
  timestampResult = await verifyTimestamp(signatureInfo.signatureTimestamp, {
10541
- signatureValue: signatureInfo.signatureValue,
10689
+ canonicalSignatureValue: signatureInfo.canonicalSignatureValue,
10542
10690
  verifyTsaCertificate: true,
10543
10691
  revocationOptions: options.revocationOptions,
10544
10692
  });
@@ -10565,14 +10713,28 @@ async function verifySignature(signatureInfo, files, options = {}) {
10565
10713
  ...options.revocationOptions,
10566
10714
  });
10567
10715
  certResult.revocation = revocationResult;
10568
- // If certificate is revoked, mark certificate as invalid
10716
+ // If certificate is revoked, check if signature was made before revocation (LTV)
10569
10717
  if (revocationResult.status === "revoked") {
10570
- certResult.isValid = false;
10571
- certResult.reason = revocationResult.reason || "Certificate has been revoked";
10572
- errors.push(`Certificate revoked: ${revocationResult.reason || "No reason provided"}`);
10718
+ const revokedAt = revocationResult.revokedAt;
10719
+ const hasValidRevocationTime = revokedAt && revokedAt.getTime() > 0;
10720
+ // Long-Term Validation: if we have a trusted timestamp proving the signature
10721
+ // was made before revocation, the signature is still valid
10722
+ if (hasValidRevocationTime && trustedSigningTime < revokedAt) {
10723
+ // Signature was made before revocation - still valid (LTV)
10724
+ certResult.revocation.isValid = true;
10725
+ certResult.revocation.reason = `Certificate was revoked on ${revokedAt.toISOString()}, but signature was made on ${trustedSigningTime.toISOString()} (before revocation)`;
10726
+ }
10727
+ else {
10728
+ // Signature was made after revocation or no valid revocation date available
10729
+ certResult.isValid = false;
10730
+ const revokedAtStr = hasValidRevocationTime ? ` on ${revokedAt.toISOString()}` : "";
10731
+ certResult.reason =
10732
+ revocationResult.reason || `Certificate has been revoked${revokedAtStr}`;
10733
+ errors.push(revocationResult.reason || `Certificate revoked${revokedAtStr}`);
10734
+ }
10573
10735
  }
10574
10736
  // Note: 'unknown' status is a soft fail - certificate remains valid
10575
- // but user can check revocation.status to see if it couldn't be verified
10737
+ // but user can check revocation.status to see the actual status
10576
10738
  }
10577
10739
  catch (error) {
10578
10740
  // Revocation check failed - soft fail, add to result but don't invalidate