Blog Post

Digital Signing on Android

Reinhard Hafenscher
Illustration: Digital Signing on Android

We’ve talked about digital signatures on our blog in the past, but today we’ll revisit this topic once more with special attention given to the Android side of things.

Here’s a quick reminder of what a digital signature is in the context of a PDF:

  • It lets you verify that the document wasn’t modified.

  • It lets you know who authored the document.

If you want more in-depth knowledge about the signing part, read our What PDF Digital Signatures Are and Why They Are Important blog post.

Signing on Android

With that out of the way, let’s look at the signing options on Android:

  • Pkcs12Signer — This allows you to load a PKCS#12 file, which contains the private key used for signing. The user will be presented with a password entry field for unlocking the private key.

  • MemorySigner — This allows you to use an instance of KeyStore.PrivateKeyEntry directly for signing.

  • ContainedSignaturesSigner — This allows you to take full control of the signing process. The only requirement is that you’re able to provide a PKCS#7 signature container with a valid signature.

The above options can be split into two sub categories, which are outlined below.

Simple Signature Creation

Both Pkcs12Signer and MemorySigner let PSPDFKit do the heavy lifting of creating a signature. All your application has to provide is the private key. These signing options are great if you can use them, but they don’t allow a lot of flexibility if you require the use of an external signing service, or if you want to include signature features that aren’t yet supported — like the addition of a signed timestamp, or making the signature long-term validation (LTV) compatible by embedding the whole certificate chain needed for validation.

Contained Signature Creation

The ContainedSignaturesSigner encapsulates the contained signature creation process, which is split into three steps:

  1. The PDF needs to be prepared for signing. This includes embedding the signature graphic, as well as reserving space in the PDF where the signature data can be stored. It creates a new PDF, which we’ll use in the second step.

  2. The prepared PDF can now be signed. Here you have full control over how the signing will work — you can do it in your app, use some hardware that’s connected to the device, or even call a web service to sign the data. This step will create a PKCS#7 structure that contains the signature.

  3. The PKCS#7 signature structure created in step 2 is now written to the space we reserved in step 1 and the new PDF is digitally signed.

Now that we know the available options, let’s look at putting this to use. The simple signature creation scenario is already documented well in our Digital Signatures guide, so let’s take this opportunity to look more closely at the contained signature creation.

Contained Signatures

As discussed before, the contained signature creation is split into three parts. Let’s now take a closer look at each of them.

Preparing the Document

Before we can do anything else, we need to prepare the document. If you’re using the ContainedSignaturesSigner, this is abstracted away from you, but we also expose all the parts required for you to prepare the document manually. The main method we use here is Signer#prepareFormFieldForSigningAsync(). Let’s try writing our own Signer to see how this works:

