Skip to content

Commit

Permalink
Add WebCrypto examples and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
dcbr authored and Valeri Buchinski committed Feb 23, 2024
1 parent 1c5fb4a commit d147584
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ This is the main package, the integrating one, the one that wraps everything up.

### Signers

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function work() {
// contributing.pdf is the file that is going to be signed
var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf');
var pdfBuffer = fs.readFileSync(sourcePath);

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

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

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

}

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

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

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

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

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

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

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

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

function work() {
// contributing.pdf is the file that is going to be signed
var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf');
var pdfBuffer = fs.readFileSync(sourcePath);

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

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

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

}

work();
2 changes: 1 addition & 1 deletion packages/internal-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@signpdf/internal-utils",
"version": "3.2.0",
"version": "3.2.3",
"description": "",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/placeholder-pdf-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/placeholder-pdfkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/placeholder-pdfkit010/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/placeholder-plain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@babel/node": "^7.0.0",
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/signer-p12/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/signer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@signpdf/signer",
"version": "3.2.0",
"version": "3.2.3",
"description": "Signer base implementations.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,7 +38,7 @@
"lint": "eslint -c .eslintrc --ignore-path ../../.eslintignore ./"
},
"dependencies": {
"@signpdf/utils": "^3.2.0"
"@signpdf/utils": "^3.2.3"
},
"peerDependencies": {
"asn1js": "^3.0.5",
Expand All @@ -51,8 +51,8 @@
"@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",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"assertion-error": "^1.1.0",
"babel-jest": "^27.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/signpdf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@signpdf/placeholder-pdfkit010": "^3.2.3",
"@signpdf/placeholder-plain": "^3.2.3",
"@signpdf/signer-p12": "^3.2.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.4.2",
"@signpdf/eslint-config": "^3.2.3",
"@signpdf/internal-utils": "^3.2.0",
"@signpdf/internal-utils": "^3.2.3",
"@types/node": ">=12.0.0",
"@types/node-forge": "^1.2.1",
"assertion-error": "^1.1.0",
Expand Down

0 comments on commit d147584

Please sign in to comment.