Hopding / pdf-lib

Create and modify PDF documents in any JavaScript environment
https://pdf-lib.js.org
MIT License
6.68k stars 643 forks source link

How to create byte-range for digital signature #112

Closed john-attrium-204 closed 5 years ago

john-attrium-204 commented 5 years ago

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.

john-attrium-204 commented 5 years ago

Please check your inbox. I sent code files email.

Hopding commented 5 years ago

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.

FilipeAraujo commented 5 years ago

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!

FilipeAraujo commented 5 years ago

@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!

mgyugcha commented 4 years ago

Please can you update the code according to the latest version of pdf-libs? I would greatly appreciate it.

scottie-schneider commented 4 years ago

Yes please, same here

Hopding commented 4 years ago

@mgyugcha @scottie-schneider I've updated the example for version 1.3.0 of pdf-lib.

index.js

const 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.js

const { 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!

hmpvillegas commented 4 years ago

can this be digitally signed twice? or more if possible? for clarification only.

Hopding commented 4 years ago

@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).

hmpvillegas commented 4 years ago

yes, that's exactly what I meant, Yes please! Hoping you can finish it sooner.

kpoman commented 3 years ago

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 !

idanlo commented 3 years ago

@Hopding This code produces errors in TypeScript, for example

Cannot extend a class 'PDFArray'. Class constructor is marked as private.ts(2675)
Constructor of class 'PDFArray' is private and only accessible within the class declaration.ts(2673)

Edit: After trying your code using PDFArray and not PDFArrayCustom looks like it works, are there any things PDFArrayCustom has that PDFArray does not have? Thank you

kevinbeal commented 3 years ago

After trying your code using PDFArray and not PDFArrayCustom looks like it works, are there any things PDFArrayCustom has that PDFArray does not have?

It's missing the byte range modification mentioned in the comment above PDFArrayCustom:

/**
 * Extends PDFArray class in order to make ByteRange look like this:
 *  /ByteRange [0 /********** /********** /**********]
 * Not this:
 *  /ByteRange [ 0 /********** /********** /********** ]
 */

The signing function does not error, but the built in plainAddPlaceholder() that node-signpdf uses has the byte range format described above and so cannot be used interchangeably. I don't actually know if or why it matters, but it was important enough for the node-signpdf guys to write unit tests around it.

DwisS2 commented 2 years ago

@mgyugcha @scottie-schneider I've updated the example for version 1.3.0 of pdf-lib.

index.js

const 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.js

