Sign PDF using an external service and iText
Asked Answered
I

2

5

I have this scenario.

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.

This webservice, offers two options, send the PDF document, and it returns a signed pdf, or send a hash that will be signed.

The first option, is not viable, because the PDF is signed without a timestamp (this is a very important requisite), so the second option is chosen.

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

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0', null, true);
appearance = stamper.getSignatureAppearance();
appearance.setCrypto(null, chain, null, PdfSignatureAppearance.SELF_SIGNED);
appearance.setVisibleSignature("Representant");
cal = Calendar.getInstance();
PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.TYPE, PdfName.SIG);
dic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
dic.put(PdfName.SUBFILTER, new PdfName("adbe.pkcs7.detached"));
dic.put(PdfName.M, new PdfDate(cal));
appearance.setCryptoDictionary(dic);
HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, Integer.valueOf(reservedSpace.intValue() * 2 + 2));
appearance.setCertificationLevel(1);
appearance.preClose(exc);

AbstractChecksum checksum = JacksumAPI.getChecksumInstance("sha1");
checksum.reset();
checksum.update(Utils.streamToByteArray(appearance.getRangeStream()));
hash = checksum.getByteArray();

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:

byte[] paddedSig = new byte[reservedSpace.intValue()];
System.arraycopy(signedHash, 0, paddedSig, 0, signedHash.length);

PdfDictionary dic = new PdfDictionary();
dic.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
appearance.close(dic);

byte[] pdf = baos.toByteArray();

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 think that we make something wrong in the process, and we don't know exactly what could be.

We appreciate help on this, or an alternative way to do that.

Thanks.


EDITED

As suggested by mkl, I have followed the 4.3.3 section of this book Digital Signatures for PDF documents, and my code now what that follows:

The first part, when we calculate the hash:

PdfReader reader = new PdfReader(Base64.decode(pdfB64));
reader.setAppendable(true);
baos = new ByteArrayOutputStream();

PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');
appearance = stamper.getSignatureAppearance();

appearance.setReason("Test");
appearance.setLocation("A casa de la caputeta");
appearance.setVisibleSignature("TMAQ-TSR[0].Pagina1[0].DadesSignatura[0].Representant[0]");
appearance.setCertificate(chain[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(reservedSpace.intValue() * 2 + 2));
appearance.preClose(exc);

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

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

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

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

And in the second part, we get the signed hash, and we put that into the PDF:

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

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

appearance.close(dic2);

byte[] pdf = baos.toByteArray();

Now, Adobe raises a Internal Cryptographic library error. Error Code: 0x2726, when we try to validate the signature.

Impanel answered 25/2, 2015 at 11:21 Comment(5)
The first option, is not viable, because the PDF is signed without a timestamp (this is a very important requisite), - Have you considered applying a PAdES part 4 style document timestamp? If that is no option, have you checked whether the integrated PDF signature returned by your service has still some reserved signature space left? In that case you can add a signature time stamp after the fact.Ictinus
Well, it could be an option for this case, but in another case, we need to sign that way (the hash is generated in the server, and needs to be signed by the user in the client).Impanel
@Impanel I use your way to sign PDFs and works correctly for me, the difference is that I generate the CMS signature myself and I've the private key (so I don't pass null as first parameter on appearance.setCrypto call), I think that some info from privateKey is included in the PDF an also necessary to perform the validation. So since you're generating the hash without this info probably the error is there. I don't know if it's even possible generate a valid signature using the method you show without the privateKey.Keese
We don't have access to the private keys, and we can not use them on the preSign step.Impanel
Thanks for the "EDIT", I'm able to successfully insert the signature from the USB token. I'm using hwcrypto.js to get the pdf hash signed.Downright
I
1

After much debugging, we finally found the problem.

For some mysterious reason, the method that generates the hash of the document, was executed twice, invalidating the first hash (which we use to send to the service).

After a refactoring work of the code, the original code worked correctly.

Very thanks to all people that help me, especially mkl.

Impanel answered 9/3, 2015 at 11:28 Comment(0)
I
8

If the web service returned a mere signed 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:

If the webservice merely returns a signed hash, then your PDF signature is incorrect: You set the signature SubFilter to adbe.pkcs7.detached. This implies that the signature Contents have to contain a full-blown PKCS#7 signature container, not merely a signed hash.

