Creating Contained Signatures using Bouncycastle

Part of our digital signatures API allows you to create what we call contained signatures. That means that you are responsible for every part of the signing process, except actually writing it to the PDF. On Android we can take advantage of Bouncycastle to create the required PKCS#7 structure that will be embeded in the PDF. This code samples uses the Spongycastle repackage but depending on your requirements using Bouncycastle directly should also work.

The following class implements our SignatureContents using Bouncycastle to perform the signing.

Things to keep in mind:

  • ContentSigner#getSignature() is where you would place your custom signing code. For example calling to a webservice, some local hardware or anything else that manages your private key.
  • https://www.freetsa.org/ the TSA we use in this code example uses a self signed root certificate, as such while Adobe Acrobat will show that a valid timestamp is embedded, it will not be able to validate the signature. For production deployments a different time stamp authority should be used.
  • CMSSignedData#getEncoded() returns data in BER encoding by default, the PDF spec requires DER encoding so you need to make sure to convert it before returning it from this method.
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class BouncyCastleSignatureContents(val privateKey: KeyStore.PrivateKeyEntry) : SignatureContents {
    override fun signData(dataToSign: ByteArray): ByteArray {
        // dataToSign contains the data of the PDF that actually needs to be signed.
        // If you hash this it will be the same value as calling PdfDocument.getHashForDocumentRange(signatureByteRange, HashAlgorithm.SHA256).

        // Setup our Certificates.
        val certList: MutableList<Certificate?> = ArrayList()
        certList.add(privateKey.certificate)
        val certs: Store<*> = JcaCertStore(certList)
        val cert = org.spongycastle.asn1.x509.Certificate.getInstance(ASN1Primitive.fromByteArray(privateKey.certificate.encoded))
        val signingCertificate = X509CertificateHolder(cert)

        // This is our Signer, here we just use a private key but it could also call out to a Server, some local device, or anything else capable of signing.
        val contentSigner = object : ContentSigner {

            // This will contain the data that actually needs to be signed.
            val byteArrayOutputStream = ByteArrayOutputStream()

            override fun getAlgorithmIdentifier(): AlgorithmIdentifier {
                // Make sure to use the same algorithm as your signing service uses.
                return DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA")
            }

            override fun getOutputStream(): OutputStream = byteArrayOutputStream

            override fun getSignature(): ByteArray {
                // Sign the data we got using the private key.
                // Here you would call to your signing service with the content.
                val content: ByteArray = byteArrayOutputStream.toByteArray()
                val dsa = Signature.getInstance("SHA256withRSA")
                dsa.initSign(privateKey.privateKey)
                dsa.update(content)
                return dsa.sign()
            }
        }

        // Setup our Generator.
        val generator = CMSSignedDataGenerator()
        // We register our ContentSigner.
        generator.addSignerInfoGenerator(JcaSignerInfoGeneratorBuilder(JcaDigestCalculatorProviderBuilder().build()).build(contentSigner, signingCertificate))
        generator.addCertificates(certs)

        // Here we pass the data we want signed to our ContentSigner.
        val cmsData = CMSProcessableByteArray(dataToSign)
        val signedData = generator.generate(cmsData, false)

        // We get the first signer as there should only be one.
        val signerInformation = signedData.signerInfos.signers.first()

        // Create the timestamp we want to embed.
        val timestampVector = ASN1EncodableVector()
        val token: Attribute = createTimeStampToken(signerInformation.signature)
        timestampVector.add(token)

        // Create a new PKCS7 structure, now containing the timestamp.
        val attributeTable = AttributeTable(timestampVector)
        val signerInformationWithTimestamp = SignerInformation.replaceUnsignedAttributes(signerInformation, attributeTable)
        val signerInformationStore = listOf(signerInformationWithTimestamp)
        val newSignerStore = SignerInformationStore(signerInformationStore)
        val newSignedData: CMSSignedData = CMSSignedData.replaceSigners(signedData, newSignerStore)

        // Convert from BER to DER since that is what the PDF spec requires.
        val signedDataAsASN1 = newSignedData.toASN1Structure().toASN1Primitive()
        return signedDataAsASN1.getEncoded("DER")
    }

    private fun createTimeStampToken(data: ByteArray): Attribute {
        // We hash the signature so we can send it to the TSA server.
        val digest: MessageDigest = MessageDigest.getInstance("SHA256")
        val response: TimeStampResponse = sendRequestToTSA(digest.digest(data))
        val timestampToken: TimeStampToken = response.timeStampToken

        // Create timestamp attribute
        return Attribute(PKCSObjectIdentifiers.id_aa_signatureTimeStampToken, DERSet(ASN1Primitive.fromByteArray(timestampToken.getEncoded())))
    }

    private fun sendRequestToTSA(data: ByteArray): TimeStampResponse {
        val timeStampRequestGenerator = TimeStampRequestGenerator()

        // The PDF spec requires the certificate to be embedded in the timestamp.
        timeStampRequestGenerator.setCertReq(true)

        // Generate a request blob we can send to our TSA server.
        val request = timeStampRequestGenerator.generate(TSPAlgorithms.SHA256, data)
        val requestEncoded = request.encoded

        // https://www.freetsa.org/ provides a free Time Stamp Authority. Keep in mind that their certificate can't be validated by Acrobat
        // and in production you should use a different TSA.
        val url = URL("https://freetsa.org/tsr")
        val connection: HttpURLConnection = url.openConnection() as HttpURLConnection

        // Setup our connection to send the TimeStampRequest.
        connection.doOutput = true
        connection.doInput = true
        connection.requestMethod = "POST"
        connection.setRequestProperty("Content-type", "application/timestamp-query")
        connection.setRequestProperty("Content-length", requestEncoded.size.toString())

        // Write the request to the connection.
        val out: OutputStream = connection.outputStream
        out.write(requestEncoded)
        out.flush()

        // Make sure the server handled the request correctly.
        if (connection.responseCode != HttpURLConnection.HTTP_OK) {
            throw IOException("Received HTTP error: " + connection.responseCode.toString() + " - " + connection.responseMessage)
        }

        // Read the body the TSA sent.
        val inputStream: InputStream = connection.inputStream
        val resp: TimeStampResp = TimeStampResp.getInstance(ASN1InputStream(inputStream).readObject())
        val response = TimeStampResponse(resp)

        // Make sure the response matches the request we sent.
        response.validate(request)

        // Make sure we got a success status.
        if (response.status != 0) {
            throw IOException("Received error: " + response.failInfo)
        }

        return response
    }
}

To use the newly created class you need to create a simple ContainedSignaturesSigner:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomContainedSignatureSigner(
    context: Context,
    displayName: String,
    private val signingKey: KeyStore.PrivateKeyEntry
) : ContainedSignaturesSigner(context, displayName) {

    override fun prepareSignatureContents(signerOptions: SignerOptions,
                                          preparedDocumentFile: File,
                                          preparedDocument: PdfDocument,
                                          preparedFormField: SignatureFormField): SignatureContents {
        return BouncyCastleSignatureContents(signingKey)
    }
}

Finally have a look at our ContainedSignaturesExample on how to use the CustomContainedSignatureSigner.