Search code examples
pdfitextdigital-signaturedeferredcorrupt

iText7 deferred signed pdf document shows “the document has been altered or corrupted since the signature was applied”


I checked with other similar issues on Stackoverflow, but it doesn’t work on my case.

Situation: I am developing an application which needs to sign the pdf document. The signing key is held by another company, let’s say it’s CompanyA.

I did the following steps:

  1. Got the pdf document to sign ready.
  2. Created a Temp pdf file which added an Empty Signature in the original pdf.
  3. Read the Temp pdf to get the message digest. (Encode it to base64)
  4. Send the message digest (Base64 encoded) to the CompanyA to get signed.
  5. Get the signed digest (base64 encoded) from CompanyA.
  6. Do the base64 decoding. And embedded the result into the Temp pdf to get the final signed pdf.

Everything goes well and I can get the final signed pdf. But When I open it in Adobe reader, it says “the document has been altered or corrupted since the signature was applied”.

I used this getHashBase64Str2Sign to get the message digest (in base64). This method calls the emptySignature() method to create the Temp file with empty signature, and then calls the getSignatureHash() method to read the Temp file to get the message digest.

public static String getHashBase64Str2Sign() {
    try {
        // Add BC provider
        BouncyCastleProvider providerBC = new BouncyCastleProvider();
        Security.addProvider(providerBC);

        // Create parent path of dest pdf file, if not exist
        File file = new File(DEST).getParentFile();
        if (!file.exists()) {
            file.mkdirs();
        }


        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        Certificate[] chain = new Certificate[1];
        try (InputStream certIs = new FileInputStream(CERT)) {
            chain[0] = factory.generateCertificate(certIs);
        }

        // Get byte[] hash
        DeferredSigning app = new DeferredSigning();

        app.emptySignature(SRC, TEMP, "sig", chain);

        byte[] sh = app.getSignatureHash(TEMP, "SHA256", chain);

        // Encode byte[] hash to base64 String and return
        return Base64.getEncoder().encodeToString(sh);
    } catch (IOException | GeneralSecurityException e) {
        e.printStackTrace();
        return null;
    }
}

    

private void emptySignature(String src, String dest, String fieldname, Certificate[] chain)
        throws IOException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), new StampingProperties());
    PdfSignatureAppearance appearance = signer.getSignatureAppearance();
    appearance.setPageRect(new Rectangle(100, 500, 200, 100));
    appearance.setPageNumber(1);
    appearance.setCertificate(chain[0]);
    appearance.setReason("For test");
    appearance.setLocation("HKSAR");
    signer.setFieldName(fieldname);

    /*
     * ExternalBlankSignatureContainer constructor will create the PdfDictionary for
     * the signature information and will insert the /Filter and /SubFilter values
     * into this dictionary. It will leave just a blank placeholder for the
     * signature that is to be inserted later.
     */
    IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite,
            PdfName.Adbe_pkcs7_detached);
    

    // Sign the document using an external container.
    // 8192 is the size of the empty signature placeholder.
    signer.signExternalContainer(external, 100000);
}

private byte[] getSignatureHash(String src, String hashAlgorithm, Certificate[] chain)
        throws IOException, GeneralSecurityException {
    InputStream is = new FileInputStream(src);
    // Get the hash
    BouncyCastleDigest digest = new BouncyCastleDigest();
    PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

    byte hash[] = DigestAlgorithms.digest(is, digest.getMessageDigest(sgn.getHashAlgorithm()));
    return sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);
}

private void createSignature(String src, String dest, String fieldName, byte[] sig)
        throws IOException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    try (FileOutputStream os = new FileOutputStream(dest)) {
        PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());

        IExternalSignatureContainer external = new MyExternalSignatureContainer(sig);

        // Signs a PDF where space was already reserved. The field must cover the whole
        // document.
        PdfSigner.signDeferred(signer.getDocument(), fieldName, os, external);
    }
}

Then, the message digest is sent to CompanyA for signing. After I got the signed digest from CompanyA (which is base64 encoded), I call the embedSignedHashToPdf() method to get the signed pdf document.

