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:
data
?DigestAlgorithms.digest(...)
?ByteArray
?fieldName
?8192
I'm using for the estimatedSize
parameter of signer.signExternalContainer
?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.
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.