diff --git a/packages/signer-p12/dist/P12Signer.d.ts b/packages/signer-p12/dist/P12Signer.d.ts index 490bb73c..58bc5309 100644 --- a/packages/signer-p12/dist/P12Signer.d.ts +++ b/packages/signer-p12/dist/P12Signer.d.ts @@ -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 @@ -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 \ No newline at end of file diff --git a/packages/signer-p12/dist/P12Signer.d.ts.map b/packages/signer-p12/dist/P12Signer.d.ts.map index cb8a89eb..652294d0 100644 --- a/packages/signer-p12/dist/P12Signer.d.ts.map +++ b/packages/signer-p12/dist/P12Signer.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"P12Signer.d.ts","sourceRoot":"","sources":["../src/P12Signer.js"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH;IACI;;;OAGG;IACH,uBAHW,MAAM,GAAG,UAAU,GAAG,MAAM,sBAC5B,aAAa,EAavB;IANG;oBAdE,MAAM;2BACN,OAAO;MAiBR;IACD,UAA8D;CA2FrE;;iBA9GS,MAAM;wBACN,OAAO;;uBALiC,gBAAgB"} \ No newline at end of file +{"version":3,"file":"P12Signer.d.ts","sourceRoot":"","sources":["../src/P12Signer.js"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH;IACI;;;OAGG;IACH,uBAHW,MAAM,GAAG,UAAU,GAAG,MAAM,sBAC5B,aAAa,EAavB;IANG;oBAdE,MAAM;2BACN,OAAO;MAiBR;IACD,UAA8D;CA2FrE;;iBA9GS,MAAM;wBACN,OAAO;;wBALkC,gBAAgB"} \ No newline at end of file diff --git a/packages/signer-p12/dist/P12Signer.js b/packages/signer-p12/dist/P12Signer.js index bd1fa688..4c2e963b 100644 --- a/packages/signer-p12/dist/P12Signer.js +++ b/packages/signer-p12/dist/P12Signer.js @@ -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 diff --git a/packages/signer-p12/src/P12Signer.js b/packages/signer-p12/src/P12Signer.js index 90c487dc..4bb3ec42 100644 --- a/packages/signer-p12/src/P12Signer.js +++ b/packages/signer-p12/src/P12Signer.js @@ -1,5 +1,5 @@ import forge from 'node-forge'; -import {convertBuffer, SignPdfError, Signer} from '@signpdf/utils'; +import {convertBuffer, SignPdfError, ISigner} from '@signpdf/utils'; /** * @typedef {object} SignerOptions @@ -7,7 +7,7 @@ import {convertBuffer, SignPdfError, Signer} from '@signpdf/utils'; * @prop {boolean} [asn1StrictParsing] */ -export class P12Signer extends Signer { +export class P12Signer extends ISigner { /** * @param {Buffer | Uint8Array | string} p12Buffer * @param {SignerOptions} additionalOptions diff --git a/packages/signer/.babelrc b/packages/signer/.babelrc new file mode 100644 index 00000000..00bdc749 --- /dev/null +++ b/packages/signer/.babelrc @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} \ No newline at end of file diff --git a/packages/signer/.eslintrc b/packages/signer/.eslintrc new file mode 100644 index 00000000..cdabc744 --- /dev/null +++ b/packages/signer/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "@signpdf/eslint-config" + ] +} \ No newline at end of file diff --git a/packages/signer/README.md b/packages/signer/README.md new file mode 100644 index 00000000..b7b5650a --- /dev/null +++ b/packages/signer/README.md @@ -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). diff --git a/packages/signer/dist/ExternalSigner.d.ts b/packages/signer/dist/ExternalSigner.d.ts new file mode 100644 index 00000000..a001df6b --- /dev/null +++ b/packages/signer/dist/ExternalSigner.d.ts @@ -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} + */ + getSignature(hash: Uint8Array, data: Uint8Array): Promise; +} +import { Signer } from './Signer'; +//# sourceMappingURL=ExternalSigner.d.ts.map \ No newline at end of file diff --git a/packages/signer/dist/ExternalSigner.d.ts.map b/packages/signer/dist/ExternalSigner.d.ts.map new file mode 100644 index 00000000..95220527 --- /dev/null +++ b/packages/signer/dist/ExternalSigner.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ExternalSigner.d.ts","sourceRoot":"","sources":["../src/ExternalSigner.js"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH;IACI;;;;;;;;OAQG;IACH,mBAJW,UAAU,QACV,UAAU,GACR,QAAQ,UAAU,CAAC,CAO/B;CAoCJ;uBA9DoB,UAAU"} \ No newline at end of file diff --git a/packages/signer/dist/ExternalSigner.js b/packages/signer/dist/ExternalSigner.js new file mode 100644 index 00000000..a1447e8e --- /dev/null +++ b/packages/signer/dist/ExternalSigner.js @@ -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} + */ + 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; \ No newline at end of file diff --git a/packages/signer/dist/Signer.d.ts b/packages/signer/dist/Signer.d.ts new file mode 100644 index 00000000..adb01bea --- /dev/null +++ b/packages/signer/dist/Signer.d.ts @@ -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} + */ + getSignAlgorithm(): Promise; + /** + * Method to retrieve the hashing algorithm used for PDF signing. + * To be implemented by subclasses or set in the `hashAlgorithm` attribute. + * @returns {Promise} + */ + getHashAlgorithm(): Promise; + /** + * 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} + */ + getCertificate(): Promise; + /** + * 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} + */ + getKey(): Promise; + /** + * Get a "crypto" extension. + * @returns {pkijs.ICryptoEngine} + */ + getCrypto(): pkijs.ICryptoEngine; + /** + * 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[]; + 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 \ No newline at end of file diff --git a/packages/signer/dist/Signer.d.ts.map b/packages/signer/dist/Signer.d.ts.map new file mode 100644 index 00000000..ff6ce3c3 --- /dev/null +++ b/packages/signer/dist/Signer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Signer.d.ts","sourceRoot":"","sources":["../src/Signer.js"],"names":[],"mappings":"AAwBA;;;;;;GAMG;AACH;IACI;;OAEG;IACH,eAFU,MAAM,CAEoB;IAEpC;;OAEG;IACH,eAFU,MAAM,CAEU;IAE1B;;;;OAIG;IACH,oBAFa,QAAQ,MAAM,CAAC,CAI3B;IAED;;;;OAIG;IACH,oBAFa,QAAQ,MAAM,CAAC,CAI3B;IAED;;;;;OAKG;IACH,kBAFa,QAAQ,UAAU,GAAG,UAAU,EAAE,CAAC,CAO9C;IAED;;;;;OAKG;IACH,UAFa,QAAQ,UAAU,CAAC,CAO/B;IAED;;;OAGG;IACH,aAFa,MAAM,aAAa,CAK/B;IAED;;;OAGG;IACH,sBAFa,MAAM,WAAW,EAAE,CAQ/B;IAED;;;OAGG;IACH,aAFa,SAAS,CAUrB;IAED;;;OAGG;IACH,oEAFa,MAAM,SAAS,EAAE,CAoD7B;IAED;;;OAGG;IACH,0CAFa,MAAM,SAAS,EAAE,CAI7B;IAmBG,4BAA8B;IAqDlC;;;;;OAKG;IACH,wBAJW,MAAM,aACN,MAAM,GACJ,OAAO,CAYnB;CACJ;wBA5QmC,gBAAgB;uBAD7B,OAAO"} \ No newline at end of file diff --git a/packages/signer/dist/Signer.js b/packages/signer/dist/Signer.js new file mode 100644 index 00000000..33699349 --- /dev/null +++ b/packages/signer/dist/Signer.js @@ -0,0 +1,296 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Signer = void 0; +var nodeCrypto = _interopRequireWildcard(require("crypto")); +var asn1js = _interopRequireWildcard(require("asn1js")); +var pkijs = _interopRequireWildcard(require("pkijs")); +var _utils = require("@signpdf/utils"); +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 */ + +// Useful references to understand what is going on here: +// * PDF: Portable Document Format (ISO 32000-1) +// * CMS: Cryptographic Message Syntax (RFC 5652) +// * CAdES: CMS Advanced Electronic Signatures (ETSI 319 122-1) +// * PAdES: PDF Advanced Electronic Signatures (ETSI 319 142-1) +// Some code comments will refer to these specifications using square brackets, +// e.g. [PDF - 12.8] means section 12.8 of the ISO 32000-1 specification. +// Object identifiers used in the created CMS Signed Data structure +const oids = { + data: '1.2.840.113549.1.7.1', + signedData: '1.2.840.113549.1.7.2', + contentType: '1.2.840.113549.1.9.3', + messageDigest: '1.2.840.113549.1.9.4', + signingTime: '1.2.840.113549.1.9.5', + signingCertificateV2: '1.2.840.113549.1.9.16.2.47' +}; + +/** + * 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). + */ +class Signer extends _utils.ISigner { + /** Signature algorithm used for PDF signing + * @type {string} + */ + signAlgorithm = 'RSASSA-PKCS1-v1_5'; + + /** Hash algorithm used for PDF signing + * @type {string} + */ + hashAlgorithm = 'SHA-256'; + + /** + * Method to retrieve the signature algorithm used for PDF signing. + * To be implemented by subclasses or set in the `signAlgorithm` attribute. + * @returns {Promise} + */ + async getSignAlgorithm() { + return this.signAlgorithm; // Use default signature algorithm if not overridden by subclass + } + + /** + * Method to retrieve the hashing algorithm used for PDF signing. + * To be implemented by subclasses or set in the `hashAlgorithm` attribute. + * @returns {Promise} + */ + async getHashAlgorithm() { + return this.hashAlgorithm; // Use default hash algorithm if not overridden by subclass + } + + /** + * 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} + */ + async getCertificate() { + throw new _utils.SignPdfError(`getCertificate() is not implemented on ${this.constructor.name}`, _utils.SignPdfError.TYPE_INPUT); + } + + /** + * 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} + */ + async getKey() { + throw new _utils.SignPdfError(`getKey() is not implemented on ${this.constructor.name}`, _utils.SignPdfError.TYPE_INPUT); + } + + /** + * Get a "crypto" extension. + * @returns {pkijs.ICryptoEngine} + */ + getCrypto() { + const crypto = new pkijs.CryptoEngine({ + name: 'SignerCrypto', + crypto: nodeCrypto + }); + return crypto; + } + + /** + * Obtain the certificates used for signing (first one) and verification (whole list). + * @returns {pkijs.Certificate[]} + */ + async obtainCertificates() { + let certBytes = await this.getCertificate(); + if (!Array.isArray(certBytes)) { + certBytes = [certBytes]; + } + return certBytes.map(cb => pkijs.Certificate.fromBER(cb)); + } + + /** + * Obtain the private key used for signing. + * @returns {CryptoKey} + */ + async obtainKey() { + const keyBytes = await this.getKey(); + const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'importkey').algorithm; + return this.crypto.importKey('pkcs8', keyBytes, { + name: this.signAlgorithm, + ...algorithmParams, + hash: { + name: this.hashAlgorithm + } + }, false, ['sign']); + } + + /** + * Obtain the signed attributes, which are the actual content that is signed in detached mode. + * @returns {pkijs.Attribute[]} + */ + async obtainSignedAttributes(signingTime, data, signCert) { + // Create a message digest + const digest = await this.crypto.digest({ + name: this.hashAlgorithm + }, data); + // Note that the signed attributes order is relevant for correct EU signature validation: + // https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation + const signedAttrs = [ + // [CAdES - 5.1.1] + new pkijs.Attribute({ + type: oids.contentType, + values: [new asn1js.ObjectIdentifier({ + value: oids.data + })] + }), + // [CAdES - 5.2.1] + new pkijs.Attribute({ + type: oids.signingTime, + values: [new asn1js.UTCTime({ + valueDate: signingTime !== null && signingTime !== void 0 ? signingTime : new Date() + })] + }), + // [CAdES - 5.1.2] + new pkijs.Attribute({ + type: oids.messageDigest, + values: [new asn1js.OctetString({ + valueHex: digest + })] + })]; + + // Add the ESS signing certificate attribute (see [CAdES - 5.2.2.3] and [RFC 5035]) + const hashOid = this.crypto.getOIDByAlgorithm({ + name: this.hashAlgorithm + }, true, 'hashAlgorithm'); + const signCertHash = await this.crypto.digest({ + name: this.hashAlgorithm + }, signCert.toSchema(true).toBER(false)); + const essCertIDv2 = new asn1js.Sequence({ + value: [ + // hashAlgorithm + new asn1js.Sequence({ + value: [new asn1js.ObjectIdentifier({ + value: hashOid + })] + }), + // certHash + new asn1js.OctetString({ + valueHex: signCertHash + }) + // issuerSerial (omitted here; contained in signerInfo) + ] + }); + + const signingCertificateV2 = new asn1js.Sequence({ + value: [ + // certs + new asn1js.Sequence({ + value: [essCertIDv2] + }) + // policies (shall not be used according to [CAdES - 5.2.2.3]) + ] + }); + + signedAttrs.push(new pkijs.Attribute({ + type: oids.signingCertificateV2, + values: [signingCertificateV2] + })); + return signedAttrs; + } + + /** + * Obtain the unsigned attributes. + * @returns {pkijs.Attribute[]} + */ + async obtainUnsignedAttributes(signature) { + return []; + } + + /** + * @param {Buffer} pdfBuffer + * @param {Date | undefined} signingTime + * @returns {Promise} + */ + async sign(pdfBuffer, signingTime = undefined) { + if (!(pdfBuffer instanceof Buffer)) { + throw new _utils.SignPdfError('PDF expected as Buffer.', _utils.SignPdfError.TYPE_INPUT); + } + + // Get signature and hash algorithms + this.signAlgorithm = await this.getSignAlgorithm(); + this.hashAlgorithm = await this.getHashAlgorithm(); + // Get a crypto extension + this.crypto = this.getCrypto(); + // Get the signing (and verification) certificates + const certificates = await this.obtainCertificates(); + const signCert = certificates[0]; + // Obtain the private key used for signing + const key = await this.obtainKey(); + + // Creation of the CMS Signed Data structure (see [PDF - 12.8.3.3] and [PAdES - 4.1]) + // Setup signer info (see [CMS - 5.3]) + const signerInfo = new pkijs.SignerInfo({ + version: 1, + sid: new pkijs.IssuerAndSerialNumber({ + issuer: signCert.issuer, + serialNumber: signCert.serialNumber + }), + signedAttrs: new pkijs.SignedAndUnsignedAttributes({ + type: 0, + attributes: await this.obtainSignedAttributes(signingTime, pdfBuffer, signCert) + }) + }); + + // Initialize CMS Signed Data structure (see [CMS - 5.1]) and sign it + const cmsSignedData = new pkijs.SignedData({ + version: 1, + encapContentInfo: new pkijs.EncapsulatedContentInfo({ + eContentType: oids.data // No actual econtent here, as we sign in detached mode + }), + + signerInfos: [signerInfo], + certificates + }); + + // Sign in detached mode. That's what Adobe.PPKLite expects for subfilters + // adbe.pkcs7.detached and ETSI.CAdES.detached. + await cmsSignedData.sign(key, 0, this.hashAlgorithm, undefined, this.crypto); + + // Append the unsigned attributes, if there are any + const unsignedAttrs = await this.obtainUnsignedAttributes(signerInfo.signature.getValue()); + if (unsignedAttrs.length > 0) { + signerInfo.unsignedAttrs = new pkijs.SignedAndUnsignedAttributes({ + type: 1, + attributes: unsignedAttrs + }); + } + + // Create final result + const cmsContentWrap = new pkijs.ContentInfo({ + contentType: oids.signedData, + content: cmsSignedData.toSchema(true) + }); + return Buffer.from(cmsContentWrap.toSchema().toBER(false)); + } + + /** + * Verify whether the signature generated by the sign function is correct. + * @param {Buffer} cmsSignedBuffer + * @param {Buffer} pdfBuffer + * @returns {boolean} + */ + async verify(cmsSignedBuffer, pdfBuffer) { + // Based on cmsSignedComplexExample from PKI.js + const cmsContentSimpl = pkijs.ContentInfo.fromBER(cmsSignedBuffer); + const cmsSignedSimpl = new pkijs.SignedData({ + schema: cmsContentSimpl.content + }); + return cmsSignedSimpl.verify({ + signer: 0, + trustedCerts: [], + data: pdfBuffer + }, this.getCrypto()); + } +} +exports.Signer = Signer; \ No newline at end of file diff --git a/packages/signer/dist/index.d.ts b/packages/signer/dist/index.d.ts new file mode 100644 index 00000000..2bedfd1b --- /dev/null +++ b/packages/signer/dist/index.d.ts @@ -0,0 +1,3 @@ +export * from "./Signer"; +export * from "./ExternalSigner"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/signer/dist/index.d.ts.map b/packages/signer/dist/index.d.ts.map new file mode 100644 index 00000000..6e08bd54 --- /dev/null +++ b/packages/signer/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/signer/dist/index.js b/packages/signer/dist/index.js new file mode 100644 index 00000000..f89290a1 --- /dev/null +++ b/packages/signer/dist/index.js @@ -0,0 +1,27 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _Signer = require("./Signer"); +Object.keys(_Signer).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _Signer[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _Signer[key]; + } + }); +}); +var _ExternalSigner = require("./ExternalSigner"); +Object.keys(_ExternalSigner).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ExternalSigner[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ExternalSigner[key]; + } + }); +}); \ No newline at end of file diff --git a/packages/signer/jest.config.js b/packages/signer/jest.config.js new file mode 100644 index 00000000..8fe322bc --- /dev/null +++ b/packages/signer/jest.config.js @@ -0,0 +1,4 @@ +const sharedConfig = require('../../jest.config.base'); +module.exports = { + ...sharedConfig, +}; \ No newline at end of file diff --git a/packages/signer/package.json b/packages/signer/package.json new file mode 100644 index 00000000..f63907d8 --- /dev/null +++ b/packages/signer/package.json @@ -0,0 +1,74 @@ +{ + "name": "@signpdf/signer", + "version": "3.2.0", + "description": "Signer base implementations.", + "repository": { + "type": "git", + "url": "https://github.com/vbuch/node-signpdf" + }, + "license": "MIT", + "keywords": [ + "sign", + "pdf", + "node", + "nodejs", + "esign", + "adobe", + "ppklite", + "sign detached", + "pkcs7", + "pkcs#7", + "pades", + "digital signature" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "engines": { + "node": ">=12", + "yarn": ">=1.22.18" + }, + "scripts": { + "test": "jest", + "build": "rm -rf ./dist/* & babel ./src -d ./dist --ignore \"**/*.test.js\" & tsc", + "lint": "eslint -c .eslintrc --ignore-path ../../.eslintignore ./" + }, + "dependencies": { + "@signpdf/utils": "^3.2.0" + }, + "peerDependencies": { + "asn1js": "^3.0.5", + "pkijs": "^3.0.15" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.4.0", + "@babel/eslint-parser": "^7.16.3", + "@babel/node": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "@babel/preset-env": "^7.4.2", + "@signpdf/eslint-config": "^3.2.0", + "@signpdf/internal-utils": "^3.0.0", + "@types/node": ">=12.0.0", + "assertion-error": "^1.1.0", + "babel-jest": "^27.3.1", + "babel-plugin-module-resolver": "^3.1.1", + "coveralls": "^3.0.2", + "eslint": "^8.50.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-import-resolver-babel-module": "^5.3.1", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.2.4", + "husky": "^7.0.4", + "jest": "^27.3.1", + "asn1js": "^3.0.5", + "pkijs": "^3.0.15", + "pdfkit": "~0.10.0", + "typescript": "^5.2.2" + }, + "gitHead": "5d5ec00c21e072613acb9776c7c6ac7697314955" +} diff --git a/packages/signer/src/ExternalSigner.js b/packages/signer/src/ExternalSigner.js new file mode 100644 index 00000000..b1ba986e --- /dev/null +++ b/packages/signer/src/ExternalSigner.js @@ -0,0 +1,66 @@ +/* eslint-disable no-unused-vars */ +import * as pkijs from 'pkijs'; +import {SignPdfError} from '@signpdf/utils'; +import {Signer} from './Signer'; + +/** + * 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} + */ + async getSignature(hash, data) { + throw new SignPdfError( + `getSignature() is not implemented on ${this.constructor.name}`, + 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; + } +} diff --git a/packages/signer/src/ExternalSigner.test.js b/packages/signer/src/ExternalSigner.test.js new file mode 100644 index 00000000..1bba598a --- /dev/null +++ b/packages/signer/src/ExternalSigner.test.js @@ -0,0 +1,144 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-restricted-syntax */ +import * as crypto from 'crypto'; +import {SignPdfError} from '@signpdf/utils'; +import {ExternalSigner} from './ExternalSigner'; + +const params = { + rsa: { + hash: 'SHA-256', + sign: 'RSASSA-PKCS1-v1_5', + cert: 'MIICsTCCAZugAwIBAgIBATALBgkqhkiG9w0BAQswHjEcMAkGA1UEBhMCTk8wDwYDVQQDHggAVABlAHMAdDAeFw0yMzEyMzAxMzU4MjNaFw0yNDEyMzAxMzU4MjNaMB4xHDAJBgNVBAYTAk5PMA8GA1UEAx4IAFQAZQBzAHQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDx7g2c2eoDMrDNXAyZqRnJ9u5eADSJ7xfkwn5UxbrQKgeBFFKXGsSfRY7dOJuW46ZgfrhDUSL0rHoTKxak1eNF8STHPoI3YTj4r/GTlEtLS0q9szlTpg0x9kX4ss6D3Y2m6ll8YuzaDYiVkuZTSKUXYE50RrD3EiNX2GhlTuV8qYq1tiALVzUGnAvlQP4OibMeHt2K+NPIE4zw57zuI1QwYyBuNLoUZHYJw9IqHSapAT1vZjWOSXg5KSzr7v6h1MUxL4zB8yWj3RReUBE6vctXi4mtTsV7cTxyRNuvyQ9Yv0eaC+ixjvHDlaIoskv/+K2KGpaFN+jgukeiLKUrBR5TAgMBAAEwCwYJKoZIhvcNAQELA4IBAQCs3WjG+gDKwHfC2qCW6xiHKwPf2O6xLsstiRtS08U29MgxayPnmqRs73iNTeR3x4dVqX7Fl+/oMac7rg7sNGi5Hkglfx/N6sp/xq7M7+jhC6vc0x0bCdevRcs7QQypMbZIT5ld9BLw0C92/HMYb1QZHhbL98cjoITjwTgzGSl2MA8kph64khBBcx77cwic7bLzOeXaFvFN9/x6H+K1bTxTpcpPaL/tQu9X/ERN4/L+EU+tI2uAM5Rmlyiw70dxB0l2Nr5jVYL33M+Kcvyv3uOZDOgPB7I01SiayJUnc67yJ5rSNY2Ciofp/Sq94Oh6Q1LqTXQswy2v8DzM6Ae5ydLb', + key: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDx7g2c2eoDMrDNXAyZqRnJ9u5eADSJ7xfkwn5UxbrQKgeBFFKXGsSfRY7dOJuW46ZgfrhDUSL0rHoTKxak1eNF8STHPoI3YTj4r/GTlEtLS0q9szlTpg0x9kX4ss6D3Y2m6ll8YuzaDYiVkuZTSKUXYE50RrD3EiNX2GhlTuV8qYq1tiALVzUGnAvlQP4OibMeHt2K+NPIE4zw57zuI1QwYyBuNLoUZHYJw9IqHSapAT1vZjWOSXg5KSzr7v6h1MUxL4zB8yWj3RReUBE6vctXi4mtTsV7cTxyRNuvyQ9Yv0eaC+ixjvHDlaIoskv/+K2KGpaFN+jgukeiLKUrBR5TAgMBAAECggEARatxPh8/FcauRaVhd4rMA25Z/LYAa0xwTA96g/0hLDs+R/zLP/qUbu2AE9Luc4iBbD5x3+josR9OVGDPN47k0Up9dedZZotX9b3tSruk5zz6fCC1vJIQMLgkNFeBw1W1KkFIndp7LNxC3hgcvhUofc/dJ/0Hj08o4G5QugP7cGdvUdCgaIv9Kc2XgVHuj/jIQ6zh/t16k5LOYbqTkLof0P8uLT5npCBSwzdNtXgq2HnSa65cPKoOvFtNijUQxcMyWlZhUnnETqLPS/QR9925HZfrJD63Swo/tUo8bxNaFuJ4sFUCyRKHuEmwCPIuMBMyyHtDnIrKdMfFLYPDGiR/yQKBgQD8cub9d/Xw5C8YwCQp3Ern043Xj5/QqRtr4CX75hvloiQqW/kDaEDLCny+O2zB6mhMyhiJ3nLiGR7vwNP9xpUFVwxB838tT1RnU3TzwGxX7U4pb+vBgPSJ4tsm6fi5a8iACCOR9Ol0mQSYHGj1V0LjtyeseSENNDDUuXlECHzinQKBgQD1VUVb4frfb4BlmidJ4cNo2Tv+L4zqvsCLBd2AsQgP3/UnvPW5G1ch8wBCaCxMsyPanEJKkNb19xotrz297qj3bnsxnpHT/9mdhysQgtq1cSS/JbgLDH5CxOJkVPYYuFcbFpyYCNjAXdWTVneM6bgDzWn7GaloSbC+z6zfrZJ5rwKBgAVP2aJtusQV2RPZPymOVBkqr2pgwEkgtc4ASmxRfDbJwZ8Ojl/O+sYK83MBRxmReB5dhkSoZJxzUN5UnWFSo2IYYc6ldACdEbaW3/gdDWqQOLvQehJ5Ryv7lQbtl2k3ZmGjSjRRBfSJH7qfpmm7K1IlUXVkmxhvB7QCLxYKY65VAoGAa/AiXfSkzd1e3lTeaFEeCmlZTdJZ64YVbJUeCVraMzHTyLIIYnK2+UbSnGT86JoAclRBjlD/YqRfLi5lucThLku7g+9CNcXhVr97oP4Rf7bR9g+AU8whWPsKqB4BUIr6N/7Q0stBohEmwHZQjvzdaz7bHBEESc/yJWF1q8vQIB0CgYEAyStccHwy2e0pXMg7pbMRzOGrnQkJbv/hISeWpX4YtJpR/IOei2dSchWjhxMcZfWofsiXUvhdcpFt3UIUquyc7e9/uS0Pqx3r9XhB3oUay3AXAIWW0lW5NixRGcLe9On+Ouub2ebDZie4dQ0o9lziQJpuzWZ+O02LYeoj3F9roUQ=', + }, + rsa2: { + hash: 'SHA-384', + sign: 'RSA-PSS', + saltLength: 48, + cert: 'MIIDHTCCAdGgAwIBAgIBATBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAgUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATAwHjEcMAkGA1UEBhMCTk8wDwYDVQQDHggAVABlAHMAdDAeFw0yMzEyMzAxNzA5NThaFw0yNDEyMzAxNzA5NThaMB4xHDAJBgNVBAYTAk5PMA8GA1UEAx4IAFQAZQBzAHQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSvg6Di73WPg9msuZ098/E0xy8kXr/2PgPj+mwahdt6FoyG7I9p2dY8gqJmbsHQ38xuFIyfIMO96f7qWjGRHKRqVJF3ioO0T/q0bh5LfrgrylVou5zy9HdmpnSVMu07elZxMDE/62DtIKaauBfpZBi6GpmErVAKDYpUyuF+HPOZYFV4prZNg7bPr5gzIpUuB4rM0YnzdnUze7BGFftQX8IyVJ2lOx+dn83Z9gBOxcQg+Zm2CAqCr6YYmnbiQZPpFuTUsvVwugUTrfw4fYKjl/u55Lpgp3YiDi0hWUEEhELopZHFwg1RlUJIWfJw9quAHZiX50Upa7zQ1QwB3Ai8Xj7AgMBAAEwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKIDAgEwA4IBAQAAscloWgW2V/oyLENmuMfQzuHsTD//v2PwSjUgCe3wgiadv7trMSNPYlLSYWgKcrTzxdnjQzgYenpvvYdV3azxwVxQDDJopsECVTQ5CXeI2GjTKQJ+NOnVzmuuksjne0qZHRtXjS0VweUC7zcYGHN6zD3kNgzbCcF+zYVFqdEebTeNZNj7fT29EygBfWM1XTVjwEjC45E9n7w+vfkSN+EKXsIh3Kq3obJh7Q4GKqxPdmXNTmoxfvVuZWohuLhQ6Bg7Ij/tj4P6pwYkE+pqnfdWYX6S1yKMtdCtjkrFf/Ki9pn4dEV8b0L9hfW4t4UeMm17nYC+896RYS5N+eNs/TB5', + key: 'MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDSvg6Di73WPg9msuZ098/E0xy8kXr/2PgPj+mwahdt6FoyG7I9p2dY8gqJmbsHQ38xuFIyfIMO96f7qWjGRHKRqVJF3ioO0T/q0bh5LfrgrylVou5zy9HdmpnSVMu07elZxMDE/62DtIKaauBfpZBi6GpmErVAKDYpUyuF+HPOZYFV4prZNg7bPr5gzIpUuB4rM0YnzdnUze7BGFftQX8IyVJ2lOx+dn83Z9gBOxcQg+Zm2CAqCr6YYmnbiQZPpFuTUsvVwugUTrfw4fYKjl/u55Lpgp3YiDi0hWUEEhELopZHFwg1RlUJIWfJw9quAHZiX50Upa7zQ1QwB3Ai8Xj7AgMBAAECggEAF/ypA0EPIQkcUKixQYQJYpJS9YWrld540bCkPT++HfTx+z0ayxi4Uy/tkh7s4lVE74BW3sc8VaJLvQHZ2qNk1P6MTkaMy/X2TACF+kkJArT9Q/GRKZ3767jSpFWhgB0KT/zsoh+l/NU5fmEG2+wo5JEmtyqO9df93jNIw211pPrE2LZeet+s5ocu/rDiqdaUvgdK3qGfILepOM9C57YExjj9O/8wB6I0H4l6TPvz/MpVlN34PbTtnu7LbEp1aKhnrYS759x1gkzUnQu58cJSKXzMQc2L9Yz3ZoU0CvDQIajPmco8dQQyvVyCFLrjE+ilEniBqwm82cfl2VWbV8gtSQKBgQDvl1ygY168gUkzIeBORVr17vVOowEWLJeCTu1JIpO/CVSgAAsXb1TUSMjD6M2RTeY5OZxoDJn0dGVjyK2G7i3+r+3kZeek3hU+FVVMjqqc4ZTY6pNoD+ZHpjaXQ8+3hnxVSZVAzZZKQoWsgckAa4OaydnyvoAMm7vQQLOAagtPuQKBgQDhLObjPEQnZQYnQ0BE0h0mbgmIDeubnNxA4SNc761FIPnjqMiZpHpw9augphYl7yaGyMxIZtemBmQ1LE8KqLJ13/cfDAua1eorJusxcBMxaQYi1ftZkyHiHE5oKbDnnhEWZbiclXaipqYu/tkSjKAjVcCjIMjkTT4NRSkMlougUwKBgQDeCiiNe4oQqMBt4rc90oi54vr0JZycMDc5TGwbmy/Rm5QRs/iTf1neYpWs+dOJFeiGoB2TUBpdsoFEOHaY1aNKpUCSqt5CrI6DOsIScSUsvuJzJPH3PsTrId710KJwMvKHF+p6ZA0kRQIdHSpqeTk9+lNB48pH5QvptAymtaBGAQKBgQDYhwEPCejPqQmdv8GRbCqzxaRMvHYPkWsKyHPk7mTD0M366Vbcx5kl/lcniA5MNAfsHDK0fJBqCkNaDl2Ddm0FhHRx2ndeUM146VfIlmkeXkuUPAgTPrSaZXXQHCcM0qbDztUTXXZTOcUb89xxReDHmbJAK0qCnvRIdnTesfVaGQKBgQCtFcrF/Kpj+MR5RHAtbFFQkq/xXZimpdhTbBLmn/AI1T3haYknMEmYDwqAXwrecss+O45U3FQp0jhuDixKp7zx4//xOZVnXqySeFJEJc4W9DZIrmmoo8HdBnJ7LY/J6IDJXyKzIasL4lVifSxNkFtc42ME6OQY4CXKZHD3FsOWxQ==', + }, + ec: { + hash: 'SHA-512', + sign: 'ECDSA', + cert: 'MIIBKDCBz6ADAgECAgEBMAoGCCqGSM49BAMEMB4xHDAJBgNVBAYTAk5PMA8GA1UEAx4IAFQAZQBzAHQwHhcNMjMxMjMwMTcxMDU5WhcNMjQxMjMwMTcxMDU5WjAeMRwwCQYDVQQGEwJOTzAPBgNVBAMeCABUAGUAcwB0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJVixslo+c/efr24AgbSkzJ/g4vDg7tizHZfnfJ/k/CsWa5fyE8lBTVipwIYFITYPBI/9JZTAB0+Hspb6yZ9RJDAKBggqhkjOPQQDBANIADBFAiEAtWiyW4uQc8aR6KaM3FSr9bng1JFPWGm5W79tcHehbqwCICWswWoGKFxcJ98EEiB/IQG/M3Itl5hIdNQrw+reYzbf', + key: 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd+wrLUcK74mALBOgcGaO7badyke95VYEAJyHAMSVQR6hRANCAAQlWLGyWj5z95+vbgCBtKTMn+Di8ODu2LMdl+d8n+T8KxZrl/ITyUFNWKnAhgUhNg8Ej/0llMAHT4eylvrJn1Ek', + }, +}; + +class CryptoSigner extends ExternalSigner { + constructor(config) { + super(); + this.signAlgorithm = params[config].sign; + this.hashAlgorithm = params[config].hash; + this.saltLength = params[config].saltLength; + this.certBase64 = params[config].cert; + this.keyBase64 = params[config].key; + } + + async getCertificate() { + return Buffer.from(this.certBase64, 'base64'); + } + + async getSignature(_hash, data) { + const key = await crypto.subtle.importKey( + 'pkcs8', + Buffer.from(this.keyBase64, 'base64'), + {name: this.signAlgorithm, hash: this.hashAlgorithm, namedCurve: 'P-256'}, + false, + ['sign'], + ); + return crypto.subtle.sign({ + name: this.signAlgorithm, + hash: this.hashAlgorithm, + saltLength: this.saltLength, + }, key, data); + } +} + +describe(ExternalSigner, () => { + it('expects pdf to be Buffer', async () => { + try { + const signer = new ExternalSigner(); + await signer.sign('non-buffer'); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"PDF expected as Buffer."'); + } + }); + it('expects an error when getCertificate is not implemented', async () => { + try { + const signer = new ExternalSigner(); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.message).toMatchInlineSnapshot('"getCertificate() is not implemented on ExternalSigner"'); + } + }); + it('expects an error when returning an empty certificate buffer', async () => { + try { + const signer = new ExternalSigner(); + signer.getCertificate = () => Buffer.from(''); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Error during parsing of ASN.1 data. Data is not correct for \'Certificate\'."'); + } + }); + it('expects an error when getSignature is not implemented', async () => { + try { + const signer = new ExternalSigner(); + signer.getCertificate = () => Buffer.from(params.rsa.cert, 'base64'); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.message).toMatchInlineSnapshot('"getSignature() is not implemented on ExternalSigner"'); + } + }); + it('expects invalid signature when returning an empty signature buffer', async () => { + const data = Buffer.from('test'); + const signer = new ExternalSigner(); + signer.getCertificate = () => Buffer.from(params.rsa.cert, 'base64'); + signer.getSignature = () => Buffer.from(''); + const signature = await signer.sign(data); + const verified = await signer.verify(signature, data); + expect(verified).toBe(false); + }); + it('expects an error when a non-supported signature algorithm is returned', async () => { + try { + const signer = new CryptoSigner('rsa'); + signer.signAlgorithm = 'non-existent'; + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Unrecognized algorithm name"'); + } + }); + it('expects an error when a non-supported hash algorithm is returned', async () => { + try { + const signer = new CryptoSigner('rsa'); + signer.hashAlgorithm = 'non-existent'; + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Unrecognized algorithm name"'); + } + }); + it('expects successful signature creation', async () => { + const data = Buffer.from('test'); + for (const cfg of Object.keys(params)) { + const signer = new CryptoSigner(cfg); + const signature = await signer.sign(data); + expect(signature instanceof Buffer).toBe(true); + const verified = await signer.verify(signature, data); + expect(verified).toBe(true); + } + }); +}); diff --git a/packages/signer/src/Signer.js b/packages/signer/src/Signer.js new file mode 100644 index 00000000..f21915f2 --- /dev/null +++ b/packages/signer/src/Signer.js @@ -0,0 +1,273 @@ +/* eslint-disable no-unused-vars */ +import * as nodeCrypto from 'crypto'; +import * as asn1js from 'asn1js'; +import * as pkijs from 'pkijs'; +import {ISigner, SignPdfError} from '@signpdf/utils'; + +// Useful references to understand what is going on here: +// * PDF: Portable Document Format (ISO 32000-1) +// * CMS: Cryptographic Message Syntax (RFC 5652) +// * CAdES: CMS Advanced Electronic Signatures (ETSI 319 122-1) +// * PAdES: PDF Advanced Electronic Signatures (ETSI 319 142-1) +// Some code comments will refer to these specifications using square brackets, +// e.g. [PDF - 12.8] means section 12.8 of the ISO 32000-1 specification. + +// Object identifiers used in the created CMS Signed Data structure +const oids = { + data: '1.2.840.113549.1.7.1', + signedData: '1.2.840.113549.1.7.2', + contentType: '1.2.840.113549.1.9.3', + messageDigest: '1.2.840.113549.1.9.4', + signingTime: '1.2.840.113549.1.9.5', + signingCertificateV2: '1.2.840.113549.1.9.16.2.47', +}; + +/** + * 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 = 'RSASSA-PKCS1-v1_5'; + + /** Hash algorithm used for PDF signing + * @type {string} + */ + hashAlgorithm = 'SHA-256'; + + /** + * Method to retrieve the signature algorithm used for PDF signing. + * To be implemented by subclasses or set in the `signAlgorithm` attribute. + * @returns {Promise} + */ + async getSignAlgorithm() { + return this.signAlgorithm; // Use default signature algorithm if not overridden by subclass + } + + /** + * Method to retrieve the hashing algorithm used for PDF signing. + * To be implemented by subclasses or set in the `hashAlgorithm` attribute. + * @returns {Promise} + */ + async getHashAlgorithm() { + return this.hashAlgorithm; // Use default hash algorithm if not overridden by subclass + } + + /** + * 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} + */ + async getCertificate() { + throw new SignPdfError( + `getCertificate() is not implemented on ${this.constructor.name}`, + SignPdfError.TYPE_INPUT, + ); + } + + /** + * 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} + */ + async getKey() { + throw new SignPdfError( + `getKey() is not implemented on ${this.constructor.name}`, + SignPdfError.TYPE_INPUT, + ); + } + + /** + * Get a "crypto" extension. + * @returns {pkijs.ICryptoEngine} + */ + getCrypto() { + const crypto = new pkijs.CryptoEngine({name: 'SignerCrypto', crypto: nodeCrypto}); + return crypto; + } + + /** + * Obtain the certificates used for signing (first one) and verification (whole list). + * @returns {pkijs.Certificate[]} + */ + async obtainCertificates() { + let certBytes = await this.getCertificate(); + if (!Array.isArray(certBytes)) { + certBytes = [certBytes]; + } + return certBytes.map((cb) => pkijs.Certificate.fromBER(cb)); + } + + /** + * Obtain the private key used for signing. + * @returns {CryptoKey} + */ + async obtainKey() { + const keyBytes = await this.getKey(); + const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'importkey').algorithm; + return this.crypto.importKey('pkcs8', keyBytes, { + name: this.signAlgorithm, + ...algorithmParams, + hash: {name: this.hashAlgorithm}, + }, false, ['sign']); + } + + /** + * Obtain the signed attributes, which are the actual content that is signed in detached mode. + * @returns {pkijs.Attribute[]} + */ + async obtainSignedAttributes(signingTime, data, signCert) { + // Create a message digest + const digest = await this.crypto.digest({name: this.hashAlgorithm}, data); + // Note that the signed attributes order is relevant for correct EU signature validation: + // https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation + const signedAttrs = [ + // [CAdES - 5.1.1] + new pkijs.Attribute({ + type: oids.contentType, + values: [new asn1js.ObjectIdentifier({value: oids.data})], + }), + // [CAdES - 5.2.1] + new pkijs.Attribute({ + type: oids.signingTime, + values: [new asn1js.UTCTime({valueDate: signingTime ?? new Date()})], + }), + // [CAdES - 5.1.2] + new pkijs.Attribute({ + type: oids.messageDigest, + values: [new asn1js.OctetString({valueHex: digest})], + }), + ]; + + // Add the ESS signing certificate attribute (see [CAdES - 5.2.2.3] and [RFC 5035]) + const hashOid = this.crypto.getOIDByAlgorithm({name: this.hashAlgorithm}, true, 'hashAlgorithm'); + const signCertHash = await this.crypto.digest( + {name: this.hashAlgorithm}, + signCert.toSchema(true).toBER(false), + ); + const essCertIDv2 = new asn1js.Sequence({ + value: [ + // hashAlgorithm + new asn1js.Sequence({value: [new asn1js.ObjectIdentifier({value: hashOid})]}), + // certHash + new asn1js.OctetString({valueHex: signCertHash}), + // issuerSerial (omitted here; contained in signerInfo) + ], + }); + const signingCertificateV2 = new asn1js.Sequence({ + value: [ + // certs + new asn1js.Sequence({value: [essCertIDv2]}), + // policies (shall not be used according to [CAdES - 5.2.2.3]) + ], + }); + signedAttrs.push(new pkijs.Attribute({ + type: oids.signingCertificateV2, + values: [signingCertificateV2], + })); + return signedAttrs; + } + + /** + * Obtain the unsigned attributes. + * @returns {pkijs.Attribute[]} + */ + async obtainUnsignedAttributes(signature) { + return []; + } + + /** + * @param {Buffer} pdfBuffer + * @param {Date | undefined} signingTime + * @returns {Promise} + */ + async sign(pdfBuffer, signingTime = undefined) { + if (!(pdfBuffer instanceof Buffer)) { + throw new SignPdfError( + 'PDF expected as Buffer.', + SignPdfError.TYPE_INPUT, + ); + } + + // Get signature and hash algorithms + this.signAlgorithm = await this.getSignAlgorithm(); + this.hashAlgorithm = await this.getHashAlgorithm(); + // Get a crypto extension + this.crypto = this.getCrypto(); + // Get the signing (and verification) certificates + const certificates = await this.obtainCertificates(); + const signCert = certificates[0]; + // Obtain the private key used for signing + const key = await this.obtainKey(); + + // Creation of the CMS Signed Data structure (see [PDF - 12.8.3.3] and [PAdES - 4.1]) + // Setup signer info (see [CMS - 5.3]) + const signerInfo = new pkijs.SignerInfo({ + version: 1, + sid: new pkijs.IssuerAndSerialNumber({ + issuer: signCert.issuer, + serialNumber: signCert.serialNumber, + }), + signedAttrs: new pkijs.SignedAndUnsignedAttributes({ + type: 0, + attributes: await this.obtainSignedAttributes(signingTime, pdfBuffer, signCert), + }), + }); + + // Initialize CMS Signed Data structure (see [CMS - 5.1]) and sign it + const cmsSignedData = new pkijs.SignedData({ + version: 1, + encapContentInfo: new pkijs.EncapsulatedContentInfo({ + eContentType: oids.data, // No actual econtent here, as we sign in detached mode + }), + signerInfos: [signerInfo], + certificates, + }); + + // Sign in detached mode. That's what Adobe.PPKLite expects for subfilters + // adbe.pkcs7.detached and ETSI.CAdES.detached. + await cmsSignedData.sign(key, 0, this.hashAlgorithm, undefined, this.crypto); + + // Append the unsigned attributes, if there are any + const unsignedAttrs = await this.obtainUnsignedAttributes(signerInfo.signature.getValue()); + if (unsignedAttrs.length > 0) { + signerInfo.unsignedAttrs = new pkijs.SignedAndUnsignedAttributes({ + type: 1, + attributes: unsignedAttrs, + }); + } + + // Create final result + const cmsContentWrap = new pkijs.ContentInfo({ + contentType: oids.signedData, + content: cmsSignedData.toSchema(true), + }); + + return Buffer.from(cmsContentWrap.toSchema().toBER(false)); + } + + /** + * Verify whether the signature generated by the sign function is correct. + * @param {Buffer} cmsSignedBuffer + * @param {Buffer} pdfBuffer + * @returns {boolean} + */ + async verify(cmsSignedBuffer, pdfBuffer) { + // Based on cmsSignedComplexExample from PKI.js + const cmsContentSimpl = pkijs.ContentInfo.fromBER(cmsSignedBuffer); + const cmsSignedSimpl = new pkijs.SignedData({schema: cmsContentSimpl.content}); + + return cmsSignedSimpl.verify({ + signer: 0, + trustedCerts: [], + data: pdfBuffer, + }, this.getCrypto()); + } +} diff --git a/packages/signer/src/Signer.test.js b/packages/signer/src/Signer.test.js new file mode 100644 index 00000000..510bd486 --- /dev/null +++ b/packages/signer/src/Signer.test.js @@ -0,0 +1,113 @@ +import {SignPdfError} from '@signpdf/utils'; +import {Signer} from './Signer'; + +const params = { + hash: 'SHA-256', + sign: 'RSASSA-PKCS1-v1_5', + cert: 'MIICsTCCAZugAwIBAgIBATALBgkqhkiG9w0BAQswHjEcMAkGA1UEBhMCTk8wDwYDVQQDHggAVABlAHMAdDAeFw0yMzEyMzAxMzU4MjNaFw0yNDEyMzAxMzU4MjNaMB4xHDAJBgNVBAYTAk5PMA8GA1UEAx4IAFQAZQBzAHQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDx7g2c2eoDMrDNXAyZqRnJ9u5eADSJ7xfkwn5UxbrQKgeBFFKXGsSfRY7dOJuW46ZgfrhDUSL0rHoTKxak1eNF8STHPoI3YTj4r/GTlEtLS0q9szlTpg0x9kX4ss6D3Y2m6ll8YuzaDYiVkuZTSKUXYE50RrD3EiNX2GhlTuV8qYq1tiALVzUGnAvlQP4OibMeHt2K+NPIE4zw57zuI1QwYyBuNLoUZHYJw9IqHSapAT1vZjWOSXg5KSzr7v6h1MUxL4zB8yWj3RReUBE6vctXi4mtTsV7cTxyRNuvyQ9Yv0eaC+ixjvHDlaIoskv/+K2KGpaFN+jgukeiLKUrBR5TAgMBAAEwCwYJKoZIhvcNAQELA4IBAQCs3WjG+gDKwHfC2qCW6xiHKwPf2O6xLsstiRtS08U29MgxayPnmqRs73iNTeR3x4dVqX7Fl+/oMac7rg7sNGi5Hkglfx/N6sp/xq7M7+jhC6vc0x0bCdevRcs7QQypMbZIT5ld9BLw0C92/HMYb1QZHhbL98cjoITjwTgzGSl2MA8kph64khBBcx77cwic7bLzOeXaFvFN9/x6H+K1bTxTpcpPaL/tQu9X/ERN4/L+EU+tI2uAM5Rmlyiw70dxB0l2Nr5jVYL33M+Kcvyv3uOZDOgPB7I01SiayJUnc67yJ5rSNY2Ciofp/Sq94Oh6Q1LqTXQswy2v8DzM6Ae5ydLb', + key: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDx7g2c2eoDMrDNXAyZqRnJ9u5eADSJ7xfkwn5UxbrQKgeBFFKXGsSfRY7dOJuW46ZgfrhDUSL0rHoTKxak1eNF8STHPoI3YTj4r/GTlEtLS0q9szlTpg0x9kX4ss6D3Y2m6ll8YuzaDYiVkuZTSKUXYE50RrD3EiNX2GhlTuV8qYq1tiALVzUGnAvlQP4OibMeHt2K+NPIE4zw57zuI1QwYyBuNLoUZHYJw9IqHSapAT1vZjWOSXg5KSzr7v6h1MUxL4zB8yWj3RReUBE6vctXi4mtTsV7cTxyRNuvyQ9Yv0eaC+ixjvHDlaIoskv/+K2KGpaFN+jgukeiLKUrBR5TAgMBAAECggEARatxPh8/FcauRaVhd4rMA25Z/LYAa0xwTA96g/0hLDs+R/zLP/qUbu2AE9Luc4iBbD5x3+josR9OVGDPN47k0Up9dedZZotX9b3tSruk5zz6fCC1vJIQMLgkNFeBw1W1KkFIndp7LNxC3hgcvhUofc/dJ/0Hj08o4G5QugP7cGdvUdCgaIv9Kc2XgVHuj/jIQ6zh/t16k5LOYbqTkLof0P8uLT5npCBSwzdNtXgq2HnSa65cPKoOvFtNijUQxcMyWlZhUnnETqLPS/QR9925HZfrJD63Swo/tUo8bxNaFuJ4sFUCyRKHuEmwCPIuMBMyyHtDnIrKdMfFLYPDGiR/yQKBgQD8cub9d/Xw5C8YwCQp3Ern043Xj5/QqRtr4CX75hvloiQqW/kDaEDLCny+O2zB6mhMyhiJ3nLiGR7vwNP9xpUFVwxB838tT1RnU3TzwGxX7U4pb+vBgPSJ4tsm6fi5a8iACCOR9Ol0mQSYHGj1V0LjtyeseSENNDDUuXlECHzinQKBgQD1VUVb4frfb4BlmidJ4cNo2Tv+L4zqvsCLBd2AsQgP3/UnvPW5G1ch8wBCaCxMsyPanEJKkNb19xotrz297qj3bnsxnpHT/9mdhysQgtq1cSS/JbgLDH5CxOJkVPYYuFcbFpyYCNjAXdWTVneM6bgDzWn7GaloSbC+z6zfrZJ5rwKBgAVP2aJtusQV2RPZPymOVBkqr2pgwEkgtc4ASmxRfDbJwZ8Ojl/O+sYK83MBRxmReB5dhkSoZJxzUN5UnWFSo2IYYc6ldACdEbaW3/gdDWqQOLvQehJ5Ryv7lQbtl2k3ZmGjSjRRBfSJH7qfpmm7K1IlUXVkmxhvB7QCLxYKY65VAoGAa/AiXfSkzd1e3lTeaFEeCmlZTdJZ64YVbJUeCVraMzHTyLIIYnK2+UbSnGT86JoAclRBjlD/YqRfLi5lucThLku7g+9CNcXhVr97oP4Rf7bR9g+AU8whWPsKqB4BUIr6N/7Q0stBohEmwHZQjvzdaz7bHBEESc/yJWF1q8vQIB0CgYEAyStccHwy2e0pXMg7pbMRzOGrnQkJbv/hISeWpX4YtJpR/IOei2dSchWjhxMcZfWofsiXUvhdcpFt3UIUquyc7e9/uS0Pqx3r9XhB3oUay3AXAIWW0lW5NixRGcLe9On+Ouub2ebDZie4dQ0o9lziQJpuzWZ+O02LYeoj3F9roUQ=', +}; + +describe(Signer, () => { + it('expects pdf to be Buffer', async () => { + try { + const signer = new Signer(); + await signer.sign('non-buffer'); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"PDF expected as Buffer."'); + } + }); + it('getCertificate method must be implemented', async () => { + try { + const signer = new Signer(); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"getCertificate() is not implemented on Signer"'); + } + }); + it('expects an error when returning an empty certificate buffer', async () => { + try { + const signer = new Signer(); + signer.getCertificate = () => Buffer.from(''); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Error during parsing of ASN.1 data. Data is not correct for \'Certificate\'."'); + } + }); + it('getKey method must be implemented', async () => { + try { + const signer = new Signer(); + signer.signAlgorithm = params.sign; + signer.hashAlgorithm = params.hash; + signer.getCertificate = () => Buffer.from(params.cert, 'base64'); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"getKey() is not implemented on Signer"'); + } + }); + it('expects an error when returning an empty key buffer', async () => { + try { + const signer = new Signer(); + signer.signAlgorithm = params.sign; + signer.hashAlgorithm = params.hash; + signer.getCertificate = () => Buffer.from(params.cert, 'base64'); + signer.getKey = () => Buffer.from(''); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Error during parsing of ASN.1 data. Data is not correct for \'keyData\'."'); + } + }); + it('expects an error when a non-supported signature algorithm is set', async () => { + try { + const signer = new Signer(); + signer.signAlgorithm = 'non-existent'; + signer.hashAlgorithm = params.hash; + signer.getCertificate = () => Buffer.from(params.cert, 'base64'); + signer.getKey = () => Buffer.from(params.key, 'base64'); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Incorrect algorithm name: NON-EXISTENT"'); + } + }); + it('expects an error when a non-supported hash algorithm is set', async () => { + try { + const signer = new Signer(); + signer.signAlgorithm = params.sign; + signer.hashAlgorithm = 'non-existent'; + signer.getCertificate = () => Buffer.from(params.cert, 'base64'); + signer.getKey = () => Buffer.from(params.key, 'base64'); + await signer.sign(Buffer.from('')); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(false); + expect(e.message).toMatchInlineSnapshot('"Incorrect hash algorithm: NON-EXISTENT"'); + } + }); + it('expects successful signature creation', async () => { + const data = Buffer.from('test'); + const signer = new Signer(); + signer.signAlgorithm = params.sign; + signer.hashAlgorithm = params.hash; + signer.getCertificate = () => Buffer.from(params.cert, 'base64'); + signer.getKey = () => Buffer.from(params.key, 'base64'); + const signature = await signer.sign(data); + expect(signature instanceof Buffer).toBe(true); + const verified = await signer.verify(signature, data); + expect(verified).toBe(true); + }); +}); diff --git a/packages/signer/src/index.js b/packages/signer/src/index.js new file mode 100644 index 00000000..e39be80b --- /dev/null +++ b/packages/signer/src/index.js @@ -0,0 +1,2 @@ +export * from './Signer'; +export * from './ExternalSigner'; diff --git a/packages/signer/tsconfig.json b/packages/signer/tsconfig.json new file mode 100644 index 00000000..49bf12f6 --- /dev/null +++ b/packages/signer/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.*"] +} diff --git a/packages/signpdf/dist/signpdf.d.ts b/packages/signpdf/dist/signpdf.d.ts index 2219f3d0..64f8c952 100644 --- a/packages/signpdf/dist/signpdf.d.ts +++ b/packages/signpdf/dist/signpdf.d.ts @@ -7,11 +7,11 @@ export class SignPdf { lastSignature: string; /** * @param {Buffer | Uint8Array | string} pdfBuffer - * @param {Signer} signer + * @param {ISigner} signer * @param {Date | undefined} signingTime * @returns {Promise} */ - sign(pdfBuffer: Buffer | Uint8Array | string, signer: Signer, signingTime?: Date | undefined): Promise; + sign(pdfBuffer: Buffer | Uint8Array | string, signer: ISigner, signingTime?: Date | undefined): Promise; } declare const _default: SignPdf; export default _default; @@ -19,7 +19,7 @@ export type SignerOptions = { passphrase?: string; asn1StrictParsing?: boolean; }; -import { Signer } from '@signpdf/utils'; +import { ISigner } from '@signpdf/utils'; import { SignPdfError } from '@signpdf/utils'; -export { Signer, SignPdfError }; +export { ISigner, SignPdfError }; //# sourceMappingURL=signpdf.d.ts.map \ No newline at end of file diff --git a/packages/signpdf/dist/signpdf.d.ts.map b/packages/signpdf/dist/signpdf.d.ts.map index 15c61c34..efdae7d1 100644 --- a/packages/signpdf/dist/signpdf.d.ts.map +++ b/packages/signpdf/dist/signpdf.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"signpdf.d.ts","sourceRoot":"","sources":["../src/signpdf.js"],"names":[],"mappings":"AAUA;;;;GAIG;AAEH;IAEQ,sBAAyB;IAG7B;;;;;OAKG;IACH,gBALW,MAAM,GAAG,UAAU,GAAG,MAAM,UAC5B,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAmF3B;CACJ;;;;iBAjGS,MAAM;wBACN,OAAO;;uBAPV,gBAAgB;6BAAhB,gBAAgB"} \ No newline at end of file +{"version":3,"file":"signpdf.d.ts","sourceRoot":"","sources":["../src/signpdf.js"],"names":[],"mappings":"AAUA;;;;GAIG;AAEH;IAEQ,sBAAyB;IAG7B;;;;;OAKG;IACH,gBALW,MAAM,GAAG,UAAU,GAAG,MAAM,UAC5B,OAAO,gBACP,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAmF3B;CACJ;;;;iBAjGS,MAAM;wBACN,OAAO;;wBAPV,gBAAgB;6BAAhB,gBAAgB"} \ No newline at end of file diff --git a/packages/signpdf/dist/signpdf.js b/packages/signpdf/dist/signpdf.js index 27e4d7d5..8f3026fc 100644 --- a/packages/signpdf/dist/signpdf.js +++ b/packages/signpdf/dist/signpdf.js @@ -3,17 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SignPdf = void 0; -Object.defineProperty(exports, "SignPdfError", { +Object.defineProperty(exports, "ISigner", { enumerable: true, get: function () { - return _utils.SignPdfError; + return _utils.ISigner; } }); -Object.defineProperty(exports, "Signer", { +exports.SignPdf = void 0; +Object.defineProperty(exports, "SignPdfError", { enumerable: true, get: function () { - return _utils.Signer; + return _utils.SignPdfError; } }); exports.default = void 0; @@ -31,12 +31,12 @@ class SignPdf { /** * @param {Buffer | Uint8Array | string} pdfBuffer - * @param {Signer} signer + * @param {ISigner} signer * @param {Date | undefined} signingTime * @returns {Promise} */ async sign(pdfBuffer, signer, signingTime = undefined) { - if (!(signer instanceof _utils.Signer)) { + if (!(signer instanceof _utils.ISigner)) { throw new _utils.SignPdfError('Signer implementation expected.', _utils.SignPdfError.TYPE_INPUT); } let pdf = (0, _utils.removeTrailingNewLine)((0, _utils.convertBuffer)(pdfBuffer, 'PDF')); diff --git a/packages/signpdf/src/signpdf.js b/packages/signpdf/src/signpdf.js index b445ad09..da4a06dc 100644 --- a/packages/signpdf/src/signpdf.js +++ b/packages/signpdf/src/signpdf.js @@ -2,11 +2,11 @@ import { convertBuffer, removeTrailingNewLine, findByteRange, + ISigner, SignPdfError, - Signer, } from '@signpdf/utils'; -export {Signer, SignPdfError}; +export {ISigner, SignPdfError}; /** * @typedef {object} SignerOptions @@ -21,7 +21,7 @@ export class SignPdf { /** * @param {Buffer | Uint8Array | string} pdfBuffer - * @param {Signer} signer + * @param {ISigner} signer * @param {Date | undefined} signingTime * @returns {Promise} */ @@ -30,7 +30,7 @@ export class SignPdf { signer, signingTime = undefined, ) { - if (!(signer instanceof Signer)) { + if (!(signer instanceof ISigner)) { throw new SignPdfError( 'Signer implementation expected.', SignPdfError.TYPE_INPUT, diff --git a/packages/signpdf/src/signpdf.test.js b/packages/signpdf/src/signpdf.test.js index bdc75609..68bf6c7d 100644 --- a/packages/signpdf/src/signpdf.test.js +++ b/packages/signpdf/src/signpdf.test.js @@ -3,7 +3,7 @@ import {plainAddPlaceholder} from '@signpdf/placeholder-plain'; import {P12Signer} from '@signpdf/signer-p12'; import { extractSignature, - Signer, + ISigner, SignPdfError, } from '@signpdf/utils'; import {readTestResource, createPdfkitDocument} from '@signpdf/internal-utils'; @@ -78,7 +78,7 @@ describe('Test signing', () => { }); it('expects PDF to contain a ByteRange placeholder', async () => { try { - await signpdf.sign(Buffer.from('No BR placeholder\n%%EOF'), new Signer()); + await signpdf.sign(Buffer.from('No BR placeholder\n%%EOF'), new ISigner()); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); diff --git a/packages/utils/dist/Signer.d.ts b/packages/utils/dist/ISigner.d.ts similarity index 77% rename from packages/utils/dist/Signer.d.ts rename to packages/utils/dist/ISigner.d.ts index 57b9af95..f47d6595 100644 --- a/packages/utils/dist/Signer.d.ts +++ b/packages/utils/dist/ISigner.d.ts @@ -1,4 +1,4 @@ -export class Signer { +export class ISigner { /** * @param {Buffer} pdfBuffer * @param {Date | undefined} signingTime @@ -6,4 +6,4 @@ export class Signer { */ sign(pdfBuffer: Buffer, signingTime?: Date | undefined): Promise; } -//# sourceMappingURL=Signer.d.ts.map \ No newline at end of file +//# sourceMappingURL=ISigner.d.ts.map \ No newline at end of file diff --git a/packages/utils/dist/ISigner.d.ts.map b/packages/utils/dist/ISigner.d.ts.map new file mode 100644 index 00000000..a2a20cec --- /dev/null +++ b/packages/utils/dist/ISigner.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ISigner.d.ts","sourceRoot":"","sources":["../src/ISigner.js"],"names":[],"mappings":"AAGA;IACI;;;;OAIG;IACH,gBAJW,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAO3B;CACJ"} \ No newline at end of file diff --git a/packages/utils/dist/Signer.js b/packages/utils/dist/ISigner.js similarity index 87% rename from packages/utils/dist/Signer.js rename to packages/utils/dist/ISigner.js index 6acebe46..42be1ca8 100644 --- a/packages/utils/dist/Signer.js +++ b/packages/utils/dist/ISigner.js @@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.Signer = void 0; +exports.ISigner = void 0; var _SignPdfError = require("./SignPdfError"); /* eslint-disable no-unused-vars */ -class Signer { +class ISigner { /** * @param {Buffer} pdfBuffer * @param {Date | undefined} signingTime @@ -17,4 +17,4 @@ class Signer { throw new _SignPdfError.SignPdfError(`sign() is not implemented on ${this.constructor.name}`, _SignPdfError.SignPdfError.TYPE_INPUT); } } -exports.Signer = Signer; \ No newline at end of file +exports.ISigner = ISigner; \ No newline at end of file diff --git a/packages/utils/dist/Signer.d.ts.map b/packages/utils/dist/Signer.d.ts.map deleted file mode 100644 index 4836fca9..00000000 --- a/packages/utils/dist/Signer.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Signer.d.ts","sourceRoot":"","sources":["../src/Signer.js"],"names":[],"mappings":"AAGA;IACI;;;;OAIG;IACH,gBAJW,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAO3B;CACJ"} \ No newline at end of file diff --git a/packages/utils/dist/index.d.ts b/packages/utils/dist/index.d.ts index ac1e5be2..42257bd0 100644 --- a/packages/utils/dist/index.d.ts +++ b/packages/utils/dist/index.d.ts @@ -2,7 +2,7 @@ export * from "./const"; export * from "./convertBuffer"; export * from "./extractSignature"; export * from "./findByteRange"; +export * from "./ISigner"; export * from "./removeTrailingNewLine"; export * from "./SignPdfError"; -export * from "./Signer"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/utils/dist/index.js b/packages/utils/dist/index.js index 247d5fc9..8c4a6d77 100644 --- a/packages/utils/dist/index.js +++ b/packages/utils/dist/index.js @@ -47,6 +47,17 @@ Object.keys(_findByteRange).forEach(function (key) { } }); }); +var _ISigner = require("./ISigner"); +Object.keys(_ISigner).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ISigner[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ISigner[key]; + } + }); +}); var _removeTrailingNewLine = require("./removeTrailingNewLine"); Object.keys(_removeTrailingNewLine).forEach(function (key) { if (key === "default" || key === "__esModule") return; @@ -68,15 +79,4 @@ Object.keys(_SignPdfError).forEach(function (key) { return _SignPdfError[key]; } }); -}); -var _Signer = require("./Signer"); -Object.keys(_Signer).forEach(function (key) { - if (key === "default" || key === "__esModule") return; - if (key in exports && exports[key] === _Signer[key]) return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function () { - return _Signer[key]; - } - }); }); \ No newline at end of file diff --git a/packages/utils/src/Signer.js b/packages/utils/src/ISigner.js similarity index 94% rename from packages/utils/src/Signer.js rename to packages/utils/src/ISigner.js index 4aea42de..6697a6d1 100644 --- a/packages/utils/src/Signer.js +++ b/packages/utils/src/ISigner.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import {SignPdfError} from './SignPdfError'; -export class Signer { +export class ISigner { /** * @param {Buffer} pdfBuffer * @param {Date | undefined} signingTime diff --git a/packages/utils/src/Signer.test.js b/packages/utils/src/ISigner.test.js similarity index 74% rename from packages/utils/src/Signer.test.js rename to packages/utils/src/ISigner.test.js index cbd12016..6b20f3a1 100644 --- a/packages/utils/src/Signer.test.js +++ b/packages/utils/src/ISigner.test.js @@ -1,16 +1,16 @@ import {SignPdfError} from './SignPdfError'; -import {Signer} from './Signer'; +import {ISigner} from './ISigner'; -describe(Signer, () => { +describe(ISigner, () => { it('sign method must be implemented', async () => { try { - const signer = new Signer(Buffer.from('')); + const signer = new ISigner(); await signer.sign('non-buffer'); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); - expect(e.message).toMatchInlineSnapshot('"sign() is not implemented on Signer"'); + expect(e.message).toMatchInlineSnapshot('"sign() is not implemented on ISigner"'); } }); }); diff --git a/packages/utils/src/index.js b/packages/utils/src/index.js index 35659068..69d86459 100644 --- a/packages/utils/src/index.js +++ b/packages/utils/src/index.js @@ -2,6 +2,6 @@ export * from './const'; export * from './convertBuffer'; export * from './extractSignature'; export * from './findByteRange'; +export * from './ISigner'; export * from './removeTrailingNewLine'; export * from './SignPdfError'; -export * from './Signer'; diff --git a/yarn.lock b/yarn.lock index 8a600b8a..351d9a5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2273,6 +2273,15 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +asn1js@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" + integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -2550,6 +2559,11 @@ byte-size@8.1.1: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-8.1.1.tgz#3424608c62d59de5bfda05d31e0313c6174842ae" integrity sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg== +bytestreamjs@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + cacache@^17.0.0: version "17.1.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" @@ -6836,6 +6850,17 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pkijs@^3.0.15: + version "3.0.15" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.0.15.tgz#d6a3f6e11ae8c8aa1ebd1daf4501e9ac49f61916" + integrity sha512-n7nAl9JpqdeQsjy+rPmswkmZ3oO/Fu5uN9me45PPQVdWjd0X7HKfL8+HYwfxihqoDSSPUIajkOcqFxEUxMqhwQ== + dependencies: + asn1js "^3.0.5" + bytestreamjs "^2.0.0" + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + png-js@>=0.1.0, png-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" @@ -6927,6 +6952,18 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +pvtsutils@^1.3.2: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.5.tgz#b8705b437b7b134cd7fd858f025a23456f1ce910" + integrity sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA== + dependencies: + tslib "^2.6.1" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -7919,7 +7956,7 @@ tslib@^1.11.1, tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: +tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==