Sign a PDF document

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

  1. Document Engine prepares the document for a signature, adding an invisible form field that will contain the signature value.

  2. Document Engine then contacts an external signing service you’re responsible for setting up, which will provide a compliant signature value.

  3. Document Engine 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 Nutrient 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 you’re responsible for maintaining and operating.

It needs to expose a single HTTP endpoint of your choice that implements the protocol required to create compliant digital signatures.

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

Once the signing service is up and running, configure Document Engine 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, refer to our configuration guide.

Information

For a full reference of the signing service protocol, refer to the Callbacks section in the API reference for the digital signatures endpoint.

Information

For more details on the signing service, refer to our signing service reference implementation on GitHub.

PKCS#7 signing

An option to implement the actual signing in the signing service is to make it produce PKCS#7 containers. This is beneficial when you already have a solution in place that can produce valid PKCS#7 containers with PDF signatures (e.g. some HSM solutions or a third-party signing API).

For example, say you want to sign a document with the ID my-document-id via Document Engine’s API, and that your signing service will return a PKCS#7 container with a basic CMS — not CAdES — signature:

curl -X 'POST' 'http://localhost:5000/api/documents/my-document-id/sign' \
  -H 'Accept: application/json' \
  -H 'Authorization: Token token="secret"' \
  -H 'Content-Type: application/json' \
  -d '{
     "signatureContainer": "pkcs7",
     "signatureType": "cms",
     "signingToken": "{\"userId\": \"user-1-with-rights\", \"signMethod\": \"privatekey\"}"
  }'

The request accepts an optional signingToken string parameter, which will be forwarded to the signing service in exactly the same shape. You can use it to forward parameters required by the signing service, such as user authentication.

Under the hood, Document Engine will call the signing service with the following request:

POST http://signing_service:6000/sign
Content-Type: application/json

{
  "action": "sign_pkcs7",
  "digest": "<base16 encoded hash of the document>",
  "encoded_contents": "<base64 encoded document contents>",
  "signature_type": "cms",
  "signing_token": "{\"userId\": \"user-1-with-rights\", \"signMethod\": \"privatekey\"}"
}

When performing a signing operation with signatureContainer set to pkcs7 (default), the signing service receives the byte range and a hash representation of the current state of the document. The signing service is responsible for digitally signing the digest and producing a valid PCKS#7 signature container, encoded in DER format, and returning it as a response body with the status 200. The document byte range is also provided as encoded_contents to allow the signing service to calculate the digest manually before signing.

RAW signing

Document Engine provides an option to perform signing if your signing service only returns a raw PKCS#1 signature. This signature format just contains the signed data, typically padded, but no additional information like the signing certificate chain or certificate revocation data. To make Document Engine work with this kind of signature format, pass the raw option as signatureType in the signing request.

For example, say you want to sign a document with the ID my-document-id via Document Engine’s API using the RAW container and requesting a CAdES signature:

curl -X 'POST' 'http://localhost:5000/api/documents/my-document-id/sign' \
  -H 'Accept: application/json' \
  -H 'Authorization: Token token="secret"' \
  -H 'Content-Type: application/json' \
  -d '{
     "signatureContainer": "raw",
     "signatureType": "cades",
     "signingToken": "{\"userId\": \"user-1-with-rights\", \"signMethod\": \"privatekey\"}"
  }'

The request accepts an optional signingToken string parameter, which will be forwarded to the signing service in exactly the same shape. Use it to forward parameters required by the signing service, such as user authentication.

Under the hood, Document Engine will call the signing service with the following requests.

1. Get certificates

When performing the signing operation with signature_type cades, the caller needs to provide a list of certificates used for signing either directly via signing request or via a signing service. If the certificates aren’t provided in the signing request, this callback will be invoked. It’s strongly recommended to use the signing service callback for providing the certificates, as this way, you don’t need to distribute them to your Document Engine API clients and can keep them collocated with the signing service:

POST http://signing_service:6000/sign
Content-Type: application/json

{ "action": "get_certificates" }

The signing service needs to respond with Base64-encoded PEM certificates:

{
  "certificates": [
    "<base64 encoded PEM certificate>",
    "<base64 encoded PEM certificate>"
  ],
  "ca_certificates": [
    "<base64 encoded PEM certificate>",
    "<base64 encoded PEM certificate>"
  ]
}

2. Signing request

Once Document Engine has the certificates, it calls the signing service with the actual sign request:

POST http://signing_service:6000/sign
Content-Type: application/json

{
  "action": "sign",
  "data_to_be_signed": "<payload that needs to be signed by signing service>",
  "hash_algorithm": "sha256",
  "signature_type": "cades",
  "signing_token": "{\"userId\": \"user-1-with-rights\", \"signMethod\": \"privatekey\"}"
}

When performing the signing operation with signatureContainer set to raw, the signing service receives a binary representation of the current state of the document. The signing service is responsible for digitally signing this payload and returning a 200 response with the DER-encoded RSASSA-PKCS1-v1_5 payload.

Applying a signature to a document via Document Engine’s API

To digitally sign a document, use the /api/documents/:document_id/sign endpoint. Refer to the API reference for the full list of available options.

Applying a signature to a document via Nutrient Web SDK

If you’re using Nutrient Web SDK to work with documents managed by Document Engine, initiate document signing via the JavaScript API. To do so, use the signDocument method on a loaded instance of Web SDK. For example:

instance.signDocument(
  {
    signingData: {
      signatureType: PSPDFKit.SignatureType.CAdES,
      padesLevel: PSPDFKit.PAdESLevel.b_lt
    }
  },
  {
    signingToken: "<optional signing token passed to signing service>"
  }
);