Blog Post

How to Add Digital Signatures to PDFs Using TypeScript

Hulya Masharipov
Illustration: How to Add Digital Signatures to PDFs Using TypeScript

In this blog post, you’ll learn how to sign PDF documents using TypeScript and PSPDFKit for Web. Digital signatures, which are generated through complex mathematical processes involving cryptographic keys, provide a high level of security and ensure the authenticity and integrity of your PDF documents, making them trustworthy in various critical contexts, such as legal contracts and official records.

PSPDFKit TypeScript Digital Signature Library

PSPDFKit offers a range of essential capabilities for PDF signing. It simplifies the process of creating and validating digital signatures, accommodating various signature types, including hand-drawn, scanned, and typed signatures. The ability to store signatures locally or remotely enhances convenience, while automated workflows can be triggered based on signature actions. PSPDFKit allows for user interface (UI) customization, ensuring a seamless user experience, and it supports client-side signing, eliminating the need for a dedicated server. Additionally, it can be extended to include forms, annotations, and other PDF-related functionalities, making it a versatile tool for various document tasks.

Signature Support

PSPDFKit offers two types of signatures: electronic signatures and digital signatures.

  1. Electronic signatures allow users to create signatures with ink drawings, bitmap images, or text. Our Electronic Signatures component provides a user-friendly interface, supporting draw, image, and type signature modes, and it enables signature storage for reuse.

  2. Digital signatures, on the other hand, are signed with certificates, offering reliable proof of a document’s origin and protection against unauthorized changes. While distinct, these signature types can be used together for a comprehensive signing solution.

PSPDFKit’s TypeScript PDF Library

We offer a commercial TypeScript PDF viewer library that can easily be integrated into your web application. It comes with 30+ features that let you view, annotate, edit, and sign documents directly in your browser. Out of the box, it has a polished and flexible UI that you can extend or simplify based on your unique use case.

  • A prebuilt and polished UI for an improved user experience
  • 15+ prebuilt annotation tools to enable document collaboration
  • Support for more file types with client-side PDF, MS Office, and image viewing
  • Dedicated support from engineers to speed up integration

Requirements to Get Started

To get started, you’ll need:

  • Node.js

  • A package manager for installing the PSPDFKit library. You can use npm or Yarn. When you install Node.js, npm is installed by default.

  • TypeScript

You can install TypeScript globally by running the following command:

npm install -g typescript

Getting Started

  1. Create a new folder and change your directory to it:

mkdir typescript-pspdfkit-viewer

cd typescript-pspdfkit-viewer
  1. Create a package.json file by running npm init --yes.

  2. Create a new tsconfig.json configuration file in the root of your project:

tsc --init

You can customize what rules you want the TypeScript compiler to follow. This post will use the following configuration:

// tsconfig.json
{
	"compilerOptions": {
		"removeComments": true,
		"preserveConstEnums": true,
		"module": "commonjs",
		"target": "es5",
		"sourceMap": true,
		"noImplicitAny": true,
		"esModuleInterop": true
	},
	"include": ["src/**/*"]
}

Check out the compiler options for more information.

Installing PSPDFKit and Configuring webpack

  1. Install the PSPDFKit for Web library as a dependency:

npm install pspdfkit
  1. You’ll use one of the most popular build tools, webpack, to bundle your project into a single file. It’ll help to both reduce the HTTP requests needed to load the PDF and minify your code.

Start by downloading the necessary dev dependencies:

npm i -D webpack webpack-cli webpack-dev-server ts-loader typescript html-webpack-plugin cross-env copy-webpack-plugin clean-webpack-plugin

Here’s what’s installed:

  • webpack — The webpack bundler.

  • webpack-cli — The command-line interface for webpack.

  • webpack-dev-server — A local server to run webpack in the browser.

  • ts-loader — A package that teaches webpack how to compile TypeScript.

  • typescript — The TypeScript compiler.

  • clean-webpack-plugin — A plugin that cleans the output directory before building.

  • copy-webpack-plugin — A plugin that copies files and directories to the output directory.

  • html-webpack-plugin — A plugin that generates an HTML file from a template.

  • cross-env — A package that allows you to set environment variables.

After the installation, you can see the dependencies in your package.json file:

"devDependencies": {
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "html-webpack-plugin": "^5.5.3",
    "ts-loader": "^9.4.4",
    "typescript": "^5.2.2",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
  1. To configure webpack, create a config directory and place your webpack configuration file inside it:

mkdir config && touch config/webpack.js
  1. If you’re using webpack 4, use the example file. If you’re using the latest version of webpack, ^5.88.2, use the following configuration:

// webpack.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const filesToCopy = [
	// PSPDFKit files.
	{
		from: './node_modules/pspdfkit/dist/pspdfkit-lib',
		to: './pspdfkit-lib',
	},
	// Application CSS.
	{
		from: './src/index.css',
		to: './index.css',
	},
	// Example PDF.
	{
		from: './assets/example.pdf',
		to: './example.pdf',
	},
];

/**
 * webpack main configuration object.
 */
const config = {
	entry: path.resolve(__dirname, '../src/index.ts'),
	mode: 'development',
	devtool: 'inline-source-map',
	output: {
		path: path.resolve(__dirname, '../dist'),
		filename: '[name].js',
	},
	resolve: {
		extensions: ['.ts', '.tsx', '.js'],
	},
	module: {
		rules: [
			// All files with a `.ts` or `.tsx` extension will be handled by `ts-loader`.
			{
				test: /\.tsx?$/,
				loader: 'ts-loader',
				exclude: /node_modules/,
			},
		],
	},
	plugins: [
		// Automatically insert <script src="[name].js"><script> to the page.
		new HtmlWebpackPlugin({
			template: './src/index.html',
		}),

		// Copy the WASM/ASM and CSS files to the `output.path`.
		new CopyWebpackPlugin({ patterns: filesToCopy }),
	],

	optimization: {
		splitChunks: {
			cacheGroups: {
				// Creates a `vendor.js` bundle that contains external libraries (including `pspdfkit.js`).
				vendor: {
					test: /node_modules/,
					chunks: 'initial',
					name: 'vendor',
					priority: 10,
					enforce: true,
				},
			},
		},
	},
};

module.exports = config;

The entry point of your application will be the ../src/index.ts file, and the output will be bundled inside the dist folder. Note that you’re copying the PDF file, PSPDFKit files, and CSS files to the output path.

You created the module property containing the rules array to handle the TypeScript files. So, when webpack finds a file with the .ts or .tsx extension, it’ll use the ts-loader to compile it. Only then will the file be added to the dist build.

Displaying the PDF

  1. Create a folder called assets and add a file named example.pdf to it. You can use our demo document as an example. Make sure to name the document example.pdf.

  2. Create a folder called src and add a file named index.html with the following code:

<!DOCTYPE html>
<html>
	<head>
		<title>PSPDFKit for Web - TypeScript example</title>
		<link rel="stylesheet" href="index.css" />
	</head>
	<body>
		<div class="container"></div>
	</body>
</html>

This adds an empty <div> element to where PSPDFKit will be mounted.

  1. You’ll declare the height of this element in your CSS file like this:

.container {
	height: 100vh;
}
  1. Now, create an index.ts file inside the src directory.

.ts is the extension used for TypeScript files. Later, you’ll run your code through the TypeScript transpiler to output a JavaScript version of the file (.js):

import PSPDFKit from 'pspdfkit';

function load(document: string) {
	console.log(`Loading ${document}...`);
	PSPDFKit.load({
		document,
		container: '.container',
	})
		.then((instance) => {
			console.log('PSPDFKit loaded', instance);
		})
		.catch(console.error);
}

load('example.pdf');

Here, you imported the PSPDFKit library and created a function that loads the PDF document.

Running the Project

  1. Now, you need to write some scripts in the package.json file to start your server:

"scripts": {
    "build": "cross-env NODE_ENV=production webpack --config config/webpack.js",
    "prestart": "npm run build",
    "dev": "tsc",
    "start": "serve -l 8080 ./dist"
},
  1. You can run npm start to start the server. Navigate to http://localhost:8080 to see the contents of the dist directory.

Resulting page

Adding a Digital Signature to a PDF Using PSPDFKit

PSPDFKit requires an X.509 certificate and a private key pair for adding a digital signature to a PDF document. To do this, follow the steps outlined below.

Step 1 — Generating a Self-Signed Certificate and Private Key

You can generate a self-signed certificate and private key using OpenSSL, which is a widely used tool for working with certificates and keys. Here’s how you can generate them:

  1. Open your terminal in the project directory.

  2. Run the following OpenSSL command to generate a self-signed certificate and private key:

openssl req -x509 -sha256 -nodes -newkey rsa:2048 -keyout private-key.pem -out cert.pem
  • -x509 — Tells OpenSSL to create a self-signed certificate.

  • -sha256 — Specifies the hash function to use for the certificate.

  • -nodes — Prevents encryption of the private key. You can remove this option for production keys if encryption is desired.

  • -newkey rsa:2048 — Generates a new RSA private key with a key size of 2,048 bits.

  • -keyout private-key.pem — Specifies the name of the private key file.

  • -out cert.pem — Specifies the name of the certificate file.