const { 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!

Hi @Hopding how to make sure the document is valid, like it hasn't been modified and see who the signer is? thank you

RichardBray commented 2 years ago

@Hopding would you know how to get the above code working with a 8192 signature length?

erickximenes commented 2 years ago

@RichardBray Did you manage to make it work?

RichardBray commented 2 years ago

@RichardBray Did you manage to make it work?

Nope, haven't managed to get this working yet :(

RichardBray commented 2 years ago

@ZaunSupremoXV I don't know if this is the best place to have that chat but you'll have to figure out the coordinates of the place you want to put the signature on the pdf, then figure out the width and height of the signature image you want to use.

The signature itself is invisible to humans as it's part of the PDF hex code, the image is just there to let people who read it know that it's been signed.

jeffstieler commented 1 year ago

While I used the example code here successfully - testing with Acrobat I saw signature fields - the signature fields weren't being picked up by DocuSign. My suspicion is that DocuSign is being very strict in parsing the PDF data and perhaps this makeshift form field creation wasn't 100% compliant - total WAG.

Looking at the pdf-lib internals for PDFForm.createTextField(), I came up with this, which works as expected:


const signatureDict = pdfPage.doc.context.obj({
    FT: 'Sig',
    Kids: [],
})
const signatureDictRef = pdfPage.doc.context.register(signatureDict)

// From PDFForm::createTextField()
const acroSigField = PDFAcroSignature.fromDict(
    signatureDict,
    signatureDictRef
)
acroSigField.setPartialName(name)

// From PDFForm::createTextField() -> addFieldToParent()
pdfPage.doc.getForm().acroForm.addField(acroSigField.ref)

const sigField = PDFSignature.of(
    acroSigField,
    acroSigField.ref,
    pdfPage.doc
)

// From PDFTextField::addToPage()
const sigWidget = PDFWidgetAnnotation.create(
    pdfPage.doc.context,
    sigField.ref
)

// From PDFTextField::addToPage() -> PDFField::createWidget()
sigWidget.setRectangle({ x, y, width, height })
sigWidget.setP(pdfPage.ref)
sigWidget.setFlagTo(AnnotationFlags.Print, true)
sigWidget.setFlagTo(AnnotationFlags.Hidden, false)
sigWidget.setFlagTo(AnnotationFlags.Invisible, false)

const sigWidgetRef = pdfPage.doc.context.register(sigWidget.dict)

sigField.acroField.addWidget(sigWidgetRef)

pdfPage.node.addAnnot(sigWidgetRef)
othnielee commented 9 months ago

Thanks for making this library and providing the example here, @Hopding. I incorporated the ideas here in a project that uses node-signpdf and forge with Azure Key Vault. Hoping this helps someone else: https://github.com/othnielee/pdf-sign

othnielee commented 9 months ago

@Hopding would you know how to get the above code working with a 8192 signature length?

I was able to use this approach with the 8192 signature length from node-signpdf - https://github.com/othnielee/pdf-sign/blob/main/src/sign/helpers/add-placeholder.helper.ts. It uses a custom byte array builder that takes a slightly different approach from the PDFCustomArray.

Thanks for your code which got me started on this project, @RichardBray!!

vbuch commented 9 months ago

The signing function does not error, but the built in plainAddPlaceholder() that node-signpdf uses has the byte range format described above and so cannot be used interchangeably. I don't actually know if or why it matters, but it was important enough for the node-signpdf guys to write unit tests around it.

Hi there. Sorry I'm a bit late to the party. @kevinbeal, bacause you were adding the placeholder with pdf-lib and not plainAddPlaceholder you shouldn't care what plainAddPlaceholder generates. The signing itself shouln't as well. The PDF specs are clear on how it should look and pdf-l;ib generates something that fits. So in fact what you were having issue with is the signing function (in node-signpdf) that was unable to find the ByteRange placeholder. I'm addressing this with this PR now: https://github.com/vbuch/node-signpdf/pull/201 Hope someone gives it an eye (review). This should eliminate the need for PDFArrayCustom.

vbuch commented 9 months ago

@Hopding would you know how to get the above code working with a 8192 signature length?

@RichardBray, @othnielee

I think this 8192 is messing your thinking when it shouldn't. See the note we wrote here when we created the package: https://github.com/vbuch/node-signpdf#append-a-signature-placeholder

Note: Signing in detached mode makes the signature length independent of the PDF's content length, but it may still vary between different signing certificates. So every time you sign using the same P12 you will get the same length of the output signature, no matter the length of the signed content. It is safe to find out the actual signature length your certificate produces and use it to properly configure the placeholder length.

I'll try to explain: You have c1.p12 and c2.p12 You want to sign p1.pdf, p2.pdf and p3.pdf with those. Whenever you sign using c1.p12 your signature's length is going to be the same. So if for example sign(p1.pdf, c1.p12) produces 1000 bytes of signature you are safe to say that sign(p2.pdf, c1.p12) and sign(p3.pdf, c1.p12) will produce 1000 bytes of signature as well. That is because a detched signature only contains a hash of the content and not the whole content. Changing the certificate will change the produced signature length but it is again safe to hardcode that once you know it. So if sign(p1.pdf, c2.p12) gives you a 5000 bytes signatures so will signing other PDFs with the same p12.

tusharp1206 commented 6 months ago

@mgyugcha @scottie-schneider I've updated the example for version 1.3.0 of pdf-lib.

index.js

const 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.js

const { 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!

HERE IS MY CHUNK OF CODE :-

export const api_get_signature_from_pfxFile = async (req, res) => { const pfxFile = fs.readFileSync("pfx.pfx", { encoding: 'base64' }); const password = "Siddhi@123";

const p12Der = forge.util.decode64(pfxFile, false); const p12Asn1 = forge.asn1.fromDer(p12Der); const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);

// Get the private key const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag }); const privateKey = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag][0].key;

// Convert private key to PEM format const privateKeyPem = forge.pki.privateKeyToPem(privateKey);

// Get the certificate const certBags = p12.getBags({ bagType: forge.pki.oids.certBag }); const certificates = certBags[forge.pki.oids.certBag].map(cert => cert.cert);

// Extract the first certificate from the array const certificate = certificates[0];

// Create a digital signature const dataToSign = 'X34Qzy'; const signer = crypto.createSign('RSA-SHA256'); signer.update(dataToSign); const signature = signer.sign(privateKeyPem, 'base64');

console.log("output of this function :", privateKey, certificate, signature);

// Read the existing PDF file const existingPdfBytes = fs.readFileSync('doc.pdf');

// Load the existing PDF document const pdfDoc = await PDFDocument.load(existingPdfBytes);

// Add the digital signature and green tick image to the PDF const page = pdfDoc.getPages()[0]; // Modify this to access the correct page

// Draw the digital signature const font = await pdfDoc.embedFont(StandardFonts.Helvetica); page.drawText(Digital Signature: ${signature}, { x: 50, y: 50, size: 12, font: font, });

// Save the modified PDF const modifiedPdfBytes = await pdfDoc.save(); fs.writeFileSync('doc.pdf', modifiedPdfBytes); };

I AM NOT GETTING WHATS WRONG BUT AM NOT ABLE TO DO THIS , CAN YOU PLS HELP ME!