edockit 0.2.3 → 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 CHANGED
@@ -5,6 +5,33 @@ 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.3.0] - 2026-01-04
9
+
10
+ ### Added
11
+
12
+ - **Multi-state validation results** - `VerificationResult` now includes granular status beyond boolean `isValid`:
13
+ - `status`: `"VALID"` | `"INVALID"` | `"INDETERMINATE"` | `"UNSUPPORTED"`
14
+ - `statusMessage`: Human-readable explanation
15
+ - `limitations`: Array describing platform/environment constraints
16
+ - **Platform limitation detection** - Detect unsupported RSA key sizes (>4096 bits) in Safari/WebKit and return `UNSUPPORTED` status instead of failing as `INVALID`
17
+ - **Cross-browser testing** - Added Safari/WebKit and Firefox to browser test suite locally
18
+
19
+ ### Fixed
20
+
21
+ - **C14N 1.1 canonicalization** - Fixed bug where C14N 1.1 incorrectly added newlines between XML elements when the original had none. This caused signature verification to fail for compact XML.
22
+ - **INDETERMINATE for expired timestamps** - Return `INDETERMINATE` status (instead of `INVALID`) when timestamp or certificate has expired but signature is otherwise valid
23
+ - **Legacy RSA DigestInfo verification** - Fix signature verification for old documents signed with pre-Java 8 tools that produced non-standard DigestInfo format (missing NULL in AlgorithmIdentifier)
24
+
25
+ ## [0.2.4] - 2025-12-31
26
+
27
+ ### Fixed
28
+
29
+ - **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
30
+ - **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
31
+ - **TSA name formatting** - Fixed timestamp TSA name showing as `[object Object]` instead of readable DN string like `CN=..., O=..., C=...`
32
+ - **Base64 whitespace handling** - Fixed browser `atob` errors when decoding base64 strings containing whitespace from XML
33
+ - **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
34
+
8
35
  ## [0.2.3] - 2025-12-30
9
36
 
10
37
  ### Fixed
@@ -60,6 +87,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
60
87
  - File checksum verification (SHA-256/384/512)
61
88
  - Browser and Node.js support
62
89
 
90
+ [Unreleased]: https://github.com/edgarsj/edockit/compare/v0.3.0...HEAD
91
+ [0.3.0]: https://github.com/edgarsj/edockit/compare/v0.2.4...v0.3.0
92
+ [0.2.4]: https://github.com/edgarsj/edockit/compare/v0.2.3...v0.2.4
63
93
  [0.2.3]: https://github.com/edgarsj/edockit/compare/v0.2.2...v0.2.3
64
94
  [0.2.2]: https://github.com/edgarsj/edockit/compare/v0.2.1...v0.2.2
65
95
  [0.2.1]: https://github.com/edgarsj/edockit/compare/v0.2.0...v0.2.1
package/README.md CHANGED
@@ -51,7 +51,14 @@ const result = await verifySignature(container.signatures[0], container.files, {
51
51
  verifyTime: new Date() // Verify certificate at specific time (default: timestamp time if present, otherwise now)
52
52
  });
53
53
  // result = {
54
- // isValid: boolean, // Overall validity
54
+ // isValid: boolean, // Overall validity (for backwards compatibility)
55
+ // status: 'VALID' | 'INVALID' | 'INDETERMINATE' | 'UNSUPPORTED', // Granular status
56
+ // statusMessage?: string, // Human-readable explanation
57
+ // limitations?: [{ // Platform/environment constraints (if any)
58
+ // code: string, // e.g., 'RSA_KEY_SIZE_UNSUPPORTED'
59
+ // description: string,
60
+ // platform?: string // e.g., 'Safari/WebKit'
61
+ // }],
55
62
  // certificate: {
56
63
  // isValid: boolean, // Certificate validity (time-based)
57
64
  // revocation: { // Revocation check result
