-
-
Notifications
You must be signed in to change notification settings - Fork 176
/
signpdf.js
187 lines (160 loc) · 6.75 KB
/
signpdf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import forge from 'node-forge';
import SignPdfError from './SignPdfError';
import {removeTrailingNewLine, findByteRange} from './helpers';
import {DEFAULT_BYTE_RANGE_PLACEHOLDER} from './helpers/const';
export {default as SignPdfError} from './SignPdfError';
export * from './helpers';
export * from './helpers/const';
export class SignPdf {
constructor() {
this.byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER;
this.lastSignature = null;
}
sign(
pdfBuffer,
p12Buffer,
additionalOptions = {},
) {
const options = {
asn1StrictParsing: false,
passphrase: '',
...additionalOptions,
};
if (!(pdfBuffer instanceof Buffer)) {
throw new SignPdfError(
'PDF expected as Buffer.',
SignPdfError.TYPE_INPUT,
);
}
if (!(p12Buffer instanceof Buffer)) {
throw new SignPdfError(
'p12 certificate expected as Buffer.',
SignPdfError.TYPE_INPUT,
);
}
let pdf = removeTrailingNewLine(pdfBuffer);
// Find the ByteRange placeholder.
const {byteRangePlaceholder} = findByteRange(pdf);
if (!byteRangePlaceholder) {
throw new SignPdfError(
`Could not find empty ByteRange placeholder: ${byteRangePlaceholder}`,
SignPdfError.TYPE_PARSE,
);
}
const byteRangePos = pdf.indexOf(byteRangePlaceholder);
// Calculate the actual ByteRange that needs to replace the placeholder.
const byteRangeEnd = byteRangePos + byteRangePlaceholder.length;
const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd);
const placeholderPos = pdf.indexOf('<', contentsTagPos);
const placeholderEnd = pdf.indexOf('>', placeholderPos);
const placeholderLengthWithBrackets = (placeholderEnd + 1) - placeholderPos;
const placeholderLength = placeholderLengthWithBrackets - 2;
const byteRange = [0, 0, 0, 0];
byteRange[1] = placeholderPos;
byteRange[2] = byteRange[1] + placeholderLengthWithBrackets;
byteRange[3] = pdf.length - byteRange[2];
let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`;
actualByteRange += ' '.repeat(byteRangePlaceholder.length - actualByteRange.length);
// Replace the /ByteRange placeholder with the actual ByteRange
pdf = Buffer.concat([
pdf.slice(0, byteRangePos),
Buffer.from(actualByteRange),
pdf.slice(byteRangeEnd),
]);
// Remove the placeholder signature
pdf = Buffer.concat([
pdf.slice(0, byteRange[1]),
pdf.slice(byteRange[2], byteRange[2] + byteRange[3]),
]);
// Convert Buffer P12 to a forge implementation.
const forgeCert = forge.util.createBuffer(p12Buffer.toString('binary'));
const p12Asn1 = forge.asn1.fromDer(forgeCert);
const p12 = forge.pkcs12.pkcs12FromAsn1(
p12Asn1,
options.asn1StrictParsing,
options.passphrase,
);
// Extract safe bags by type.
// We will need all the certificates and the private key.
const certBags = p12.getBags({
bagType: forge.pki.oids.certBag,
})[forge.pki.oids.certBag];
const keyBags = p12.getBags({
bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
})[forge.pki.oids.pkcs8ShroudedKeyBag];
const privateKey = keyBags[0].key;
// Here comes the actual PKCS#7 signing.
const p7 = forge.pkcs7.createSignedData();
// Start off by setting the content.
p7.content = forge.util.createBuffer(pdf.toString('binary'));
// Then add all the certificates (-cacerts & -clcerts)
// Keep track of the last found client certificate.
// This will be the public key that will be bundled in the signature.
let certificate;
Object.keys(certBags).forEach((i) => {
const {publicKey} = certBags[i].cert;
p7.addCertificate(certBags[i].cert);
// Try to find the certificate that matches the private key.
if (privateKey.n.compareTo(publicKey.n) === 0
&& privateKey.e.compareTo(publicKey.e) === 0
) {
certificate = certBags[i].cert;
}
});
if (typeof certificate === 'undefined') {
throw new SignPdfError(
'Failed to find a certificate that matches the private key.',
SignPdfError.TYPE_INPUT,
);
}
// Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
p7.addSigner({
key: privateKey,
certificate,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
}, {
type: forge.pki.oids.messageDigest,
// value will be auto-populated at signing time
}, {
type: forge.pki.oids.signingTime,
// value can also be auto-populated at signing time
// We may also support passing this as an option to sign().
// Would be useful to match the creation time of the document for example.
value: new Date(),
},
],
});
// Sign in detached mode.
p7.sign({detached: true});
// Check if the PDF has a good enough placeholder to fit the signature.
const raw = forge.asn1.toDer(p7.toAsn1()).getBytes();
// placeholderLength represents the length of the HEXified symbols but we're
// checking the actual lengths.
if ((raw.length * 2) > placeholderLength) {
throw new SignPdfError(
`Signature exceeds placeholder length: ${raw.length * 2} > ${placeholderLength}`,
SignPdfError.TYPE_INPUT,
);
}
let signature = Buffer.from(raw, 'binary').toString('hex');
// Store the HEXified signature. At least useful in tests.
this.lastSignature = signature;
// Pad the signature with zeroes so the it is the same length as the placeholder
signature += Buffer
.from(String.fromCharCode(0).repeat((placeholderLength / 2) - raw.length))
.toString('hex');
// Place it in the document.
pdf = Buffer.concat([
pdf.slice(0, byteRange[1]),
Buffer.from(`<${signature}>`),
pdf.slice(byteRange[1]),
]);
// Magic. Done.
return pdf;
}
}
export default new SignPdf();