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

Provide default sign method, add external signer and webcrypto example. #220

Merged
merged 2 commits into from
Feb 9, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ This is the main package, the integrating one, the one that wraps everything up.

### Signers

Signers are small libraries that `@signpdf/signpdf` will call with a PDF and they will know how to provide an e-signature in return. Their output is then fed as the signature in the resulting document.
Signers are small libraries that `@signpdf/signpdf` will call with a PDF and they will know how to provide an e-signature in return. Their output is then fed as the signature in the resulting document. Example implementations of the abstract `Signer` base class are provided in the [WebCrypto](./packages/examples/src/webcrypto.js) and [WebCrypto-External](./packages/examples/src/webcrypto-external.js) examples, both leveraging the `WebCrypto` API.

#### [@signpdf/signer-p12](./packages/signer-p12)

Expand Down
1 change: 1 addition & 0 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@signpdf/placeholder-plain": "^3.2.0",
"@signpdf/signer-p12": "^3.2.0",
"@signpdf/signpdf": "^3.2.0",
"@signpdf/signer": "^3.2.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
Expand Down
46 changes: 46 additions & 0 deletions packages/examples/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
var nodeCrypto = require('crypto');
var asn1js = require('asn1js');
var pkijs = require('pkijs');

// Get crypto extension
const crypto = new pkijs.CryptoEngine({name: 'CertCrypto', crypto: nodeCrypto});

async function createCertificate(keypair, hashAlg) {
// Create a new certificate for the given keypair and hash algorithm.
// Based on the certificateComplexExample from PKI.js.
const certificate = new pkijs.Certificate();

// Basic attributes
certificate.version = 2;
certificate.serialNumber = new asn1js.Integer({ value: 1 });
certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
type: "2.5.4.6", // Country name
value: new asn1js.PrintableString({value: "NO"}),
}));
certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
type: "2.5.4.3", // Common name
value: new asn1js.BmpString({value: "Test"}),
}));
certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
type: "2.5.4.6", // Country name
value: new asn1js.PrintableString({value: "NO"}),
}));
certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
type: "2.5.4.3", // Common name
value: new asn1js.BmpString({value: "Test"}),
}));

certificate.notBefore.value = new Date();
certificate.notAfter.value = new Date();
certificate.notAfter.value.setFullYear(certificate.notAfter.value.getFullYear() + 1);

// Export public key into "subjectPublicKeyInfo" value of certificate
await certificate.subjectPublicKeyInfo.importKey(keypair.publicKey, crypto);

// Sign certificate
await certificate.sign(keypair.privateKey, hashAlg, crypto);

return certificate.toSchema(true).toBER(false);
}

module.exports.createCertificate = createCertificate;
105 changes: 105 additions & 0 deletions packages/examples/src/webcrypto-external.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
var fs = require('fs');
var path = require('path');
var signpdf = require('@signpdf/signpdf').default;
var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder;
var ExternalSigner = require('@signpdf/signer').ExternalSigner;
var crypto = require('crypto');
var createCertificate = require('./utils').createCertificate;

// ExternalSigner implementation using the WebCrypto API
// Note that this is just an example implementation of the ExternalSigner abstract class.
// WebCrypto signing can also be implemented more easily by subclassing the Signer abstract
// class directly, as is done in the `webcrypto.js` example script.
class CryptoSigner extends ExternalSigner {
// 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto
supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512'];

// 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto
supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA'];

constructor(signAlgorithm = 'ECDSA', hashAlgorithm = 'SHA-512') {
super();

// Verify and set signature and hash algorithms
if (!this.supportedSignAlgorithms.includes(signAlgorithm)) {
throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`);
}
this.signAlgorithm = signAlgorithm;
if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) {
throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`);
}
this.hashAlgorithm = hashAlgorithm;

// Salt lengths for RSA-PSS algorithm used by PKI.js
// If you want to modify these, the crypto.getSignatureParameters
// method needs to be overridden in the getCrypto function.
this.saltLengths = {
'SHA-256': 32,
'SHA-384': 48,
'SHA-512': 64,
}

this.cert = undefined;
this.key = undefined;
}