@@ -90,7 +97,22 @@ for (const signature of container.signatures) {
90
97
  checkRevocation: true,
91
98
  verifyTimestamps: true
92
99
  });
93
- console.log(`Signature valid: ${result.isValid}`);
100
+
101
+ // Use granular status for detailed handling
102
+ console.log(`Status: ${result.status}`); // VALID, INVALID, INDETERMINATE, or UNSUPPORTED
103
+
104
+ if (result.status === 'VALID') {
105
+ console.log('Signature is valid');
106
+ } else if (result.status === 'UNSUPPORTED') {
107
+ // Platform limitation (e.g., RSA >4096 bits in Safari)
108
+ console.log(`Cannot verify: ${result.statusMessage}`);
109
+ } else if (result.status === 'INDETERMINATE') {
110
+ // Can't conclude (e.g., revocation status unknown)
111
+ console.log(`Inconclusive: ${result.statusMessage}`);
112
+ } else {
113
+ // INVALID - definitely wrong
114
+ console.log(`Invalid: ${result.statusMessage}`);
115
+ }
94
116
 
95
117
  if (result.timestamp?.info) {
96
118
  console.log(`Signed at (TSA): ${result.timestamp.info.genTime}`);
@@ -99,10 +121,6 @@ for (const signature of container.signatures) {
99
121
  if (result.certificate.revocation) {
100
122
  console.log(`Revocation status: ${result.certificate.revocation.status}`);
101
123
  }
102
-
103
- if (!result.isValid && result.errors) {
104
- console.log(`Errors: ${result.errors.join(', ')}`);
105
- }
106
124
  }
107
125
  ```
108
126
 
@@ -128,14 +146,19 @@ async function verifyDocument(url) {
128
146
  },
129
147
  });
130
148
 
131
- console.log(`Valid: ${result.isValid}`);
149
+ // Handle different validation states
150
+ if (result.status === 'VALID') {
151
+ console.log('Signature verified successfully');
152
+ } else if (result.status === 'UNSUPPORTED') {
153
+ // Some signatures can't be verified in certain browsers (e.g., RSA >4096 in Safari)
154
+ console.log(`Browser limitation: ${result.statusMessage}`);
155
+ } else {
156
+ console.log(`${result.status}: ${result.statusMessage}`);
157
+ }
132
158
 
133
159
  if (result.timestamp?.info) {
134
160
  console.log(`Timestamp: ${result.timestamp.info.genTime}`);
135
161
  }
136
- if (result.certificate.revocation) {
137
- console.log(`Revocation: ${result.certificate.revocation.status}`);
138
- }
139
162
  }
140
163
  }
141
164
  ```
@@ -34,6 +34,7 @@ export interface SignatureInfo {
34
34
  references: string[];
35
35
  algorithm?: string;
36
36
  signatureValue?: string;
37
+ canonicalSignatureValue?: string;
37
38
  signedInfoXml?: string;
38
39
  rawXml?: string;
39
40
  canonicalizationMethod?: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * RSA DigestInfo Workaround
3
+ *
4
+ * Some older signing tools (particularly pre-Java 8) produced RSA signatures with
5
+ * non-standard DigestInfo format - missing the NULL parameter in AlgorithmIdentifier.
6
+ *
7
+ * Standard DigestInfo for SHA-1: 30 21 30 09 06 05 2b0e03021a 05 00 04 14 [hash]
8
+ * Non-standard (missing NULL): 30 1f 30 07 06 05 2b0e03021a 04 14 [hash]
9
+ *
10
+ * Web Crypto API's subtle.verify() is strict and rejects the non-standard format.
11
+ * This module provides a fallback that manually performs RSA verification using
12
+ * BigInt math, which works in both browser and Node.js environments.
13
+ */
14
+ /**
15
+ * Verify RSA signature with non-standard DigestInfo format.
16
+ *
17
+ * This function performs RSA signature verification that tolerates
18
+ * non-standard DigestInfo formats (missing NULL in AlgorithmIdentifier).
19
+ *
20
+ * - Node.js: Uses native crypto.publicDecrypt() for speed
21
+ * - Browser: Uses BigInt math (Web Crypto doesn't expose raw RSA)
22
+ *
23
+ * @param publicKeyData SPKI-formatted public key
24
+ * @param signatureBytes Raw signature bytes
25
+ * @param dataToVerify The data that was signed
26
+ * @param hashAlgorithm Hash algorithm name (e.g., "SHA-1", "SHA-256")
27
+ * @returns true if signature is valid, false otherwise
28
+ */
29
+ export declare function verifyRsaWithNonStandardDigestInfo(publicKeyData: ArrayBuffer, signatureBytes: Uint8Array, dataToVerify: Uint8Array, hashAlgorithm: string): Promise<boolean>;
@@ -39,8 +39,8 @@ export interface TimestampVerificationResult {
39
39
  * Options for timestamp verification
40
40
  */
41
41
  export interface TimestampVerificationOptions {
42
- /** The signature value that the timestamp should cover (base64) */
43
- signatureValue?: string;
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
- * XAdES timestamps can cover either:
11
- * 1. The decoded signature value bytes (standard per ETSI EN 319 132-1)
12
- * 2. The base64-encoded string (some implementations)
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 signatureValueBase64 Base64-encoded signature value
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, signatureValueBase64: string): Promise<boolean>;
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
@@ -35,6 +35,8 @@ export interface ChecksumVerificationResult {
35
35
  export interface SignatureVerificationResult {
36
36
  isValid: boolean;
37
37
  reason?: string;
38
+ /** True if verification failed due to platform limitation (e.g., RSA >4096 in Safari) */
39
+ unsupportedPlatform?: boolean;
38
40
  errorDetails?: {
39
41
  category: string;
40
42
  originalMessage: string;
@@ -53,11 +55,37 @@ export interface CertificateVerificationResult {
53
55
  /** Revocation check result (if checkRevocation was enabled) */
54
56
  revocation?: RevocationResult;
55
57
  }
58
+ /**
59
+ * Validation status for granular verification results
60
+ * - VALID: Signature cryptographically valid, all checks pass
61
+ * - INVALID: Definitely wrong (bad checksum, tampered content, crypto failure with supported key)
62
+ * - INDETERMINATE: Can't conclude (expired cert without POE, missing chain, revocation unknown)
63
+ * - UNSUPPORTED: Platform can't verify (e.g., RSA >4096 bits in Safari/WebKit)
64
+ */
65
+ export type ValidationStatus = "VALID" | "INVALID" | "INDETERMINATE" | "UNSUPPORTED";
66
+ /**
67
+ * Describes a limitation that prevented full verification
68
+ */
69
+ export interface ValidationLimitation {
70
+ /** Machine-readable code (e.g., 'RSA_KEY_SIZE_UNSUPPORTED', 'CERT_EXPIRED_NO_POE') */
71
+ code: string;
72
+ /** Human-readable description */
73
+ description: string;
74
+ /** Platform where this limitation applies (e.g., 'Safari/WebKit') */
75
+ platform?: string;
76
+ }
56
77
  /**
57
78
  * Complete verification result
58
79
  */
59
80
  export interface VerificationResult {
81
+ /** Whether the signature is valid (for backwards compatibility) */
60
82
  isValid: boolean;
83
+ /** Granular validation status */
84
+ status: ValidationStatus;
85
+ /** Human-readable status explanation */
86
+ statusMessage?: string;
87
+ /** Limitations that prevented full verification (for INDETERMINATE/UNSUPPORTED) */
88
+ limitations?: ValidationLimitation[];
61
89
  certificate: CertificateVerificationResult;
62
90
  checksums: ChecksumVerificationResult;
63
91
  signature?: SignatureVerificationResult;