External signing PDF with iText

There are a number of issues in your code.

First of all your code mixes different iText signing API generations. There is the older API generation which requires you to work very near to the PDF internals, and there is the newer (since version 5.3.x) API which is implemented as a layer over the older API and does not require you to know those internals.

The “Digital Signatures for PDF documents” white paper focuses on showing the newer API, only section 4.3.3 “Signing a document on the server using a signature created on the client” uses the old API because the use case does not allow for the use of the newer API.

Your use case does allow for the use of the newer API, though, so you should try and use only it.

(In certain situations one can mix those APIs, but you then should really know what you’re doing and still can get it awfully wrong…)

But now some more specific issues:

Working on closed objects

The MakeSignature.Sign* methods implicitly close the underlying PdfStamper and SignatureAppearance objects, so working with those objects thereafter should not be assumed to result in sensible information.

But in GetBytesToSign you do

MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");

Thus, the sap.GetRangeStream() probably returns something wrong. (Probably it does still return the correct data but you shouldn’t count on that.)

Signing the wrong bytes

GetBytesToSign returns the hash of the signed PDF document ranges:

signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

return hash;

Later, though, your code takes that return value, signs it, and tries to embed the returned signature bytes into the PdfPKCS7 signature container. This is wrong, the signature bytes have to be created for the authenticated attributes of the signer info of the signature container, not the document hash.

(By the way, here you use the older signature API without understanding it and, therefore, use it incorrectly.)

Putting the signed bytes in the wrong position

In MyExternalSignatureContainer you use the signed bytes in two calls:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);

The first call is correct, here they belong. In the second call, though, the original hash of the signed document ranges should have been used.

(Here you again use the older signature API without understanding it and again use it incorrectly.)

###Supplying the wrong certificate

Analyzing your example PDF you appear to declare the wrong certificate as signer certificate. I think so because

  • its public key cannot properly decrypt the signature bytes and
  • the certificate is a CA certificate, not an end entity certificate, with inappropriate key usages for signing PDF documents.

How to improve your code

First of all, if I understand you correctly you request the signature from some other server, and that other server reacts quickly, so there is no need to free all resources while waiting for the signature. In such a situation there is no need for a two-phase signing process, you should do it in one step. All you need is a custom IExternalSignature implementation, something like

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        //
        // Request signature for hash value messageHash
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

and use it like this for signing:

PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);

Update of the IExternalSignature implementation

In the update of your question you added a PDF signed with the changes above applied. Analyzing the signature bytes in the signature container it became clear that your signing service is designed to be extremely dumb, it applies PKCS1 v1.5 padding and RSA encryption but it assumes its input to be already packed into a DigestInfo structure. In my experience this is an uncommon assumption, you should tell your signature provider to properly document that.

For your code this means that you have to pack the hash into a DigestInfo structure before sending it to the service.

A simple way to do this is explained in RFC 8017 section 9.2 note 1:

For the nine hash functions mentioned in Appendix B.1, the DER
encoding T of the DigestInfo value is equal to the following:

    ...
    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
    ...

I.e. you only have to prefix your hash with the byte sequence 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

Thus, a variant of the RemoteSignature class for services that require the caller to pack the digest into a DigestInfo structure could look like this:

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
        byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
        sha256Prefix.CopyTo(digestInfo, 0);
        messageHash.CopyTo(digestInfo, sha256Prefix.Length);
        //
        // Request signature for DigestInfo value digestInfo
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

Will Adobe Reader accept the fixed signature?

I doubt it. The key usage of the signer certificate only contains the value for signing other certificates.

If you look into the Adobe Digital Signatures Guide for IT, you’ll see that valid key usage extensions are

  • absent, i.e. no key usage extension at all, or
  • present with one or more of the following values:
    • nonRepudiation
    • signTransaction (11.0.09 only)
    • digitalSignature (11.0.10 and later)

Thus, the signCertificate value of your certificate might be a problem.

Leave a Comment