Digital Signatures on PSPDFKit for Web

ℹ️ Note: For an introduction to the concept of digital signatures, please start with this guide.

PSPDFKit for Web can validate digitally signed documents and sign documents with a digital signature as well. Even when there’s no signature form field in the current document, PSPDFKit for Web 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 are 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.

Preparing a Document for Digital Signatures

With PSPDFKit for Web, digitally signing documents is easy either way, but the procedure varies slightly between Server-backed and Standalone deployments.

Setting Up Digital Signatures on the Server

Under the hood, the process of signing a document via PSPDFKit Server is divided into three phases:

  1. PSPDFKit Server prepares the document for a signature, adding an invisible form field that will contain the signature value.
  2. PSPDFKit Server then contacts an external signing service you’re responsible for setting up, which will provide a compliant signature value.
  3. PSPDFKit Server applies the returned signature to the document and saves it, storing the final file as an asset associated with the document and the used Instant layer.

This architecture ensures that PSPDFKit doesn’t need access to the private key that ultimately will be used to produce the signature value, leaving you complete freedom to choose which strategy to use to manage its lifecycle and security.

The Signing Service

The signing service is a network service that you’re responsible for maintaining and operating.

It needs to expose a single HTTP endpoint of your choice that receives all the information required to calculate a compliant digital signature, and it should return a DER PKCS#7 container that can be set as a value of the digital signature field.

For example, let’s say you want to sign a document with the ID my-document-id via the Server API:

Copy
1
2
3
4
5
6
7
POST http://localhost:5000/api/documents/my-document-id/sign
Authorization: Token token="<secret token>"
Content-Type: application/json

{
  "signingToken" : "custom-token"
}

The request accepts an optional signingToken string parameter, which will be forwarded to the signing service in the exact same shape.

You can use it to pass a token that can be used to verify the authenticity of the signing request or to provide identity information about the user applying the signature.

The signing endpoint will receive a request with the following schema:

Copy
1
2
3
4
5
6
7
8
POST http://signing-server:6000/sign
Content-Type: application/json

{
  "encoded_contents" : "CkVudW1lcmF0aW5nIG9iamVjdHM6IDExLCBkb25lLgpDb3VudGluZyBvYmplY3RzOiAxMDAlICg...",
  "digest" : "aab7fe5d814e7e8048275d19693435013727ee8002b85ba8edc29321fc2edfc9",
  "signing_token" : "custom-token"
}

In the example above, we assume that the signing service can be accessed at http://signing-server:6000/sign.

The endpoint will receive a JSON-encoded POST request containing:

  • The Base64-encoded contents of the file to sign. This represents the portion of the PDF document that is covered by the digital signature, minus the byte range that will contain the signature itself. Note that since it is Base64 encoded, you will need to decode it before signing it.
  • The digest for the contents to be signed (with the hash calculated before the contents are encoded to Base64). If your language and encryption libraries support it, you can perform the signature operation using the hash as the signature contents. In such a case, please make sure you configure PSPDFKit Server to use at least sha256 as its hashing algorithm.
  • The signing token, forwarded from the previous step.

For more details, you can look at our signing service reference implementation on GitHub.

We recommend setting up the signing service as a container on the same network as PSPDFKit Server and without external network access to guarantee fast, consistent performance and better security.

Once the signing service is up and running, you can configure PSPDFKit Server to use it by setting the SIGNING_SERVICE_URL to the signing service endpoint, e.g. http://signing-service:6000/sign. For more information on configuration and customization, you can look at our configuration guide.

Signing from a PSPDFKit for Web Instance

To digitally sign a document, you need to perform a call to the PSPDFKit.Instance#signDocument method. As its first argument, you can optionally pass 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 PKCS#7 container.

As a second argument, you can optionally specify an object with the signingToken string property that was described in the previous subsection. Please see the API documentation for more details.

Copy
1
2
3
4
5
6
7
8
9
10
instance
  .signDocument(null, {
    signingToken: "user-1-with-rights"
  })
  .then(() => {
    console.log("document signed.");
  })
  .catch(error => {
    console.error("The document could not be signed.", error);
  });

Setting Up Digital Signatures on Standalone

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.

In order 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.

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

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 in order 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:

Copy
1
2
3
4
5
6
7
8
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.

Digital Signatures Validation

Using the UI for Displaying the Validation Status of Digital Signatures

By default, the digital signatures validation UI is not enabled. You can easily turn it on by specifying the desired option on the PSPDFKit.ViewState.showSignatureValidationStatus property. The available options are:

  • PSPDFKit.ShowSignatureValidationStatusMode.NEVER (default) — Do not show the digital signature validation UI at any time, even if there are digital signatures on the document.
  • PSPDFKit.ShowSignatureValidationStatusMode.IF_SIGNED — Show the digital signature validation UI whenever the document is digitally signed.
  • PSPDFKit.ShowSignatureValidationStatusMode.HAS_WARNINGS — Only show the digital signature validation UI if there are warnings for the document’s digital signatures.
  • PSPDFKit.ShowSignatureValidationStatusMode.HAS_ERRORS — Only show the digital signature validation UI if there are invalid signatures in the document.

The signature validation UI consists of a colored bar shown under the main toolbar and, if they exist, under the annotation toolbars. The bar will have the background color corresponding to the current document’s validation status: red for “error,” yellow for “warning,” and green for “OK.” These colors are adapted for the default supported themes, light and dark. The status bar will show an informative text about the validation status of the document.

The diagram below shows the decision tree that leads to each possible validation status text and color. The bar will be shown or hidden on each case depending upon the value of PSPDFKit.ViewState.showSignatureValidationStatus.

The validation status bar will pop up either when the document is loaded (or reloaded), or when PSPDFKit.ViewState.showSignatureValidationStatus is updated, depending on its value. The bar can be closed at any time by pressing the Close button at the end of the bar. The validation status displayed is automatically updated whenever the document changes, e.g. if an annotation is added, the bar will reflect that modifications were made to the document since it was signed.

Using the API for Gathering the Validation Status of Digital Signatures

You can obtain the overall validation status of the current document and information about each one of the digital signatures found on it with the PSPDFKIt.Instance#getSignaturesInfo method. It returns a Promise that resolves with a PSPDFKit.SignaturesInfo object.

The status field returns a value indicating the result of the signatures’ validation of the document. Additionally, the documentModifiedSinceSignature property can be queried to determine if the document was altered in any way after all signatures were applied. If true, it means there is a signature that doesn’t cover the entire document. See the API documentation for more information.

If you need granular information about each one of the digital signatures found on the document, the signatures property of PSPDFKit.SignaturesInfo returns an Array with PSPDFKit.SignatureInfo objects. The array is sorted from least recent to most recent signature. The general status of each signature is present on the signatureValidationStatus field, wherein the field is PSPDFKit.SignatureValidationStatus.valid if no issues have been found on the signature, PSPDFKit.SignatureValidationStatus.warning if there are certain concerns with it, and PSPDFKit.SignatureValidationStatus.error if the signature is invalid.

For more details about the status of the certificate chain or the integrity of the document, you can check out the certificateChainValidationStatus and documentIntegrityStatus fields. Additionally, there are flags that indicate whether the signing certificate is trusted, self-signed, or expired.