How to add digital signatures to PDFs using React
This article was first published in December 2023 and was updated in August 2024.
In this blog post, you’ll learn how to digitally sign PDF documents using React and Nutrient Web SDK. Digital signatures have become a must for modern document handling, and they’re typically used for legal and security purposes, especially in contexts where the authenticity of a document is critical, such as in legal contracts or official documents. Throughout this tutorial, you’ll navigate the intricacies of self-signed certificate generation and the application of digital signatures to your PDFs.
Nutrient React digital signature library
Nutrient 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, or typed signatures. The ability to store signatures locally or remotely enhances convenience, while automated workflows can be triggered based on signature actions. Nutrient allows for 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
Nutrient offers two types of signatures: electronic signatures and digital signatures.
-
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.
-
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.
Nutrient’s React PDF library
We offer a commercial React.js 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 user interface (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:
-
A package manager compatible with npm. This guide contains usage examples for Yarn and the npm client (installed with Node.js by default).
Creating a new React project with Vite
-
Create a new React app using Vite:
yarn create vite pspdfkit-react-example --template react
npm create vite@latest pspdfkit-react-example -- --template react
-
Change to the created project directory:
cd pspdfkit-react-example
Adding Nutrient to your project
-
Add the Nutrient dependency:
yarn add pspdfkit
npm install pspdfkit
-
Copy the Nutrient Web SDK library assets to the
public
directory:
cp -R ./node_modules/pspdfkit/dist/pspdfkit-lib public/pspdfkit-lib
The above code will copy the pspdfkit-lib
directory from within node_modules/
into the public/
directory to make it available to the SDK at runtime.
-
Make sure your
public
directory contains apspdfkit-lib
directory with the Nutrient library assets.
Displaying a PDF
-
Add the PDF document you want to display to the
public
directory. You can use our demo document as an example. -
Add a component wrapper for the Nutrient library and save it as
components/PdfViewerComponent.jsx
:
import { useEffect, useRef } from 'react'; export default function PdfViewerComponent(props) { const containerRef = useRef(null); useEffect(() => { const container = containerRef.current; let instance, PSPDFKit; (async function () { PSPDFKit = await import('pspdfkit'); PSPDFKit.unload(container); // Ensure there's only one Nutrient instance. instance = await PSPDFKit.load({ // Container where Nutrient should be mounted. container, // The document to open. document: props.document, // Use the public directory URL as a base URL. Nutrient will download its library assets from here. baseUrl: `${window.location.protocol}//${ window.location.host }/${import.meta.env.BASE_URL}`, }); })(); return () => PSPDFKit && PSPDFKit.unload(container); }, []); return ( <div ref={containerRef} style={{ width: '100%', height: '100vh' }} /> ); }
-
Include the newly created component in
App.jsx
:
// src/App.jsx import PdfViewerComponent from './components/PdfViewerComponent'; function App() { return ( <div className="App" style={{ width: '100vw' }}> <div className="PDF-viewer"> <PdfViewerComponent document={'document.pdf'} /> </div> </div> ); } export default App;
-
Your project structure should now look like this:
pspdfkit-react-example ├── public │ ├── pspdfkit-lib │ └── document.pdf ├── src │ ├── components │ | └── PdfViewerComponent.jsx | └── App.jsx ├── package.json └── yarn.lock
-
Start the app and run it in your default browser:
yarn dev
npm run dev
Adding a digital signature to a PDF using Nutrient
Nutrient 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 in the next section.
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 do this:
-
Open your terminal in the project directory.
-
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 you store these files securely, as they’re essential for signing documents.
For more information on adding a digital signature to a PDF using Nutrient, refer to our digital signatures guide.
Signing a PDF document using Nutrient
To add a digital signature to your PDF document using Nutrient, follow the steps below.
Step 1: Installing the Forge library
-
Install the Forge library using npm. Open your terminal, navigate to the project directory, and run the following command:
yarn add node-forge
npm install node-forge
-
Import the Forge library in your
PdfViewerComponent
file:
import forge from 'node-forge';
Step 2: Generating the PKCS#7 signature
Nutrient 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:
import { useCallback } from 'react'; export default function PdfViewerComponent(props) { // Other code... const generatePKCS7 = useCallback(({ fileContents }) => { 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]) => { const certificate = forge.pki.certificateFromPem( certificatePem, ); const privateKey = forge.pki.privateKeyFromPem( privateKeyPem, ); const p7 = forge.pkcs7.createSignedData(); p7.content = new forge.util.ByteBuffer(fileContents); p7.addCertificate(certificate); 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(), }, ], }); p7.sign({ detached: true }); const result = stringToArrayBuffer( forge.asn1.toDer(p7.toAsn1()).getBytes(), ); resolve(result); }) .catch(reject); }); }, []); //.... }
This function fetches your certificate and private key, and then it 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 Nutrient and signing the document
Now you can initialize Nutrient 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, you should supply
fileContents
. This parameter is used as a callback object containing anArrayBuffer
housing a document’s content. The method returns a promise that resolves to theArrayBuffer
, or it rejects if an error arises.
useEffect(() => { const container = containerRef.current; let instance, PSPDFKit; (async function () { try { PSPDFKit = await import('pspdfkit'); PSPDFKit.unload(container); // Ensure there's only one Nutrient instance. const instance = await PSPDFKit.load({ // Container where Nutrient should be mounted. container, // The document to open. document: props.document, // Use the public directory URL as a base URL. Nutrient will download its library assets from here. baseUrl: `${window.location.protocol}//${ window.location.host }/${import.meta.env.BASE_URL}`, }); await instance.signDocument(null, generatePKCS7); console.log('document signed.'); } catch (error) { console.error('The document could not be signed.', error); } })(); return () => PSPDFKit && PSPDFKit.unload(container); }, [generatePKCS7, props.document]);
This code initiates the signing process. Upon successful completion, you’ll see the message Document signed. in the console. If there were 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 complete code for the PdfViewerComponent
:
import { useEffect, useRef, useCallback } from 'react'; import forge from 'node-forge'; export default function PdfViewerComponent(props) { const containerRef = useRef(null); const generatePKCS7 = useCallback(({ fileContents }) => { 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]) => { const certificate = forge.pki.certificateFromPem( certificatePem, ); const privateKey = forge.pki.privateKeyFromPem( privateKeyPem, ); const p7 = forge.pkcs7.createSignedData(); p7.content = new forge.util.ByteBuffer(fileContents); p7.addCertificate(certificate); 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(), }, ], }); p7.sign({ detached: true }); const result = stringToArrayBuffer( forge.asn1.toDer(p7.toAsn1()).getBytes(), ); resolve(result); }) .catch(reject); }); }, []); 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; } useEffect(() => { const container = containerRef.current; let instance, PSPDFKit; (async function () { try { PSPDFKit = await import('pspdfkit'); PSPDFKit.unload(container); // Ensure there's only one Nutrient instance. const instance = await PSPDFKit.load({ // Container where Nutrient should be mounted. container, // The document to open. document: props.document, // Use the public directory URL as a base URL. Nutrient will download its library assets from here. baseUrl: `${window.location.protocol}//${ window.location.host }/${import.meta.env.BASE_URL}`, }); await instance.signDocument(null, generatePKCS7); console.log('document signed.'); } catch (error) { console.error('The document could not be signed.', error); } })(); return () => PSPDFKit && PSPDFKit.unload(container); }, [generatePKCS7, props.document]); return ( <div ref={containerRef} style={{ width: '100%', height: '100vh' }} /> ); }
We recently added support for CAdES signatures, which are advanced digital signatures. To learn more about CAdES signatures, refer to our digital signatures guide.
Conclusion
In this blog post, you learned how to add digital signatures to PDF documents using React and Nutrient Web SDK. If you’re interested in exploring Nutrient further, you can request a free trial of our SDK or visit our demo page to experience the capabilities of our product firsthand.
FAQ
Here are a few frequently asked questions about adding digital signatures to PDF documents using React.
How can I integrate Nutrient into a React project?
Nutrient can be integrated into a React project by installing the Nutrient NPM package and configuring it within your React components. The tutorial provides step-by-step instructions on setting up Nutrient Web SDK in a React environment.
What are the prerequisites for adding digital signatures to PDFs in React?
To add digital signatures to PDFs in React, you need a React project set up with Nutrient Web SDK. Additionally, you’ll need access to a Nutrient license and some basic understanding of React components.
Does this tutorial use Vite instead of Create React App?
Yes, the tutorial has been updated to use Vite as the build tool instead of Create React App, providing faster development and more modern features.
What are the benefits of using Nutrient for digital signatures?
Nutrient offers a comprehensive solution for adding digital signatures to PDFs, including support for various signature formats, robust security features, and seamless integration with React applications.
How do I customize the appearance of the signature field in Nutrient?
The appearance of the signature field can be customized by modifying the Nutrient configuration and using custom styles in your React application. The tutorial includes examples of how to style the signature field to match your design requirements.