Search code examples
javaitextpdf-generationdigital-signatureitext7

Java IText7 PDF Sign Problem - Document has been altered or corrupted since it was signed


I try to sign a pdf file but encounter "Document has been altered or corrupted since it was signed" error when I open the signed pdf file in Adobe.

screenshot

The error is not so descriptive and I'm not sure where to look at because the code seems good to me but apparently it's not..

The code that I use is:

public class SafeService_INPUT_Pdf {

    private static final String input = "";
    private static final String tmp = "";
    private static final String output =  "";
    private static final String token = "";

    public static void main(String[] args) throws Exception {

        BouncyCastleProvider providerBC = new BouncyCastleProvider();
        Security.addProvider(providerBC);

        CredentialsInfoResponseDto credentialsInfoResponseDto = SafeAmaHelper.getCredentialsInfo(token);

        String cert0 = "-----BEGIN CERTIFICATE-----\n"+ credentialsInfoResponseDto.getCert().getCertificates().get(0) +"\n-----END CERTIFICATE-----";
        String cert1 = "-----BEGIN CERTIFICATE-----\n"+ credentialsInfoResponseDto.getCert().getCertificates().get(1) +"\n-----END CERTIFICATE-----";
        String cert2 = "-----BEGIN CERTIFICATE-----\n"+ credentialsInfoResponseDto.getCert().getCertificates().get(2) +"\n-----END CERTIFICATE-----";

        Certificate[] chain = new Certificate[3];

        try {
            chain[0] = SafePdfHelper.convertStringCert(cert0);
            chain[1] = SafePdfHelper.convertStringCert(cert1);
            chain[2] = SafePdfHelper.convertStringCert(cert2);
        }catch (Exception e){
            System.out.println(e.getCause().getMessage());
        }

        byte[] hash4Sign = SafePdfHelper.emptySignature(input, tmp, "sig", chain);

        //concatenate sha_prefix with hash4Sign and convert to BASE64
        String **hashToSign** = SafePdfHelper.getHashtoSign(hash4Sign);
        
        //CALL AMA and gets 
        String amaSignature = SafeAmaHelper.getAssinat(token,**hashToSign**).getSignatures().get(0);

        byte[] signedFinallyHash = Base64.getDecoder().decode(String.valueOf(amaSignature.toCharArray()));

        //insert HASH (AMA) to PDF temp and creates a signed PDF
        SafePdfHelper.createSignature(signedFinallyHash, tmp, output, "sig", chain);
    }
public class SafePdfHelper {

    public static String getHashtoSign(byte[] hash4Sign) throws NoSuchAlgorithmException {

        byte[] sha256SigPrefix = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 };

        byte[] hash4SignWithPrefix = new byte[sha256SigPrefix.length + hash4Sign.length];
        System.arraycopy(sha256SigPrefix, 0, hash4SignWithPrefix, 0, sha256SigPrefix.length);
        System.arraycopy(hash4Sign, 0, hash4SignWithPrefix, sha256SigPrefix.length, hash4Sign.length);

        return Base64.getEncoder().encodeToString(hash4SignWithPrefix);
    }

    public static Certificate convertStringCert(String certificate) throws Exception{
        InputStream targetStream = new ByteArrayInputStream(certificate.getBytes());

        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        Certificate cert = cf.generateCertificate(targetStream);

        return cert;
    }

    public static byte[] emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, GeneralSecurityException, IOException {

        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        PdfSigner signer = new PdfSigner(reader, os, new StampingProperties().useAppendMode());

        signer.setFieldName(fieldname);

        SafePdfHelper.MyExternalBlankSignatureContainer external = new SafePdfHelper.MyExternalBlankSignatureContainer(chain, PdfName.Adobe_PPKMS, PdfName.Adbe_pkcs7_detached);

        signer.signExternalContainer(external, 12000);
        byte[] hash4Sign = external.getHash4Sign();

        os.close();
        reader.close();

        return hash4Sign;
    }

    static class MyExternalBlankSignatureContainer implements IExternalSignatureContainer {

        /* Signature dictionary. Filter and SubFilter.  */
        private final PdfDictionary sigDic;
        private byte[] hash4Sign = null;
        private Certificate[] chain = null;

        public MyExternalBlankSignatureContainer(Certificate[] _chain, PdfName filter, PdfName subFilter) {
            sigDic = new PdfDictionary();
            sigDic.put(PdfName.Filter, filter);
            sigDic.put(PdfName.SubFilter, subFilter);
            chain = _chain;
        }

        public byte[] getHash4Sign() {
            return hash4Sign;
        }

        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {

            try {
                String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
                BouncyCastleDigest digest = new BouncyCastleDigest();
                MessageDigest md = digest.getMessageDigest(hashAlgorithm);

                byte[] hash = DigestAlgorithms.digest(data, md);
                PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

                OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
                Collection<byte[]> ocsp = new ArrayList<>();
                for (var i = 0; i < chain.length - 1; i++) {
                    byte[] encoded = ocspClient.getEncoded((X509Certificate) chain[i], (X509Certificate) chain[i + 1], null);
                    if (encoded != null) ocsp.add(encoded);
                }

                byte[] attributeBytes = sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, ocsp, null);

                //create sha256 message digest
                hash4Sign = MessageDigest.getInstance(hashAlgorithm).digest(attributeBytes);

                return new byte[0];
            } catch (IOException | GeneralSecurityException de) {
                de.printStackTrace();
                throw new GeneralSecurityException(de);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.putAll(sigDic);
        }
    }


