Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@signpdf/placeholder-pdf-lib #203

Merged
merged 8 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/monorepo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
base-path: packages/placeholder-plain
path-to-lcov: packages/placeholder-plain/coverage/lcov.info
- name: Coveralls (placeholder-pdf-lib)
uses: coverallsapp/github-action@master
with:
parallel: true
flag-name: placeholder-pdf-lib
github-token: ${{ secrets.GITHUB_TOKEN }}
base-path: packages/placeholder-pdf-lib
path-to-lcov: packages/placeholder-pdf-lib/coverage/lcov.info
- name: Close Coveralls
uses: coverallsapp/github-action@master
with:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
* [placeholder-pdfkit010] Uses SIG_FLAGS and ANNOTATION_FLAGS instead of magic numbers;
* [placeholder-pdfkit010] Allow passing in widgetRect to override the default [0, 0, 0, 0] one;
* [placeholder-plain] Allow passing in widgetRect to override the default [0, 0, 0, 0] one;
* [placeholder-pdf-lib] Introduce the package that uses PDF-LIB for adding a placeholder;
* [signpdf] Use the BR position findByteRange provides to spare a search for it;
* [examples] Introduce [an example that provides a visible widget](packages/examples/src/pdfkit010-with-visual.js) (implemented with pdfkit);
* [examples] Introduce [a `placeholder-pdf-lib` example](packages/examples/src/pdf-lib.js);
* Bumped version of axios;

## [3.0.0]
Expand Down
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Depending on your usecase you may need different combinations of packages.

### I am getting PDFs that already have placeholders

This is the most simple case of them all. `$ npm i -S @signpdf/signpdf @signpdf/signer-p12 node-forge`. Then have a look at the [with-placeholder.js example](/packages/examples/with-placeholder.js). It should be as simple as:
This is the most simple case of them all. `$ npm i -S @signpdf/signpdf @signpdf/signer-p12 node-forge`. Then have a look at the [with-placeholder.js example](/packages/examples/src/with-placeholder.js). It should be as simple as:

```javascript
import signpdf from '@signpdf/signpdf';
Expand All @@ -43,11 +43,19 @@ const signedPdf = await signpdf.sign(fs.readFileSync(PATH_TO_PDF_FILE), signer);

### I am generating a PDF with PDFKit

This is how the library was started as we needed to sign a document that we were generating on the fly. You will need `$ npm i -S @signpdf/signpdf @signpdf/placeholder-pdfkit010 @signpdf/signer-p12 node-forge` and a look at the [pdfkit010.js example](/packages/examples/pdfkit010.js).
This is how the library was started as we needed to sign a document that we were generating on the fly. You will need `$ npm i -S @signpdf/signpdf @signpdf/placeholder-pdfkit010 @signpdf/signer-p12 node-forge` and a look at the [pdfkit010.js example](/packages/examples/src/pdfkit010.js).

### I have a .pdf file and I want to sign it

This seems to be the most common usecase - people work with PDF documents coming from different sources and they need to digitally sign them. The [placeholder-plain](#placeholder-plain) helper can help here. Start with `$ npm i -S @signpdf/signpdf @signpdf/placeholder-plain @signpdf/signer-p12 node-forge`. Head over to either [the JS example](/packages/examples/javascript.js) or [the TS one](/packages/examples/typescript.ts). And note that the process may look simple on the surface but it is very fragile inside. Should you need some help go stright to [our GitHub Issues](https://github.com/vbuch/node-signpdf/issues?q=is%3Aissue).
This seems to be the most common usecase - people work with PDF documents coming from different sources and they need to digitally sign them. Both [placeholder helpers](#placeholder-helpers) placeholder-plain and placeholder-pdf-lib can help here.

#### Plain

Start with `$ npm i -S @signpdf/signpdf @signpdf/placeholder-plain @signpdf/signer-p12 node-forge`. Head over to either [the JS example](/packages/examples/src/javascript.js) or [the TS one](/packages/examples/src/typescript.ts). An advantage of working with the plain version would be that in theory it should be quicker and use less memory (not benchmarked). A great disadvantage: it is very fragile relying on strings being poisitioned in a certain way.

#### PDF-LIB

`$ npm i -S @signpdf/signpdf @signpdf/placeholder-pdf-lib pdf-lib @signpdf/signer-p12 node-forge` gets you started. Then comes the [the PDF-LIB example](/packages/examples/src/pdf-lib.js). PDF-LIB provides tremendous PDF API, it is very well documented and well supported.

## Packages

Expand Down Expand Up @@ -85,6 +93,12 @@ Works on top of `PDFKit 0.10.0` and given a `PDFDocument` that is in the works (

Uses the process and knowledge from `placeholder-pdfkit010` on how to add e-signature placeholder but implements it with plain string operations (`.indexOf()`, `.replace()`, `.match()`, etc.). Because of the lack of semantics it is rather *fragile*. Additionally it doesn't support streams and only works on PDF version <= 1.3. Regardless of those disadvantages this helper seems to be the most popular among the users of `@signpdf`. When the placeholder is in place `@signpdf/signpdf` can complete the process.

#### [@signpdf/placeholder-pdf-lib](/packages/placeholder-pdf-lib)

[![npm version](https://badge.fury.io/js/@signpdf%2Fplaceholder-pdf-lib.svg)](https://badge.fury.io/js/@signpdf%2Fplaceholder-pdf-lib)

Works with PDF-LIB and given a loaded `PDFDocument`, adds an e-signature placeholder. When the placeholder is in place `@signpdf/signpdf` can complete the process.

## Notes

* The process of signing a document is described in the [Digital Signatures in PDF](https://www.adobe.com/devnet-docs/etk_deprecated/tools/DigSig/Acrobat_DigitalSignatures_in_PDF.pdf) document. As Adobe's files are deprecated, [here is the standard as defined by ETSI](<https://ec.europa.eu/digital-building-blocks/wikis/display/DIGITAL/Standards+and+specifications#Standardsandspecifications-PAdES(PDFAdvancedElectronicSignature)BaselineProfile>).
Expand All @@ -101,16 +115,15 @@ We have examples of PDFKit generation of documents and we also have some where a

What's needed is a `Sig` element and a `Widget` that is also linked in a `Form`. The form needs to be referenced in the `Root` descriptor of the PDF as well. A (hopefully) [readable sample](/packages/placeholder-pdfkit010/src/pdfkitAddPlaceholder.js) is available in the helpers. Note the `Contents` descriptor of the `Sig` where zeros are placed that will later be replaced with the actual signature.

We provides two helpers for adding the signature placeholder:
We provides [placeholder helpers](#placeholder-helpers) that do that.

* [`@signpdf/placeholder-pdfkit010`](#placeholder-pdfkit010)
* [`@signpdf/placeholder-plain`](#placeholder-plain)
#### Signature length

**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.

#### PAdES compliant signatures

To produce PAdES compliant signatures, the ETSI Signature Dictionary SubFilter value must be `ETSI.CAdES.detached` instead of the standard Adobe value. This can be declared using the subFilter option argument passed to `pdfkitAddPlaceholder` and `plainAddPlaceholder`.
To produce PAdES compliant signatures, the ETSI Signature Dictionary SubFilter value must be `ETSI.CAdES.detached` instead of the standard Adobe value. This can be declared using the `subFilter` option argument passed to placeholder helpers.

```js
import { pdfkitAddPlaceholder } from '@signpdf/placeholder-pdfkit010';
Expand Down
45 changes: 45 additions & 0 deletions packages/examples/src/pdf-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
var fs = require('fs');
var path = require('path');
var PDFDocument = require('pdf-lib').PDFDocument;
var pdflibAddPlaceholder = require('@signpdf/placeholder-pdf-lib').pdflibAddPlaceholder;
var signpdf = require('@signpdf/signpdf').default;
var P12Signer = require('@signpdf/signer-p12').P12Signer;