async getCertificate() {
// Create a new keypair and certificate
let params = {namedCurve: 'P-256'}; // EC parameters
if (this.signAlgorithm.startsWith("RSA")) {
// RSA parameters
params = {
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: this.hashAlgorithm,
};
}
const keypair = await crypto.subtle.generateKey({
name: this.signAlgorithm,
...params,
}, true, ['sign', 'verify']);
this.cert = await createCertificate(keypair, this.hashAlgorithm);
this.key = keypair.privateKey;
return this.cert;
}

async getSignature(_hash, data) {
// WebCrypto's sign function automatically computes the hash of the passed data before signing.
return crypto.subtle.sign({
name: this.signAlgorithm,
hash: this.hashAlgorithm, // Required for ECDSA algorithm
saltLength: this.saltLengths[this.hashAlgorithm], // Required for RSA-PSS algorithm
}, this.key, data);
}
}

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);

// Create new CryptoSigner
var signAlgorithm = 'ECDSA';
var hashAlgorithm = 'SHA-512';
var signer = new CryptoSigner(signAlgorithm, hashAlgorithm);

// The PDF needs to have a placeholder for a signature to be signed.
var pdfWithPlaceholder = plainAddPlaceholder({
pdfBuffer: pdfBuffer,
reason: 'The user is declaring consent through JavaScript.',
contactInfo: '[email protected]',
name: 'John Doe',
location: 'Free Text Str., Free World',
});

// pdfWithPlaceholder is now a modified Buffer that is ready to be signed.
signpdf.sign(pdfWithPlaceholder, signer)
.then(function (signedPdf) {
// signedPdf is a Buffer of an electronically signed PDF. Store it.
var targetPath = path.join(__dirname, '/../output/webcrypto-external.pdf');
fs.writeFileSync(targetPath, signedPdf);
});

}

work();
81 changes: 81 additions & 0 deletions packages/examples/src/webcrypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
var fs = require('fs');
var path = require('path');
var signpdf = require('@signpdf/signpdf').default;
var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder;
var Signer = require('@signpdf/signer').Signer;
var createCertificate = require('./utils').createCertificate;

// Signer implementation using the WebCrypto API
class CryptoSigner extends Signer {
// 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto
supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512'];

// 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto
supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA'];

constructor(signAlgorithm = 'RSA-PSS', hashAlgorithm = 'SHA-512') {
super();

// Verify and set signature and hash algorithms
if (!this.supportedSignAlgorithms.includes(signAlgorithm)) {
throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`);
}
this.signAlgorithm = signAlgorithm;
if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) {
throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`);
}
this.hashAlgorithm = hashAlgorithm;

this.cert = undefined;
this.key = undefined;
}

async getCertificate() {
// Create a new keypair and certificate
const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'generatekey').algorithm;
const keypair = await this.crypto.generateKey({
name: this.signAlgorithm,
...algorithmParams,
hash: {name: this.hashAlgorithm},
}, true, ['sign', 'verify']);
this.cert = await createCertificate(keypair, this.hashAlgorithm);
this.key = keypair.privateKey;
return this.cert;
}

async getKey() {
// Convert private key to binary PKCS#8 representation
return this.crypto.exportKey("pkcs8", this.key);
}
}

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);

// Create new CryptoSigner
var signAlgorithm = 'RSA-PSS';
var hashAlgorithm = 'SHA-512';
var signer = new CryptoSigner(signAlgorithm, hashAlgorithm);

// The PDF needs to have a placeholder for a signature to be signed.
var pdfWithPlaceholder = plainAddPlaceholder({
pdfBuffer: pdfBuffer,
reason: 'The user is declaring consent through JavaScript.',
contactInfo: '[email protected]',
name: 'John Doe',
location: 'Free Text Str., Free World',
});