    public static void createSignature(byte[] hashSigned, String src, String dest, String fieldName, Certificate[] chain) 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 SafePdfHelper.MyExternalSignatureContainer(hashSigned, chain, PdfName.Adobe_PPKMS, PdfName.Adbe_pkcs7_detached);

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

    static class MyExternalSignatureContainer implements IExternalSignatureContainer {

        /* Signature dictionary. Filter and SubFilter.  */
        private PdfDictionary sigDic;
        private byte[] signedHash = null;
        private Certificate[] chain = null;

        public MyExternalSignatureContainer(byte[] _signedHash, Certificate[] _chain, PdfName filter, PdfName subFilter) {
            sigDic = new PdfDictionary();
            sigDic.put(PdfName.Filter, filter);
            sigDic.put(PdfName.SubFilter, subFilter);
            signedHash = _signedHash;
            chain = _chain;
        }

        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            try {
                String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
                BouncyCastleDigest digest = new BouncyCastleDigest();
                MessageDigest md = digest.getMessageDigest(hashAlgorithm);

                byte[] hash = DigestAlgorithms.digest(data, md);
                PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

                OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
                Collection<byte[]> ocsp = new ArrayList<>();
                for (var i = 0; i < chain.length - 1; i++) {
                    byte[] encoded = ocspClient.getEncoded((X509Certificate) chain[i], (X509Certificate) chain[i + 1], null);
                    if (encoded != null) ocsp.add(encoded);
                }

                sgn.setExternalDigest(signedHash, null, "RSA");

                ITSAClient tsaClient = null;//new GSTSAClient(access);
                return sgn.getEncodedPKCS7(hash, PdfSigner.CryptoStandard.CMS, tsaClient, ocsp, null);
            } catch (IOException | GeneralSecurityException de) {
                de.printStackTrace();
                throw new GeneralSecurityException(de);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.putAll(sigDic);
        }
    }

Base64 format of the hash that is signed is (tmp file + sha_prefix):

MDEwDQYJYIZIAWUDBAIBBQAEIKCZG8Xc6M2de3fuj8CMHLhW8XvMArW6Smy75TgABlGQ

Base64 format of the signature (AMA) is:

X5vg7qXJNsiB8hYtauih/wMFNf9uLAnT8h4M7DvHyw0bLdM03BJc7Ar1yGIoA0MTXaEdq85DP6JJFeMJZBRRc/NTA1C4IpjBN5N5Fpaa7HFnNxORQBc00d/bXuSzV1DNwCdIfcDYSUjh5Z3OWFdWzqmDhmAWRK/Hudf90m34B1mpfTtvtRAzrgn79fIBUd9D09iXpnClqTVYIzWcJ+Dz6yU75a0gvR79wNLCpUYNw2kxdmp/odAMm5cn10x9hLB+UhaNSUsnYyQUZtFsSkIE+oPXFqZc9ky4j5ha9Xfz8GGcLPEkAupyxOb5f9/NGicOeegX793swY09O4NxDW9RVtMtmdKt8kZAxB70PG1r18Ui2gheY4yuMg2aqpkcw5vgBO1GYe2DwDp99Qs4xHJjhbiUZOKT0moU+tDb3EySHZkkkci/GQTUg8IYHU8umv9TyuD7A3NRBQTyQud6j9H6bG3zQE+V4T8N2fnUPmoFEfDWyIvxvV+7YL+BymeZX4A0

Can anyone help?


Solution

  • This is a synopsis of our discussion in the comments.

    The problem is that you retrieve OCSP responses both in MyExternalBlankSignatureContainer.sign and in MyExternalSignatureContainer.sign, in both methods you execute

    OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
    Collection<byte[]> ocsp = new ArrayList<>();
    for (var i = 0; i < chain.length - 1; i++) {
        byte[] encoded = ocspClient.getEncoded((X509Certificate) chain[i], (X509Certificate) chain[i + 1], null);
        if (encoded != null) ocsp.add(encoded);
    }
    

    and use that list ocsp with PdfPKCS7.

    As you use MyExternalBlankSignatureContainer to calculate the digest of the attributes that are to be signed and MyExternalSignatureContainer to build a signature container based on a signature value for that very digest, the attributes must be identical in both cases. As the OCSP responses are included in one of those attributes, this means that you have to use the identical OCSP responses in both cases.

    But OCSP responses usually are created anew for each request (or at most cached for a very short time), you usually get different OCSP responses for different requests for the same certificate.

    Thus, you must not retrieve them anew in MyExternalSignatureContainer.sign but instead have to re-use the ones you retrieved in MyExternalBlankSignatureContainer.sign.

    According to your final comment, doing so worked for you.