function work() {
// contributing.pdf is the file that is going to be signed
var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf');
var pdfBuffer = fs.readFileSync(sourcePath);

// certificate.p12 is the certificate that is going to be used to sign
var certificatePath = path.join(__dirname, '/../../../resources/certificate.p12');
var certificateBuffer = fs.readFileSync(certificatePath);
var signer = new P12Signer(certificateBuffer);

// Load the document into PDF-LIB
PDFDocument.load(pdfBuffer).then(function (pdfDoc) {
// Add a placeholder for a signature.
pdflibAddPlaceholder({
pdfDoc: pdfDoc,
reason: 'The user is decalaring consent through JavaScript.',
contactInfo: 'signpdf@example.com',
name: 'John Doe',
location: 'Free Text Str., Free World',
});

// Convert the PDF-LIB PDFDocument to Buffer
pdfDoc.save({useObjectStreams: false}).then(function (pdfBytes) {
var pdfWithPlaceholder = Buffer.from(pdfBytes);

// And finally sign the document.
signpdf
.sign(pdfWithPlaceholder, signer)
.then(function (signedPdf) {
// signedPdf is a Buffer of an electronically signed PDF. Store it.
var targetPath = path.join(__dirname, '/../output/pdf-lib.pdf');
fs.writeFileSync(targetPath, signedPdf);
})
})
})
}

work();
3 changes: 3 additions & 0 deletions packages/placeholder-pdf-lib/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../babel.config.json"
}
5 changes: 5 additions & 0 deletions packages/placeholder-pdf-lib/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": [
"@signpdf/eslint-config"
]
}
38 changes: 38 additions & 0 deletions packages/placeholder-pdf-lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Placehodler providing helper using PDF-LIB

for [![@signpdf](https://raw.githubusercontent.com/vbuch/node-signpdf/master/resources/logo-horizontal.svg?sanitize=true)](https://github.com/vbuch/node-signpdf/)

[![npm version](https://badge.fury.io/js/@signpdf%2Fplaceholder-pdf-lib.svg)](https://badge.fury.io/js/@signpdf%2Fplaceholder-pdf-lib)
[![Donate to this project using Buy Me A Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://buymeacoffee.com/vbuch)

Works with `PDF-LIB` and given a PDFDocument that is in the works, adds an e-signature placeholder. When the PDF is ready you can convert it to Buffer and pass it to `@signpdf/signpdf` to complete the process.

## Usage

You will need `$ npm i -S @signpdf/placeholder-pdf-lib pdf-lib @signpdf/signpdf node-forge` and a look at the [pdf-lib.js example](/packages/examples/pdf-lib.js).

## Notes

* Make sure to have a look at the docs of the [@signpdf family of packages](https://github.com/vbuch/node-signpdf/).
* Feel free to copy and paste any part of this code. See its defined [Purpose](https://github.com/vbuch/node-signpdf#purpose).

### Signature length

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.

### PAdES compliant signatures

To produce PAdES compliant signatures, the ETSI Signature Dictionary SubFilter value must be `ETSI.CAdES.detached` instead of the standard Adobe value.

This can be declared using the subFilter option argument.

```js
import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
import { SUBFILTER_ETSI_CADES_DETACHED } from '@signpdf/utils';

pdflibAddPlaceholder({
pdfDoc: pdfToSign,
...,
subFilter: SUBFILTER_ETSI_CADES_DETACHED,
});
```
20 changes: 20 additions & 0 deletions packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function pdflibAddPlaceholder({ pdfDoc, reason, contactInfo, name, location, signatureLength, byteRangePlaceholder, subFilter, widgetRect, }: InputType): void;
export type PDFDocument = import('pdf-lib').PDFDocument;
export type InputType = {
pdfDoc: PDFDocument;
reason: string;
contactInfo: string;
name: string;
location: string;
signatureLength?: number;
byteRangePlaceholder?: string;
/**
* One of SUBFILTER_* from \@signpdf/utils
*/
subFilter?: string;
/**
* [x1, y1, x2, y2] widget rectangle
*/
widgetRect?: number[];
};
//# sourceMappingURL=pdflibAddPlaceholder.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.pdflibAddPlaceholder = void 0;
var _utils = require("@signpdf/utils");
var _pdfLib = require("pdf-lib");
/**
* @typedef {import('pdf-lib').PDFDocument} PDFDocument
*/

/**
* @typedef {object} InputType
* @property {PDFDocument} pdfDoc
* @property {string} reason
* @property {string} contactInfo
* @property {string} name
* @property {string} location
* @property {number} [signatureLength]
* @property {string} [byteRangePlaceholder]
* @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils
* @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle
*/

/**
* Adds a signature placeholder to a PDF-LIB PDFDocument.
*
* Alters the passed pdfDoc and returns void.
*
* @param {InputType}
* @returns {void}
*/
const pdflibAddPlaceholder = ({
pdfDoc,
reason,
contactInfo,
name,
location,
signatureLength = _utils.DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = _utils.DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = _utils.SUBFILTER_ADOBE_PKCS7_DETACHED,
widgetRect = [0, 0, 0, 0]
}) => {
const page = pdfDoc.getPage(0);

// Create a placeholder where the the last 3 parameters of the
// actual range will be replaced when signing is done.
const byteRange = _pdfLib.PDFArray.withContext(pdfDoc.context);
byteRange.push(_pdfLib.PDFNumber.of(0));
byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder));
byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder));
byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder));

// Fill the contents of the placeholder with 00s.
const placeholder = _pdfLib.PDFHexString.of(String.fromCharCode(0).repeat(signatureLength));

// Create a signature dictionary to be referenced in the signature widget.
const signatureDict = pdfDoc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: subFilter,
ByteRange: byteRange,
Contents: placeholder,
Reason: _pdfLib.PDFString.of(reason),
M: _pdfLib.PDFString.fromDate(new Date()),
ContactInfo: _pdfLib.PDFString.of(contactInfo),
Name: _pdfLib.PDFString.of(name),
Location: _pdfLib.PDFString.of(location)
}, pdfDoc.index);
const signatureDictRef = pdfDoc.context.register(signatureDict);

// Create the signature widget
const rect = _pdfLib.PDFArray.withContext(pdfDoc.context);
widgetRect.forEach(c => rect.push(_pdfLib.PDFNumber.of(c)));
const widgetDict = pdfDoc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: rect,
V: signatureDictRef,
T: _pdfLib.PDFString.of('Signature1'),
F: _utils.ANNOTATION_FLAGS.PRINT,
P: page.ref
}, pdfDoc.index);
const widgetDictRef = pdfDoc.context.register(widgetDict);

