Skip to content

Commit 5738071

Browse files
committed
Add WebCrypto examples and update readme
1 parent bc2ba45 commit 5738071

5 files changed

Lines changed: 234 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ This is the main package, the integrating one, the one that wraps everything up.
6969

7070
### Signers
7171

72-
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.
72+
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.
7373

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

packages/examples/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@signpdf/placeholder-plain": "^3.1.0",
1919
"@signpdf/signer-p12": "^3.1.0",
2020
"@signpdf/signpdf": "^3.1.0",
21+
"@signpdf/utils": "^3.1.0",
2122
"ts-node": "^10.9.1",
2223
"typescript": "^5.2.2"
2324
},

packages/examples/src/utils.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
var nodeCrypto = require('crypto');
2+
var asn1js = require('asn1js');
3+
var pkijs = require('pkijs');
4+
5+
// Get crypto extension
6+
const crypto = new pkijs.CryptoEngine({name: 'CertCrypto', crypto: nodeCrypto});
7+
8+
async function createCertificate(keypair, hashAlg) {
9+
// Create a new certificate for the given keypair and hash algorithm.
10+
// Based on the certificateComplexExample from PKI.js.
11+
const certificate = new pkijs.Certificate();
12+
13+
// Basic attributes
14+
certificate.version = 2;
15+
certificate.serialNumber = new asn1js.Integer({ value: 1 });
16+
certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
17+
type: "2.5.4.6", // Country name
18+
value: new asn1js.PrintableString({value: "NO"}),
19+
}));
20+
certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
21+
type: "2.5.4.3", // Common name
22+
value: new asn1js.BmpString({value: "Test"}),
23+
}));
24+
certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
25+
type: "2.5.4.6", // Country name
26+
value: new asn1js.PrintableString({value: "NO"}),
27+
}));
28+
certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
29+
type: "2.5.4.3", // Common name
30+
value: new asn1js.BmpString({value: "Test"}),
31+
}));
32+
33+
certificate.notBefore.value = new Date();
34+
certificate.notAfter.value = new Date();
35+
certificate.notAfter.value.setFullYear(certificate.notAfter.value.getFullYear() + 1);
36+
37+
// Export public key into "subjectPublicKeyInfo" value of certificate
38+
await certificate.subjectPublicKeyInfo.importKey(keypair.publicKey, crypto);
39+
40+
// Sign certificate
41+
await certificate.sign(keypair.privateKey, hashAlg, crypto);
42+
43+
return certificate.toSchema(true).toBER(false);
44+
}
45+
46+
module.exports.createCertificate = createCertificate;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
var fs = require('fs');
2+
var path = require('path');
3+
var signpdf = require('@signpdf/signpdf').default;
4+
var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder;
5+
var ExternalSigner = require('@signpdf/utils').ExternalSigner;
6+
var crypto = require('crypto');
7+
var createCertificate = require('./utils').createCertificate;
8+
9+
// ExternalSigner implementation using the WebCrypto API
10+
// Note that this is just an example implementation of the ExternalSigner abstract class.
11+
// WebCrypto signing can also be implemented more easily by subclassing the Signer abstract
12+
// class directly, as is done in the `webcrypto.js` example script.
13+
class CryptoSigner extends ExternalSigner {
14+
// 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto
15+
supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512'];
16+
17+
// 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto
18+
supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA'];
19+
20+
constructor(signAlgorithm = 'ECDSA', hashAlgorithm = 'SHA-512') {
21+
super();
22+
23+
// Verify and set signature and hash algorithms
24+
if (!this.supportedSignAlgorithms.includes(signAlgorithm)) {
25+
throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`);
26+
}
27+
this.signAlgorithm = signAlgorithm;
28+
if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) {
29+
throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`);
30+
}
31+
this.hashAlgorithm = hashAlgorithm;
32+
33+
// Salt lengths for RSA-PSS algorithm used by PKI.js
34+
// If you want to modify these, the crypto.getSignatureParameters
35+
// method needs to be overridden in the getCrypto function.
36+
this.saltLengths = {
37+
'SHA-256': 32,
38+
'SHA-384': 48,
39+
'SHA-512': 64,
40+
}
41+
42+
this.cert = undefined;
43+
this.key = undefined;
44+
}
45+
46+
async getCertificate() {
47+
// Create a new keypair and certificate
48+
let params = {namedCurve: 'P-256'}; // EC parameters
49+
if (this.signAlgorithm.startsWith("RSA")) {
50+
// RSA parameters
51+
params = {
52+
modulusLength: 2048,
53+
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
54+
hash: this.hashAlgorithm,
55+
};
56+
}
57+
const keypair = await crypto.subtle.generateKey({
58+
name: this.signAlgorithm,
59+
...params,
60+
}, true, ['sign', 'verify']);
61+
this.cert = await createCertificate(keypair, this.hashAlgorithm);
62+
this.key = keypair.privateKey;
63+
return this.cert;
64+
}
65+
66+
async getSignature(_hash, data) {
67+
// WebCrypto's sign function automatically computes the hash of the passed data before signing.
68+
return crypto.subtle.sign({
69+
name: this.signAlgorithm,
70+
hash: this.hashAlgorithm, // Required for ECDSA algorithm
71+
saltLength: this.saltLengths[this.hashAlgorithm], // Required for RSA-PSS algorithm
72+
}, this.key, data);
73+
}
74+
}
75+
76+
function work() {
77+
// contributing.pdf is the file that is going to be signed
78+
var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf');
79+
var pdfBuffer = fs.readFileSync(sourcePath);
80+
81+
// Create new CryptoSigner
82+
var signAlgorithm = 'ECDSA';
83+
var hashAlgorithm = 'SHA-512';
84+
var signer = new CryptoSigner(signAlgorithm, hashAlgorithm);
85+
86+
// The PDF needs to have a placeholder for a signature to be signed.
87+
var pdfWithPlaceholder = plainAddPlaceholder({
88+
pdfBuffer: pdfBuffer,
89+
reason: 'The user is declaring consent through JavaScript.',
90+
contactInfo: 'signpdf@example.com',
91+
name: 'John Doe',
92+
location: 'Free Text Str., Free World',
93+
});
94+
95+
// pdfWithPlaceholder is now a modified Buffer that is ready to be signed.
96+
signpdf.sign(pdfWithPlaceholder, signer)
97+
.then(function (signedPdf) {
98+
// signedPdf is a Buffer of an electronically signed PDF. Store it.
99+
var targetPath = path.join(__dirname, '/../output/webcrypto-external.pdf');
100+
fs.writeFileSync(targetPath, signedPdf);
101+
});
102+
103+
}
104+
105+
work();

