This application implements a signing service compatible with PSPDFKit Document Engine and its digital signatures signing API in Node JS.
PSPDFKit offers support for customers with an active SDK license via https://pspdfkit.com/support/request/
Are you evaluating our SDK? That's great, we're happy to help out! To make sure this is fast, please use a work email and have someone from your company fill out our sales form: https://pspdfkit.com/sales/
Please refer to the dedicated document.
The example can be built and tagged with:
docker build -t pspdfkit/signing-server-example .
Once built it can be run as:
docker run \
--env SIGNING_SERVICE_PORT=6000 \
--publish 6000:6000 \
pspdfkit/signing-server-example:latest
This will make it accessible on the host machine at http://localhost:6000
.
With Docker Compose, it can be setup along PSPDFKit Document Engine as:
services:
signing_service:
build: .
environment:
SIGNING_SERVICE_PORT: 6000
ports:
- 6000:6000
pspdfkit:
...
environment:
SIGNING_SERVICE_URL: http://signing_service:6000/sign
See docker-compose.yml
for a complete example.
Install dependencies with npm install
.
You can then:
- Run the example with
npm run start
- Run tests with
npm test
The application exposes the /sign
endpoint that represents an implementation of Document Engine's signing service callback. Please refer to "Callbacks" section of Document Engine's /sign
endpoint reference to understand the full protocol.
For an introduction to digital signing refer to our digital signatures guides.
You can use Document Engine's API to sign document my-document
:
curl -X 'POST' 'http://localhost:5000/api/documents/my-document/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signingToken": "user-1-with-rights"
}'
The request contains a signingToken
attribute whose value is entirely under your control. The example in this repository shows how you can potentially use it to forward an authentication and authorization token that the signing service uses to determine if the caller of the API has enough rights to perform the signature.
Signing service should respond with 200
on success. If the signing service responds with an error in the 4xx
range, Document Engine will fail the original request with a 400
error, logging the response received by the signing service.
ℹ️ Refer to the app.js file to discover how the signing service callbacks are implemented.
💡 Note: The example logs all requests and responses to the
/sign
endpoint to give you better idea how Document Engine interacts with the signing service.
Document Engine's digital signatures process can be customized simply by passing additional options during sign request:
curl -X 'POST' 'http://localhost:5000/api/documents/7KPSD93NEZ9G25DK5XC0S5MQ69/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signatureContainer": "pkcs7",
"signatureType": "cades",
"signingToken": "user-1-with-rights",
"flatten": false,
"estimatedSize": 16384,
"hashAlgorithm": "sha256",
"appearance": {
"mode": "signatureOnly",
"contentType": "image/png",
"showSigner": true,
"showReason": true,
"showLocation": true,
"showWatermark": true,
"showSignDate": true
},
"position": {
"pageIndex": 0,
"rect": [
0,
0,
100,
100
]
},
"signatureMetadata": {
"signerName": "John Appleseed",
"signatureReason": "accepted",
"signatureLocation": "Vienna"
},
"cadesLevel": "b-lt"
}'
For detailed explanation of these options, refer to our API reference
ℹ️ Source code
Document Engine sign request that creates CAdES signature via PKCS#7 container created by the signing service:
curl -X 'POST' 'http://localhost:5000/api/documents/my_document/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signatureContainer": "pkcs7",
"signatureType": "cades",
"signingToken": "user-1-with-rights"
}'
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: 'cades',
signing_token: 'user-1-with-rights'
}
When performing signing operation with signatureContainer
set to pkcs7
(default), signing service receives the byte range and a hash representation of the current state of the document. The signing service is responsible to digitally sign the digest and produce a valid PCKS#7 signature container, encoded in DER format and return it as a response body with status 200
. Document byte range is also provided as encoded_contents
to allow signing service to calculate the digest manually before signing.
curl http://localhost:6000/sign \
-H "Content-Type: application/json" \
-X POST \
-d '{"digest : "<content-hash>", "encoded_contents" : "<base64-encoded-contents>", "signing_token" : "user-1-with-rights"}'
| openssl pkcs7 -noout -text -print
ℹ️ Source code
Document Engine provides an option to perform signing without producing PKCS#7 container.
For example, this sign request creates CAdES signature with raw signature:
curl -X 'POST' 'http://localhost:5000/api/documents/my_document/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signatureContainer": "raw",
"signatureType": "cades",
"signingToken": "user-1-with-rights"
}'
Under the hood, Document Engine will call the signing service with the following requests:
When performing signing operation with signature_type
cades
, caller needs to provide a list of certificates used for signing either via signing request directly or via a signing service. If the certificates are not 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' }
Signing service needs to respond with a JSON with base64 encoded PEM certificates.
{
certificates: [
'<base64 encoded PEM certificate>',
'<base64 encoded PEM certificate>'
],
ca_certificates: [
'<base64 encoded PEM certificate>',
'<base64 encoded PEM certificate>'
]
}
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: 'user-1-with-rights'
}
When performing signing operation with signatureContainer
set to raw
, signing service receives a binary representation of the current state of the document. Signing service is responsible to digitally sign this payload and return 200
response with DER encoded RSASSA-PKCS1-v1_5 payload.
Document Engine signing module can easily integrate with a broad range of third-party devices or cloud services. For instance, integration with a Hardware Security Module (HSM) adds an additional level of security to document processing and management workflows. HSMs are special devices designed to safeguard sensitive data and cryptographic keys. By delegating cryptographic operations to an HSM, the confidentiality, integrity, and availability of your documents are significantly enhanced.
This example shows how to implement signing via a software based HSM (SoftHSMv2) running locally. Clients interact with HSMs via PKCS#11 interface. This means that this example should provide you with a good starting point in integrating with your specific HSM.
If you run this example via the provided Docker file, the SoftHSM is already ready to use.
To setup SoftHSM without Docker, follow these steps:
-
Install the SoftHSMv2 for your platform of choice.
- If you are using macOS, you can install it via brew:
brew install softhsm
- Take a note of the install location, you'll need it later:
==> Pouring softhsm--2.6.1.arm64_ventura.bottle.2.tar.gz 🍺 /opt/homebrew/Cellar/softhsm/2.6.1: 16 files, 2.6MB
- If you are using macOS, you can install it via brew:
-
Generate test certificates if you haven't done so already.
-
Import the test private key into the HSM.
- Initialize the token with name test-key in the first free slot in the HSM.
For example purposes, we are setting the PIN to 1234 and Security Officer pin to 0000.
softhsm2-util --init-token --free --label test-key --pin 1234 --so-pin 0000
- Import the signer private key into the created slot. You can pick any ID and label.
softhsm2-util --import certs/test-signer.key --token test-key --label test-key --id CAFED00D --pin 1234
- Initialize the token with name test-key in the first free slot in the HSM.
For example purposes, we are setting the PIN to 1234 and Security Officer pin to 0000.
-
Export your pin as env variable
export HSM_PIN=1234
-
Export the location of your HSM module as env variable
export HSM_MODULE=$(brew --prefix)/lib/softhsm/libsofthsm2.so
-
Run the example as explained before:
npm install npm run start
To sign with our HSM example, use special signingToken
user-1-with-rights-hsm
that signals to the example to use SoftHSM for signing.
ℹ️ Source code
curl -X 'POST' 'http://localhost:5000/api/documents/my_document/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signatureContainer": "pkcs7",
"signatureType": "cades",
"signingToken": "user-1-with-rights-hsm"
}'
ℹ️ Source code
curl -X 'POST' 'http://localhost:5000/api/documents/my_document/sign' \
-H 'accept: application/json' \
-H 'Authorization: Token token="secret"' \
-H 'Content-Type: application/json' \
-d '{
"signatureContainer": "raw",
"signatureType": "cades",
"signingToken": "user-1-with-rights-hsm"
}'