Digital Signatures

A digital signature is an electronic fingerprint uniquely identifying the signing person. A digital signature on a PDF document is both reliable proof of the document’s origin and protection against modifications by third parties.

ℹ️ Info: For more information on digital signatures, please look at the Digital Signatures in a PDF guide by Adobe. You should also take a look at the 12.8 Digital Signatures section of Adobe’s PDF 1.7 Specification. For general information on how digital signatures work, please read the digital signature entry on Wikipedia.


Considerations

PSPDFKit allows signing existing signature form elements using the RSA / SHA256 and ECDSA signing algorithms. You can use a self-signed certificate for testing purposes, but you will need to make sure that certificate is trusted by all the devices the PDF is opened on (including PCs/Macs with Acrobat). A self-signed certificate will probably also generate warnings about its keyUsage extension (the self-signed certificate must permit certificate signing — keyCertSign, see RFC 5280).

⚠️ Important: In production, always use a certificate from a valid certificate authority. Make sure the certificate’s keyUsage has the digitalSignature permission set (see RFC 5280).

How to Create Digital Signatures

The Signer class allows signing of documents by adding a digital signature to a SignatureFormField. Its methods, signFormField() and signFormFieldAsync(), allow both computation and saving of digital signatures to a definable output file.

PSPDFKit ships two default implementations of the Signer class, but you can always create a custom signer if your use case requires it. The two signers are:

  • MemorySigner, which takes a KeyStore.PrivateKeyEntry that was loaded by your app and uses it to sign the document directly.
  • Pkcs12Signer, which loads the signing certificate from a PKCS#12 file (usually having the .p12 file extension) to sign the document. If necessary, the signer will request a password for unlocking the PKCS#12 file, in order to load the required keys from it:
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Create a signer loading the signing certificate from a PKCS#12 file.
// The provided displayName can be used by PSPDFKit in various views.
val signer: Signer = Pkcs12Signer(
	"John Appleseed",
	Uri.parse("file:///android_asset/JohnAppleseed.p12")
)

// Sign the form field, writing the signed document to a destination.
// You can optionally provide additional biometric signature data.
signer.signFormField(
	formField,
	biometricSignatureData,
	destination,
	object : Signer.OnSigningCompleteCallback() {
		override fun onSigningFailed(ex: Exception) {
			// Handle signing errors here...
		}

		override fun onSigningCompleted() {
			// The document was successfully signed!
		}
	}
);
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
// Create a signer loading the signing certificate from a PKCS#12 file.
// The provided displayName can be used by PSPDFKit in various views.
final Signer signer = new Pkcs12Signer(
	"John Appleseed",
	Uri.parse("file:///android_asset/JohnAppleseed.p12")
);

// Sign the form field, writing the signed document to a destination.
// You can optionally provide additional biometric signature data.
signer.signFormField(
	formField,
	biometricSignatureData,
	destination,
	new Signer.OnSigningCompleteCallback() {
		@Override
		public void onSigningFailed(Exception ex) {
			if (!e.isDisposed()) e.onError(ex);
		}

		@Override
		public void onSigningCompleted() {
			if (!e.isDisposed()) e.onComplete();
		}
	}
);

💡 Tip: For an interactive example of digital signatures, check out the DigitalSignatureExample in the Catalog app.

Custom Signers

There are various use cases in which PSPDFKit’s default signing implementation can’t be used. In such cases, developers can create custom Signer implementations. Some example use cases are when:

  • You are required to use a specific crypto library (for example, Bouncy Castle).
  • You aren’t in direct possession of a signing key (for example, when using an HSM or a signing service).
  • You have a specific multi-step signing flow that is not supported by PSPDFKit’s default implementation (for example, with multiple passwords).

Implementing a Custom Signer

To build a custom signer, create a class that derives from Signer and implement the abstract prepareSigningParameters() method. The method needs to load all parameters required for a successful signing operation. These parameters are:

  • The X509Certificate that is going to be embedded into the signed PDF document.
  • A SignatureProvider that will perform the actual signing operation of the PDF data. Implement this to support virtually any entity capable of signing data in a document.

