Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add base Signer and ExternalSigner implementations using PKI.js
  • Loading branch information
dcbr authored and Valeri Buchinski committed Feb 23, 2024
commit 1c5fb4af50c9347ebee80fbf9d4db3e91591625e
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.

76 changes: 76 additions & 0 deletions packages/signer/dist/ExternalSigner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ExternalSigner = void 0;
var pkijs = _interopRequireWildcard(require("pkijs"));
var _utils = require("@signpdf/utils");
var _Signer = require("./Signer");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/* eslint-disable no-unused-vars */

/**
* 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).
*/
class ExternalSigner extends _Signer.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>}
*/
async getSignature(hash, data) {
throw new _utils.SignPdfError(`getSignature() is not implemented on ${this.constructor.name}`, _utils.SignPdfError.TYPE_INPUT);
}

/**
* Get a "crypto" extension and override the function used by SignedData.sign to support
* external signing.
* @returns {pkijs.ICryptoEngine}
*/
getCrypto() {
const crypto = super.getCrypto();
crypto.sign = async (_algo, _key, data) => {
// Calculate hash
const hash = await crypto.digest({
name: this.hashAlgorithm
}, data);
// And pass it to the external signature provider
const signature = await this.getSignature(Buffer.from(hash), Buffer.from(data));
return signature;
};
return crypto;
}

/**
* Obtain a dummy private key to pass the correct signing parameters to the sign function.
* @returns {CryptoKey}
*/
async obtainKey() {
// The algorithm parameters cannot be passed directly to the SignedData.sign function, so we
// need to generate a dummy private key with the required parameters and pass that to the
// sign function. The private key is not actually used for signing, as we override the
// crypto.sign function in the getCrypto method.
const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'generatekey').algorithm;
const keypair = await this.crypto.generateKey({
name: this.signAlgorithm,
...algorithmParams,
hash: {
name: this.hashAlgorithm
}
}, false, ['sign', 'verify']);
return keypair.privateKey;
}
}
exports.ExternalSigner = ExternalSigner;
79 changes: 79 additions & 0 deletions packages/signer/dist/Signer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Abstract Signer class taking care of creating a suitable signature for a given pdf.
* Subclasses should specify the required signature and hashing algorithms (either through
* the `signAlgorithm` and `hashAlgorithm` attributes, or by overriding the `getSignAlgorithm`
* and `getHashAlgorithm` methods), as well as provide the signing certificate and private key
* used for signing (by implementing the `getCertificate` and `getKey` methods).
*/
export class Signer extends ISigner {
/** Signature algorithm used for PDF signing
* @type {string}
*/
signAlgorithm: string;
/** Hash algorithm used for PDF signing
* @type {string}
*/
hashAlgorithm: string;
/**
* Method to retrieve the signature algorithm used for PDF signing.
* To be implemented by subclasses or set in the `signAlgorithm` attribute.
* @returns {Promise<string>}
*/
getSignAlgorithm(): Promise<string>;
/**
* Method to retrieve the hashing algorithm used for PDF signing.
* To be implemented by subclasses or set in the `hashAlgorithm` attribute.
* @returns {Promise<string>}
*/
getHashAlgorithm(): Promise<string>;
Comment on lines +9 to +28

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the redundant functions here. A "getter" function can be used if a static prop is not desirable (or we should just always use a function). I don't see a benefit to increasing the API surface here.

For example (if you need a dynamic prop or run some logic):

class MySigner extends ISigner {
  get signAlgorithm() {
    // some logic here
    return alg;
  }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the "getter" approach. To be honest, I didn't know javascript had this functionality.
The original idea for having both a static attribute and the method is that subclasses that don't need custom logic (e.g. request to external service) can just use the "shorthand" notation

class MySigner extends Signer {
    signAlgorithm = '...';
}

But it's probably less confusing if there is just a single method/getter.

/**
* Method to retrieve the signing certificate. If multiple certificates are returned, the first
* one is used for the actual signing, while the others are added for verification purposes.
* To be implemented by subclasses.
* @returns {Promise<Uint8Array | Uint8Array[]>}
*/
getCertificate(): Promise<Uint8Array | Uint8Array[]>;
/**
* Method to retrieve the private key used for signing.
* The returned private key should be in its PKCS#8 binary representation.
* To be implemented by subclasses.
* @returns {Promise<Uint8Array>}
*/
getKey(): Promise<Uint8Array>;
/**
* Get a "crypto" extension.
* @returns {pkijs.ICryptoEngine}
*/
getCrypto(): pkijs.ICryptoEngine;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our abstract class should not be tightly bound to pkijs types. If you have a signer and you don't want to be using PKIJS, then this typing is overly restrictive. We should have our own interface for what a crypto engine looks like (it should be really simple IMO, just really a "sign" interface, I'd have thought), and that's it.

What is the "crypto engine" needed for?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a "private" method that does not need to be further modified/extended by subclasses (see my comment below).

/**
* Obtain the certificates used for signing (first one) and verification (whole list).
* @returns {pkijs.Certificate[]}
*/
obtainCertificates(): pkijs.Certificate[];
/**
* Obtain the private key used for signing.
* @returns {CryptoKey}
*/
obtainKey(): CryptoKey;
/**
* Obtain the signed attributes, which are the actual content that is signed in detached mode.
* @returns {pkijs.Attribute[]}
*/
obtainSignedAttributes(signingTime: any, data: any, signCert: any): pkijs.Attribute[];
/**
* Obtain the unsigned attributes.
* @returns {pkijs.Attribute[]}
*/
obtainUnsignedAttributes(signature: any): pkijs.Attribute[];
Comment on lines +48 to +67

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be mixing of sync/async methods. Why would these not be async whilst others are? If we want to allow the use of external signers, it's feasible that obtaining certificates may well be an async task.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should all be async, but the generated typings do not show this. How can I fix this? I also now notice that the return type has to be changed to Promise as well.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto: pkijs.ICryptoEngine;
/**
* Verify whether the signature generated by the sign function is correct.
* @param {Buffer} cmsSignedBuffer
* @param {Buffer} pdfBuffer
* @returns {boolean}
*/
verify(cmsSignedBuffer: Buffer, pdfBuffer: Buffer): boolean;
}
import { ISigner } from '@signpdf/utils';
import * as pkijs from 'pkijs';
//# sourceMappingURL=Signer.d.ts.map
1 change: 1 addition & 0 deletions packages/signer/dist/Signer.d.ts.map

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

Loading