From 57380712e4adfe27a5f741074b4e2a6cf83ef4e7 Mon Sep 17 00:00:00 2001 From: dcbr <15089458+dcbr@users.noreply.github.com> Date: Sun, 7 Jan 2024 21:09:32 +0000 Subject: [PATCH] Add WebCrypto examples and update readme --- README.md | 2 +- packages/examples/package.json | 1 + packages/examples/src/utils.js | 46 +++++++++ packages/examples/src/webcrypto-external.js | 105 ++++++++++++++++++++ packages/examples/src/webcrypto.js | 81 +++++++++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 packages/examples/src/utils.js create mode 100644 packages/examples/src/webcrypto-external.js create mode 100644 packages/examples/src/webcrypto.js diff --git a/README.md b/README.md index 25b2aec7..950f4b2c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ This is the main package, the integrating one, the one that wraps everything up. ### Signers -Signers are small libraries that `@signpdf/signpdf` will call with a PDF and they will know how to provide an e-signature in return. Their output is then fed as the signature in the resulting document. +Signers are small libraries that `@signpdf/signpdf` will call with a PDF and they will know how to provide an e-signature in return. Their output is then fed as the signature in the resulting document. Example implementations of the abstract `Signer` base class are provided in the [WebCrypto](./packages/examples/src/webcrypto.js) and [WebCrypto-External](./packages/examples/src/webcrypto-external.js) examples, both leveraging the `WebCrypto` API. #### [@signpdf/signer-p12](./packages/signer-p12) diff --git a/packages/examples/package.json b/packages/examples/package.json index 578d45ed..dbcecbcd 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -18,6 +18,7 @@ "@signpdf/placeholder-plain": "^3.1.0", "@signpdf/signer-p12": "^3.1.0", "@signpdf/signpdf": "^3.1.0", + "@signpdf/utils": "^3.1.0", "ts-node": "^10.9.1", "typescript": "^5.2.2" }, diff --git a/packages/examples/src/utils.js b/packages/examples/src/utils.js new file mode 100644 index 00000000..1514d52d --- /dev/null +++ b/packages/examples/src/utils.js @@ -0,0 +1,46 @@ +var nodeCrypto = require('crypto'); +var asn1js = require('asn1js'); +var pkijs = require('pkijs'); + +// Get crypto extension +const crypto = new pkijs.CryptoEngine({name: 'CertCrypto', crypto: nodeCrypto}); + +async function createCertificate(keypair, hashAlg) { + // Create a new certificate for the given keypair and hash algorithm. + // Based on the certificateComplexExample from PKI.js. + const certificate = new pkijs.Certificate(); + + // Basic attributes + certificate.version = 2; + certificate.serialNumber = new asn1js.Integer({ value: 1 }); + certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.6", // Country name + value: new asn1js.PrintableString({value: "NO"}), + })); + certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.3", // Common name + value: new asn1js.BmpString({value: "Test"}), + })); + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.6", // Country name + value: new asn1js.PrintableString({value: "NO"}), + })); + certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({ + type: "2.5.4.3", // Common name + value: new asn1js.BmpString({value: "Test"}), + })); + + certificate.notBefore.value = new Date(); + certificate.notAfter.value = new Date(); + certificate.notAfter.value.setFullYear(certificate.notAfter.value.getFullYear() + 1); + + // Export public key into "subjectPublicKeyInfo" value of certificate + await certificate.subjectPublicKeyInfo.importKey(keypair.publicKey, crypto); + + // Sign certificate + await certificate.sign(keypair.privateKey, hashAlg, crypto); + + return certificate.toSchema(true).toBER(false); +} + +module.exports.createCertificate = createCertificate; diff --git a/packages/examples/src/webcrypto-external.js b/packages/examples/src/webcrypto-external.js new file mode 100644 index 00000000..b2d7980a --- /dev/null +++ b/packages/examples/src/webcrypto-external.js @@ -0,0 +1,105 @@ +var fs = require('fs'); +var path = require('path'); +var signpdf = require('@signpdf/signpdf').default; +var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder; +var ExternalSigner = require('@signpdf/utils').ExternalSigner; +var crypto = require('crypto'); +var createCertificate = require('./utils').createCertificate; + +// ExternalSigner implementation using the WebCrypto API +// Note that this is just an example implementation of the ExternalSigner abstract class. +// WebCrypto signing can also be implemented more easily by subclassing the Signer abstract +// class directly, as is done in the `webcrypto.js` example script. +class CryptoSigner extends ExternalSigner { + // 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto + supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512']; + + // 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto + supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA']; + + constructor(signAlgorithm = 'ECDSA', hashAlgorithm = 'SHA-512') { + super(); + + // Verify and set signature and hash algorithms + if (!this.supportedSignAlgorithms.includes(signAlgorithm)) { + throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`); + } + this.signAlgorithm = signAlgorithm; + if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) { + throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`); + } + this.hashAlgorithm = hashAlgorithm; + + // Salt lengths for RSA-PSS algorithm used by PKI.js + // If you want to modify these, the crypto.getSignatureParameters + // method needs to be overridden in the getCrypto function. + this.saltLengths = { + 'SHA-256': 32, + 'SHA-384': 48, + 'SHA-512': 64, + } + + this.cert = undefined; + this.key = undefined; + } + + async getCertificate() { + // Create a new keypair and certificate + let params = {namedCurve: 'P-256'}; // EC parameters + if (this.signAlgorithm.startsWith("RSA")) { + // RSA parameters + params = { + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: this.hashAlgorithm, + }; + } + const keypair = await crypto.subtle.generateKey({ + name: this.signAlgorithm, + ...params, + }, true, ['sign', 'verify']); + this.cert = await createCertificate(keypair, this.hashAlgorithm); + this.key = keypair.privateKey; + return this.cert; + } + + async getSignature(_hash, data) { + // WebCrypto's sign function automatically computes the hash of the passed data before signing. + return crypto.subtle.sign({ + name: this.signAlgorithm, + hash: this.hashAlgorithm, // Required for ECDSA algorithm + saltLength: this.saltLengths[this.hashAlgorithm], // Required for RSA-PSS algorithm + }, this.key, data); + } +} + +function work() { + // contributing.pdf is the file that is going to be signed + var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf'); + var pdfBuffer = fs.readFileSync(sourcePath); + + // Create new CryptoSigner + var signAlgorithm = 'ECDSA'; + var hashAlgorithm = 'SHA-512'; + var signer = new CryptoSigner(signAlgorithm, hashAlgorithm); + + // The PDF needs to have a placeholder for a signature to be signed. + var pdfWithPlaceholder = plainAddPlaceholder({ + pdfBuffer: pdfBuffer, + reason: 'The user is declaring consent through JavaScript.', + contactInfo: 'signpdf@example.com', + name: 'John Doe', + location: 'Free Text Str., Free World', + }); + + // pdfWithPlaceholder is now a modified Buffer that is ready to be signed. + signpdf.sign(pdfWithPlaceholder, signer) + .then(function (signedPdf) { + // signedPdf is a Buffer of an electronically signed PDF. Store it. + var targetPath = path.join(__dirname, '/../output/webcrypto-external.pdf'); + fs.writeFileSync(targetPath, signedPdf); + }); + +} + +work(); diff --git a/packages/examples/src/webcrypto.js b/packages/examples/src/webcrypto.js new file mode 100644 index 00000000..cfddf29c --- /dev/null +++ b/packages/examples/src/webcrypto.js @@ -0,0 +1,81 @@ +var fs = require('fs'); +var path = require('path'); +var signpdf = require('@signpdf/signpdf').default; +var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder; +var Signer = require('@signpdf/utils').Signer; +var createCertificate = require('./utils').createCertificate; + +// Signer implementation using the WebCrypto API +class CryptoSigner extends Signer { + // 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto + supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512']; + + // 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto + supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA']; + + constructor(signAlgorithm = 'RSA-PSS', hashAlgorithm = 'SHA-512') { + super(); + + // Verify and set signature and hash algorithms + if (!this.supportedSignAlgorithms.includes(signAlgorithm)) { + throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`); + } + this.signAlgorithm = signAlgorithm; + if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) { + throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`); + } + this.hashAlgorithm = hashAlgorithm; + + this.cert = undefined; + this.key = undefined; + } + + async getCertificate() { + // Create a new keypair and certificate + const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'generatekey').algorithm; + const keypair = await this.crypto.generateKey({ + name: this.signAlgorithm, + ...algorithmParams, + hash: {name: this.hashAlgorithm}, + }, true, ['sign', 'verify']); + this.cert = await createCertificate(keypair, this.hashAlgorithm); + this.key = keypair.privateKey; + return this.cert; + } + + async getKey() { + // Convert private key to binary PKCS#8 representation + return this.crypto.exportKey("pkcs8", this.key); + } +} + +function work() { + // contributing.pdf is the file that is going to be signed + var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf'); + var pdfBuffer = fs.readFileSync(sourcePath); + + // Create new CryptoSigner + var signAlgorithm = 'RSA-PSS'; + var hashAlgorithm = 'SHA-512'; + var signer = new CryptoSigner(signAlgorithm, hashAlgorithm); + + // The PDF needs to have a placeholder for a signature to be signed. + var pdfWithPlaceholder = plainAddPlaceholder({ + pdfBuffer: pdfBuffer, + reason: 'The user is declaring consent through JavaScript.', + contactInfo: 'signpdf@example.com', + name: 'John Doe', + location: 'Free Text Str., Free World', + }); + + // pdfWithPlaceholder is now a modified Buffer that is ready to be signed. + signpdf.sign(pdfWithPlaceholder, signer) + .then(function (signedPdf) { + // signedPdf is a Buffer of an electronically signed PDF. Store it. + var targetPath = path.join(__dirname, '/../output/webcrypto.pdf'); + fs.writeFileSync(targetPath, signedPdf); + }); + +} + +work();