packages/examples/src/webcrypto.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
var fs = require('fs');
2+
var path = require('path');
3+
var signpdf = require('@signpdf/signpdf').default;
4+
var plainAddPlaceholder = require('@signpdf/placeholder-plain').plainAddPlaceholder;
5+
var Signer = require('@signpdf/utils').Signer;
6+
var createCertificate = require('./utils').createCertificate;
7+
8+
// Signer implementation using the WebCrypto API
9+
class CryptoSigner extends Signer {
10+
// 'SHA-256', 'SHA-384' or 'SHA-512' are supported by webcrypto
11+
supportedHashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512'];
12+
13+
// 'RSASSA-PKCS1-v1_5', 'RSA-PSS' or 'ECDSA' are supported by webcrypto
14+
supportedSignAlgorithms = ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA'];
15+
16+
constructor(signAlgorithm = 'RSA-PSS', hashAlgorithm = 'SHA-512') {
17+
super();
18+
19+
// Verify and set signature and hash algorithms
20+
if (!this.supportedSignAlgorithms.includes(signAlgorithm)) {
21+
throw new Error(`Signature algorithm ${signAlgorithm} is not supported by WebCrypto.`);
22+
}
23+
this.signAlgorithm = signAlgorithm;
24+
if (!this.supportedHashAlgorithms.includes(hashAlgorithm)) {
25+
throw new Error(`Hash algorithm ${hashAlgorithm} is not supported by WebCrypto.`);
26+
}
27+
this.hashAlgorithm = hashAlgorithm;
28+
29+
this.cert = undefined;
30+
this.key = undefined;
31+
}
32+
33+
async getCertificate() {
34+
// Create a new keypair and certificate
35+
const algorithmParams = this.crypto.getAlgorithmParameters(this.signAlgorithm, 'generatekey').algorithm;
36+
const keypair = await this.crypto.generateKey({
37+
name: this.signAlgorithm,
38+
...algorithmParams,
39+
hash: {name: this.hashAlgorithm},
40+
}, true, ['sign', 'verify']);
41+
this.cert = await createCertificate(keypair, this.hashAlgorithm);
42+
this.key = keypair.privateKey;
43+
return this.cert;
44+
}
45+
46+
async getKey() {
47+
// Convert private key to binary PKCS#8 representation
48+
return this.crypto.exportKey("pkcs8", this.key);
49+
}
50+
}
51+
52+
function work() {
53+
// contributing.pdf is the file that is going to be signed
54+
var sourcePath = path.join(__dirname, '/../../../resources/contributing.pdf');
55+
var pdfBuffer = fs.readFileSync(sourcePath);
56+
57+
// Create new CryptoSigner
58+
var signAlgorithm = 'RSA-PSS';
59+
var hashAlgorithm = 'SHA-512';
60+
var signer = new CryptoSigner(signAlgorithm, hashAlgorithm);
61+
62+
// The PDF needs to have a placeholder for a signature to be signed.
63+
var pdfWithPlaceholder = plainAddPlaceholder({
64+
pdfBuffer: pdfBuffer,
65+
reason: 'The user is declaring consent through JavaScript.',
66+
contactInfo: 'signpdf@example.com',
67+
name: 'John Doe',
68+
location: 'Free Text Str., Free World',
69+
});
70+
71+
// pdfWithPlaceholder is now a modified Buffer that is ready to be signed.
72+
signpdf.sign(pdfWithPlaceholder, signer)
73+
.then(function (signedPdf) {
74+
// signedPdf is a Buffer of an electronically signed PDF. Store it.
75+
var targetPath = path.join(__dirname, '/../output/webcrypto.pdf');
76+
fs.writeFileSync(targetPath, signedPdf);
77+
});
78+
79+
}
80+
81+
work();

0 commit comments

Comments
 (0)