Whenever PSPDFKit tries to digitally sign a PDF using your custom signer, its prepareSigningParameters() method will be called. It is the responsibility of the signer to retrieve all signing parameters and return them using the provided callback. For example:

Copy
1
2
3
4
5
6
override fun prepareSigningParameters(callback: OnSigningParametersReadyCallback) {
	// Load all parameters.
	loadSigningParameters()
	// Call this whenever your signer is ready. This will finish the signing process.
	callback.onSigningParametersReady(signatureProvider, certificate)
}
Copy
1
2
3
4
5
6
7
@Override
protected void prepareSigningParameters(@NonNull OnSigningParametersReadyCallback callback) {
	// Load all parameters.
	loadSigningParameters();
	// Call this whenever your signer is ready. This will finish the signing process.
	callback.onSigningParametersReady(signatureProvider, certificate);
}

Supported Certificate Types

For signing documents, PSPDFKit supports PEM-encoded and DER-encoded X.509 certificates, as well as DER-encoded PKCS#7.

You can check the encoding of a certificate file by using the OpenSSL command-line tool as follows:

1
  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.

1
  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.

Implementing the Custom Signature Provider

The SignatureProvider interface defines entities capable of signing data of a PDF. A custom signature provider has to implement two methods:

  • getEncryptionAlgorithm() has to return the encryption algorithm that will be applied to sign the PDF. The signature provider can return any of the available EncryptionAlgorithm values. Usually this is dependent on the private key used (i.e. whether you use an RSA or DSA key).
  • signData() has to perform the actual signing operation. This involves digesting the given PDF data using the requested HashAlgorithm and then encrypting it using the signer’s private key (using the selected encryption algorithm).
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CustomSignatureProvider: SignatureProvider {
	/**
	 * For example, this uses Java's crypto APIs for signing a PDF.
	 * The getSignatureAlgorithm() method will find the approriate
	 * signing algorithm. You can see this example in our Catalog app.
	 */
	override fun signData(data: ByteArray, hashAlgorithm: HashAlgorithm): ByteArray
		= Signature.getInstance(getSignatureAlgorithm(hashAlgorithm)).run {
			initSign(signingKey.privateKey)
			update(data)
			sign()
		}

	override fun getEncryptionAlgorithm() = EncryptionAlgorithm.RSA
}
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
public class CustomSignatureProvider implements SignatureProvider {
	/**
	 * For example, this uses Java's crypto APIs for signing a PDF.
	 * The getSignatureAlgorithm() method will find the approriate
	 * signing algorithm. You can see this example in our Catalog app.
	 */
	@NonNull
	@Override
	public byte[] signData(@NonNull byte[] data, @NonNull HashAlgorithm hashAlgorithm) {
		try {
			final Signature rsa = Signature.getInstance(getSignatureAlgorithm(hashAlgorithm));
			rsa.initSign(signingKey.getPrivateKey());
			rsa.update(data);
			return rsa.sign();
		} catch (Exception e) {
			...
		}
	}

	@NonNull
	@Override
	public EncryptionAlgorithm getEncryptionAlgorithm() {
		return EncryptionAlgorithm.RSA;
	}
}

How to Remove a Digital Signature

If you want to remove a signature again you just need to access the signed SignatureFormField and call removeSignature() or removeSignatureAsync(). This will remove the DigitalSignatureInfo from the given SignatureFormField.

How to Validate a Digital Signature

The DigitalSignatureValidator extracts the DigitalSignatureInfo from a signed SignatureFormField and will perform a validation check on the signature. Validation can be performed using validateSignature() and validateSignatureAsync() and will return a DigitalSignatureValidationResult once complete. The result will hold information about both the validity of the digital signature and a variety of possible validation warnings and errors:

Copy
1
2
3
4
5
6
val result = DigitalSignatureValidator.validate(signatureFormField)
if(result.validationStatus == ValidationStatus.SUCCESS) {
	onSignatureValidated()
} else {
	showValidationProblems(result.problems)
}
Copy
1
2
3
4
5
6
final DigitalSignatureValidationResult result = DigitalSignatureValidator.validate(signatureFormField);
if(result.getValidationStatus() == ValidationStatus.SUCCESS) {
	onSignatureValidated();
} else {
	showValidationProblems(result.getProblems());
}