Follow the prompts to provide information for the certificate, such as the Common Name (CN), organization, and location. These details will be embedded in the certificate.

Step 2 — Verifying Your Certificate

After generating the certificate and private key, you can verify if the certificate is correctly PEM-encoded using the following command:

openssl pkcs7 -noout -text -print_certs -in cert.pem

This command will display certificate details and shouldn’t produce any errors. It confirms that “cert.pem” is a PEM-encoded X.509 certificate.

Alternatively, if you want to verify DER-encoded certificates, you can use the following command:

openssl pkcs7 -inform der -noout -text -print_certs -in cert.pem

This command checks if “cert.pem” is a DER-encoded PKCS#7 certificate.

With these steps, you’ll have generated the required X.509 certificate and private key pair for adding digital signatures to your PDF documents. Ensure that you store these files securely, as they’re essential for signing documents.

For more information on adding a digital signature to a PDF using PSPDFKit, refer to our digital signatures guide.

Signing a PDF Document Using PSPDFKit

To add a digital signature to your PDF document using PSPDFKit, follow the steps outlined below.

Step 1 — Installing the Forge Library:

  1. Install the Forge library using npm. Open your terminal, navigate to the project directory, and run the following command:

npm install node-forge
npm i --save-dev @types/node-forge
  1. Import the Forge library in the index.ts file:

import * as forge from 'node-forge';

Step 2 — Generating the PKCS#7 Signature

PSPDFKit utilizes the cryptographic Distinguished Encoding Rules (DER) PKCS#7 format for digital signatures. You’ll need to create a valid PKCS#7 signature containing your certificate and other relevant information.

Define a function, generatePKCS7, to generate the digital signature for your PDF. This function will perform the necessary cryptographic operations:

function generatePKCS7({
	fileContents,
}: {
	fileContents: ArrayBuffer | null,
}): Promise<ArrayBuffer> {
	// Fetch the certificate and private key.
	const certificatePromise = fetch('cert.pem').then((response) =>
		response.text(),
	);
	const privateKeyPromise = fetch('private-key.pem').then((response) =>
		response.text(),
	);

	return new Promise((resolve, reject) => {
		Promise.all([certificatePromise, privateKeyPromise])
			.then(([certificatePem, privateKeyPem]) => {
				// Parse the certificate and private key using Forge.js.
				const certificate = forge.pki.certificateFromPem(
					certificatePem,
				);
				const privateKey = forge.pki.privateKeyFromPem(
					privateKeyPem,
				);

				// Create a PKCS7 signature.
				const p7 = forge.pkcs7.createSignedData();
				if (!fileContents) {
					throw new Error('No file contents provided.');
				}
				const buffer = forge.util.createBuffer(fileContents);
				p7.content = buffer.getBytes();
				p7.addCertificate(certificate);

				// Add the signer information.
				p7.addSigner({
					key: privateKey,
					certificate: certificate,
					digestAlgorithm: forge.pki.oids['sha256'],
					authenticatedAttributes: [
						{
							type: forge.pki.oids['contentType'],
							value: forge.pki.oids['data'],
						},
						{
							type: forge.pki.oids['messageDigest'],
						},
						{
							type: forge.pki.oids['signingTime'],
							value: new Date().toISOString(),
						},
					],
				});

				// Sign the data.
				p7.sign({ detached: true });

				// Convert the result to an `ArrayBuffer`.
				const result = stringToArrayBuffer(
					forge.asn1.toDer(p7.toAsn1()).getBytes(),
				);

				resolve(result);
			})
			.catch(reject);
	});
}

This function fetches your certificate and private key. It then uses Forge to create a PKCS#7 signed data structure.

Step 3 — Converting a String to an Array Buffer

You’ll need a utility function, stringToArrayBuffer, to convert a binary string into an ArrayBuffer:

function stringToArrayBuffer(binaryString) {
	const buffer = new ArrayBuffer(binaryString.length);
	let bufferView = new Uint8Array(buffer);
	for (let i = 0, len = binaryString.length; i < len; i++) {
		bufferView[i] = binaryString.charCodeAt(i);
	}
	return buffer;
}

Step 4 — Initializing PSPDFKit and Signing the Document

