pdf-lite 1.0.1 → 1.0.3
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/EXAMPLES.md +1866 -0
- package/README.md +5 -4
- package/dist/core/tokens/number-token.js +11 -6
- package/dist/pdf/pdf-document.d.ts +9 -3
- package/dist/pdf/pdf-document.js +15 -3
- package/dist/signing/index.d.ts +1 -0
- package/dist/signing/index.js +1 -0
- package/dist/signing/signatures/adbe-pkcs7-detached.d.ts +7 -0
- package/dist/signing/signatures/adbe-pkcs7-detached.js +36 -0
- package/dist/signing/signatures/adbe-pkcs7-sha1.d.ts +7 -0
- package/dist/signing/signatures/adbe-pkcs7-sha1.js +54 -0
- package/dist/signing/signatures/adbe-x509-rsa-sha1.d.ts +7 -0
- package/dist/signing/signatures/adbe-x509-rsa-sha1.js +77 -0
- package/dist/signing/signatures/base.d.ts +16 -1
- package/dist/signing/signatures/base.js +18 -0
- package/dist/signing/signatures/etsi-cades-detached.d.ts +7 -0
- package/dist/signing/signatures/etsi-cades-detached.js +36 -0
- package/dist/signing/signatures/etsi-rfc3161.d.ts +7 -0
- package/dist/signing/signatures/etsi-rfc3161.js +102 -0
- package/dist/signing/signer.d.ts +55 -0
- package/dist/signing/signer.js +196 -1
- package/dist/signing/types.d.ts +26 -0
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -8,10 +8,6 @@ A low-level, minimal-dependency, type-safe PDF library that works in the browser
|
|
|
8
8
|
|
|
9
9
|
PRs and issues are welcome!
|
|
10
10
|
|
|
11
|
-
## License
|
|
12
|
-
|
|
13
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
14
|
-
|
|
15
11
|
## Features
|
|
16
12
|
|
|
17
13
|
- **Zero dependencies**: No external libraries are required, making it lightweight and easy to integrate.
|
|
@@ -99,6 +95,7 @@ Long-Term Validation (LTV) support ensures that digital signatures remain valid
|
|
|
99
95
|
**Other features:**
|
|
100
96
|
|
|
101
97
|
- [x] Timestamping
|
|
98
|
+
- [x] Verification of existing signatures
|
|
102
99
|
|
|
103
100
|
## Future Plans
|
|
104
101
|
|
|
@@ -283,3 +280,7 @@ import {
|
|
|
283
280
|
PdfEtsiRfc3161SignatureObject,
|
|
284
281
|
} from 'pdf-lite'
|
|
285
282
|
```
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -13,13 +13,14 @@ export class PdfNumberToken extends PdfToken {
|
|
|
13
13
|
options instanceof Ref ||
|
|
14
14
|
options instanceof PdfNumberToken) {
|
|
15
15
|
this.#value = PdfNumberToken.getValue(options);
|
|
16
|
-
this.padTo = padTo ??
|
|
17
|
-
this.decimalPlaces =
|
|
16
|
+
this.padTo = padTo ?? PdfNumberToken.getPadding(options);
|
|
17
|
+
this.decimalPlaces =
|
|
18
|
+
decimalPlaces ?? PdfNumberToken.getDecimalPlaces(options);
|
|
18
19
|
this.isByteToken = false;
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
this.#value = PdfNumberToken.getValue(options.value);
|
|
22
|
-
this.padTo = options.padTo ??
|
|
23
|
+
this.padTo = options.padTo ?? PdfNumberToken.getPadding(options.value);
|
|
23
24
|
this.decimalPlaces =
|
|
24
25
|
options.decimalPlaces ??
|
|
25
26
|
PdfNumberToken.getDecimalPlaces(options.value);
|
|
@@ -63,12 +64,16 @@ export class PdfNumberToken extends PdfToken {
|
|
|
63
64
|
if (typeof bytes === 'number') {
|
|
64
65
|
bytes = PdfNumberToken.toBytes(bytes);
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
// Count leading zeros
|
|
68
|
+
let leadingZeros = 0;
|
|
69
|
+
const originalLength = bytes.length;
|
|
67
70
|
while (bytes.length && bytes[0] === 0x30) {
|
|
68
71
|
bytes = bytes.slice(1);
|
|
69
|
-
|
|
72
|
+
leadingZeros++;
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
// If all characters were zeros (value is 0), padding should be total length
|
|
75
|
+
// Otherwise, padding should be total length (to maintain original formatting)
|
|
76
|
+
return originalLength;
|
|
72
77
|
}
|
|
73
78
|
static getDecimalPlaces(bytes) {
|
|
74
79
|
if (bytes instanceof PdfNumberToken) {
|
|
@@ -11,7 +11,7 @@ import { PdfEncryptionDictionaryObject } from '../security/types';
|
|
|
11
11
|
import { PdfTrailerEntries } from '../core/objects/pdf-trailer';
|
|
12
12
|
import { PdfDocumentSecurityStoreObject } from '../signing/document-security-store';
|
|
13
13
|
import { ByteArray } from '../types';
|
|
14
|
-
import { PdfSigner } from '../signing/signer';
|
|
14
|
+
import { PdfDocumentVerificationResult, PdfSigner } from '../signing/signer';
|
|
15
15
|
/**
|
|
16
16
|
* Represents a PDF document with support for reading, writing, and modifying PDF files.
|
|
17
17
|
* Handles document structure, revisions, encryption, and digital signatures.
|
|
@@ -214,9 +214,9 @@ export declare class PdfDocument extends PdfObject {
|
|
|
214
214
|
* Sets whether the document should use incremental updates.
|
|
215
215
|
* When true, locks all existing revisions to preserve original content.
|
|
216
216
|
*
|
|
217
|
-
* @param value - True to enable incremental mode, false to disable
|
|
217
|
+
* @param value - True to enable incremental mode, false to disable. Defaults to true.
|
|
218
218
|
*/
|
|
219
|
-
setIncremental(value
|
|
219
|
+
setIncremental(value?: boolean): void;
|
|
220
220
|
/**
|
|
221
221
|
* Checks if the document is in incremental mode.
|
|
222
222
|
*
|
|
@@ -274,4 +274,10 @@ export declare class PdfDocument extends PdfObject {
|
|
|
274
274
|
*/
|
|
275
275
|
static fromBytes(input: AsyncIterable<ByteArray> | Iterable<ByteArray>): Promise<PdfDocument>;
|
|
276
276
|
isModified(): boolean;
|
|
277
|
+
/**
|
|
278
|
+
* Verifies all digital signatures in the document.
|
|
279
|
+
*
|
|
280
|
+
* @returns A promise that resolves to the verification result
|
|
281
|
+
*/
|
|
282
|
+
verifySignatures(): Promise<PdfDocumentVerificationResult>;
|
|
277
283
|
}
|
package/dist/pdf/pdf-document.js
CHANGED
|
@@ -148,7 +148,11 @@ export class PdfDocument extends PdfObject {
|
|
|
148
148
|
const revision = this.revisions.find((rev) => rev.xref.offset === lastStartXRef.offset.ref) ??
|
|
149
149
|
this.revisions.find((rev) => rev.xref.offset.equals(lastStartXRef.offset.value));
|
|
150
150
|
if (!revision) {
|
|
151
|
-
throw new Error('Cannot find revision for last StartXRef'
|
|
151
|
+
throw new Error('Cannot find revision for last StartXRef with offset ' +
|
|
152
|
+
`${lastStartXRef.offset.value}. Options are: ` +
|
|
153
|
+
this.revisions
|
|
154
|
+
.map((rev) => rev.xref.offset.value)
|
|
155
|
+
.join(', '));
|
|
152
156
|
}
|
|
153
157
|
return revision;
|
|
154
158
|
}
|
|
@@ -497,9 +501,9 @@ export class PdfDocument extends PdfObject {
|
|
|
497
501
|
* Sets whether the document should use incremental updates.
|
|
498
502
|
* When true, locks all existing revisions to preserve original content.
|
|
499
503
|
*
|
|
500
|
-
* @param value - True to enable incremental mode, false to disable
|
|
504
|
+
* @param value - True to enable incremental mode, false to disable. Defaults to true.
|
|
501
505
|
*/
|
|
502
|
-
setIncremental(value) {
|
|
506
|
+
setIncremental(value = true) {
|
|
503
507
|
for (const revision of this.revisions) {
|
|
504
508
|
revision.locked = value;
|
|
505
509
|
}
|
|
@@ -685,4 +689,12 @@ export class PdfDocument extends PdfObject {
|
|
|
685
689
|
isModified() {
|
|
686
690
|
return (super.isModified() || this.revisions.some((rev) => rev.isModified()));
|
|
687
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Verifies all digital signatures in the document.
|
|
694
|
+
*
|
|
695
|
+
* @returns A promise that resolves to the verification result
|
|
696
|
+
*/
|
|
697
|
+
async verifySignatures() {
|
|
698
|
+
return await this.signer.verify(this);
|
|
699
|
+
}
|
|
688
700
|
}
|
package/dist/signing/index.d.ts
CHANGED
package/dist/signing/index.js
CHANGED
|
@@ -54,4 +54,11 @@ export declare class PdfAdbePkcs7DetachedSignatureObject extends PdfSignatureObj
|
|
|
54
54
|
* @returns The CMS SignedData and revocation information.
|
|
55
55
|
*/
|
|
56
56
|
sign: PdfSignatureObject['sign'];
|
|
57
|
+
/**
|
|
58
|
+
* Verifies the signature against the provided document bytes.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Verification options including the signed bytes.
|
|
61
|
+
* @returns The verification result.
|
|
62
|
+
*/
|
|
63
|
+
verify: PdfSignatureObject['verify'];
|
|
57
64
|
}
|
|
@@ -114,4 +114,40 @@ export class PdfAdbePkcs7DetachedSignatureObject extends PdfSignatureObject {
|
|
|
114
114
|
revocationInfo,
|
|
115
115
|
};
|
|
116
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* Verifies the signature against the provided document bytes.
|
|
119
|
+
*
|
|
120
|
+
* @param options - Verification options including the signed bytes.
|
|
121
|
+
* @returns The verification result.
|
|
122
|
+
*/
|
|
123
|
+
verify = async (options) => {
|
|
124
|
+
const { bytes, certificateValidation } = options;
|
|
125
|
+
try {
|
|
126
|
+
const signedData = SignedData.fromCms(this.signedBytes);
|
|
127
|
+
const certValidationOptions = certificateValidation === true
|
|
128
|
+
? {}
|
|
129
|
+
: certificateValidation || undefined;
|
|
130
|
+
const result = await signedData.verify({
|
|
131
|
+
data: bytes,
|
|
132
|
+
certificateValidation: certValidationOptions,
|
|
133
|
+
});
|
|
134
|
+
if (result.valid) {
|
|
135
|
+
return { valid: true };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return {
|
|
139
|
+
valid: false,
|
|
140
|
+
reasons: result.reasons,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
reasons: [
|
|
148
|
+
`Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
117
153
|
}
|
|
@@ -52,4 +52,11 @@ export declare class PdfAdbePkcs7Sha1SignatureObject extends PdfSignatureObject
|
|
|
52
52
|
* @returns The CMS SignedData and revocation information.
|
|
53
53
|
*/
|
|
54
54
|
sign: PdfSignatureObject['sign'];
|
|
55
|
+
/**
|
|
56
|
+
* Verifies the signature against the provided document bytes.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Verification options including the signed bytes.
|
|
59
|
+
* @returns The verification result.
|
|
60
|
+
*/
|
|
61
|
+
verify: PdfSignatureObject['verify'];
|
|
55
62
|
}
|
|
@@ -118,4 +118,58 @@ export class PdfAdbePkcs7Sha1SignatureObject extends PdfSignatureObject {
|
|
|
118
118
|
revocationInfo,
|
|
119
119
|
};
|
|
120
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* Verifies the signature against the provided document bytes.
|
|
123
|
+
*
|
|
124
|
+
* @param options - Verification options including the signed bytes.
|
|
125
|
+
* @returns The verification result.
|
|
126
|
+
*/
|
|
127
|
+
verify = async (options) => {
|
|
128
|
+
const { bytes, certificateValidation } = options;
|
|
129
|
+
try {
|
|
130
|
+
const signedData = SignedData.fromCms(this.signedBytes);
|
|
131
|
+
// For adbe.pkcs7.sha1, the signed content is the SHA-1 hash of the document
|
|
132
|
+
// We need to compute the SHA-1 hash of the data and compare with the embedded content
|
|
133
|
+
const expectedHash = await DigestAlgorithmIdentifier.digestAlgorithm('SHA-1').digest(bytes);
|
|
134
|
+
const certValidationOptions = certificateValidation === true
|
|
135
|
+
? {}
|
|
136
|
+
: certificateValidation || undefined;
|
|
137
|
+
// Verify the signature with the hash as the data (non-detached mode)
|
|
138
|
+
const result = await signedData.verify({
|
|
139
|
+
certificateValidation: certValidationOptions,
|
|
140
|
+
});
|
|
141
|
+
if (!result.valid) {
|
|
142
|
+
return {
|
|
143
|
+
valid: false,
|
|
144
|
+
reasons: result.reasons,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Additionally verify that the embedded hash matches the document hash
|
|
148
|
+
const embeddedContent = signedData.encapContentInfo.eContent;
|
|
149
|
+
if (!embeddedContent) {
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
reasons: ['No embedded content in SignedData'],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Compare the hashes
|
|
156
|
+
if (!this.compareArrays(embeddedContent, expectedHash)) {
|
|
157
|
+
return {
|
|
158
|
+
valid: false,
|
|
159
|
+
reasons: [
|
|
160
|
+
'Document hash does not match embedded signature hash',
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { valid: true };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
valid: false,
|
|
169
|
+
reasons: [
|
|
170
|
+
`Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
121
175
|
}
|
|
@@ -46,4 +46,11 @@ export declare class PdfAdbePkcsX509RsaSha1SignatureObject extends PdfSignatureO
|
|
|
46
46
|
* @returns The signature bytes and revocation information.
|
|
47
47
|
*/
|
|
48
48
|
sign: PdfSignatureObject['sign'];
|
|
49
|
+
/**
|
|
50
|
+
* Verifies the signature against the provided document bytes.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Verification options including the signed bytes.
|
|
53
|
+
* @returns The verification result.
|
|
54
|
+
*/
|
|
55
|
+
verify: PdfSignatureObject['verify'];
|
|
49
56
|
}
|
|
@@ -2,7 +2,10 @@ import { PrivateKeyInfo } from 'pki-lite/keys/PrivateKeyInfo';
|
|
|
2
2
|
import { PdfSignatureObject } from './base';
|
|
3
3
|
import { OctetString } from 'pki-lite/asn1/OctetString';
|
|
4
4
|
import { AlgorithmIdentifier } from 'pki-lite/algorithms/AlgorithmIdentifier';
|
|
5
|
+
import { Certificate } from 'pki-lite/x509/Certificate';
|
|
5
6
|
import { fetchRevocationInfo } from '../utils';
|
|
7
|
+
import { PdfArray } from '../../core/objects/pdf-array';
|
|
8
|
+
import { PdfHexadecimal } from '../../core/objects/pdf-hexadecimal';
|
|
6
9
|
/**
|
|
7
10
|
* X.509 RSA-SHA1 signature object (adbe.x509.rsa_sha1).
|
|
8
11
|
* Creates a raw RSA-SHA1 signature with certificates in the Cert entry.
|
|
@@ -78,4 +81,78 @@ export class PdfAdbePkcsX509RsaSha1SignatureObject extends PdfSignatureObject {
|
|
|
78
81
|
revocationInfo,
|
|
79
82
|
};
|
|
80
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Verifies the signature against the provided document bytes.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Verification options including the signed bytes.
|
|
88
|
+
* @returns The verification result.
|
|
89
|
+
*/
|
|
90
|
+
verify = async (options) => {
|
|
91
|
+
const { bytes } = options;
|
|
92
|
+
try {
|
|
93
|
+
const certificates = [];
|
|
94
|
+
const Cert = this.content.get('Cert');
|
|
95
|
+
if (Cert instanceof PdfArray) {
|
|
96
|
+
for (const certObj of Cert.items) {
|
|
97
|
+
const certBytes = certObj instanceof PdfHexadecimal
|
|
98
|
+
? certObj.bytes
|
|
99
|
+
: certObj.raw;
|
|
100
|
+
const certificate = Certificate.fromDer(certBytes);
|
|
101
|
+
certificates.push(certificate);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (Cert instanceof PdfHexadecimal) {
|
|
105
|
+
const certificate = Certificate.fromDer(Cert.bytes);
|
|
106
|
+
certificates.push(certificate);
|
|
107
|
+
}
|
|
108
|
+
else if (Cert) {
|
|
109
|
+
const certificate = Certificate.fromDer(Cert.raw);
|
|
110
|
+
certificates.push(certificate);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new Error('No Cert entry found in signature dictionary');
|
|
114
|
+
}
|
|
115
|
+
if (certificates.length === 0) {
|
|
116
|
+
return {
|
|
117
|
+
valid: false,
|
|
118
|
+
reasons: [
|
|
119
|
+
'No certificates available for adbe.x509.rsa_sha1 verification',
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Parse the signature as an OctetString
|
|
124
|
+
const signatureOctetString = OctetString.fromDer(this.signedBytes);
|
|
125
|
+
const signatureValue = signatureOctetString.bytes;
|
|
126
|
+
const signatureAlgorithm = AlgorithmIdentifier.signatureAlgorithm(PdfAdbePkcsX509RsaSha1SignatureObject.ALGORITHM);
|
|
127
|
+
for (const cert of certificates) {
|
|
128
|
+
const isValid = await signatureAlgorithm.verify(bytes, signatureValue, cert.tbsCertificate.subjectPublicKeyInfo);
|
|
129
|
+
if (isValid) {
|
|
130
|
+
if (options.certificateValidation === true) {
|
|
131
|
+
await cert.validate({
|
|
132
|
+
//TODO: implement default validation options
|
|
133
|
+
checkSignature: true,
|
|
134
|
+
validateChain: true,
|
|
135
|
+
otherCertificates: certificates,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (options.certificateValidation) {
|
|
139
|
+
await cert.validate(options.certificateValidation);
|
|
140
|
+
}
|
|
141
|
+
return { valid: true };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
valid: false,
|
|
146
|
+
reasons: ['Signature verification failed for all certificates'],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
reasons: [
|
|
153
|
+
`Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
81
158
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PdfDictionary } from '../../core/objects/pdf-dictionary';
|
|
2
|
-
import { PdfSignatureDictionaryEntries, PdfSignatureSubType, RevocationInfo } from '../types';
|
|
2
|
+
import { PdfSignatureDictionaryEntries, PdfSignatureSubType, PdfSignatureVerificationOptions, PdfSignatureVerificationResult, RevocationInfo } from '../types';
|
|
3
3
|
import { PdfHexadecimal } from '../../core/objects/pdf-hexadecimal';
|
|
4
4
|
import { PdfIndirectObject } from '../../core/objects/pdf-indirect-object';
|
|
5
5
|
import { ByteArray } from '../../types';
|
|
@@ -71,6 +71,13 @@ export declare abstract class PdfSignatureObject extends PdfIndirectObject<PdfSi
|
|
|
71
71
|
signedBytes: ByteArray;
|
|
72
72
|
revocationInfo?: RevocationInfo;
|
|
73
73
|
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Verifies the signature against the provided document bytes.
|
|
76
|
+
*
|
|
77
|
+
* @param options - Verification options including the signed bytes.
|
|
78
|
+
* @returns The verification result.
|
|
79
|
+
*/
|
|
80
|
+
abstract verify(options: PdfSignatureVerificationOptions): Promise<PdfSignatureVerificationResult>;
|
|
74
81
|
/**
|
|
75
82
|
* Gets the signature hexadecimal content.
|
|
76
83
|
*
|
|
@@ -104,4 +111,12 @@ export declare abstract class PdfSignatureObject extends PdfIndirectObject<PdfSi
|
|
|
104
111
|
* @returns High order value to place signature near end of document.
|
|
105
112
|
*/
|
|
106
113
|
order(): number;
|
|
114
|
+
/**
|
|
115
|
+
* Compares two byte arrays for equality.
|
|
116
|
+
*
|
|
117
|
+
* @param a - First byte array.
|
|
118
|
+
* @param b - Second byte array.
|
|
119
|
+
* @returns True if arrays are equal, false otherwise.
|
|
120
|
+
*/
|
|
121
|
+
protected compareArrays(a: ByteArray, b: ByteArray): boolean;
|
|
107
122
|
}
|
|
@@ -142,4 +142,22 @@ export class PdfSignatureObject extends PdfIndirectObject {
|
|
|
142
142
|
order() {
|
|
143
143
|
return PdfIndirectObject.MAX_ORDER_INDEX - 10;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Compares two byte arrays for equality.
|
|
147
|
+
*
|
|
148
|
+
* @param a - First byte array.
|
|
149
|
+
* @param b - Second byte array.
|
|
150
|
+
* @returns True if arrays are equal, false otherwise.
|
|
151
|
+
*/
|
|
152
|
+
compareArrays(a, b) {
|
|
153
|
+
if (a.length !== b.length) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
for (let i = 0; i < a.length; i++) {
|
|
157
|
+
if (a[i] !== b[i]) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
145
163
|
}
|
|
@@ -61,4 +61,11 @@ export declare class PdfEtsiCadesDetachedSignatureObject extends PdfSignatureObj
|
|
|
61
61
|
* @returns The CMS SignedData and revocation information.
|
|
62
62
|
*/
|
|
63
63
|
sign: PdfSignatureObject['sign'];
|
|
64
|
+
/**
|
|
65
|
+
* Verifies the signature against the provided document bytes.
|
|
66
|
+
*
|
|
67
|
+
* @param options - Verification options including the signed bytes.
|
|
68
|
+
* @returns The verification result.
|
|
69
|
+
*/
|
|
70
|
+
verify: PdfSignatureObject['verify'];
|
|
64
71
|
}
|
|
@@ -156,4 +156,40 @@ export class PdfEtsiCadesDetachedSignatureObject extends PdfSignatureObject {
|
|
|
156
156
|
revocationInfo,
|
|
157
157
|
};
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Verifies the signature against the provided document bytes.
|
|
161
|
+
*
|
|
162
|
+
* @param options - Verification options including the signed bytes.
|
|
163
|
+
* @returns The verification result.
|
|
164
|
+
*/
|
|
165
|
+
verify = async (options) => {
|
|
166
|
+
const { bytes, certificateValidation } = options;
|
|
167
|
+
try {
|
|
168
|
+
const signedData = SignedData.fromCms(this.signedBytes);
|
|
169
|
+
const certValidationOptions = certificateValidation === true
|
|
170
|
+
? {}
|
|
171
|
+
: certificateValidation || undefined;
|
|
172
|
+
const result = await signedData.verify({
|
|
173
|
+
data: bytes,
|
|
174
|
+
certificateValidation: certValidationOptions,
|
|
175
|
+
});
|
|
176
|
+
if (result.valid) {
|
|
177
|
+
return { valid: true };
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
reasons: result.reasons,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
reasons: [
|
|
190
|
+
`Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
159
195
|
}
|
|
@@ -34,4 +34,11 @@ export declare class PdfEtsiRfc3161SignatureObject extends PdfSignatureObject {
|
|
|
34
34
|
* @throws Error if no timestamp token is received.
|
|
35
35
|
*/
|
|
36
36
|
sign: PdfSignatureObject['sign'];
|
|
37
|
+
/**
|
|
38
|
+
* Verifies the timestamp signature against the provided document bytes.
|
|
39
|
+
*
|
|
40
|
+
* @param options - Verification options including the signed bytes.
|
|
41
|
+
* @returns The verification result.
|
|
42
|
+
*/
|
|
43
|
+
verify: PdfSignatureObject['verify'];
|
|
37
44
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { PdfSignatureObject } from './base';
|
|
2
2
|
import { DigestAlgorithmIdentifier } from 'pki-lite/algorithms/AlgorithmIdentifier';
|
|
3
3
|
import { TimeStampReq } from 'pki-lite/timestamp/TimeStampReq';
|
|
4
|
+
import { TSTInfo } from 'pki-lite/timestamp/TSTInfo';
|
|
4
5
|
import { MessageImprint } from 'pki-lite/timestamp/MessageImprint';
|
|
5
6
|
import { SignedData } from 'pki-lite/pkcs7/SignedData';
|
|
6
7
|
import { fetchRevocationInfo } from '../utils';
|
|
8
|
+
import { Certificate } from 'pki-lite/x509/Certificate';
|
|
9
|
+
import { OIDs } from 'pki-lite/core/OIDs';
|
|
7
10
|
/**
|
|
8
11
|
* RFC 3161 timestamp signature object (ETSI.RFC3161).
|
|
9
12
|
* Creates document timestamps using a Time Stamp Authority (TSA).
|
|
@@ -67,4 +70,103 @@ export class PdfEtsiRfc3161SignatureObject extends PdfSignatureObject {
|
|
|
67
70
|
revocationInfo,
|
|
68
71
|
};
|
|
69
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Verifies the timestamp signature against the provided document bytes.
|
|
75
|
+
*
|
|
76
|
+
* @param options - Verification options including the signed bytes.
|
|
77
|
+
* @returns The verification result.
|
|
78
|
+
*/
|
|
79
|
+
verify = async (options) => {
|
|
80
|
+
const { bytes, certificateValidation } = options;
|
|
81
|
+
const digestAlgorithm = DigestAlgorithmIdentifier.digestAlgorithm('SHA-512');
|
|
82
|
+
const expectedMessageImprint = new MessageImprint({
|
|
83
|
+
hashAlgorithm: digestAlgorithm,
|
|
84
|
+
hashedMessage: await digestAlgorithm.digest(bytes),
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const signedData = SignedData.fromCms(this.signedBytes);
|
|
88
|
+
// Extract TSTInfo from the signed data's encapsulated content
|
|
89
|
+
// The eContentType should be id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4)
|
|
90
|
+
const encapContentInfo = signedData.encapContentInfo;
|
|
91
|
+
if (encapContentInfo.eContentType.toString() !== OIDs.PKCS7.TST_INFO) {
|
|
92
|
+
return {
|
|
93
|
+
valid: false,
|
|
94
|
+
reasons: [
|
|
95
|
+
`Invalid content type: expected id-ct-TSTInfo (${OIDs.PKCS7.TST_INFO}), got ${encapContentInfo.eContentType.toString()}`,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!encapContentInfo.eContent) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
reasons: ['No TSTInfo content found in timestamp token'],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Parse the TSTInfo from the encapsulated content
|
|
106
|
+
const tstInfo = TSTInfo.fromDer(encapContentInfo.eContent);
|
|
107
|
+
// Verify that the messageImprint in TSTInfo matches what we computed
|
|
108
|
+
const tstMessageImprint = tstInfo.messageImprint;
|
|
109
|
+
if (tstMessageImprint.hashAlgorithm.algorithm.toString() !==
|
|
110
|
+
expectedMessageImprint.hashAlgorithm.algorithm.toString()) {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
reasons: [
|
|
114
|
+
`Hash algorithm mismatch: TSTInfo uses ${tstMessageImprint.hashAlgorithm.algorithm.toString()}, expected ${expectedMessageImprint.hashAlgorithm.algorithm.toString()}`,
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const tstHash = tstMessageImprint.hashedMessage;
|
|
119
|
+
const expectedHash = expectedMessageImprint.hashedMessage;
|
|
120
|
+
if (tstHash.length !== expectedHash.length) {
|
|
121
|
+
return {
|
|
122
|
+
valid: false,
|
|
123
|
+
reasons: [
|
|
124
|
+
`Hash length mismatch: TSTInfo has ${tstHash.length} bytes, expected ${expectedHash.length} bytes`,
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
for (let i = 0; i < tstHash.length; i++) {
|
|
129
|
+
if (tstHash[i] !== expectedHash[i]) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
reasons: [
|
|
133
|
+
'Message imprint mismatch: the timestamp does not match the document',
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Verify the signature on the SignedData
|
|
139
|
+
const certValidationOptions = certificateValidation === true
|
|
140
|
+
? {}
|
|
141
|
+
: certificateValidation || undefined;
|
|
142
|
+
if (options.certificateValidation) {
|
|
143
|
+
const certificates = signedData.certificates ?? [];
|
|
144
|
+
for (const cert of certificates) {
|
|
145
|
+
if (!(cert instanceof Certificate)) {
|
|
146
|
+
//TODO: support other cert types
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!(await cert.validate(certValidationOptions))) {
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
reasons: [
|
|
153
|
+
'Certificate validation failed for timestamp signer',
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
valid: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
valid: false,
|
|
166
|
+
reasons: [
|
|
167
|
+
`Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
70
172
|
}
|