2

Following the eBook section 4.3.3 "Digital Signature for PDF document" I'm trying to create a working example where :

  • the client has a PDF to sign and only a public Cert
  • the External HW (with private cert) takes an HASH and returns a SIGNED HASH

I tried to do it, but the signature inside the PDF shows me that the file was modified after the signing process.

The following code takes the original PDF and a public certificate and create a temporary pdf with empty sign and return an HASH

This hash is sent externally to another remote application (where there is a corrispondent Private cert) and returns the signed hash, I read the signed hash and added it into the temporary pdf.

Full Working Code updated:

package com.Marloo;


import org.apache.commons.codec.Charsets;
import org.bouncycastle.util.encoders.Base64;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;

public class Test {

    public static final String CERT = "src/main/resources/certificate.pem";
    public static final String SRC = "src/main/resources/tmp.pdf";
    public static final String DEST = "src/main/resources/signed.pdf";

    public static void main(String args[]) throws IOException {
        getHash(SRC, CERT);
    }



    public static void getHash(String doc, String cert) throws IOException {

        try {

            File initialFile = new File(cert);
            InputStream is = new FileInputStream(initialFile);

            // We get the self-signed certificate from the client
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            Certificate[] chain = new Certificate[1];
            chain[0] = factory.generateCertificate(is);

            // we create a reader and a stamper
            PdfReader reader = new PdfReader(doc);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');

            // we create the signature appearance
            PdfSignatureAppearance sap = stamper.getSignatureAppearance();
            sap.setReason("TEST REASON");
            sap.setLocation("TEST LOCATION");
            //sap.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, "sig"); //visible
            sap.setVisibleSignature(new Rectangle(36, 748, 36, 748), 1, "sig"); //invisible
            sap.setCertificate(chain[0]);

            // we create the signature infrastructure
            PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
            dic.setReason(sap.getReason());
            dic.setLocation(sap.getLocation());
            dic.setContact(sap.getContact());
            dic.setDate(new PdfDate(sap.getSignDate()));
            sap.setCryptoDictionary(dic);
            HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
            exc.put(PdfName.CONTENTS, new Integer(8192 * 2 + 2));
            sap.preClose(exc);
            ExternalDigest externalDigest = new ExternalDigest() {
                public MessageDigest getMessageDigest(String hashAlgorithm)
                        throws GeneralSecurityException {
                    return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
                }
            };
            PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, externalDigest, false);
            InputStream data = sap.getRangeStream();
            byte hash[] = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));


            // we get OCSP and CRL for the cert
            OCSPVerifier ocspVerifier = new OCSPVerifier(null, null);
            OcspClient ocspClient = new OcspClientBouncyCastle(ocspVerifier);
            byte[] ocsp = null;
            if (chain.length >= 2 && ocspClient != null) {
                ocsp = ocspClient.getEncoded((X509Certificate) chain[0], (X509Certificate) chain[1], null);
            }

        byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null, MakeSignature.CryptoStandard.CMS);
        InputStream sh_is = new ByteArrayInputStream(sh);
        byte[] signedAttributesHash = DigestAlgorithms.digest(sh_is, externalDigest.getMessageDigest("SHA256"));


        System.out.println("----------------------------------------------");
        System.out.println("Hash to be sign:");
        System.out.println( new String(Base64.encode(signedAttributesHash), Charsets.UTF_8));
            System.out.println("----------------------------------------------");
            System.out.println("Insert b64 signed hash [ENTER]");
            System.out.println("----------------------------------------------");

            Scanner in = new Scanner(System.in);
            String signedHashB64 = in.nextLine();
            System.out.println( signedHashB64);

            ByteArrayOutputStream os = baos;

            byte[] signedHash = org.apache.commons.codec.binary.Base64.decodeBase64(signedHashB64.getBytes());

            // we complete the PDF signing process
            sgn.setExternalDigest(signedHash, null, "RSA");
            Collection<byte[]> crlBytes = null;
            TSAClientBouncyCastle tsaClient = new TSAClientBouncyCastle("http://timestamp.gdca.com.cn/tsa", null, null);

            byte[] encodedSig = sgn.getEncodedPKCS7(hash, tsaClient, ocsp, crlBytes, MakeSignature.CryptoStandard.CMS);
            byte[] paddedSig = new byte[8192];
            System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);
            PdfDictionary dic2 = new PdfDictionary();
            dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

            try {
                sap.close(dic2);
            } catch (DocumentException e) {
                throw new IOException(e);
            }

            FileOutputStream fos = new FileOutputStream(new File(DEST));
            os.writeTo(fos);

            System.out.println("pdfsig " + System.getProperty("user.dir") + "/" + DEST);
            System.out.println("------------------End Of Life --------------------------");

            System.exit(0);


        } catch (GeneralSecurityException e) {
            throw new IOException(e);
        } catch (DocumentException e) {
            throw new IOException(e);
        }

    }


}

Here some screenshot

Some hint: in this incomplete post the author says:

"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."

but no futher information was provided, also the Time write on the stamper and the time from TSA is different on purpose. I think this wouldn't be the problem.

Some hints ?

Thanks

Update 1

(the previus code was updated)

the external services doesn't accept in input the whole Sign structure but only the 32byte hash

