Add Digital Signatures to PDFs Using JavaScript

PSPDFKit supports digitally signing documents by using a private key to encrypt the hash of the snapshot of the current state of the document. The certificate with its public key is added to the signature and saved in the PDF file.

PSPDFKit gives you the byte range and a hash representation of the current state of the document, so you can digitally sign it by obtaining a DER PKCS#7 container from either the byte range or the hash and then applying that DER PKCS#7 container to a new, ad hoc invisible signature form field.

You’re only responsible for providing the signing service that will receive the byte range and the hash as input values and return the DER PKCS#7 container to be applied to the prepared signature form field. Our implementation allows you to produce, validate, and display digitally signed documents in a totally flexible way.

ℹ️ Note: If you want to use the Digital Signatures feature, make sure to include it in your license. Please follow this link to contact sales to start using it.

Creating a Digital Signature

Adding a digital signature on a PDF document is both reliable proof of the document’s origin and protection against modification by third parties.

To create a digital signature, you need two things.

  • First, you need an X.509 certificate that contains your public key and your signer information. PSPDFKit supports PEM-encoded and DER-encoded X.509 certificates, as well as DER-encoded PKCS#7 certificates. You can verify if a PKCS#7 certificate file is correctly PEM-encoded by using the OpenSSL command-line tool as follows:

openssl pkcs7 -noout -text -print_certs -in example.p7b

The above command will print an error message if “example.p7b” is not a PEM-encoded PKCS#7 certificate or certificate chain.

To verify if a PKCS#7 certificate file is correctly DER encoded, you can use this command instead:

openssl pkcs7 -inform der -noout -text -print_certs -in example.p7b

The above command will print an error message if “example.p7b” is not a DER-encoded PKCS#7 certificate or certificate chain.

  • Second, you need your private key. A self-signed private key and certificate pair can be created with the command shown in the previous section.

Signing Process

ℹ️ Note: The following guide outlines the signing process when deploying PSPDFKit for Web as a standalone client-side JavaScript library. For a server-backed deployment, see our guide for adding digital signatures using PSPDFKit Server.

On Standalone, you are responsible for generating a valid digital signature in the cryptographic DER PKCS#7 format for the document.

To digitally sign a document, you need to perform a call to the PSPDFKit.Instance#signDocument method. As its first argument, you can optionally specify data to adjust aspects of the signing process.

Currently, we support passing an object with a placeholderSize property that can be used to override the default size that is reserved for the signature during the signing preparation of the document for the DER PKCS#7 container.

As a second argument, you must specify a callback that receives an object containing a fileContents field with an ArrayBuffer of the contents of the document, as well as a hash field with a string representing the sha256 hash of the fileContents buffer received. The callback must return a Promise that either resolves with the DER PKCS#7 ArrayBuffer to be used for signing the document or rejects in the case of a user-side error.

To generate the DER PKCS#7 container, you can adopt any strategy that fits your requirements. One simple option can be to make use of a cryptography library such as Forge.

ℹ️ Note: If you use Forge, make sure you’re using the most recent version (>= 0.10.0), since some old versions don’t work correctly.

ℹ️ Note: The reserved space for the signature itself, placeholderSize, is never used as the input value for generating the signature. So, no matter what the size of the reserved space is, it will never influence the length of the value that will be hashed and encrypted in the resulting container used as a signature (i.e. fileContents). placeholderSize will however influence the size of the final document.

Here is an example of a naive implementation where, for the sake of simplicity, we fetch the private key over the network:

function generatePKCS7({ fileContents }) {
  const certificatePromise = fetch("certs/certificate.pem").then((response) =>
    response.text()
  );
  const privateKeyPromise = fetch("certs/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);
  });
}

// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
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;
}

In the previous snippet, we fetch the certificate and private key that we need and we start building our DER PKCS#7 container using the file contents, setting the certificate and signer accordingly. It’s important to keep in mind that in the case of an error, the returned Promise should reject for PSPDFKit for Web to abort the current signing process.

Once we have our signing callback defined, we can perform the call to PSPDFKit.Instance#signDocument that we mentioned earlier in this section:

instance
  .signDocument(null, generatePKCS7)
  .then(() => {
    console.log("document signed.");
  })
  .catch((error) => {
    console.error("The document could not be signed.", error);
  });

If the signing process is successful, the document is reloaded with the new invisible digital signature added to it.

During the call to PSPDFKit.Instance#signDocument, and until either the DER PKCS#7 container is generated and PSPDFKit applies the signature to the document or the process is disregarded due to a rejection from the callback, all interactions from the user are disabled. This ensures that no modifications are made to the document while a new digital signature is about to be added to it.