How to Use the Digital Signatures UI

PSPDFKit comes with a ready-to-use user interface collection that allows you to provide digital signing capabilities inside your apps with minimal effort. All you need to do is register all Signer instances you would like to provide your users with by using the addSigner() from the SignatureManager class. PSPDFKit will then pick up these signers to use them in the signature dialog:

Copy
1
2
val johnAppleseed: Signer = Pkcs12Signer("John Appleseed", Uri.parse("file:///android_asset/JohnAppleseed.p12"))
SignatureManager.addSigner("john-appleseed", johnAppleseed)
Copy
1
2
final Signer johnAppleseed = new Pkcs12Signer("John Appleseed", Uri.parse("file:///android_asset/JohnAppleseed.p12"));
SignatureManager.addSigner("john-appleseed", johnAppleseed);

After registering the signer, PSPDFKit will automatically show it inside the SignaturePickerFragment as an available signer (using its display name).

💡 Tip: To see an example of this, check out our DocumentSigningExample in the Catalog app.

Retrieving the Signed Document After a User Signs It

When the user signs a document by clicking on a SignatureFormField and choosing a signature to sign the document with, PSPDFKit will write the signed document to a temporary location. To get access to the signed document, you need to register a DocumentSigningListener on the PdfFragment:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pdfFragment.setDocumentSigningListener(object : DocumentSigningListener {
    override fun onDocumentSigned(signedDocumentUri: Uri) {
        // Move the signed document to its permanent location.
        // Then reopen it.
    }

    override fun onDocumentSigningError(error: Throwable?) {
        // An error occurred during signing.
    }

    override fun onSigningCancelled() {
        // The signing process was canceled.
    }
})
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getPdfFragment().setDocumentSigningListener(new DocumentSigningListener() {
    @Override
    public void onDocumentSigned(@NonNull Uri signedDocumentUri) {
        // Move the signed document to its permanent location.
        // Then reopen it.
    }

    @Override
    public void onDocumentSigningError(@Nullable Throwable error) {
        // An error occurred during signing.
    }

    @Override
    public void onSigningCancelled() {
        // The signing process was canceled.
    }
});

Using the Signature Picker Fragment

The SignaturePickerFragment is fully integrated into the PdfActivity and the PdfFragment. However, it can also be used independently, which allows you to create and collect Signature instances outside of the context of a document:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
SignaturePickerFragment.show(
	fragmentManager,
	object : SignaturePickerFragment.OnSignaturePickedListener {
		override fun onSignaturePicked(signature: Signature) {
			// The user selected a signature.
		}

		override fun onDismiss() {
			// The user dismissed the dialog without selecting a signature.
		}
	}
)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SignaturePickerFragment.show(
	fragmentManager,
	new OnSignaturePickedListener() {
		@Override
		public void onSignaturePicked(@NonNull final Signature signature) {
			// The user selected a signature.
		}

		@Override
		public void onDismiss() {
			// The user dismissed the dialog without selecting a signature.
		}
	}
);

The Signature is a container for the following properties of the created signature:

  • The ink annotation data, which is the handwritten signature of the user.
  • An optional unique identifier of a registered Signer. This is the same identifier used to register the signer using SignatureManager.addSigner().
  • An optional BiometricSignatureData holding additional information about the signature (see section below).

Biometric Signature Data

BiometricSignatureData is “real-world information” that can be attached to a digital signature. This information includes things like whether or not the signature was created with a stylus, the size of the signee’s finger, and the timing and pressure information that was collected while writing the signature. Ultimately, this data can be used to create solutions that provide a higher grade of security than traditional digital signatures do.

Creating Biometric Data

You can create a BiometricSignatureData instance using its Builder. All values of the biometric data are optional and can be left out. Once created, the BiometricSignatureData is immutable:

Copy
1
2
3
4
val biometricData = BiometricSignatureData.Builder()
	.setInputMethod(BiometricSignatureData.InputMethod.FINGER)
	.setPressurePoints(listOf(0.4f, 0.1f, 0.94f, 0.6f))
	.build()
Copy
1
2
3
4
final BiometricSignatureData biometricData = new BiometricSignatureData.Builder()
	.setInputMethod(BiometricSignatureData.InputMethod.FINGER)
	.setPressurePoints(Arrays.asList(new Float[]{ 0.4f, 0.1f, 0.94f, 0.6f }))
	.build();

💡 Tip: BiometricSignatureData is a Parcelable; this allows it to be passed around activities or saved to your instance state.

Collecting Biometric Data

When a user creates a Signature using the SignaturePickerFragment, the signature will also hold BiometricSignatureData that was collected during the creation of the signature. You can retrieve this data using signature.getBiometricData():

1
2
// Retrieve the biometric data that was collected during signature creation.
val biometricData = signature.biometricData
Copy
1
2
// Retrieve the biometric data that was collected during signature creation.
final BiometricSignatureData biometricData = signature.getBiometricData();

Digitally Signing with Biometric Data

To add biometric data to a digital signature, pass it to your Signer during the signing process. The Signer will automatically verify the biometric data and attach it to the signature:

Copy
1
2
3
4
5
6
7
8
9
val biometricData = signature.biometricData
signer.requestSigningCertificate("secretkey", object : Signer.CertificateRequestCallback() {
	override fun onSuccess(signingKey: KeyStore.PrivateKeyEntry signingKey) {
		// Simply pass in BiometricSignatureData as an extra argument.
		signer.signFormField(formField, signingKey, biometricData, outputFile)
	}

	...
})
Copy
1
2
3
4
5
6
7
8
9
10
final BiometricSignatureData biometricData = signature.getBiometricData();
signer.requestSigningCertificate("secretkey", new Signer.CertificateRequestCallback() {
	@Override
	public void onSuccess(@NonNull KeyStore.PrivateKeyEntry signingKey) {
		// Simply pass in BiometricSignatureData as an extra argument.
		signer.signFormField(formField, signingKey, biometricData, outputFile);
	}

	...
});

External Signature Providers

PSPDFKit supports external signature providers such as hardware security modules (HSMs) and other signing entities. The SignatureProvider interface defines classes capable of signing PDF blob data, which is required as part of the digital signing process of a PDF.

The default implementation used by PSPDFKit, aka the PrivateKeySignatureProvider, signs PDF data using a java.security.KeyStore.PrivateKeyEntry. Both Pkcs12Signer and MemorySigner derive from PrivateKeySignatureProvider and can be used if your app stores the private key of the user directly on the device. If you are using an external signing service (such as an HSM), implement a custom Signer as shown in the CustomSignatureProviderExample.

Troubleshooting

I have a chain of certificates I used for signing, but when I validate the digital signature in PSPDFKit, it shows as “not trusted”

When you have a chain of certificates comprising a leaf certificate and some intermediate certificate authorities, we require that you explicitly trust every intermediate certificate authority in the chain. If any certificate in the path is not trusted, we can’t perform proper path validation, and as a result, we can show two possible error messages:

  • If there is not any expired certificate in the chain, we show a warning explaining that we couldn’t verify the chain of certificates.
  • If there is an expired certificate in the chain, we show an error because we consider an expired certificate to be a very important issue during digital signature validation.

In order to trust certificates, you can use the addTrustedCertificates() method. Note that, by default, our SDK will automatically add and trust those CAs already included in the Android device, but you will need to manually trust any other CA that issued your certificate.

I am signing a document with multiple signatures and previous signatures show as “not valid”

When digitally signing documents with multiple signature fields, you need to make sure that the document can be saved incrementally. When incremental saving is disabled or not supported, PSPDFKit rewrites already signed document data, which could result in the invalidation of existing signatures.

Incremental saving is enabled only for documents that are not password protected with document sources that support appending. Appendable sources include local files and WritableDataProvider instances that return true inside WritableDataProvider#supportsAppending().