-
-
Notifications
You must be signed in to change notification settings - Fork 201
Another attempt at "Provide default sign method, add external signer and webcrypto example" #230
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
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "../../babel.config.json" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "extends": [ | ||
| "@signpdf/eslint-config" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Signer base implementation with PKI.js | ||
|
|
||
| for [](https://github.com/vbuch/node-signpdf/) | ||
|
|
||
| [](https://badge.fury.io/js/@signpdf%2Fsigner) | ||
| [](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). |
| 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 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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; |
| 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>; | ||
| /** | ||
| * 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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our abstract class should not be tightly bound to What is the "crypto engine" needed for?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should all be
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see #230 (comment) |
||
| 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 | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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):
There was a problem hiding this comment.
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
But it's probably less confusing if there is just a single method/getter.