class MySigner(private val context: Context,
               private val signingKey: KeyStore.PrivateKeyEntry) : Signer("Custom Display Name") {

    override fun signFormFieldAsync(signerOptions: SignerOptions): Completable {
        return Completable.defer {
            // Create a temporary file where we'll write the prepared document.
            val tempFile = File.createTempFile("prepared_document", null, context.cacheDir)

            // Build signer options for the prepare step — copy signer options but use the temp file for output.
            val prepareSignatureOptions = SignerOptions.Builder(signerOptions, FileOutputStream(tempFile))
                // Biometric signatures aren't supported when using custom signature contents. Clear them now.
                // Set blank signature contents that will tell the prepare step to embed zeroes as the signature contents in the prepared document.

            // Step 1 — We prepare the form field for signing.
                .andThen(Completable.defer {
                    // Here our PDF stored at `tempFile` is already prepared and ready for signing.

                    // Step 2 — Here we create the actual signature we want to embed.
                    val (signatureContents, signatureFormField) = prepareSignature(tempFile, signerOptions)
                    // Step 3 — We embed the signature and write the signed file to the initially specified destination.
                    embedSignatureInFormFieldAsync(signatureFormField, signatureContents, signerOptions.destination)

This takes care of the first step. We now have a PDF ready to be digitally signed. Note that we used BlankSignatureContents since we need to zero out the area where the signature will be placed before we can sign it.

Creating the PKCS#7 Signature

Now that everything is ready, we can move on to the next step: actually creating a digital signature. Depending on your exact requirements, doing this ranges from fairly simple all the way to quite complicated. For now though, let’s get some of the boilerplate work out of the way and implement our prepareSignature() method:

private fun prepareSignature(preparedFile: File, signerOptions: SignerOptions): Pair<SignatureContents, SignatureFormField> {
    // Open the prepared document.
    val pdfDocument = PdfDocumentLoader.openDocument(context, Uri.fromFile(preparedFile))

    // Retrieve the form field that should be signed. Its signature contents are already prefilled with zeroes after the preparation step.
    val formField = pdfDocument.formProvider.getFormFieldWithFullyQualifiedName(signerOptions.signatureFormField.fullyQualifiedName)
        ?: throw IllegalStateException("Can't retrieve form field to sign in prepared document.")
    check(formField is SignatureFormField) { "Form field to sign must be a signature field." }

    // Prepare the signature contents.
    val signatureContents: SignatureContents = prepareSignatureContents(formField)

    return Pair(signatureContents, formField)

With that, we’re ready to sign the document, which is the responsibility of the SignatureContents class. We already encountered one subclass of this before, BlankSignatureContents, which only writes zeroes. PSPDFKit comes with one more subclass out of the box, PKCS7SignatureContents, which returns a PKCS#7 container using a provided KeyStore.PrivateKeyEntry. This is also what we’ll be using for our example:

private fun prepareSignatureContents(signatureFormField: SignatureFormField): SignatureContents {
    // `PKCS7SignatureContents` handles everything required to create a valid digital signature.
    // It'll hash the areas of the document covered by the signature and then
    // sign them using the provided private key.
    // It'll also encode the signature in the PKCS#7 format so it can be embedded in the document.
    return PKCS7SignatureContents(signatureFormField, signingKey, HashAlgorithm.SHA256)

The PKCS7SignatureContents class handles everything needed to create a valid digital signature. That being said, let’s see in pseudocode how the SignatureContents class might look:

class MySignatureContents : SignatureContents {
    override fun signData(dataToSign: ByteArray): ByteArray {
        // `dataToSign` — These are all the bytes of the document that need to be covered by the signature.
        // To be more specific, these are the bytes before and the bytes after the signature form field where we
        // want to place the signature.

        // Next it's likely necessary to hash this data. It depends on how your signing is structured, but most
        // external signing services only operate on the hash so that the whole PDF doesn't have to be transmitted.

        // Then you can sign the hashed data. This can take many shapes — you can use an external service,
        // some hardware you have connected to the device, or even something like Bouncy Castle to do it
        // directly inside your app.

        // Finally, return the PKCS#7 structure, making sure it's in DER encoding.

Equipped with all this new knowledge, it’s time for the final piece: embedding the signature.

Embedding the Signature

We already had the following piece of code in the first code block, so let’s go over what happens when this is called:

embedSignatureInFormFieldAsync(signatureFormField, signatureContents, signerOptions.destination)

As you might have noticed, we never actually called SignatureContents#signData(); we simply returned our SignatureContents instance in MySigner#prepareSignature(). The reason for this is that SignatureContents#signData() will be called during the embed step once we have all the data required to actually sign the document.

Essentially what happens is we tell PSPDFKit that we now have everything, and then internally, we prepare the signature dictionary that’s written to the PDF. This dictionary includes information like the version of PSPDFKit that was used for signing, which OS was used for signing, and important metadata such as what format the signature has. Finally, we call SignatureContents#signData() with the exact bytes covered by the signature so it can be properly signed, and we embed that return value.

Now we have a digitally signed document.

Where to Go from Here

This post covered how our ContainedSignaturesSigner looks from the inside and hopefully gave you a better idea of how digital signatures work in general. If you want to know how to use the new Signer we built, look no further than our Contained Signatures Example. Digital signatures are a complicated topic, so we hope this look under the hood gave you a better idea of how they work and where to use them.

Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
PRODUCTS  |  Android • Releases

Android 2024.1 Update: Advanced Content Editing and Digital Signatures, Plus Expanded Jetpack Compose Support

TUTORIALS  |  Android • How To

How to Persist Zoom While Scrolling through a Document Using the PSPDFKit Android Library

CUSTOMER STORIES  |  Case Study • React Native • iOS • Android

Case Study: How Trinoor Uses PSPDFKit to Drive Operational Excellence with Flexible, Mobile Applications for the Energy and Utilities Market