You might want to download Digital Signatures for PDF documents, A White Paper by Bruno Lowagie (iText Software) on creating and verifying digital PDF signatures using iText. It especially contains a section "4.3 Client/server architectures for signing" which should encompass your use cases.

But the web service returns a full-fledged CMS signature container

Following to the explanation above, the OP started using code from section 4.3.3 of the above-mentioned white paper which is intended for signing using externally generated signed hashes. As this also resulted in a signed document Adobe Reader was not happy with, he provided a sample document created with this new code.

Analysis of the sample showed that the CMS signature container embedded in the document contained another CMS signature container where there should have been the signature bytes (the signed hash) for the signed attributes:

2417   13:           SEQUENCE {
2419    9:             OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
2430    0:             NULL
         :             }
2432 5387:           OCTET STRING, encapsulates {
2436 NDEF:             SEQUENCE {
2438    9:               OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
2449 NDEF:               [0] {
2451 NDEF:                 SEQUENCE {

(The OCTET STRING following the signature algorithm should contain the signature bytes and not embed another SignedData structure.)

This indicates that the web service indeed already returns a full-fledged CMS container.

For such a scenario the original code looked quite ok. The issue might be due to a detail like the use of a wrong hashing algorithm (the original code hashed using SHA1).

A possible issue: BER encoding

The CMS signature container from the web service embedded in the CMS container generated by iText from the first sample provided by the OP hints at a possible issue: Looking at the ASN.1 dump above the sizes of the outer structures in the embedded CMS container are often NDEF.

This indicates that these outer structures are created using the less strict BER (Basic encoding Rules), not the more strict DER (Distinguished Encoding Rules) because the BER option to start a structure without stating its size is forbidden in DER.

The CMS specification (RFC 3852) referenced from the PDF specification does allow any BER encoding for the outer structures of the container, the PDF specification on the other hand requires:

the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature. The PKCS#7 object shall conform to RFC3852 Cryptographic Message Syntax.

Strictly speaking, therefore, signature containers embedded in PDFs are required to be DER encoded all over.

As far as I know no PDF signature validator rejects such signatures as long as the signature container DER-encodes certain pivotal elements. Concerning future tools such signatures are a possible point of failure, though.

Ictinus answered 25/2, 2015 at 12:36 Comment(10)
I followed the 4.3.3 section of the guide, I generated the PDF, but when I open it, Adobe rises a Internal Cryptographic library error. Error Code: 0x2726. Any idea of what could be wrong ?Impanel
Can you provide a sample signed document Got analysis?Ictinus
Here you can find one: drive.google.com/…Impanel
Hhmmm, your document is interesting: As long as the document is not changed, the Adobe Reader behaves as if it couldn't even parse the signature container. As soon as one changes the document, the Reader immediately recognizes this and additionally shows certificate information. Thus, I assume, Adobe Reader is unhappy about some detail checked or used only in the final steps of a successful signature validation.Ictinus
I updated the question with the new code whitch I use to sign the document. Maybe you will see something I do wrong.Impanel
Ah... Inspecting the ASN.1 structure of your signature I see that your Web Service does not merely return a signed hash but instead a SignedData object. The code you used, though, assumes that only a signed hash is returned.Ictinus
Ok, if I get a SignedData object, how do I use to put the signature to the document? Is there some example ?Impanel
@Impanel can you also provide a sample signed with your original source? As the web service actually returns a signature container, it is fairly correct, most likely some minor deatil is wrong. Probably the hashing algorithm I see you had used SHA1 then but your other sample file looks like SHA-256 is more appropriate.Ictinus
Here you can find the PDF obtined in the first way: drive.google.com/…Impanel
It looks like the hash value you calculate simply is wrong. You use the JacksumAPI to calculate the hash. There actually is no reason to not use the normal Java MessageDigest which works just fine. Furthermore you use a helper method Utils.streamToByteArray. If it works incorrect, this might be the culprit. Furthermore it is a bad idea to read that whole ranged stream into a byte array. If you eventually have to deal with big documents, this might cause a resource issue.Ictinus
I
1

After much debugging, we finally found the problem.

For some mysterious reason, the method that generates the hash of the document, was executed twice, invalidating the first hash (which we use to send to the service).

After a refactoring work of the code, the original code worked correctly.

Very thanks to all people that help me, especially mkl.

Impanel answered 9/3, 2015 at 11:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.