1
votes

I have an application that generates a PDF, and that needs to be signed.

We have not the certificates to sign the document, because they're in a HSM, and the only way we could make use of the certificates is using a webservice.

    PdfReader reader = new PdfReader(src);
    reader.setAppendable(true);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    FileOutputStream fout = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, fout, '\0');
    PdfSignatureAppearance appearance  = stamper.getSignatureAppearance();

    appearance.setReason("Test");
    appearance.setLocation("footer");
    appearance.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1, null);
    appearance.setCertificate(certChain[0]);

    PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    dic.setReason(appearance.getReason());
    dic.setLocation(appearance.getLocation());
    dic.setContact(appearance.getContact());
    dic.setDate(new PdfDate(appearance.getSignDate()));
    appearance.setCryptoDictionary(dic);

    HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
    exc.put(PdfName.CONTENTS, new Integer(8192 * 2 + 2));
    appearance.preClose(exc);

    ExternalDigest externalDigest = new ExternalDigest()
    {
        public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException
        {
            return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
        }
    };

    PdfPKCS7 sgn = new PdfPKCS7(null, certChain, "SHA256", null, externalDigest, false);
    InputStream data = appearance.getRangeStream();
    byte[] hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
    Calendar cal = Calendar.getInstance();

    byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
    sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);

    String hashPdf = new String(Base64.encodeBytes(sh));

    String hashSignat = hashPdf;

This is our code, first, we get the signature appearance, and calculate the hash

In this point, we have the hash code of the document. Then we send the hash to the webservice, and we get the signed hash code.

Finally, we put the signed hash to the PDF:

    sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA");
    byte[] encodedSign = sgn.getEncodedPKCS7(hash, cal, null, null, 
    null, CryptoStandard.CMS);
    byte[] paddedSig = new byte[8192];
    System.arraycopy(encodedSign, 0, paddedSig, 0, 
    encodedSign.length);

    PdfDictionary dic2 = new PdfDictionary();
    dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

    appearance.close(dic2);

In this point, we get a PDF signed, but with an invalid signature. Adobe says that "Document has been altered or corrupted since it was signed".

I have gone through Sign PDF using an external service and iText, PDF signature itext pkcs7 multi sign and Is it possible to sign a PDF document with hash and signed hash? but no luck.

1
iText has many limitations, I would suggest try using PDFlibDhanraj
@Dhanraj "iText has many limitations, I would suggest try using PDFlib" - How exactly does that help anyone to sign PDF hash using java and iText which this question is about? Not at all I'd say.mkl
Priyanka, please share a sample signed PDF for analysis.mkl
Hhmmm, the actually signed hash is 1134ED96F42AC7352E546BE0E906C0BF5D44229AEAFAC39145DB40A0BB7E817B which should be the hash of the authenticated attributes but their hash is E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BDFB67911C0CC5D5D949. The signed byte ranges hash is D7917CF3BA9FE5D5B626ED825965D699F7C54DBBF9F18DECE18EF8DD36DC4C28, so its not this hash, either. So I don't really know the hash of what that 11... is. You might want to share more code of what happens between String hashSignat = hashPdf and sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA")...mkl

1 Answers

0
votes

The example file

You shared an example file signed by your code in a comment to your question.

An analysis of that file (executed using the AnalyzeSignatures test testPriyankaSignatureSampleinformedconsent_Signed) shows that the actually signed hash is

1134ED96F42AC7352E546BE0E906C0BF5D44229AEAFAC39145DB40A0BB7E817B

which should be the hash of the authenticated attributes but their hash is

E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BDFB67911C0CC5D5D949.

The signed byte ranges hash is

D7917CF3BA9FE5D5B626ED825965D699F7C54DBBF9F18DECE18EF8DD36DC4C28,

so its not this hash, either. Thus it was not clear where that signed hash came from.

It eventually turned out that

the encoding utility on web service was different due to which the decoded hash value was wrong and MessageDigest(sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);) is not needed. this line was giving the wrong hash.

