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/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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
10059
|
-
*
|
|
10060
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
10079
|
-
const
|
|
10080
|
-
const
|
|
10081
|
-
|
|
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.
|
|
10112
|
-
coversSignature = await verifyTimestampCoversSignature(info, options.
|
|
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
|
|
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
|
-
|
|
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,
|
|
10716
|
+
// If certificate is revoked, check if signature was made before revocation (LTV)
|
|
10569
10717
|
if (revocationResult.status === "revoked") {
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
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
|
|
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
|