Hello everyone!
Hope everyone is doing great.
We have following code to create ByteRange placeholder (dictionary) for digital signature. We required to sign PDF digitally. We have used node-signpdf to sign the PDF file.
const fs = require('fs');
const signer = require('node-signpdf');
//pdf buffer need to be sign
const pdfBuffer = fs.readFileSync("./unsigned.pdf");
//buffer of p12 certificate
const PDFDocumentFactory = require('pdf-lib').PDFDocumentFactory;
const PDFDocumentWriter = require('pdf-lib').PDFDocumentWriter;
const PDFDictionary = require('pdf-lib').PDFDictionary;
const PDFName = require('pdf-lib').PDFName;
const PDFArray = require('pdf-lib').PDFArray;
const PDFNumber = require('pdf-lib').PDFNumber;
const PDFHexString = require('pdf-lib').PDFHexString;
const PDFString = require('pdf-lib').PDFString;
const pdfBytes = new Uint8Array(pdfBuffer)
const pdfDoc = PDFDocumentFactory.load(pdfBytes);
const signatureDict = PDFDictionary.from({
Type: PDFName.from('Sig'),
Filter: PDFName.from('Adobe.PPKLite'),
SubFilter: PDFName.from('adbe.pkcs7.detached'),
ByteRange: PDFArray.fromArray([
PDFNumber.fromNumber(0),
PDFName.from("**"),
PDFName.from("**"),
PDFName.from("**")
], pdfDoc.index),
Contents: PDFHexString.fromString(''),
Reason: PDFString.fromString('We need your signature for reasons...'),
M: PDFString.fromString('D:20190508091657Z')
}, pdfDoc.index);
const signatureDictRef = pdfDoc.register(signatureDict);
const widgetDict = PDFDictionary.from({
Type: PDFName.from('Annot'),
Subtype: PDFName.from('Widget'),
FT: PDFName.from('Sig'),
Rect: PDFArray.fromArray([
PDFNumber.fromNumber(0),
PDFNumber.fromNumber(0),
PDFNumber.fromNumber(0),
PDFNumber.fromNumber(0),
], pdfDoc.index),
V: signatureDictRef,
T: PDFString.fromString('Signature1'),
F: PDFNumber.fromNumber(4),
P: pdfDoc.catalog.Pages.get('Kids').get(0),
}, pdfDoc.index);
const widgetDictRef = pdfDoc.register(widgetDict);
// Add our signature widget to the first page
const pages = pdfDoc.getPages();
pages[0].set(
'Annots',
PDFArray.fromArray([widgetDictRef], pdfDoc.index),
);
// Create an AcroForm object containing our signature widget
const formDict = PDFDictionary.from({
SigFlags: PDFNumber.fromNumber(3),
Fields: PDFArray.fromArray([widgetDictRef], pdfDoc.index),
}, pdfDoc.index);
pdfDoc.catalog.set('AcroForm', formDict);
const modifiedPdfBytes = PDFDocumentWriter.saveToBytes(pdfDoc);
const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes)
const p12Buffer = fs.readFileSync("./identity.p12");
const signObj = new signer.SignPdf();
const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer, { passphrase: "debut" });
//write the signed file
fs.createWriteStream('./signed.pdf').end(signedPdfBuffer);
We have blocker on creating ByteRange for Digital signature. Please help to get break through.
Please check your inbox. I sent code files email.
Hello @john-attrium-204!
I made a few modifications to your script, and it all seems to be working fine now: index.js.zip
Here's the diff showing the changes I made:
diff --git a/index.js b/index.js
index 9a8602e..cd0fe1f 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,6 @@
const fs = require('fs');
const signer = require('node-signpdf');
+const { DEFAULT_BYTE_RANGE_PLACEHOLDER } = require('node-signpdf');
//pdf buffer need to be sign
const pdfBuffer = fs.readFileSync('./unsigned.pdf');
//buffer of p12 certificate
@@ -13,6 +14,12 @@ const PDFNumber = require('pdf-lib').PDFNumber;
const PDFHexString = require('pdf-lib').PDFHexString;
const PDFString = require('pdf-lib').PDFString;
+const PDFArrayCustom = require('./PDFArrayCustom');
+
+// This length can be derived from the following `node-signpdf` error message:
+// ./node_modules/node-signpdf/dist/signpdf.js:155:19
+const SIGNATURE_LENGTH = 3322;
+
const pdfBytes = new Uint8Array(pdfBuffer);
const pdfDoc = PDFDocumentFactory.load(pdfBytes);
const signatureDict = PDFDictionary.from(
@@ -20,16 +27,16 @@ const signatureDict = PDFDictionary.from(
Type: PDFName.from('Sig'),
Filter: PDFName.from('Adobe.PPKLite'),
SubFilter: PDFName.from('adbe.pkcs7.detached'),
- ByteRange: PDFArray.fromArray(
+ ByteRange: new PDFArrayCustom(
[
PDFNumber.fromNumber(0),
- PDFName.from(''),
- PDFName.from(''),
- PDFName.from('**********'),
+ PDFName.from(DEFAULT_BYTE_RANGE_PLACEHOLDER),
+ PDFName.from(DEFAULT_BYTE_RANGE_PLACEHOLDER),
+ PDFName.from(DEFAULT_BYTE_RANGE_PLACEHOLDER),
],
pdfDoc.index,
),
- Contents: PDFHexString.fromString(''),
+ Contents: PDFHexString.fromString('A'.repeat(SIGNATURE_LENGTH)),
Reason: PDFString.fromString('We need your signature for reasons...'),
M: PDFString.fromString('D:20190508091657Z'),
},
@@ -74,7 +81,9 @@ const formDict = PDFDictionary.from(
pdfDoc.catalog.set('AcroForm', formDict);
-const modifiedPdfBytes = PDFDocumentWriter.saveToBytes(pdfDoc);
+const modifiedPdfBytes = PDFDocumentWriter.saveToBytes(pdfDoc, {
+ useObjectStreams: false,
+});
const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes);
You'll notice that one of the changes is a new import:
const PDFArrayCustom = require('./PDFArrayCustom');
You'll need to create a new PDFArrayCustom.js file. Here's the implementation of that file:
const { PDFArray, PDFIndirectObject, PDFObject } = require('pdf-lib');
const { arrayToString, addStringToBuffer } = require('pdf-lib/lib/utils');
const add = require('lodash/add');
/**
* Extends PDFArray class in order to make ByteRange look like this:
* /ByteRange [0 /********** /********** /**********]
* Not this:
* /ByteRange [ 0 /********** /********** /********** ]
*/
class PDFArrayCustom extends PDFArray {
constructor(array, index) {
super(array, index);
this.bytesSize = function() {
return (
1 + // "["
this.array
.map(function(e) {
if (e instanceof PDFIndirectObject)
return e.toReference().length + 1;
else if (e instanceof PDFObject) return e.bytesSize() + 1;
throw new Error('Not a PDFObject: ' + e);
})
.reduce(add, 0)
);
};
this.copyBytesInto = function(buffer) {
let remaining = addStringToBuffer('[', buffer);
this.array.forEach((e, idx) => {
if (e instanceof PDFIndirectObject) {
remaining = addStringToBuffer(e.toReference(), remaining);
} else if (e instanceof PDFObject) {
remaining = e.copyBytesInto(remaining);
} else {
throw new Error('Not a PDFObject: ' + e);
}
if (idx !== this.array.length - 1) {
remaining = addStringToBuffer(' ', remaining);
}
});
remaining = addStringToBuffer(']', remaining);
return remaining;
};
}
}
module.exports = PDFArrayCustom;
I can explain the reason for making each of the changes in detail, if you like. But I'm not sure how useful the explanations would be to you, unless you have an understanding of the structure of PDF files.
I hope this helps! Please let me know if you have any further questions.
Hi @Hopding and @john-attrium-204, did you manage to create a working example? I mean, I've replicated the code (with @Hopding suggestions) and it doesn't return any error, however, the generated PDF has some errors.
At least while opening with acrobat reader and tapping the signature annotation it returns "Bad parameter".
Did you guys come up with something similar?
Thanks for the help!
@Hopding @john-attrium-204 I think I've found the issue, it was the date format.
Will keep you posted if I find anything. Thanks!
Please can you update the code according to the latest version of pdf-libs?
I would greatly appreciate it.
Yes please, same here
@mgyugcha @scottie-schneider I've updated the example for version 1.3.0 of pdf-lib.
index.jsconst fs = require('fs');
const signer = require('node-signpdf');
const {
PDFDocument,
PDFName,
PDFNumber,
PDFHexString,
PDFString,
} = require('pdf-lib');
const PDFArrayCustom = require('./PDFArrayCustom');
// The PDF we're going to sign
const pdfBuffer = fs.readFileSync('./unsigned.pdf');
// The p12 certificate we're going to sign with
const p12Buffer = fs.readFileSync('./identity.p12');
// This length can be derived from the following `node-signpdf` error message:
// ./node_modules/node-signpdf/dist/signpdf.js:155:19
const SIGNATURE_LENGTH = 3322;
(async () => {
const pdfDoc = await PDFDocument.load(pdfBuffer);
const pages = pdfDoc.getPages();
const ByteRange = PDFArrayCustom.withContext(pdfDoc.context);
ByteRange.push(PDFNumber.of(0));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER));
const signatureDict = pdfDoc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: 'adbe.pkcs7.detached',
ByteRange,
Contents: PDFHexString.of('A'.repeat(SIGNATURE_LENGTH)),
Reason: PDFString.of('We need your signature for reasons...'),
M: PDFString.fromDate(new Date()),
});
const signatureDictRef = pdfDoc.context.register(signatureDict);
const widgetDict = pdfDoc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: [0, 0, 0, 0],
V: signatureDictRef,
T: PDFString.of('Signature1'),
F: 4,
P: pages[0].ref,
});
const widgetDictRef = pdfDoc.context.register(widgetDict);
// Add our signature widget to the first page
pages[0].node.set(PDFName.of('Annots'), pdfDoc.context.obj([widgetDictRef]));
// Create an AcroForm object containing our signature widget
pdfDoc.catalog.set(
PDFName.of('AcroForm'),
pdfDoc.context.obj({
SigFlags: 3,
Fields: [widgetDictRef],
}),
);
const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes);
const signObj = new signer.SignPdf();
const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer, {
passphrase: 'debut',
});
// Write the signed file
fs.writeFileSync('./signed.pdf', signedPdfBuffer);
})();
PDFArrayCustom.jsconst { PDFArray, CharCodes } = require('pdf-lib');
/**
* Extends PDFArray class in order to make ByteRange look like this:
* /ByteRange [0 /********** /********** /**********]
* Not this:
* /ByteRange [ 0 /********** /********** /********** ]
*/
class PDFArrayCustom extends PDFArray {
static withContext(context) {
return new PDFArrayCustom(context);
}
clone(context) {
const clone = PDFArrayCustom.withContext(context || this.context);
for (let idx = 0, len = this.size(); idx < len; idx++) {
clone.push(this.array[idx]);
}
return clone;
}
toString() {
let arrayString = '[';
for (let idx = 0, len = this.size(); idx < len; idx++) {
arrayString += this.get(idx).toString();
if (idx < len - 1) arrayString += ' ';
}
arrayString += ']';
return arrayString;
}
sizeInBytes() {
let size = 2;
for (let idx = 0, len = this.size(); idx < len; idx++) {
size += this.get(idx).sizeInBytes();
if (idx < len - 1) size += 1;
}
return size;
}
copyBytesInto(buffer, offset) {
const initialOffset = offset;
buffer[offset++] = CharCodes.LeftSquareBracket;
for (let idx = 0, len = this.size(); idx < len; idx++) {
offset += this.get(idx).copyBytesInto(buffer, offset);
if (idx < len - 1) buffer[offset++] = CharCodes.Space;
}
buffer[offset++] = CharCodes.RightSquareBracket;
return offset - initialOffset;
}
}
module.exports = PDFArrayCustom;
My apologies for the (very) delayed response!
can this be digitally signed twice? or more if possible? for clarification only.
@hmpvillegas Do you mean can this method be used to apply a digital signature to a document that already has one? If so, I'm afraid the answer is no. Achieving this would require the signature objects to be added via an incremental update. pdf-lib provides all the primitives to do this, but this example does not illustrate how. I'm hoping to provide better support for this type of thing in the near future though (see https://github.com/Hopding/pdf-lib/issues/172#issuecomment-569310383).
yes, that's exactly what I meant, Yes please! Hoping you can finish it sooner.
Sorry, I know this message comes late !
Is there something expected to be published on the repository ? Should we try to write the .ts version and compile a final js dist of it ? Should we just plug PDFArrayCustom.js when including cdn's pdf-lib file ?
Thank you !
Most helpful comment
@mgyugcha @scottie-schneider I've updated the example for version
1.3.0ofpdf-lib.index.jsPDFArrayCustom.jsMy apologies for the (very) delayed response!