Signing previously signed files

Thereafter a new issue popped up:

when I am signing file which is already signed is getting sign but with an invalid signature. its showing "the signature byte range is invalid".

For signing already signed pdfs you have to use the append mode. For that you need a different PdfStamper.createSignature overload with one more parameter, a boolean, which you set to true.

The reason is that usually (i.e. without activating the append mode) iText reorganizes the internal PDF structures and drops unused objects. In already signed PDFs this usually moves the position of the (already existing) signature which invalidates the signature structure. Using append mode, though, iText keeps the original PDF bytes as they were and only appends new data.

Changing hashes

I m just confused that why the value of byte "sh" is getting change for the same file every time? actually, I want to store the hash value of the file. is it because of "cal"?

Indeed, each time you start manipulating a PDF, the result gets a new unique ID. Furthermore, the modification time is stored. And in case of a signing use case, also the signing time differs.

can I get the hash at a time and store it in db and sign the hash later and append in pdf?

You either have to

  • keep the stamper and appearance open for the time, or you have to
  • keep the prepared file, or you have to
  • patch iText to allow you to set fixed values for signing time, manipulation time, and id.

1st option is not possible for me. I didn't get the 2nd 3rd option. can you elaborate both or give any reference which I can refer?

Patching iText to enforce fixed hashes for each file

Ok, first off the third option, patching iText, usually is something you don't want to do because it makes incorporating later iText updates difficult.

OpenPdf (an older iText fork) contains a patch adding EnforcedModificationDate, OverrideFileId, and IncludeFileID properties to PdfStamper. (PdfSignatureAppearance already has a SignDate property.) This patch has been applied to allow eSignature DSS to use OpenPdf in its signing process (which also includes creating the signed PDF twice and so requires fixed hash values).

You might not want to switch to this old iText fork because it misses many fixes and new options of newer iText versions.

Keeping the already prepared file

Thus, you probably should instead keep the originally created file with an empty signature, e.g. in some temporary folder, and apply deferred signing as soon as you retrieve the final signature.

This essentially is what the iText example C4_09_DeferredSigning is all about, it first creates an intermediary PDF with everything in it, only the signatrue bytes are missing:

public void emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
    PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
    appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
    appearance.setCertificate(chain[0]);
    ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    MakeSignature.signExternalContainer(appearance, external, 8192);
}

Only in a second step an actual signature is injected:

public void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    ExternalSignatureContainer external = new MyExternalSignatureContainer(pk, chain);
    MakeSignature.signDeferred(reader, fieldname, os, external);
}

using

class MyExternalSignatureContainer implements ExternalSignatureContainer {

    protected PrivateKey pk;
    protected Certificate[] chain;
    
    public MyExternalSignatureContainer(PrivateKey pk, Certificate[] chain) {
        this.pk = pk;
        this.chain = chain;
    }
    
    public byte[] sign(InputStream is) throws GeneralSecurityException {
        try {
            PrivateKeySignature signature = new PrivateKeySignature(pk, "SHA256", "BC");
            String hashAlgorithm = signature.getHashAlgorithm();
            BouncyCastleDigest digest = new BouncyCastleDigest();
            PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
            byte hash[] = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
            Calendar cal = Calendar.getInstance();
            byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
            byte[] extSignature = signature.sign(sh);
            sgn.setExternalDigest(extSignature, null, signature.getEncryptionAlgorithm());
            return sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);
        }
        catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
    }

    public void modifySigningDictionary(PdfDictionary signDic) {
    }
    
}

Here the has is calculated in the second step, but you don't need to do so, you can

  • already calculate it in the first step by not using the original ExternalBlankSignatureContainer as in emptySignature() above but instead extending it to calculate the hash as MyExternalSignatureContainer does,
  • storing this value in your database, and
  • (as soon as you retrieve the signature) injecting that signature using a variant of MyExternalSignatureContainer that does not calculate the hash but instead injects exactly the returned signature.