screenshot2

now the sh var is never used !

i take the hash byte[], send to it but again Adobe Reader say that the file was modified.

Maybe can i try with the "invisible signature" method. Or the "visible stamper" didn't make difference in the signature validation process ?

Or Maybe i need to recreate a ANS.1 structure somehow with the signed byte and then sign the doc ?

Maybe the time must be the same between the tsa and the sign ?

Screenshot3

Any kind of help would be appreciates.

Thanks

Update 2 - WORKING SOLUTION !!!

Really really really thanks to mkl for the answer !

the working fix was that we need to generate the hash of the signed/authenticated attributes inside the PKCS#7 package !!! See the original code at the signedAttributesHash variable signed pdf image

1 Answers1

2

Your current code

Your current code signs the entirely wrong hash.

You sign hash which is calculated as

InputStream data = sap.getRangeStream();
byte hash[] = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));

I.e. you directly sign the hash of the signed ranges of the document. This is wrong because you are building a PKCS#7 signature container with signed attributes, i.e. the hash of the signed ranges of the document must be the value of one of those signed attributes and you have to sign the hash of the signed attributes!

Your former code

Your former code signed the wrong bytes, too, but was nearer to the correct bytes.

You used to sign last32 which was calculated as

byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, ocsp, null, MakeSignature.CryptoStandard.CMS);
byte[] last32 = Arrays.copyOfRange(sh, sh.length - 32, sh.length);

I.e. you correctly generated the signed attribute bytes (aka authenticated attribute bytes) with hash included as attribute value, but then you simply took the last 32 bytes thereof. This is wrong, you must sign the hash of the signed attribute bytes, not their final 32 bytes.

What should work

You should sign signedAttributesHash, the hash of the signed attribute bytes, i.e.

byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, ocsp, null, MakeSignature.CryptoStandard.CMS);
byte[] signedAttributesHash = DigestAlgorithms.digest(new ByteArrayInputStream(sh), externalDigest.getMessageDigest("SHA256"));
Community
  • 1
  • 1
mkl
  • 85,104
  • 14
  • 121
  • 242
  • Thanks for the reply: 1) i notice that HASH and "last32" was the same so i roll back to use hash 2) if i try to use your code : DigestAlgorithms.digest(sh, externalDigest.getMessageDigest("SHA256")); on the "sh" variable i get an error, is required an inputstream not an array ! but your entire explanation is like a gold ! Thanks ! I try to find how create the signedAttributesHash from the sh var ! – Fabrizio Barone May 08 '19 at 09:26
  • *"on the "sh" variable i get an error, is required an inputstream not an array"* - try replacing `sh` by a `new ByteArrayInputStream(sh)`. If that works, great. If not, please share a PDF signed like that for further analysis. – mkl May 08 '19 at 09:34
  • 1
    Great! As an aside, though, I assume you could have made things easier for yourself. You more or less restricted yourself to using methods and classes from the low-level signing APIs. By using the newer, higher level APIs you would not have had to care about such details as which bytes to sign. Resorting to the low-level API is only necessary in very specific use cases. – mkl May 08 '19 at 09:54
  • I spent the last month studying the hash signature, from zero to this working code! The problem was mine ignorance about signing hash in general and the lack of documentation from the external service, now if there is an easy alternative i can take it :) You suggest to switch to itext7 ? Aside also i need to split this process in 2 parts because the authorization process involves a phone call and some pin code. I can use almost anything like java, csharp, php with free and paid component. – Fabrizio Barone May 08 '19 at 10:04
  • At this point what u suggest ? Process 1: INPUT: public cert + filetobesigned.pdf Output 32byte hash Process 2: INPUT: signedHash + filetobesigned.pdf (with empty signature field?) Output signedfile.pdf – Fabrizio Barone May 08 '19 at 10:06
  • If those processes indeed are so divided and even include the necessity to make a phone call, the solution as is maybe is not that wrong after all... I had thought that console communication part of the program was for debugging purposes only... ;) – mkl May 08 '19 at 14:03
  • Yes in this case the whole code is like a PoC state ! Now maybe i split this in 2 "service" the first return me an hash and a temporary pdf "half signed" and the second take the temp file and the signed hash and return the pdf. I hope that can i close and reopen the file and don't need keep a java service alive with variable in memory. (the original example was web based and store into Session) `sgn,hash,ocsp,sap and baos` – Fabrizio Barone May 08 '19 at 15:01
  • Well, some changes will become necessary. – mkl May 08 '19 at 15:09
  • the client express require to use the free versioni itext 2 via : https://ec.europa.eu/cefdigital/code/projects/ESIG/repos/dss/browse oh gosh ! i open a question for the downgrade here : https://stackoverflow.com/questions/56171378/get-hash-from-pdf-with-itext-2-1-7-or-digital-signature-service this start to be a never ending story ! – Fabrizio Barone May 16 '19 at 15:11
  • Hi @mkl, I want to sign a PDF document with a signed Hash that i receive from user. I tried to find out any such example but lucky find your post. Can you please guide/refer me to implement such requirement. – Zeeshan Bilal Aug 08 '20 at 18:00
  • Which hash did your user sign? – mkl Aug 09 '20 at 06:11