edockit 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +33 -10
- package/dist/core/rsa-digestinfo-workaround.d.ts +29 -0
- package/dist/core/verification.d.ts +28 -0
- package/dist/index.cjs.js +502 -25
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +502 -25
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +12 -15
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -1
package/dist/index.esm.js
CHANGED
|
@@ -367,25 +367,11 @@ const methods = {
|
|
|
367
367
|
isCanonicalizationMethod: "c14n",
|
|
368
368
|
},
|
|
369
369
|
c14n11: {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
},
|
|
376
|
-
afterChildren: (hasElementChildren, hasMixedContent) => {
|
|
377
|
-
// If it's mixed content, don't add newlines
|
|
378
|
-
if (hasMixedContent)
|
|
379
|
-
return "";
|
|
380
|
-
return hasElementChildren ? "\n" : "";
|
|
381
|
-
},
|
|
382
|
-
betweenChildren: (prevIsElement, nextIsElement, hasMixedContent) => {
|
|
383
|
-
// If it's mixed content, don't add newlines between elements
|
|
384
|
-
if (hasMixedContent)
|
|
385
|
-
return "";
|
|
386
|
-
// Only add newline between elements
|
|
387
|
-
return prevIsElement && nextIsElement ? "\n" : "";
|
|
388
|
-
},
|
|
370
|
+
// C14N 1.1 should NOT add newlines - it should preserve original whitespace
|
|
371
|
+
// The difference from C14N is in xml:id normalization, not formatting
|
|
372
|
+
beforeChildren: () => "",
|
|
373
|
+
afterChildren: () => "",
|
|
374
|
+
betweenChildren: () => "",
|
|
389
375
|
afterElement: () => "",
|
|
390
376
|
isCanonicalizationMethod: "c14n11",
|
|
391
377
|
},
|
|
@@ -10221,6 +10207,273 @@ function getTimestampTime(timestampBase64) {
|
|
|
10221
10207
|
return info?.genTime || null;
|
|
10222
10208
|
}
|
|
10223
10209
|
|
|
10210
|
+
/**
|
|
10211
|
+
* RSA DigestInfo Workaround
|
|
10212
|
+
*
|
|
10213
|
+
* Some older signing tools (particularly pre-Java 8) produced RSA signatures with
|
|
10214
|
+
* non-standard DigestInfo format - missing the NULL parameter in AlgorithmIdentifier.
|
|
10215
|
+
*
|
|
10216
|
+
* Standard DigestInfo for SHA-1: 30 21 30 09 06 05 2b0e03021a 05 00 04 14 [hash]
|
|
10217
|
+
* Non-standard (missing NULL): 30 1f 30 07 06 05 2b0e03021a 04 14 [hash]
|
|
10218
|
+
*
|
|
10219
|
+
* Web Crypto API's subtle.verify() is strict and rejects the non-standard format.
|
|
10220
|
+
* This module provides a fallback that manually performs RSA verification using
|
|
10221
|
+
* BigInt math, which works in both browser and Node.js environments.
|
|
10222
|
+
*/
|
|
10223
|
+
/**
|
|
10224
|
+
* Parse RSA public key from SPKI format to extract modulus and exponent
|
|
10225
|
+
*/
|
|
10226
|
+
function parseRSAPublicKey(spkiData) {
|
|
10227
|
+
const bytes = new Uint8Array(spkiData);
|
|
10228
|
+
// SPKI structure:
|
|
10229
|
+
// SEQUENCE {
|
|
10230
|
+
// SEQUENCE { algorithm OID, parameters (NULL or absent) }
|
|
10231
|
+
// BIT STRING { RSAPublicKey }
|
|
10232
|
+
// }
|
|
10233
|
+
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
|
10234
|
+
let pos = 0;
|
|
10235
|
+
// Helper to read ASN.1 length
|
|
10236
|
+
const readLength = () => {
|
|
10237
|
+
const first = bytes[pos++];
|
|
10238
|
+
if ((first & 0x80) === 0) {
|
|
10239
|
+
return first;
|
|
10240
|
+
}
|
|
10241
|
+
const numBytes = first & 0x7f;
|
|
10242
|
+
let length = 0;
|
|
10243
|
+
for (let i = 0; i < numBytes; i++) {
|
|
10244
|
+
length = (length << 8) | bytes[pos++];
|
|
10245
|
+
}
|
|
10246
|
+
return length;
|
|
10247
|
+
};
|
|
10248
|
+
// Helper to read INTEGER as BigInt
|
|
10249
|
+
const readInteger = () => {
|
|
10250
|
+
if (bytes[pos++] !== 0x02)
|
|
10251
|
+
return BigInt(0); // INTEGER tag
|
|
10252
|
+
const len = readLength();
|
|
10253
|
+
let value = BigInt(0);
|
|
10254
|
+
for (let i = 0; i < len; i++) {
|
|
10255
|
+
value = (value << BigInt(8)) | BigInt(bytes[pos++]);
|
|
10256
|
+
}
|
|
10257
|
+
return value;
|
|
10258
|
+
};
|
|
10259
|
+
try {
|
|
10260
|
+
// Outer SEQUENCE
|
|
10261
|
+
if (bytes[pos++] !== 0x30)
|
|
10262
|
+
return null;
|
|
10263
|
+
readLength();
|
|
10264
|
+
// AlgorithmIdentifier SEQUENCE
|
|
10265
|
+
if (bytes[pos++] !== 0x30)
|
|
10266
|
+
return null;
|
|
10267
|
+
const algoLen = readLength();
|
|
10268
|
+
pos += algoLen; // Skip algorithm identifier
|
|
10269
|
+
// BIT STRING containing RSAPublicKey
|
|
10270
|
+
if (bytes[pos++] !== 0x03)
|
|
10271
|
+
return null;
|
|
10272
|
+
readLength();
|
|
10273
|
+
pos++; // Skip unused bits byte
|
|
10274
|
+
// RSAPublicKey SEQUENCE
|
|
10275
|
+
if (bytes[pos++] !== 0x30)
|
|
10276
|
+
return null;
|
|
10277
|
+
readLength();
|
|
10278
|
+
// Read modulus and exponent
|
|
10279
|
+
const n = readInteger();
|
|
10280
|
+
const e = readInteger();
|
|
10281
|
+
return { n, e };
|
|
10282
|
+
}
|
|
10283
|
+
catch {
|
|
10284
|
+
return null;
|
|
10285
|
+
}
|
|
10286
|
+
}
|
|
10287
|
+
/**
|
|
10288
|
+
* Perform modular exponentiation: base^exp mod mod
|
|
10289
|
+
* Uses square-and-multiply algorithm for efficiency
|
|
10290
|
+
*/
|
|
10291
|
+
function modPow(base, exp, mod) {
|
|
10292
|
+
let result = BigInt(1);
|
|
10293
|
+
base = base % mod;
|
|
10294
|
+
while (exp > 0) {
|
|
10295
|
+
if (exp % BigInt(2) === BigInt(1)) {
|
|
10296
|
+
result = (result * base) % mod;
|
|
10297
|
+
}
|
|
10298
|
+
exp = exp >> BigInt(1);
|
|
10299
|
+
base = (base * base) % mod;
|
|
10300
|
+
}
|
|
10301
|
+
return result;
|
|
10302
|
+
}
|
|
10303
|
+
/**
|
|
10304
|
+
* Convert Uint8Array to BigInt
|
|
10305
|
+
*/
|
|
10306
|
+
function bytesToBigInt(bytes) {
|
|
10307
|
+
let result = BigInt(0);
|
|
10308
|
+
for (const byte of bytes) {
|
|
10309
|
+
result = (result << BigInt(8)) | BigInt(byte);
|
|
10310
|
+
}
|
|
10311
|
+
return result;
|
|
10312
|
+
}
|
|
10313
|
+
/**
|
|
10314
|
+
* Convert BigInt to Uint8Array with specified length
|
|
10315
|
+
*/
|
|
10316
|
+
function bigIntToBytes(value, length) {
|
|
10317
|
+
const result = new Uint8Array(length);
|
|
10318
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
10319
|
+
result[i] = Number(value & BigInt(0xff));
|
|
10320
|
+
value = value >> BigInt(8);
|
|
10321
|
+
}
|
|
10322
|
+
return result;
|
|
10323
|
+
}
|
|
10324
|
+
/**
|
|
10325
|
+
* Verify PKCS#1 v1.5 signature padding and extract DigestInfo
|
|
10326
|
+
* @param decrypted The decrypted signature block
|
|
10327
|
+
* @returns The DigestInfo bytes, or null if padding is invalid
|
|
10328
|
+
*/
|
|
10329
|
+
function extractDigestInfoFromPKCS1(decrypted) {
|
|
10330
|
+
// PKCS#1 v1.5 signature format:
|
|
10331
|
+
// 0x00 0x01 [0xFF padding] 0x00 [DigestInfo]
|
|
10332
|
+
if (decrypted[0] !== 0x00 || decrypted[1] !== 0x01) {
|
|
10333
|
+
return null;
|
|
10334
|
+
}
|
|
10335
|
+
// Find the 0x00 separator after padding
|
|
10336
|
+
let separatorIndex = -1;
|
|
10337
|
+
for (let i = 2; i < decrypted.length; i++) {
|
|
10338
|
+
if (decrypted[i] === 0x00) {
|
|
10339
|
+
separatorIndex = i;
|
|
10340
|
+
break;
|
|
10341
|
+
}
|
|
10342
|
+
if (decrypted[i] !== 0xff) {
|
|
10343
|
+
return null; // Invalid padding byte
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
if (separatorIndex === -1 || separatorIndex < 10) {
|
|
10347
|
+
return null; // No separator found or padding too short
|
|
10348
|
+
}
|
|
10349
|
+
return decrypted.slice(separatorIndex + 1);
|
|
10350
|
+
}
|
|
10351
|
+
/**
|
|
10352
|
+
* Extract hash from DigestInfo structure
|
|
10353
|
+
* Handles both standard (with NULL) and non-standard (without NULL) formats
|
|
10354
|
+
*/
|
|
10355
|
+
function extractHashFromDigestInfo(digestInfo, expectedHashLength) {
|
|
10356
|
+
// DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTET STRING }
|
|
10357
|
+
// Look for OCTET STRING tag (0x04) followed by the hash
|
|
10358
|
+
for (let i = 0; i < digestInfo.length - 1; i++) {
|
|
10359
|
+
if (digestInfo[i] === 0x04) {
|
|
10360
|
+
const len = digestInfo[i + 1];
|
|
10361
|
+
if (len === expectedHashLength && i + 2 + len <= digestInfo.length) {
|
|
10362
|
+
return digestInfo.slice(i + 2, i + 2 + len);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
}
|
|
10366
|
+
return null;
|
|
10367
|
+
}
|
|
10368
|
+
/**
|
|
10369
|
+
* Get hash length in bytes for a given algorithm
|
|
10370
|
+
*/
|
|
10371
|
+
function getHashLength(hashAlgorithm) {
|
|
10372
|
+
const algo = hashAlgorithm.toLowerCase().replace("-", "");
|
|
10373
|
+
switch (algo) {
|
|
10374
|
+
case "sha1":
|
|
10375
|
+
return 20;
|
|
10376
|
+
case "sha256":
|
|
10377
|
+
return 32;
|
|
10378
|
+
case "sha384":
|
|
10379
|
+
return 48;
|
|
10380
|
+
case "sha512":
|
|
10381
|
+
return 64;
|
|
10382
|
+
default:
|
|
10383
|
+
return 32;
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
/**
|
|
10387
|
+
* Detects if code is running in a browser environment
|
|
10388
|
+
*/
|
|
10389
|
+
function isBrowser$1() {
|
|
10390
|
+
return (typeof window !== "undefined" &&
|
|
10391
|
+
typeof window.crypto !== "undefined" &&
|
|
10392
|
+
typeof window.crypto.subtle !== "undefined");
|
|
10393
|
+
}
|
|
10394
|
+
/**
|
|
10395
|
+
* Verify RSA signature with non-standard DigestInfo format.
|
|
10396
|
+
*
|
|
10397
|
+
* This function performs RSA signature verification that tolerates
|
|
10398
|
+
* non-standard DigestInfo formats (missing NULL in AlgorithmIdentifier).
|
|
10399
|
+
*
|
|
10400
|
+
* - Node.js: Uses native crypto.publicDecrypt() for speed
|
|
10401
|
+
* - Browser: Uses BigInt math (Web Crypto doesn't expose raw RSA)
|
|
10402
|
+
*
|
|
10403
|
+
* @param publicKeyData SPKI-formatted public key
|
|
10404
|
+
* @param signatureBytes Raw signature bytes
|
|
10405
|
+
* @param dataToVerify The data that was signed
|
|
10406
|
+
* @param hashAlgorithm Hash algorithm name (e.g., "SHA-1", "SHA-256")
|
|
10407
|
+
* @returns true if signature is valid, false otherwise
|
|
10408
|
+
*/
|
|
10409
|
+
async function verifyRsaWithNonStandardDigestInfo(publicKeyData, signatureBytes, dataToVerify, hashAlgorithm) {
|
|
10410
|
+
try {
|
|
10411
|
+
let digestInfo;
|
|
10412
|
+
if (isBrowser$1()) {
|
|
10413
|
+
// Browser: Use BigInt math (Web Crypto doesn't expose raw RSA decryption)
|
|
10414
|
+
const keyParams = parseRSAPublicKey(publicKeyData);
|
|
10415
|
+
if (!keyParams) {
|
|
10416
|
+
return false;
|
|
10417
|
+
}
|
|
10418
|
+
const { n, e } = keyParams;
|
|
10419
|
+
const keyLength = Math.ceil(n.toString(16).length / 2);
|
|
10420
|
+
const signatureInt = bytesToBigInt(signatureBytes);
|
|
10421
|
+
const decryptedInt = modPow(signatureInt, e, n);
|
|
10422
|
+
const decrypted = bigIntToBytes(decryptedInt, keyLength);
|
|
10423
|
+
digestInfo = extractDigestInfoFromPKCS1(decrypted);
|
|
10424
|
+
}
|
|
10425
|
+
else {
|
|
10426
|
+
// Node.js: Use native crypto.publicDecrypt() for speed
|
|
10427
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
10428
|
+
const nodeCrypto = require("crypto");
|
|
10429
|
+
const publicKey = nodeCrypto.createPublicKey({
|
|
10430
|
+
key: Buffer.from(publicKeyData),
|
|
10431
|
+
format: "der",
|
|
10432
|
+
type: "spki",
|
|
10433
|
+
});
|
|
10434
|
+
const decrypted = nodeCrypto.publicDecrypt({ key: publicKey, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }, Buffer.from(signatureBytes));
|
|
10435
|
+
// Node's publicDecrypt already strips PKCS#1 padding, returns DigestInfo directly
|
|
10436
|
+
digestInfo = new Uint8Array(decrypted);
|
|
10437
|
+
}
|
|
10438
|
+
if (!digestInfo) {
|
|
10439
|
+
return false;
|
|
10440
|
+
}
|
|
10441
|
+
// Extract hash from DigestInfo (tolerates missing NULL)
|
|
10442
|
+
const hashLength = getHashLength(hashAlgorithm);
|
|
10443
|
+
const extractedHash = extractHashFromDigestInfo(digestInfo, hashLength);
|
|
10444
|
+
if (!extractedHash) {
|
|
10445
|
+
return false;
|
|
10446
|
+
}
|
|
10447
|
+
// Compute expected hash
|
|
10448
|
+
let expectedHash;
|
|
10449
|
+
if (isBrowser$1()) {
|
|
10450
|
+
// Normalize to Web Crypto format: SHA-1, SHA-256, SHA-384, SHA-512
|
|
10451
|
+
let hashName = hashAlgorithm.toUpperCase().replace(/-/g, "");
|
|
10452
|
+
hashName = hashName.replace(/^SHA(\d)/, "SHA-$1");
|
|
10453
|
+
const hashBuffer = await window.crypto.subtle.digest(hashName, dataToVerify);
|
|
10454
|
+
expectedHash = new Uint8Array(hashBuffer);
|
|
10455
|
+
}
|
|
10456
|
+
else {
|
|
10457
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
10458
|
+
const nodeCrypto = require("crypto");
|
|
10459
|
+
const hashName = hashAlgorithm.toLowerCase().replace("-", "");
|
|
10460
|
+
expectedHash = nodeCrypto.createHash(hashName).update(Buffer.from(dataToVerify)).digest();
|
|
10461
|
+
}
|
|
10462
|
+
// Compare hashes (constant-time comparison)
|
|
10463
|
+
if (extractedHash.length !== expectedHash.length) {
|
|
10464
|
+
return false;
|
|
10465
|
+
}
|
|
10466
|
+
let diff = 0;
|
|
10467
|
+
for (let i = 0; i < extractedHash.length; i++) {
|
|
10468
|
+
diff |= extractedHash[i] ^ expectedHash[i];
|
|
10469
|
+
}
|
|
10470
|
+
return diff === 0;
|
|
10471
|
+
}
|
|
10472
|
+
catch {
|
|
10473
|
+
return false;
|
|
10474
|
+
}
|
|
10475
|
+
}
|
|
10476
|
+
|
|
10224
10477
|
/**
|
|
10225
10478
|
* Detects if code is running in a browser environment
|
|
10226
10479
|
* @returns true if in browser, false otherwise
|
|
@@ -10230,6 +10483,105 @@ function isBrowser() {
|
|
|
10230
10483
|
typeof window.crypto !== "undefined" &&
|
|
10231
10484
|
typeof window.crypto.subtle !== "undefined");
|
|
10232
10485
|
}
|
|
10486
|
+
/**
|
|
10487
|
+
* Detects if running in Safari/WebKit browser
|
|
10488
|
+
* Safari/WebKit handles RSA key DER encoding correctly and doesn't need the modulus padding fix
|
|
10489
|
+
* This also detects Playwright's headless WebKit
|
|
10490
|
+
* @returns true if Safari/WebKit, false otherwise
|
|
10491
|
+
*/
|
|
10492
|
+
function isWebKit() {
|
|
10493
|
+
if (typeof navigator === "undefined")
|
|
10494
|
+
return false;
|
|
10495
|
+
const ua = navigator.userAgent;
|
|
10496
|
+
// Detect WebKit-based browsers (Safari, or Playwright WebKit which includes "AppleWebKit" but not Chrome)
|
|
10497
|
+
const hasWebKit = /AppleWebKit/.test(ua);
|
|
10498
|
+
const isChromium = /Chrome/.test(ua) || /Chromium/.test(ua) || /Edg/.test(ua);
|
|
10499
|
+
return hasWebKit && !isChromium;
|
|
10500
|
+
}
|
|
10501
|
+
/**
|
|
10502
|
+
* Get RSA modulus length in bits from SPKI public key data
|
|
10503
|
+
* @param publicKeyData The SPKI-formatted public key
|
|
10504
|
+
* @returns Modulus length in bits, or 0 if not RSA or can't determine
|
|
10505
|
+
*/
|
|
10506
|
+
function getRSAModulusLength(publicKeyData) {
|
|
10507
|
+
const keyBytes = new Uint8Array(publicKeyData);
|
|
10508
|
+
// Check for RSA OID (1.2.840.113549.1.1.1)
|
|
10509
|
+
const RSA_OID = [0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
|
|
10510
|
+
let oidPosition = -1;
|
|
10511
|
+
for (let i = 0; i <= keyBytes.length - RSA_OID.length; i++) {
|
|
10512
|
+
let match = true;
|
|
10513
|
+
for (let j = 0; j < RSA_OID.length; j++) {
|
|
10514
|
+
if (keyBytes[i + j] !== RSA_OID[j]) {
|
|
10515
|
+
match = false;
|
|
10516
|
+
break;
|
|
10517
|
+
}
|
|
10518
|
+
}
|
|
10519
|
+
if (match) {
|
|
10520
|
+
oidPosition = i;
|
|
10521
|
+
break;
|
|
10522
|
+
}
|
|
10523
|
+
}
|
|
10524
|
+
if (oidPosition === -1)
|
|
10525
|
+
return 0; // Not RSA
|
|
10526
|
+
// Find BIT STRING containing the key
|
|
10527
|
+
let bitStringPos = -1;
|
|
10528
|
+
for (let i = oidPosition + RSA_OID.length; i < keyBytes.length; i++) {
|
|
10529
|
+
if (keyBytes[i] === 0x03) {
|
|
10530
|
+
bitStringPos = i;
|
|
10531
|
+
break;
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
if (bitStringPos === -1)
|
|
10535
|
+
return 0;
|
|
10536
|
+
// Skip BIT STRING header to find inner SEQUENCE
|
|
10537
|
+
let pos = bitStringPos + 1;
|
|
10538
|
+
if ((keyBytes[pos] & 0x80) === 0) {
|
|
10539
|
+
pos += 1; // short length
|
|
10540
|
+
}
|
|
10541
|
+
else {
|
|
10542
|
+
pos += 1 + (keyBytes[pos] & 0x7f); // long length
|
|
10543
|
+
}
|
|
10544
|
+
pos += 1; // skip unused bits byte
|
|
10545
|
+
if (keyBytes[pos] !== 0x30)
|
|
10546
|
+
return 0; // Should be SEQUENCE
|
|
10547
|
+
// Skip inner SEQUENCE header to find modulus INTEGER
|
|
10548
|
+
pos += 1;
|
|
10549
|
+
if ((keyBytes[pos] & 0x80) === 0) {
|
|
10550
|
+
pos += 1;
|
|
10551
|
+
}
|
|
10552
|
+
else {
|
|
10553
|
+
pos += 1 + (keyBytes[pos] & 0x7f);
|
|
10554
|
+
}
|
|
10555
|
+
if (keyBytes[pos] !== 0x02)
|
|
10556
|
+
return 0; // Should be INTEGER (modulus)
|
|
10557
|
+
// Get modulus length
|
|
10558
|
+
pos += 1;
|
|
10559
|
+
let modulusLength = 0;
|
|
10560
|
+
if ((keyBytes[pos] & 0x80) === 0) {
|
|
10561
|
+
modulusLength = keyBytes[pos];
|
|
10562
|
+
}
|
|
10563
|
+
else {
|
|
10564
|
+
const numLenBytes = keyBytes[pos] & 0x7f;
|
|
10565
|
+
for (let i = 0; i < numLenBytes; i++) {
|
|
10566
|
+
modulusLength = (modulusLength << 8) | keyBytes[pos + 1 + i];
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
// Modulus might have leading 0x00 padding byte, subtract if present
|
|
10570
|
+
// Return bits (bytes * 8), accounting for padding
|
|
10571
|
+
return modulusLength * 8;
|
|
10572
|
+
}
|
|
10573
|
+
/**
|
|
10574
|
+
* Check if RSA key size is supported in the current platform
|
|
10575
|
+
* Safari/WebKit only supports RSA keys up to 4096 bits
|
|
10576
|
+
*/
|
|
10577
|
+
function isRSAKeySizeSupported(modulusLengthBits) {
|
|
10578
|
+
if (!isBrowser())
|
|
10579
|
+
return true; // Node.js supports all sizes
|
|
10580
|
+
if (!isWebKit())
|
|
10581
|
+
return true; // Chrome/Firefox support large keys
|
|
10582
|
+
// Safari/WebKit: max 4096 bits
|
|
10583
|
+
return modulusLengthBits <= 4096;
|
|
10584
|
+
}
|
|
10233
10585
|
/**
|
|
10234
10586
|
* Compute a digest (hash) of file content with browser/node compatibility
|
|
10235
10587
|
* @param fileContent The file content as Uint8Array
|
|
@@ -10602,11 +10954,40 @@ async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, alg
|
|
|
10602
10954
|
let publicKey;
|
|
10603
10955
|
try {
|
|
10604
10956
|
const subtle = getCryptoSubtle();
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10957
|
+
const isRSA = algorithm.name === "RSASSA-PKCS1-v1_5" || algorithm.name === "RSA-PSS";
|
|
10958
|
+
// Check RSA key size support before attempting import
|
|
10959
|
+
if (isRSA) {
|
|
10960
|
+
const modulusLengthBits = getRSAModulusLength(publicKeyData);
|
|
10961
|
+
if (modulusLengthBits > 0 && !isRSAKeySizeSupported(modulusLengthBits)) {
|
|
10962
|
+
return {
|
|
10963
|
+
isValid: false,
|
|
10964
|
+
unsupportedPlatform: true,
|
|
10965
|
+
reason: `RSA key size (${modulusLengthBits} bits) not supported in this browser`,
|
|
10966
|
+
errorDetails: {
|
|
10967
|
+
category: "RSA_KEY_SIZE_UNSUPPORTED",
|
|
10968
|
+
originalMessage: `Safari/WebKit only supports RSA keys up to 4096 bits`,
|
|
10969
|
+
algorithm: { ...algorithm },
|
|
10970
|
+
environment: "browser",
|
|
10971
|
+
keyLength: publicKeyData.byteLength,
|
|
10972
|
+
},
|
|
10973
|
+
};
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
if (isBrowser() && isRSA) {
|
|
10977
|
+
// Try importing original key first, then try with padding fix if it fails
|
|
10978
|
+
// This handles browser differences (Chrome vs Safari/WebKit)
|
|
10979
|
+
try {
|
|
10980
|
+
publicKey = await subtle.importKey("spki", publicKeyData, algorithm, false, ["verify"]);
|
|
10981
|
+
}
|
|
10982
|
+
catch {
|
|
10983
|
+
// Original key failed, try with modulus padding fix
|
|
10984
|
+
const fixedKeyData = fixRSAModulusPadding(publicKeyData);
|
|
10985
|
+
publicKey = await subtle.importKey("spki", fixedKeyData, algorithm, false, ["verify"]);
|
|
10986
|
+
}
|
|
10987
|
+
}
|
|
10988
|
+
else {
|
|
10989
|
+
publicKey = await subtle.importKey("spki", publicKeyData, algorithm, false, ["verify"]);
|
|
10608
10990
|
}
|
|
10609
|
-
publicKey = await subtle.importKey("spki", publicKeyData, algorithm, false, ["verify"]);
|
|
10610
10991
|
}
|
|
10611
10992
|
catch (unknownError) {
|
|
10612
10993
|
// First cast to Error type if applicable
|
|
@@ -10653,12 +11034,36 @@ async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, alg
|
|
|
10653
11034
|
try {
|
|
10654
11035
|
const subtle = getCryptoSubtle();
|
|
10655
11036
|
const result = await subtle.verify(algorithm, publicKey, signatureBytes, signedData);
|
|
11037
|
+
if (result) {
|
|
11038
|
+
return {
|
|
11039
|
+
isValid: true,
|
|
11040
|
+
};
|
|
11041
|
+
}
|
|
11042
|
+
// Standard verification failed - try fallback for RSA signatures
|
|
11043
|
+
// Some older signatures use non-standard DigestInfo format (missing NULL in AlgorithmIdentifier)
|
|
11044
|
+
if (algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
11045
|
+
const fallbackResult = await verifyRsaWithNonStandardDigestInfo(publicKeyData, signatureBytes, signedData, algorithm.hash);
|
|
11046
|
+
if (fallbackResult) {
|
|
11047
|
+
return {
|
|
11048
|
+
isValid: true,
|
|
11049
|
+
};
|
|
11050
|
+
}
|
|
11051
|
+
}
|
|
10656
11052
|
return {
|
|
10657
|
-
isValid:
|
|
10658
|
-
reason:
|
|
11053
|
+
isValid: false,
|
|
11054
|
+
reason: "Signature verification failed",
|
|
10659
11055
|
};
|
|
10660
11056
|
}
|
|
10661
11057
|
catch (error) {
|
|
11058
|
+
// Try fallback for RSA signatures when subtle.verify throws
|
|
11059
|
+
if (algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
11060
|
+
const fallbackResult = await verifyRsaWithNonStandardDigestInfo(publicKeyData, signatureBytes, signedData, algorithm.hash);
|
|
11061
|
+
if (fallbackResult) {
|
|
11062
|
+
return {
|
|
11063
|
+
isValid: true,
|
|
11064
|
+
};
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
10662
11067
|
return {
|
|
10663
11068
|
isValid: false,
|
|
10664
11069
|
reason: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -10852,9 +11257,81 @@ async function verifySignature(signatureInfo, files, options = {}) {
|
|
|
10852
11257
|
options.verifyTimestamps === false ||
|
|
10853
11258
|
(timestampResult?.isValid ?? true);
|
|
10854
11259
|
const isValid = certResult.isValid && checksumResult.isValid && signatureResult.isValid && timestampValid;
|
|
11260
|
+
// Determine validation status and limitations
|
|
11261
|
+
let status = "VALID";
|
|
11262
|
+
let statusMessage;
|
|
11263
|
+
const limitations = [];
|
|
11264
|
+
if (!isValid) {
|
|
11265
|
+
// Check for platform unsupported (RSA key size)
|
|
11266
|
+
if (signatureResult.unsupportedPlatform) {
|
|
11267
|
+
status = "UNSUPPORTED";
|
|
11268
|
+
statusMessage = signatureResult.reason;
|
|
11269
|
+
limitations.push({
|
|
11270
|
+
code: "RSA_KEY_SIZE_UNSUPPORTED",
|
|
11271
|
+
description: signatureResult.reason || "RSA key size not supported",
|
|
11272
|
+
platform: "Safari/WebKit",
|
|
11273
|
+
});
|
|
11274
|
+
}
|
|
11275
|
+
// Check for checksum failure (definitely invalid)
|
|
11276
|
+
else if (!checksumResult.isValid) {
|
|
11277
|
+
status = "INVALID";
|
|
11278
|
+
statusMessage = "File integrity check failed";
|
|
11279
|
+
}
|
|
11280
|
+
// Check for signature crypto failure with supported key
|
|
11281
|
+
else if (!signatureResult.isValid && !signatureResult.unsupportedPlatform) {
|
|
11282
|
+
status = "INVALID";
|
|
11283
|
+
statusMessage = signatureResult.reason || "Signature verification failed";
|
|
11284
|
+
}
|
|
11285
|
+
// Check for certificate issues
|
|
11286
|
+
else if (!certResult.isValid) {
|
|
11287
|
+
// If cert expired and no valid timestamp, it's indeterminate
|
|
11288
|
+
if (certResult.reason?.includes("expired") && !timestampResult?.isValid) {
|
|
11289
|
+
status = "INDETERMINATE";
|
|
11290
|
+
statusMessage = "Certificate expired and no valid timestamp proof";
|
|
11291
|
+
limitations.push({
|
|
11292
|
+
code: "CERT_EXPIRED_NO_POE",
|
|
11293
|
+
description: "Certificate has expired and there is no valid timestamp to prove signature was made when certificate was valid",
|
|
11294
|
+
});
|
|
11295
|
+
}
|
|
11296
|
+
else if (certResult.revocation?.status === "revoked") {
|
|
11297
|
+
status = "INVALID";
|
|
11298
|
+
statusMessage = "Certificate has been revoked";
|
|
11299
|
+
}
|
|
11300
|
+
else {
|
|
11301
|
+
status = "INDETERMINATE";
|
|
11302
|
+
statusMessage = certResult.reason || "Certificate validation inconclusive";
|
|
11303
|
+
}
|
|
11304
|
+
}
|
|
11305
|
+
// Timestamp parsing/verification failed - can't establish POE
|
|
11306
|
+
else if (timestampResult && !timestampResult.isValid) {
|
|
11307
|
+
status = "INDETERMINATE";
|
|
11308
|
+
statusMessage = timestampResult.reason || "Timestamp verification failed";
|
|
11309
|
+
limitations.push({
|
|
11310
|
+
code: "TIMESTAMP_VERIFICATION_FAILED",
|
|
11311
|
+
description: timestampResult.reason || "Could not verify timestamp to establish proof of existence",
|
|
11312
|
+
});
|
|
11313
|
+
}
|
|
11314
|
+
// Revocation unknown
|
|
11315
|
+
else if (certResult.revocation?.status === "unknown") {
|
|
11316
|
+
status = "INDETERMINATE";
|
|
11317
|
+
statusMessage = "Certificate revocation status could not be determined";
|
|
11318
|
+
limitations.push({
|
|
11319
|
+
code: "REVOCATION_UNKNOWN",
|
|
11320
|
+
description: certResult.revocation.reason || "Could not check certificate revocation status",
|
|
11321
|
+
});
|
|
11322
|
+
}
|
|
11323
|
+
// Fallback
|
|
11324
|
+
else {
|
|
11325
|
+
status = "INVALID";
|
|
11326
|
+
statusMessage = errors[0] || "Verification failed";
|
|
11327
|
+
}
|
|
11328
|
+
}
|
|
10855
11329
|
// Return the complete result
|
|
10856
11330
|
return {
|
|
10857
11331
|
isValid,
|
|
11332
|
+
status,
|
|
11333
|
+
statusMessage,
|
|
11334
|
+
limitations: limitations.length > 0 ? limitations : undefined,
|
|
10858
11335
|
certificate: certResult,
|
|
10859
11336
|
checksums: checksumResult,
|
|
10860
11337
|
signature: options.verifySignatures !== false ? signatureResult : undefined,
|