Now, you can initialize PSPDFKit and invoke the PSPDFKit.Instance#signDocument method. This method takes two arguments.

  • Argument 1 — You can use this argument to fine-tune the signing process by providing essential data, such as certificates and private keys. If you don’t have specific signing requirements, you can pass null.

  • Argument 2 — In the second argument, supply fileContents. This parameter is used as a callback object containing an ArrayBuffer housing the document’s content. The method returns a promise that resolves to the ArrayBuffer or rejects if an error arises:

function load(document: string) {
	console.log(`Loading ${document}...`);
	PSPDFKit.load({
		document,
		container: '.container',
	})
		.then((instance) => {
			console.log('PSPDFKit loaded', instance);
			// Sign the document when PSPDFKit is loaded.
			instance
				.signDocument(null, generatePKCS7)
				.then(() => {
					console.log('Document signed.');
				})
				.catch((error) => {
					console.error(
						'The document could not be signed.',
						error,
					);
				});
		})
		.catch(console.error);
}

This code initiates the signing process. Upon successful completion, you’ll see the message ‘Document signed.’ in the console. In case of errors during the signing process, an error message will be logged.

After successfully building and implementing this code, the signing process will run automatically, and the document will reload with the freshly added digital signature.

Here’s the full code for your reference:

import PSPDFKit from 'pspdfkit';
import * as forge from 'node-forge';

function generatePKCS7({
	fileContents,
}: {
	fileContents: ArrayBuffer | null;
}): Promise<ArrayBuffer> {
	// Fetch the certificate and private key.
	const certificatePromise = fetch('cert.pem').then((response) =>
		response.text(),
	);
	const privateKeyPromise = fetch('private-key.pem').then((response) =>
		response.text(),
	);

	return new Promise((resolve, reject) => {
		Promise.all([certificatePromise, privateKeyPromise])
			.then(([certificatePem, privateKeyPem]) => {
				// Parse the certificate and private key using Forge.js.
				const certificate = forge.pki.certificateFromPem(
					certificatePem,
				);
				const privateKey = forge.pki.privateKeyFromPem(
					privateKeyPem,
				);

				// Create a PKCS7 signature.
				const p7 = forge.pkcs7.createSignedData();
				if (!fileContents) {
					throw new Error('No file contents provided.');
				}
				const buffer = forge.util.createBuffer(fileContents);
				p7.content = buffer.getBytes();
				p7.addCertificate(certificate);

				// Add the signer information.
				p7.addSigner({
					key: privateKey,
					certificate: certificate,
					digestAlgorithm: forge.pki.oids['sha256'],
					authenticatedAttributes: [
						{
							type: forge.pki.oids['contentType'],
							value: forge.pki.oids['data'],
						},
						{
							type: forge.pki.oids['messageDigest'],
						},
						{
							type: forge.pki.oids['signingTime'],
							value: new Date().toISOString(),
						},
					],
				});

				// Sign the data.
				p7.sign({ detached: true });

				// Convert the result to an `ArrayBuffer`.
				const result = stringToArrayBuffer(
					forge.asn1.toDer(p7.toAsn1()).getBytes(),
				);

				resolve(result);
			})
			.catch(reject);
	});
}

function stringToArrayBuffer(binaryString: string): ArrayBuffer {
	const buffer = new ArrayBuffer(binaryString.length);
	let bufferView = new Uint8Array(buffer);

	for (let i = 0, len = binaryString.length; i < len; i++) {
		bufferView[i] = binaryString.charCodeAt(i);
	}

	return buffer;
}

function load(document: string) {
	console.log(`Loading ${document}...`);
	PSPDFKit.load({
		document,
		container: '.container',
	})
		.then((instance) => {
			console.log('PSPDFKit loaded', instance);
			// Sign the document when PSPDFKit is loaded.
			instance
				.signDocument(null, generatePKCS7)
				.then(() => {
					console.log('Document signed.');
				})
				.catch((error) => {
					console.error(
						'The document could not be signed.',
						error,
					);
				});
		})
		.catch(console.error);
}

load('example.pdf');
Information

We recently added support for CAdES-based signatures, which are advanced digital signatures. To learn more about them, check out our guide.

Conclusion

In this blog post, you learned how to add digital signatures to PDF documents using TypeScript and PSPDFKit for Web. If you’re interested in exploring PSPDFKit further, you can request a free trial of our SDK or visit our demo page to experience the capabilities of our product firsthand.

Author
Hulya Masharipov Technical Writer

Hulya is a frontend web developer and technical writer at Nutrient who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

Related products
Share post
Free trial Ready to get started?
Free trial