You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java使用IText7签名PDF后Adobe提示签署后文档被篡改损坏求助

PDF签名后Adobe打开报文档篡改错误排查

对PDF文件进行签名后,使用Adobe打开已签署PDF时遇到「Document has been altered or corrupted since it was signed」报错,报错截图如下:
screenshot

该报错无详细说明,排查方向不明确,代码逻辑看起来正常但实际运行报错,所用代码如下:

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

        //将sha_prefix与hash4Sign拼接后转换为BASE64格式
        String hashToSign = SafePdfHelper.getHashtoSign(hash4Sign);
        
        //调用AMA接口获取签名
        String amaSignature = SafeAmaHelper.getAssinat(token,hashToSign).getSignatures().get(0);

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

        //将AMA返回的签名哈希写入临时PDF,生成最终已签署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 {

        /* 签名字典:Filter和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);

                //生成sha256消息摘要
                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);

            //对已预留签名空间的PDF进行签名,签名域需覆盖整个文档
            signer.signDeferred(signer.getDocument(), fieldName, os, external);
        }
        reader.close();
    }

    static class MyExternalSignatureContainer implements IExternalSignatureContainer {

        /* 签名字典:Filter和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(临时文件内容加sha前缀):
MDEwDQYJYIZIAWUDBAIBBQAEIKCZG8Xc6M2de3fuj8CMHLhW8XvMArW6Smy75TgABlGQ
  • AMA接口返回的签名值Base64:
X5vg7qXJNsiB8hYtauih/wMFNf9uLAnT8h4M7DvHyw0bLdM03BJc7Ar1yGIoA0MTXaEdq85DP6JJFeMJZBRRc/NTA1C4IpjBN5N5Fpaa7HFnNxORQBc00d/bXuSzV1DNwCdIfcDYSUjh5Z3OWFdWzqmDhmAWRK/Hudf90m34B1mpfTtvtRAzrgn79fIBUd9D09iXpnClqTVYIzWcJ+Dz6yU75a0gvR79wNLCpUYNw2kxdmp/odAMm5cn10x9hLB+UhaNSUsnYyQUZtFsSkIE+oPXFqZc9ky4j5ha9Xfz8GGcLPEkAupyxOb5f9/NGicOeegX793swY09O4NxDW9RVtMtmdKt8kZAxB70PG1r18Ui2gheY4yuMg2aqpkcw5vgBO1GYe2DwDp99Qs4xHJjhbiUZOKT0moU+tDb3EySHZkkkci/GQTUg8IYHU8umv9TyuD7A3NRBQTyQud6j9H6bG3zQE+V4T8N2fnUPmoFEfDWyIvxvV+7YL+BymeZX4A0

问题原因&修复方案

1. OCSP调用结果不一致

你在两次签名容器中分别独立调用OCSP接口获取证书吊销状态,两次返回的OCSP响应存在时间戳等差异,导致两次构造的认证属性字节不匹配,签名校验失败。
修复方法:在第一步预留签名域时就将OCSP响应列表缓存下来,第二步注入签名时复用同一批OCSP数据,避免两次调用结果不一致。

2. 延迟签名未开启追加模式

第二步调用PdfSigner做延迟签名时,StampingProperties没有开启useAppendMode(),会导致PDF原有结构被重写,预留的签名域位置、哈希值不匹配。
修复方法:修改createSignature方法中的PdfSigner初始化代码:

PdfSigner signer = new PdfSigner(reader, os, new StampingProperties().useAppendMode());

3. 签名值编码转换错误

String.valueOf(amaSignature.toCharArray())属于冗余操作,特殊编码场景下可能会破坏Base64解码结果。
修复方法:直接传入amaSignature字符串做Base64解码:

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

4. 哈希前缀拼接逻辑不匹配

getAuthenticatedAttributeBytes返回的已经是包含签名属性的完整待签名内容,你额外拼接的SHA256前缀可能和AMA接口要求的签名输入格式不匹配,可根据AMA接口文档调整:

  • 如果AMA要求直接对attributeBytes做签名,去掉额外的前缀拼接逻辑,直接对attributeBytes做Base64编码传给AMA
  • 如果AMA要求签名的是原始哈希值,不要对属性字节做二次哈希,直接传入原始文档哈希即可

内容的提问来源于stack exchange,提问作者NunoMMC

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 04:54:03