public static void embedSignedHashToPdf(String signedHash) {
    try {
        byte[] sig = Base64.getDecoder().decode(signedHash);
        // Get byte[] hash
        DeferredSigning app = new DeferredSigning();
        app.createSignature(TEMP, DEST, "sig", sig);
    } catch (IOException | GeneralSecurityException e) {
        e.printStackTrace();
    }
}

class MyExternalSignatureContainer implements IExternalSignatureContainer {

    protected byte[] sig;

    public MyExternalSignatureContainer(byte[] sig) {
        this.sig = sig;
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {

    }

    @Override
    public byte[] sign(InputStream arg0) throws GeneralSecurityException {
        return sig;
    }

}

At last, I can get the signed pdf document, but it shows error in Adobe Reader, like this:

Adobe shows error on signature

Please check the original pdf, temp pdf, and the final signed pdf file as follows:

Original pdf - helloworld.pdf

Temp pdf - helloworld_empty_signed.pdf

Final pdf - helloworld_signed_ok.pdf


Solution

  • Ok, I see a number of issues in your code:

    You determine the hash of the wrong bytes

    In getSignatureHash with src containing the path of the intermediary PDF prepared for signing you do

    InputStream is = new FileInputStream(src);
    ...
    byte hash[] = DigestAlgorithms.digest(is, ...);
    

    I.e. you calculate the hash value of the whole intermediary PDF.

    This is incorrect!

    The hash must be calculated for the PDF except the placeholder for the signature container which shall later be embedded:

    The easiest way to hash that range, is to already calculate the hash in emptySignature and return it from there by using a hash-calculating IExternalSignatureContainer implementation instead of the dumb ExternalBlankSignatureContainer.

    For example use this implementation:

    public class PreSignatureContainer implements IExternalSignatureContainer {
        private PdfDictionary sigDic;
        private byte hash[];
    
        public PreSignatureContainer(PdfName filter, PdfName subFilter) {
            sigDic = new PdfDictionary();
            sigDic.put(PdfName.Filter, filter);
            sigDic.put(PdfName.SubFilter, subFilter);
        }
    
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            String hashAlgorithm = "SHA256";
            BouncyCastleDigest digest = new BouncyCastleDigest();
    
            try {
                this.hash = DigestAlgorithms.digest(data, digest.getMessageDigest(hashAlgorithm));
            } catch (IOException e) {
                throw new GeneralSecurityException("PreSignatureContainer signing exception", e);
            }
    
            return new byte[0];
        }
    
        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.putAll(sigDic);
        }
    
        public byte[] getHash() {
            return hash;
        }
    }
    

    like this:

    PreSignatureContainer external = new PreSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
    signer.signExternalContainer(external, 16000);
    byte[] documentHash = external.getHash();
    

    You process the hash as if your CompanyA only returned naked signature bytes but you embed the bytes returned by CompanyA as if they were a full CMS signature container

    In getSignatureHash you eventually don't return the alleged document hash but instead start constructing a CMS signature container and return its signed attributes:

    PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
    ...
    return sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);
    

    Calculating PdfPKCS7.getAuthenticatedAttributeBytes(...) would only make sense if you then retrieved naked signature bytes for those attribute bytes and created a CMS signature container using the same PdfPKCS7 object:

    byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, sigtype, ocspList, crlBytes);
    byte[] extSignature = RETRIEVE_NAKED_SIGNATURE_BYTES_FOR(sh);
    sgn.setExternalDigest(extSignature, null, ENCRYPTION_ALGORITHM_USED_FOR_SIGNING);
    
    byte[] encodedSig = sgn.getEncodedPKCS7(hash, sigtype, tsaClient, ocspList, crlBytes);
    

    In particular your approach of calculating the signed attributes and then forgetting the PdfPKCS7 object does never make any sense at all.

    But it actually looks like your CompanyA can return full CMS signature containers, not merely naked signature bytes as you immediately embed the returned bytes in embedSignedHashToPdf and createSignature and your example PDF does contain a full CMS signature container.

    In such a case you don't need to use PdfPKCS7 at all but directly send the pre-calculated document digest to CompanyA to sign.

    Thus, most likely you don't need PdfPKCS7 at all but instead send the document hash determined as explained above to CompanyA and embed their returned signature container.