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/CHANGELOG.md +18 -0
- package/dist/core/parser/types.d.ts +1 -0
- package/dist/core/timestamp/types.d.ts +2 -2
- package/dist/core/timestamp/verify.d.ts +5 -5
- package/dist/index.cjs.js +201 -39
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +201 -39
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +14 -14
- package/dist/index.umd.js.map +1 -1
- package/dist/utils/encoding.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.4] - 2025-12-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **OCSP issuerKeyHash calculation** - Fixed critical bug where OCSP requests used wrong hash (full SPKI instead of public key BIT STRING), causing incorrect revocation status responses
|
|
13
|
+
- **Timestamp signature coverage verification** - Now correctly verifies that timestamps cover the canonicalized ds:SignatureValue XML element per XAdES (ETSI EN 319 132-1) specification, fixing `coversSignature: false` issue
|
|
14
|
+
- **TSA name formatting** - Fixed timestamp TSA name showing as `[object Object]` instead of readable DN string like `CN=..., O=..., C=...`
|
|
15
|
+
- **Base64 whitespace handling** - Fixed browser `atob` errors when decoding base64 strings containing whitespace from XML
|
|
16
|
+
- **ECDSA signature format normalization** - Fixed signature verification failures for ECDSA signatures with leading zero padding by normalizing to IEEE P1363 format expected by Web Crypto API
|
|
17
|
+
|
|
18
|
+
## [0.2.3] - 2025-12-30
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Long-Term Validation (LTV) for revoked certificates** - Signatures made before certificate revocation are now correctly validated as valid when a trusted timestamp proves the signing time
|
|
23
|
+
|
|
8
24
|
## [0.2.2] - 2025-12-30
|
|
9
25
|
|
|
10
26
|
### Fixed
|
|
@@ -54,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
70
|
- File checksum verification (SHA-256/384/512)
|
|
55
71
|
- Browser and Node.js support
|
|
56
72
|
|
|
73
|
+
[0.2.4]: https://github.com/edgarsj/edockit/compare/v0.2.3...v0.2.4
|
|
74
|
+
[0.2.3]: https://github.com/edgarsj/edockit/compare/v0.2.2...v0.2.3
|
|
57
75
|
[0.2.2]: https://github.com/edgarsj/edockit/compare/v0.2.1...v0.2.2
|
|
58
76
|
[0.2.1]: https://github.com/edgarsj/edockit/compare/v0.2.0...v0.2.1
|
|
59
77
|
[0.2.0]: https://github.com/edgarsj/edockit/compare/v0.1.2...v0.2.0
|
|
@@ -39,8 +39,8 @@ export interface TimestampVerificationResult {
|
|
|
39
39
|
* Options for timestamp verification
|
|
40
40
|
*/
|
|
41
41
|
export interface TimestampVerificationOptions {
|
|
42
|
-
/** The
|
|
43
|
-
|
|
42
|
+
/** The canonicalized ds:SignatureValue XML element (per XAdES spec) */
|
|
43
|
+
canonicalSignatureValue?: string;
|
|
44
44
|
/** Verify the TSA certificate chain */
|
|
45
45
|
verifyTsaCertificate?: boolean;
|
|
46
46
|
/** Check TSA certificate revocation */
|
|
@@ -7,15 +7,15 @@ import { TimestampInfo, TimestampVerificationResult, TimestampVerificationOption
|
|
|
7
7
|
export declare function parseTimestamp(timestampBase64: string): TimestampInfo | null;
|
|
8
8
|
/**
|
|
9
9
|
* Verify that timestamp covers the signature value
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Per XAdES (ETSI EN 319 132-1), the SignatureTimeStamp covers the canonicalized
|
|
12
|
+
* ds:SignatureValue XML element, not just its base64 content.
|
|
13
13
|
*
|
|
14
14
|
* @param timestampInfo Parsed timestamp info
|
|
15
|
-
* @param
|
|
15
|
+
* @param canonicalSignatureValue Canonicalized ds:SignatureValue XML element
|
|
16
16
|
* @returns True if the timestamp covers the signature
|
|
17
17
|
*/
|
|
18
|
-
export declare function verifyTimestampCoversSignature(timestampInfo: TimestampInfo,
|
|
18
|
+
export declare function verifyTimestampCoversSignature(timestampInfo: TimestampInfo, canonicalSignatureValue: string): Promise<boolean>;
|
|
19
19
|
/**
|
|
20
20
|
* Verify an RFC 3161 timestamp token
|
|
21
21
|
* @param timestampBase64 Base64-encoded timestamp token
|
package/dist/index.cjs.js
CHANGED
|
@@ -1212,6 +1212,17 @@ function parseSignatureElement(signatureElement, xmlDoc) {
|
|
|
1212
1212
|
// Get signature value
|
|
1213
1213
|
const signatureValueEl = querySelector(signatureElement, "ds\\:SignatureValue, SignatureValue");
|
|
1214
1214
|
const signatureValue = signatureValueEl?.textContent?.replace(/\s+/g, "") || "";
|
|
1215
|
+
// Compute canonicalized SignatureValue element for timestamp verification
|
|
1216
|
+
let canonicalSignatureValue;
|
|
1217
|
+
if (signatureValueEl) {
|
|
1218
|
+
try {
|
|
1219
|
+
const canonicalizer = new XMLCanonicalizer();
|
|
1220
|
+
canonicalSignatureValue = canonicalizer.canonicalize(signatureValueEl);
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
// Canonicalization failed - leave undefined
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1215
1226
|
// Get certificate(s)
|
|
1216
1227
|
let certificate = "";
|
|
1217
1228
|
let certificatePEM = "";
|
|
@@ -1343,6 +1354,7 @@ function parseSignatureElement(signatureElement, xmlDoc) {
|
|
|
1343
1354
|
references,
|
|
1344
1355
|
algorithm: signatureAlgorithm,
|
|
1345
1356
|
signatureValue,
|
|
1357
|
+
canonicalSignatureValue,
|
|
1346
1358
|
signedInfoXml,
|
|
1347
1359
|
canonicalizationMethod,
|
|
1348
1360
|
signatureTimestamp,
|
|
@@ -8068,11 +8080,14 @@ function arrayBufferToBase64(buffer) {
|
|
|
8068
8080
|
}
|
|
8069
8081
|
/**
|
|
8070
8082
|
* Convert base64 string to ArrayBuffer
|
|
8083
|
+
* Handles base64 strings with whitespace (common in XML)
|
|
8071
8084
|
*/
|
|
8072
8085
|
function base64ToArrayBuffer(base64) {
|
|
8086
|
+
// Strip whitespace (newlines, spaces, tabs) that may be present in XML
|
|
8087
|
+
const cleanBase64 = base64.replace(/\s/g, "");
|
|
8073
8088
|
if (typeof atob === "function") {
|
|
8074
8089
|
// Browser
|
|
8075
|
-
const binary = atob(
|
|
8090
|
+
const binary = atob(cleanBase64);
|
|
8076
8091
|
const bytes = new Uint8Array(binary.length);
|
|
8077
8092
|
for (let i = 0; i < binary.length; i++) {
|
|
8078
8093
|
bytes[i] = binary.charCodeAt(i);
|
|
@@ -8080,7 +8095,7 @@ function base64ToArrayBuffer(base64) {
|
|
|
8080
8095
|
return bytes.buffer;
|
|
8081
8096
|
}
|
|
8082
8097
|
// Node.js
|
|
8083
|
-
const buffer = Buffer.from(
|
|
8098
|
+
const buffer = Buffer.from(cleanBase64, "base64");
|
|
8084
8099
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
8085
8100
|
}
|
|
8086
8101
|
/**
|
|
@@ -8233,10 +8248,12 @@ async function fetchIssuerFromAIA(cert, timeout = 5000, proxyUrl) {
|
|
|
8233
8248
|
*/
|
|
8234
8249
|
async function buildOCSPRequest(cert, issuerCert) {
|
|
8235
8250
|
// Get issuer name hash (SHA-1 of issuer's DN in DER)
|
|
8236
|
-
|
|
8251
|
+
// Parse the raw certificate to get the proper ASN.1 structures for serialization
|
|
8252
|
+
const issuerCertAsn = AsnParser.parse(issuerCert.rawData, Certificate);
|
|
8253
|
+
const issuerNameDer = AsnConvert.serialize(issuerCertAsn.tbsCertificate.subject);
|
|
8237
8254
|
const issuerNameHash = await computeSHA1(issuerNameDer);
|
|
8238
|
-
// Get issuer key hash (SHA-1 of issuer's public key)
|
|
8239
|
-
const issuerKeyHash = await computeSHA1(
|
|
8255
|
+
// Get issuer key hash (SHA-1 of issuer's public key BIT STRING value, not the full SPKI)
|
|
8256
|
+
const issuerKeyHash = await computeSHA1(issuerCertAsn.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey);
|
|
8240
8257
|
// Get certificate serial number
|
|
8241
8258
|
const serialNumber = hexToArrayBuffer(cert.serialNumber);
|
|
8242
8259
|
// Build CertID
|
|
@@ -9948,6 +9965,38 @@ function getHashAlgorithmName(oid) {
|
|
|
9948
9965
|
};
|
|
9949
9966
|
return hashAlgorithms[oid] || oid;
|
|
9950
9967
|
}
|
|
9968
|
+
/**
|
|
9969
|
+
* Common OID to attribute name mappings for X.500 distinguished names
|
|
9970
|
+
*/
|
|
9971
|
+
const oidToAttributeName = {
|
|
9972
|
+
"2.5.4.3": "CN",
|
|
9973
|
+
"2.5.4.6": "C",
|
|
9974
|
+
"2.5.4.7": "L",
|
|
9975
|
+
"2.5.4.8": "ST",
|
|
9976
|
+
"2.5.4.10": "O",
|
|
9977
|
+
"2.5.4.11": "OU",
|
|
9978
|
+
"2.5.4.5": "serialNumber",
|
|
9979
|
+
"1.2.840.113549.1.9.1": "emailAddress",
|
|
9980
|
+
};
|
|
9981
|
+
/**
|
|
9982
|
+
* Format an X.500 Name (directoryName) to a readable string
|
|
9983
|
+
* @param name The Name object to format
|
|
9984
|
+
* @returns Formatted string like "CN=Example, O=Company, C=US"
|
|
9985
|
+
*/
|
|
9986
|
+
function formatDirectoryName(name) {
|
|
9987
|
+
const parts = [];
|
|
9988
|
+
for (const rdn of name) {
|
|
9989
|
+
for (const attr of rdn) {
|
|
9990
|
+
const attrName = oidToAttributeName[attr.type] || attr.type;
|
|
9991
|
+
// attr.value can be various ASN.1 string types
|
|
9992
|
+
const value = attr.value?.toString() || "";
|
|
9993
|
+
if (value) {
|
|
9994
|
+
parts.push(`${attrName}=${value}`);
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
return parts.join(", ");
|
|
9999
|
+
}
|
|
9951
10000
|
/**
|
|
9952
10001
|
* Parse RFC 3161 TimeStampToken from base64
|
|
9953
10002
|
* @param timestampBase64 Base64-encoded timestamp token
|
|
@@ -10003,7 +10052,7 @@ function parseTimestamp(timestampBase64) {
|
|
|
10003
10052
|
let tsaName;
|
|
10004
10053
|
if (tstInfo.tsa) {
|
|
10005
10054
|
if (tstInfo.tsa.directoryName) {
|
|
10006
|
-
tsaName = tstInfo.tsa.directoryName
|
|
10055
|
+
tsaName = formatDirectoryName(tstInfo.tsa.directoryName);
|
|
10007
10056
|
}
|
|
10008
10057
|
else if (tstInfo.tsa.uniformResourceIdentifier) {
|
|
10009
10058
|
tsaName = tstInfo.tsa.uniformResourceIdentifier;
|
|
@@ -10059,33 +10108,22 @@ async function computeHash(data, algorithm) {
|
|
|
10059
10108
|
}
|
|
10060
10109
|
/**
|
|
10061
10110
|
* Verify that timestamp covers the signature value
|
|
10062
|
-
*
|
|
10063
|
-
*
|
|
10064
|
-
*
|
|
10111
|
+
*
|
|
10112
|
+
* Per XAdES (ETSI EN 319 132-1), the SignatureTimeStamp covers the canonicalized
|
|
10113
|
+
* ds:SignatureValue XML element, not just its base64 content.
|
|
10065
10114
|
*
|
|
10066
10115
|
* @param timestampInfo Parsed timestamp info
|
|
10067
|
-
* @param
|
|
10116
|
+
* @param canonicalSignatureValue Canonicalized ds:SignatureValue XML element
|
|
10068
10117
|
* @returns True if the timestamp covers the signature
|
|
10069
10118
|
*/
|
|
10070
|
-
async function verifyTimestampCoversSignature(timestampInfo,
|
|
10119
|
+
async function verifyTimestampCoversSignature(timestampInfo, canonicalSignatureValue) {
|
|
10071
10120
|
try {
|
|
10072
10121
|
const messageImprintLower = timestampInfo.messageImprint.toLowerCase();
|
|
10073
|
-
// Try 1: Hash of decoded signature value bytes (standard approach)
|
|
10074
|
-
const signatureValue = base64ToArrayBuffer(signatureValueBase64);
|
|
10075
|
-
const computedHash = await computeHash(signatureValue, timestampInfo.hashAlgorithm);
|
|
10076
|
-
const computedHashHex = arrayBufferToHex(computedHash);
|
|
10077
|
-
if (computedHashHex.toLowerCase() === messageImprintLower) {
|
|
10078
|
-
return true;
|
|
10079
|
-
}
|
|
10080
|
-
// Try 2: Hash of base64 string (some implementations)
|
|
10081
10122
|
const encoder = new TextEncoder();
|
|
10082
|
-
const
|
|
10083
|
-
const
|
|
10084
|
-
const
|
|
10085
|
-
|
|
10086
|
-
return true;
|
|
10087
|
-
}
|
|
10088
|
-
return false;
|
|
10123
|
+
const canonicalBytes = encoder.encode(canonicalSignatureValue);
|
|
10124
|
+
const canonicalHash = await computeHash(canonicalBytes.buffer, timestampInfo.hashAlgorithm);
|
|
10125
|
+
const canonicalHashHex = arrayBufferToHex(canonicalHash);
|
|
10126
|
+
return canonicalHashHex.toLowerCase() === messageImprintLower;
|
|
10089
10127
|
}
|
|
10090
10128
|
catch (error) {
|
|
10091
10129
|
console.error("Failed to verify timestamp coverage:", error instanceof Error ? error.message : String(error));
|
|
@@ -10108,15 +10146,12 @@ async function verifyTimestamp(timestampBase64, options = {}) {
|
|
|
10108
10146
|
};
|
|
10109
10147
|
}
|
|
10110
10148
|
// Verify timestamp covers the signature if provided
|
|
10111
|
-
// Note: coversSignature failure is informational - the timestamp is still valid
|
|
10112
|
-
// and can be used for genTime. The signature value hashing varies by implementation.
|
|
10113
10149
|
let coversSignature;
|
|
10114
10150
|
let coversSignatureReason;
|
|
10115
|
-
if (options.
|
|
10116
|
-
coversSignature = await verifyTimestampCoversSignature(info, options.
|
|
10151
|
+
if (options.canonicalSignatureValue) {
|
|
10152
|
+
coversSignature = await verifyTimestampCoversSignature(info, options.canonicalSignatureValue);
|
|
10117
10153
|
if (!coversSignature) {
|
|
10118
|
-
coversSignatureReason =
|
|
10119
|
-
"Could not verify timestamp covers signature (implementation-specific hashing)";
|
|
10154
|
+
coversSignatureReason = "Could not verify timestamp covers signature (hash mismatch)";
|
|
10120
10155
|
}
|
|
10121
10156
|
}
|
|
10122
10157
|
// Verify TSA certificate if requested
|
|
@@ -10148,7 +10183,7 @@ async function verifyTimestamp(timestampBase64, options = {}) {
|
|
|
10148
10183
|
};
|
|
10149
10184
|
}
|
|
10150
10185
|
// Note: 'unknown' status is a soft fail - timestamp remains valid
|
|
10151
|
-
// but user can check tsaRevocation.status to see
|
|
10186
|
+
// but user can check tsaRevocation.status to see the actual status
|
|
10152
10187
|
}
|
|
10153
10188
|
catch (error) {
|
|
10154
10189
|
// Revocation check failed - soft fail, add to result but don't invalidate
|
|
@@ -10403,6 +10438,115 @@ async function verifyCertificate(certificatePEM, verifyTime = new Date()) {
|
|
|
10403
10438
|
};
|
|
10404
10439
|
}
|
|
10405
10440
|
}
|
|
10441
|
+
/**
|
|
10442
|
+
* Get the expected component size for an ECDSA curve
|
|
10443
|
+
*/
|
|
10444
|
+
function getEcdsaComponentSize(namedCurve) {
|
|
10445
|
+
switch (namedCurve) {
|
|
10446
|
+
case "P-256":
|
|
10447
|
+
return 32;
|
|
10448
|
+
case "P-384":
|
|
10449
|
+
return 48;
|
|
10450
|
+
case "P-521":
|
|
10451
|
+
return 66;
|
|
10452
|
+
default:
|
|
10453
|
+
return 32; // Default to P-256
|
|
10454
|
+
}
|
|
10455
|
+
}
|
|
10456
|
+
/**
|
|
10457
|
+
* Normalize ECDSA signature to IEEE P1363 format (raw R||S) expected by Web Crypto API
|
|
10458
|
+
* Handles both raw format with potential padding and DER-encoded signatures
|
|
10459
|
+
*/
|
|
10460
|
+
function normalizeEcdsaSignature(signatureBytes, namedCurve) {
|
|
10461
|
+
const componentSize = getEcdsaComponentSize(namedCurve);
|
|
10462
|
+
const expectedLength = componentSize * 2;
|
|
10463
|
+
// If already correct length, return as-is
|
|
10464
|
+
if (signatureBytes.length === expectedLength) {
|
|
10465
|
+
return signatureBytes;
|
|
10466
|
+
}
|
|
10467
|
+
// Check if it's DER-encoded (starts with SEQUENCE tag 0x30)
|
|
10468
|
+
if (signatureBytes[0] === 0x30) {
|
|
10469
|
+
return derToRawEcdsa(signatureBytes, componentSize);
|
|
10470
|
+
}
|
|
10471
|
+
// Handle raw R||S with potential leading zeros
|
|
10472
|
+
// Some implementations pad R and S with leading zeros
|
|
10473
|
+
if (signatureBytes.length > expectedLength) {
|
|
10474
|
+
const halfLength = signatureBytes.length / 2;
|
|
10475
|
+
if (Number.isInteger(halfLength)) {
|
|
10476
|
+
const r = signatureBytes.slice(0, halfLength);
|
|
10477
|
+
const s = signatureBytes.slice(halfLength);
|
|
10478
|
+
// Normalize R and S to exact component size
|
|
10479
|
+
const normalizedR = normalizeComponent(r, componentSize);
|
|
10480
|
+
const normalizedS = normalizeComponent(s, componentSize);
|
|
10481
|
+
const result = new Uint8Array(expectedLength);
|
|
10482
|
+
result.set(normalizedR, 0);
|
|
10483
|
+
result.set(normalizedS, componentSize);
|
|
10484
|
+
return result;
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
// Return as-is if we can't normalize
|
|
10488
|
+
return signatureBytes;
|
|
10489
|
+
}
|
|
10490
|
+
/**
|
|
10491
|
+
* Normalize a single ECDSA component (R or S) to exact size
|
|
10492
|
+
* Strips leading zeros or pads with leading zeros as needed
|
|
10493
|
+
*/
|
|
10494
|
+
function normalizeComponent(component, size) {
|
|
10495
|
+
// Strip leading zeros
|
|
10496
|
+
let start = 0;
|
|
10497
|
+
while (start < component.length - 1 && component[start] === 0) {
|
|
10498
|
+
start++;
|
|
10499
|
+
}
|
|
10500
|
+
const stripped = component.slice(start);
|
|
10501
|
+
if (stripped.length === size) {
|
|
10502
|
+
return stripped;
|
|
10503
|
+
}
|
|
10504
|
+
else if (stripped.length < size) {
|
|
10505
|
+
// Pad with leading zeros
|
|
10506
|
+
const padded = new Uint8Array(size);
|
|
10507
|
+
padded.set(stripped, size - stripped.length);
|
|
10508
|
+
return padded;
|
|
10509
|
+
}
|
|
10510
|
+
else {
|
|
10511
|
+
// Component too large - take the last 'size' bytes
|
|
10512
|
+
return stripped.slice(stripped.length - size);
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
/**
|
|
10516
|
+
* Convert DER-encoded ECDSA signature to raw IEEE P1363 format
|
|
10517
|
+
*/
|
|
10518
|
+
function derToRawEcdsa(derSignature, componentSize) {
|
|
10519
|
+
// DER structure: SEQUENCE { INTEGER r, INTEGER s }
|
|
10520
|
+
// 0x30 len 0x02 rLen r... 0x02 sLen s...
|
|
10521
|
+
let offset = 0;
|
|
10522
|
+
// Skip SEQUENCE tag
|
|
10523
|
+
if (derSignature[offset++] !== 0x30) {
|
|
10524
|
+
throw new Error("Invalid DER signature: missing SEQUENCE tag");
|
|
10525
|
+
}
|
|
10526
|
+
// Skip sequence length (may be 1 or 2 bytes)
|
|
10527
|
+
const seqLen = derSignature[offset++];
|
|
10528
|
+
if (seqLen & 0x80) {
|
|
10529
|
+
offset += seqLen & 0x7f; // Skip length bytes
|
|
10530
|
+
}
|
|
10531
|
+
// Parse R
|
|
10532
|
+
if (derSignature[offset++] !== 0x02) {
|
|
10533
|
+
throw new Error("Invalid DER signature: missing INTEGER tag for R");
|
|
10534
|
+
}
|
|
10535
|
+
const rLen = derSignature[offset++];
|
|
10536
|
+
const r = derSignature.slice(offset, offset + rLen);
|
|
10537
|
+
offset += rLen;
|
|
10538
|
+
// Parse S
|
|
10539
|
+
if (derSignature[offset++] !== 0x02) {
|
|
10540
|
+
throw new Error("Invalid DER signature: missing INTEGER tag for S");
|
|
10541
|
+
}
|
|
10542
|
+
const sLen = derSignature[offset++];
|
|
10543
|
+
const s = derSignature.slice(offset, offset + sLen);
|
|
10544
|
+
// Normalize and concatenate
|
|
10545
|
+
const result = new Uint8Array(componentSize * 2);
|
|
10546
|
+
result.set(normalizeComponent(r, componentSize), 0);
|
|
10547
|
+
result.set(normalizeComponent(s, componentSize), componentSize);
|
|
10548
|
+
return result;
|
|
10549
|
+
}
|
|
10406
10550
|
/**
|
|
10407
10551
|
* Safely get the crypto.subtle implementation in either browser or Node.js
|
|
10408
10552
|
* @returns The SubtleCrypto interface
|
|
@@ -10454,6 +10598,10 @@ async function verifySignedInfo(signatureXml, signatureValue, publicKeyData, alg
|
|
|
10454
10598
|
reason: `Failed to decode signature value: ${error instanceof Error ? error.message : String(error)}`,
|
|
10455
10599
|
};
|
|
10456
10600
|
}
|
|
10601
|
+
// For ECDSA, normalize signature to IEEE P1363 format (raw R||S) expected by Web Crypto
|
|
10602
|
+
if (algorithm.name === "ECDSA") {
|
|
10603
|
+
signatureBytes = normalizeEcdsaSignature(signatureBytes, algorithm.namedCurve);
|
|
10604
|
+
}
|
|
10457
10605
|
// Import the public key
|
|
10458
10606
|
let publicKey;
|
|
10459
10607
|
try {
|
|
@@ -10542,7 +10690,7 @@ async function verifySignature(signatureInfo, files, options = {}) {
|
|
|
10542
10690
|
let trustedSigningTime = options.verifyTime || signatureInfo.signingTime;
|
|
10543
10691
|
if (signatureInfo.signatureTimestamp && options.verifyTimestamps !== false) {
|
|
10544
10692
|
timestampResult = await verifyTimestamp(signatureInfo.signatureTimestamp, {
|
|
10545
|
-
|
|
10693
|
+
canonicalSignatureValue: signatureInfo.canonicalSignatureValue,
|
|
10546
10694
|
verifyTsaCertificate: true,
|
|
10547
10695
|
revocationOptions: options.revocationOptions,
|
|
10548
10696
|
});
|
|
@@ -10569,14 +10717,28 @@ async function verifySignature(signatureInfo, files, options = {}) {
|
|
|
10569
10717
|
...options.revocationOptions,
|
|
10570
10718
|
});
|
|
10571
10719
|
certResult.revocation = revocationResult;
|
|
10572
|
-
// If certificate is revoked,
|
|
10720
|
+
// If certificate is revoked, check if signature was made before revocation (LTV)
|
|
10573
10721
|
if (revocationResult.status === "revoked") {
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10722
|
+
const revokedAt = revocationResult.revokedAt;
|
|
10723
|
+
const hasValidRevocationTime = revokedAt && revokedAt.getTime() > 0;
|
|
10724
|
+
// Long-Term Validation: if we have a trusted timestamp proving the signature
|
|
10725
|
+
// was made before revocation, the signature is still valid
|
|
10726
|
+
if (hasValidRevocationTime && trustedSigningTime < revokedAt) {
|
|
10727
|
+
// Signature was made before revocation - still valid (LTV)
|
|
10728
|
+
certResult.revocation.isValid = true;
|
|
10729
|
+
certResult.revocation.reason = `Certificate was revoked on ${revokedAt.toISOString()}, but signature was made on ${trustedSigningTime.toISOString()} (before revocation)`;
|
|
10730
|
+
}
|
|
10731
|
+
else {
|
|
10732
|
+
// Signature was made after revocation or no valid revocation date available
|
|
10733
|
+
certResult.isValid = false;
|
|
10734
|
+
const revokedAtStr = hasValidRevocationTime ? ` on ${revokedAt.toISOString()}` : "";
|
|
10735
|
+
certResult.reason =
|
|
10736
|
+
revocationResult.reason || `Certificate has been revoked${revokedAtStr}`;
|
|
10737
|
+
errors.push(revocationResult.reason || `Certificate revoked${revokedAtStr}`);
|
|
10738
|
+
}
|
|
10577
10739
|
}
|
|
10578
10740
|
// Note: 'unknown' status is a soft fail - certificate remains valid
|
|
10579
|
-
// but user can check revocation.status to see
|
|
10741
|
+
// but user can check revocation.status to see the actual status
|
|
10580
10742
|
}
|
|
10581
10743
|
catch (error) {
|
|
10582
10744
|
// Revocation check failed - soft fail, add to result but don't invalidate
|