// pdfWithPlaceholder is now a modified Buffer that is ready to be signed.
signpdf.sign(pdfWithPlaceholder, signer)
.then(function (signedPdf) {
// signedPdf is a Buffer of an electronically signed PDF. Store it.
var targetPath = path.join(__dirname, '/../output/webcrypto.pdf');
fs.writeFileSync(targetPath, signedPdf);
});

}

work();
4 changes: 2 additions & 2 deletions packages/signer-p12/dist/P12Signer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @prop {string} [passphrase]
* @prop {boolean} [asn1StrictParsing]
*/
export class P12Signer extends Signer {
export class P12Signer extends ISigner {
/**
* @param {Buffer | Uint8Array | string} p12Buffer
* @param {SignerOptions} additionalOptions
Expand All @@ -19,5 +19,5 @@ export type SignerOptions = {
passphrase?: string;
asn1StrictParsing?: boolean;
};
import { Signer } from '@signpdf/utils';
import { ISigner } from '@signpdf/utils';
//# sourceMappingURL=P12Signer.d.ts.map
2 changes: 1 addition & 1 deletion packages/signer-p12/dist/P12Signer.d.ts.map

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

2 changes: 1 addition & 1 deletion packages/signer-p12/dist/P12Signer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
* @prop {boolean} [asn1StrictParsing]
*/

class P12Signer extends _utils.Signer {
class P12Signer extends _utils.ISigner {
/**
* @param {Buffer | Uint8Array | string} p12Buffer
* @param {SignerOptions} additionalOptions
Expand Down
4 changes: 2 additions & 2 deletions packages/signer-p12/src/P12Signer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import forge from 'node-forge';
import {convertBuffer, SignPdfError, Signer} from '@signpdf/utils';
import {convertBuffer, SignPdfError, ISigner} from '@signpdf/utils';

/**
* @typedef {object} SignerOptions
* @prop {string} [passphrase]
* @prop {boolean} [asn1StrictParsing]
*/

export class P12Signer extends Signer {
export class P12Signer extends ISigner {
/**
* @param {Buffer | Uint8Array | string} p12Buffer
* @param {SignerOptions} additionalOptions
Expand Down
3 changes: 3 additions & 0 deletions packages/signer/.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/signer/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": [
"@signpdf/eslint-config"
]
}
17 changes: 17 additions & 0 deletions packages/signer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Signer base implementation with PKI.js

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%2Fsigner.svg)](https://badge.fury.io/js/@signpdf%2Fsigner)
[![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)

Uses `PKI.js` to create a detached signature of a given `Buffer`.

## Usage

This is an abstract base implementation of the `Signer` and `ExternalSigner` classes. Use any of the available implementations (or subclass any of these classes yourself) to sign an actual PDF file. See for example the [P12Signer package](/packages/signer-p12) for signing with a PKCS#12 certificate bundle.

## 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).
23 changes: 23 additions & 0 deletions packages/signer/dist/ExternalSigner.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Abstract ExternalSigner class taking care of creating a suitable signature for a given pdf
* using an external signature provider.
* Subclasses should specify the required signature and hashing algorithms used by the external
* provider (either through the `signAlgorithm` and `hashAlgorithm` attributes, or by overriding
* the `getSignAlgorithm` and `getHashAlgorithm` methods), as well as provide the used signing
* certificate and final signature (by implementing the `getCertificate` and `getSignature`
* methods).
*/
export class ExternalSigner extends Signer {
/**
* Method to retrieve the signature of the given hash (of the given data) from the external
* service. The original data is included in case the external signature provider computes
* the hash automatically before signing.
* To be implemented by subclasses.
* @param {Uint8Array} hash
* @param {Uint8Array} data
* @returns {Promise<Uint8Array>}
*/
getSignature(hash: Uint8Array, data: Uint8Array): Promise<Uint8Array>;
}
import { Signer } from './Signer';
//# sourceMappingURL=ExternalSigner.d.ts.map
1 change: 1 addition & 0 deletions packages/signer/dist/ExternalSigner.d.ts.map

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

Loading