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 committed Feb 2, 2024
1 parent 8a7bbb3 commit ee61df1
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 1 deletion.
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 @@ -18,6 +18,7 @@
"@signpdf/placeholder-plain": "^3.2.0",
"@signpdf/signer-p12": "^3.2.0",
"@signpdf/signpdf": "^3.2.0",
"@signpdf/signer": "^3.2.0",
"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: '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();
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: '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();

0 comments on commit ee61df1

Please sign in to comment.