Java IText7 PDF 签名问题 - 文档自签名后已被更改或损坏

Posted

技术标签:

【中文标题】Java IText7 PDF 签名问题 - 文档自签名后已被更改或损坏【英文标题】:Java IText7 PDF Sign Problem - Document has been altered or corrupted since it was signed 【发布时间】:2021-11-06 21:03:40 【问题描述】:

我尝试签署 pdf 文件,但在 Adob​​e 中打开已签署的 pdf 文件时遇到“文档自签署后已被更改或损坏”错误。

该错误描述性不强,我不知道该看哪里,因为代码对我来说似乎不错,但显然不是..

我使用的代码是:

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格式是(tmp文件+sha_prefix):

MDEwDQYJYIZIAWUDBAIBBQAEIKCZG8Xc6M2de3fuj8CMHLhW8XvMArW6Smy75TgABlGQ

签名 (AMA) 的 Base64 格式为:

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

谁能帮忙?

【问题讨论】:

格式错误请见谅。 @mkl 你能帮忙吗? 请分享一个由您的代码签名的示例 PDF。通常,对 PDF 的快速分析可以明确在代码或环境中的何处查找。 啊,一个错误是您在 MyExternalBlankSignatureContainer.signMyExternalSignatureContainer.sign 中都检索到 OCSP 响应。这是错误的,您必须在两种情况下使用相同的 OCSP 响应集,因此您只需检索它们一次,例如在MyExternalBlankSignatureContainer.sign 中,从那里检索它们,然后在MyExternalSignatureContainer.sign 中重新使用它们。如何简化错误搜索并且暂时不检索 OCSP 响应? 成功了!!!谢谢@mkl 【参考方案1】:

这是我们在 cmets 中讨论的概要。

问题是你在MyExternalBlankSignatureContainer.signMyExternalSignatureContainer.sign 中检索OCSP 响应,在你执行的这两种方法中

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);

并使用该列表ocspPdfPKCS7

当您使用MyExternalBlankSignatureContainer 计算要签名的属性的摘要并使用MyExternalSignatureContainer 根据该摘要的签名值构建签名容器时,两种情况下的属性必须相同。由于 OCSP 响应包含在其中一个属性中,这意味着您必须在两种情况下使用相同的 OCSP 响应。

但是 OCSP 响应通常会为每个请求重新创建(或最多缓存很短的时间),对于同一证书的不同请求,您通常会得到不同的 OCSP 响应。

因此,您不得在MyExternalSignatureContainer.sign 中重新检索它们,而是必须重新使用您在MyExternalBlankSignatureContainer.sign 中检索到的那些。

根据您的最终评论,这样做对您有用。

【讨论】:

以上是关于Java IText7 PDF 签名问题 - 文档自签名后已被更改或损坏的主要内容,如果未能解决你的问题,请参考以下文章

itext7 pdf与书签合并

使用 itext 7 在 PDF 中添加新页面

iText 7:此 pdf 文档可能无法正确显示 Firefox

如何使用 itext7 Java 将多个图像添加到 PDF?

itext7 pdf转图片

无法使用 itext7 使用 Java 语言在仅 skia 生成的 pdf 上放置印章(显示倒置印章)