2

I am trying to sign pdf document digitally and need to attach signature to signature panel using MSSP(mobile signature service provider). I researched some stackoverflow questions and i did things as below.

First i create checksum of pdf. Before generate the checksum add empty signature to pdf. After i generated the checksum i send that as data to sign document to the server. The server gives to me base64 signature and i found certificate chain from base64 signature. Now i need to attach signature to the pdf, show to the "Signature Panel" section of Adobe reader.

I extract certificate chain from base64 signature and i dont know how to attach this to pdf.

My code is :

this function does create empty signature to pdf.

public static void emptySignature(String src, String dest, String fieldname) 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);
    ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    MakeSignature.signExternalContainer(appearance, external, 8192);
}

this function does get SHA-256 hash value of pdf.

public static String getHashValue(String filename) throws NoSuchAlgorithmException, IOException {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    String hex = checksum("output.pdf", md);
    System.out.println("CHECKSUM: " + hex);
    return hex;
}

private static String checksum(String filepath, MessageDigest md) throws IOException {

    try (DigestInputStream dis = new DigestInputStream(new FileInputStream(filepath), md)) {
        while (dis.read() != -1) ;
        md = dis.getMessageDigest();
    }

    StringBuilder result = new StringBuilder();
    for (byte b : md.digest()) {
        result.append(String.format("%02x", b));
    }
    return result.toString();
}

then i send the hash of pdf to server and got base 64 signature value: "MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggAQEVEVTVAAAAACggDCCBhwwggQEoAMCAQIC ... NKodC346j0GKueTJ595rhi2NbT679XZwMaMMqEyT41pimV76Nm85eW/2yYjHt08gCNVSJGP7laR8taVAAAAAAAAA="

The certifcate chain looks like this: enter image description here

i tried some methods to attach signature to pdf's Signature panel but it requires private key. So please helping me giving for some advise, Thank you.

UPDATE 1:

After i attach signature to pdf using public certificate i got message in pdf "signature is invalid" enter image description here

This code is how i attach signature(I generated pem file from first certificate of chain):

final String SRC = "test.pdf";
final String DEST = "signed.pdf";
final String CERT = "cert.pem";

    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);
    System.out.println("chain[0]: -----> " + chain[0]);

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

    // we create the signature appearance
    PdfSignatureAppearance sap = stamper.getSignatureAppearance();
    sap.setReason("test");
    sap.setLocation("test");
    sap.setVisibleSignature(new Rectangle(36, 748, 36, 748), 1, "signature"); //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, ocsp, null, MakeSignature.CryptoStandard.CMS);
    byte[] signedAttributesHash = DigestAlgorithms.digest(new ByteArrayInputStream(sh), externalDigest.getMessageDigest("SHA256"));

    ByteArrayOutputStream os = baos;

    byte[] signedHash = java.util.Base64.getDecoder().decode(base64Signature);

    // we complete the PDF signing process
    sgn.setExternalDigest(signedHash, null, "RSA");
    Collection<byte[]> crlBytes = null;
    TSAClientBouncyCastle tsaClient = 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);

UPDATE 2:

public byte[] sign(byte[] message) throws GeneralSecurityException {

    MessageDigest messageDigest = MessageDigest.getInstance(getHashAlgorithm());
    byte[] messageHash  = messageDigest.digest(message);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < messageHash.length; ++i) {
        sb.append(Integer.toHexString((messageHash[i] & 0xFF) | 0x100).substring(1, 3));
    }

    byte[] signedByte = null;
    String msisdn = "97688888888";
    Client client = null;
    try {
        client = new Client( msisdn, sb.toString());
    } catch (JSONException e) {
        e.printStackTrace();
    }
    try {
        String strResult = client.sendRequest();
        JSONObject jsonResult = new JSONObject(strResult);
        System.out.println("Response:" + jsonResult);
        String base64Signature = jsonResult.getJSONObject("MSS_SignatureResp").getJSONObject("MSS_Signature").getString("Base64Signature");

        System.out.println(base64Signature);

        signedByte = Base64.getDecoder().decode(base64Signature);
    } catch (IOException | JSONException e) {
        e.printStackTrace();
    }

    return signedByte;
}

UPDATE 3:

enter image description here