// Annotate the widget on the first page
let annotations = page.node.lookupMaybe(_pdfLib.PDFName.of('Annots'), _pdfLib.PDFArray);
if (typeof annotations === 'undefined') {
annotations = pdfDoc.context.obj([]);
}
annotations.push(widgetDictRef);
page.node.set(_pdfLib.PDFName.of('Annots'), annotations);

// Add an AcroForm or update the existing one
let acroForm = pdfDoc.catalog.lookupMaybe(_pdfLib.PDFName.of('AcroForm'), _pdfLib.PDFDict);
if (typeof acroForm === 'undefined') {
// Need to create a new AcroForm
acroForm = pdfDoc.context.obj({
Fields: []
});
const acroFormRef = pdfDoc.context.register(acroForm);
pdfDoc.catalog.set(_pdfLib.PDFName.of('AcroForm'), acroFormRef);
}

/**
* @type {PDFNumber}
*/
let sigFlags;
if (acroForm.has(_pdfLib.PDFName.of('SigFlags'))) {
// Already has some flags, will merge
sigFlags = acroForm.get(_pdfLib.PDFName.of('SigFlags'));
} else {
// Create blank flags
sigFlags = _pdfLib.PDFNumber.of(0);
}
const updatedFlags = _pdfLib.PDFNumber.of(sigFlags.asNumber() | _utils.SIG_FLAGS.SIGNATURES_EXIST | _utils.SIG_FLAGS.APPEND_ONLY);
acroForm.set(_pdfLib.PDFName.of('SigFlags'), updatedFlags);
const fields = acroForm.get(_pdfLib.PDFName.of('Fields'));
fields.push(widgetDictRef);
};
exports.pdflibAddPlaceholder = pdflibAddPlaceholder;
4 changes: 4 additions & 0 deletions packages/placeholder-pdf-lib/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const sharedConfig = require('../../jest.config.base');
module.exports = {
...sharedConfig,
};
Loading
Loading