Search code examples
pdfitextitext7

How do I apply an externally generated signature to a PDF using itext7?


I'm in the process of taking my first shot at applying a signature generated by a third party AATL service to a PDF.

I feel like I'm calling everything almost-correctly, but somewhat unsurprisingly, when I view the signed document, I'm told that the signature is not valid. There are many steps in this process where things can go wrong and as I am learning, this stumbling is expected. 😉

Anyway, I'm looking for some guidance and to get some gaps in my knowledge filled. Before my code, I'll lay out what I think are the questions I need answered, you might see them in comments in the code as well:

  1. Does it look like I'm using the right source for my digest? Namely data?
  2. If the source is right, am I using the right technique to generate the digest? Namely, DigestAlgorithms.digest(...)?
  3. The signature the third party service I call returns is base64 encoded. Do I need to convert it from base64 to something else before returning the representation as a ByteArray?
  4. I know I have to somehow use the certificates they make available to me, I'm just not sure where or how?
  5. What is the signer fieldName?
  6. How do I determine the right value for the 8192 I'm using for the estimatedSize parameter of signer.signExternalContainer?
  7. How would I add CRL/OCSP information to the PDF? Where does it typically come from when using a third party AATL signing service?

Please feel free to point out any other suggestions or errors not in the list above! 🙏🏻

data class ThirdPartyCertificateResponse(val certs: List<String>)
data class ThirdPartySigningResponse(val sig: String, val nonce: String)

class ThirdPartySignatureContainer : IExternalSignatureContainer {

    private lateinit var data: ByteArray

    override fun sign(data: InputStream): ByteArray {

        // note: I've omitted any validation of the nonce from this sample.
        val nonce = UUID.randomUUID()
        val digest = DigestAlgorithms.digest(data, BouncyCastleDigest().getMessageDigest("sha256"))

        // note: The service I'm calling expects requests to look like this.
        //       I'm including this on the offchance that I'm accidentally 
        //       corrupting any of the data that I'm preparing for them.
        val bodyJson = JWSObject(
            JWSHeader(JWSAlgorithm.HS256),
            Payload(
                mapOf<String, Any>(
                    // note: `digestInfo` is an extension method that returns an `org.bouncycastle.asn1.x509.DigestInfo` instance from a `ByteArray`
                    // note: Would love a tool that generates digests for me to check mine against! Hard to know if I'm doing the right thing here by basing it off of `data`??
                    "digestInfo" to digest.digestInfo().toBase64String(),
                    "nonce" to nonce.toString(),
                    "version" to 1,
                )
            )
        )

        bodyJson.sign(MACSigner("SHARED_SECRET"))

        val (_, _, signatureResult) = "https://thidpartyservice.notreal/api/v1/signatures"
            .httpPost()
            .body(bodyJson.serialize())
            .responseObject<ThirdPartySigningResponse>()

        val signatureText = when (signatureResult) {
            is Result.Failure -> throw signatureResult.getException()
            is Result.Success -> signatureResult.value.sig
        }

        return signatureText.decodeBase64()
            ?: throw Exception("Unable to decode response from third party service")
    }

    override fun modifySigningDictionary(dictionary: PdfDictionary) {

        val (_, _, certificateResult) = "https://thidpartyservice.notreal/api/v1/certs"
            .httpGet()
            .responseObject<ThirdPartyCertificateResponse>()

        val certificateFactory = CertificateFactory.getInstance("x.509")

        // note: I have no idea what to do with these, but I've got them!
        val certificateChain = when (certificateResult) {
            is Result.Failure -> throw certificateResult.getException()
            is Result.Success -> certificateResult.value.certs.map {
                certificateFactory.generateCertificate(it.decodeBase64()?.inputStream())
            }
        }

        // note: What are these doing?
        dictionary.put(PdfName.Filter, PdfName.Adobe_PPKLite)
        dictionary.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached)

        // note: I feel like I should be adding the certificate(s) to `dictionary` here...?
    }
}

The above class is instantiated and called from code as follows:

val stampingProperties = StampingProperties()
val signer = PdfSigner(thisPdfReader, thisSigned.outputStream(), stampingProperties)
// note: What's this?
signer.fieldName = null

signer.signatureAppearance
    .setPageRect(Rectangle(0.0f, 0.0f, 0.0f, 0.0f))
    .setPageNumber(1)

val container: IExternalSignatureContainer = ThirdPartySignatureContainer()

// note: I don't know how to determine a correct value for it, so I've left 8192 in here from examples I've seen.
signer.signExternalContainer(container, 8192)

This is a sample of a PDF that I'm currently generating.


Solution

  • Your class ThirdPartySignatureContainer implements IExternalSignatureContainer; thus, its sign method is expected to return a CMS signature container to embed as is in the PDF. Inspecting your example file, though, it becomes clear that your remote signature service - and so also your sign method - returns naked signature bytes.

    Thus, you should instead implement IExternalSignature the sign method of which is expected to return naked signature bytes. To sign you would then use a signer.signDetached overload instead of signer.signExternalContainer.

    This also would imply an answer to your question how to add CRL/OCSP information to the PDF - the signDetached overloads have ICrlClient and IOcspClient parameters, too, which can provide CRLs and OCSP responses to embed.