Banzragch. B
  • 78
  • 10
  • There are numerous errors in your approach, in particular you calculate your hash over the wrong bytes (you must not hash the whole prepared file but merely that prepared file except the placeholder to be replaced by the signature to inject), and you inject your signature into yet another signature field. Before going into details, please clarify what kind of signatures your signature server returns? Full-fledged CMS signature containers? Or naked signature values? – mkl Nov 05 '20 at 15:18
  • @mkl thank you for your reply, the server returns base 64 encoded long string(in this case it was 7216 characters). i generated SignedData object from the b64 signature using bouncycastle SignedData. i am sorry i dont know much about "Full-fledged CMS signature containers? Or naked signature values?" – Banzragch. B Nov 06 '20 at 00:59
  • i've found that the server returns to me CMS – Banzragch. B Nov 06 '20 at 03:57
  • 1
    Great. That makes it easier to implement this. I'll try and write something up when I'm in office. – mkl Nov 06 '20 at 05:33
  • One question first: Do you really need the process of PDF preparation and of signature container injection strictly separated? In my eyes that only is necessary if requesting the actual signature from your server takes long; in that case one does not want to block resources (in particular memory) for that long. If the signature is returned quickly (after at most a few seconds), a single-step approach is more apropos. – mkl Nov 06 '20 at 09:49
  • The single-step approach by the way is essentially the port of the section *"How to improve your code"* of [this answer](https://stackoverflow.com/a/54575350/1729265) from C# to Java. – mkl Nov 06 '20 at 09:59
  • i tried to implement in java using this answer but i confused on the sign method of RemoteSignature. i updated my question about it. could you please tell me what is wrong? The sign method of RemoteSignature takes byte[] message and i pass this message parameter using getHashValue() function above i mention. Is this right? And MakeSignature.signDetached() function takes different parameters from C# implementation – Banzragch. B Nov 09 '20 at 03:23
  • The server gives response quickly(just few seconds). So i dont need to generate emptySignature in original pdf ? – Banzragch. B Nov 09 '20 at 06:48
  • Ah, I just see that the referenced answer is not completely the right template for you as it is based on a remote service that returns a naked signature, not a full cms signature container. I'll write an answer here. – mkl Nov 09 '20 at 08:40

1 Answers1

1

The code in the question is based on a multi-phase approach:

  • Sign the PDF injecting an empty byte array as signature container (by use of the ExternalBlankSignatureContainer).
  • Calculate the digest to sign (here incorrectly a digest for the whole file from the previous step is calculated, the digest must be calculated for the whole file except the signature container placeholder).
  • Request signature container for that document hash.
  • Inject signature container into the placeholder in the file from the first step.

If the signature server can take a long time to return a signature container, such an approach is appropriate but in the case at hand comments clarify

The server gives response quickly(just few seconds)

In such a use case one should proceed in a single step approach (as far as iText signing API calls are concerned):

PdfReader reader = new PdfReader(...);
PdfStamper stamper = PdfStamper.createSignature(reader, ..., '\0');
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, "Signature");
ExternalSignatureContainer external = new RemoteSignatureContainer();
MakeSignature.signExternalContainer(appearance, external, 8192);

with a custom ExternalSignatureContainer implementation RemoteSignatureContainer:

class RemoteSignatureContainer implements ExternalSignatureContainer {

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        [return a CMS signature container signing the data from the InputStream argument]
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
        signDic.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
    }
}

I don't know your API to access your signature server but based on your UPDATE 2 I assume the sign method in your RemoteSignatureContainer would look like this:

@Override
public byte[] sign(InputStream data) throws GeneralSecurityException {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA256");
    byte[] messageHash  = messageDigest.digest(StreamUtil.inputStreamToArray(data));
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < messageHash.length; ++i) {
        sb.append(Integer.toHexString((messageHash[i] & 0xFF) | 0x100).substring(1, 3));
    }

    String msisdn = "97688888888";
    try {
        Client client = new Client(msisdn, sb.toString());

        String strResult = client.sendRequest();
        JSONObject jsonResult = new JSONObject(strResult);
        String base64Signature = jsonResult
                   .getJSONObject("MSS_SignatureResp")
                   .getJSONObject("MSS_Signature")
                   .getString("Base64Signature");

        return Base64.getDecoder().decode(base64Signature);
    } catch (IOException | JSONException e) {
        throw new GeneralSecurityException(e);
    }
}
mkl
  • 85,104
  • 14
  • 121
  • 242
  • Thank you mkl, your advise is very helpful. After I follow your advise I achieved signer details and certificate details are shown now. But there is a problem. Why Signature validity is unknown now? I updated my quiestion UPDATE 3. – Banzragch. B Nov 09 '20 at 13:56
  • Well, as your screen shot says, neither your signer certificate nor its parent CA and root certificates (as far as available to the Adobe Reader in question) are trusted. As I don't have details, I cannot tell whether that is to be expected and all right (e.g. in case you currently work against a test signature server, not the production server) or not. – mkl Nov 09 '20 at 15:06
  • Maybe it depends on pdf reader's versions. I clicked "validate" button of Adobe Reader and its valid now. Grateful for your amazing assistance. I have achieved all my goal. – Banzragch. B Nov 10 '20 at 01:59
  • Most of pdf readers have list of approved trust list. May your CA is not in pdf reader ATL. You can manually added your CA crt and suppress warning or choice new provider which is listed in ATL. – Dreamcatcher Apr 18 '22 at 10:27