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

PDF外部签名插入SignedHash验证失败问题及解决方案

PDF数字签名后提示文件被修改的解决方案

我最近在实现客户端持有待签名PDF+公钥证书,外部硬件存储对应私钥并返回签名哈希的场景时,踩了个大坑:签名完成后用Adobe Reader打开,始终提示"文件在签名后被修改",试过切换可见/不可见签名、调整时间戳等方法都没用,最终在帮助下找到了核心问题的解决办法。

问题场景与现象

需求场景

  • 客户端拥有待签名PDF和对应的公钥证书
  • 私钥存储在外部硬件设备中,需要将哈希值发送给硬件,由硬件返回SignedHash
  • 用硬件返回的SignedHash完成PDF签名流程

异常现象

签名生成后,Adobe Reader识别到签名,但判定文件已被修改,签名无法通过验证。

已修复的完整实现代码

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);
            // 获取客户端的自签名证书
            CertificateFactory factory = CertificateFactory.getInstance("X.509");
            Certificate[] chain = new Certificate[1];
            chain[0] = factory.generateCertificate(is);

            // 创建PDF阅读器和签名Stamper
            PdfReader reader = new PdfReader(doc);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PdfStamper stamper = PdfStamper.createSignature(reader, baos, '\0');

            // 设置签名外观
            PdfSignatureAppearance sap = stamper.getSignatureAppearance();
            sap.setReason("TEST REASON");
            sap.setLocation("TEST LOCATION");
            //sap.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, "sig"); //可见签名
            sap.setVisibleSignature(new Rectangle(36, 748, 36, 748), 1, "sig"); //不可见签名
            sap.setCertificate(chain[0]);

            // 创建签名字典
            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"));

            // 获取OCSP和CRL(可选,用于证书有效性验证)
            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);
            }

            // 关键修复:生成PKCS#7认证属性的哈希值,而非直接使用文档哈希
            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("需发送到硬件签名的哈希(Base64格式):");
            System.out.println( new String(Base64.encode(signedAttributesHash), Charsets.UTF_8));
            System.out.println("----------------------------------------------");
            System.out.println("请输入硬件返回的Base64格式签名哈希,按回车确认:");
            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());

            // 完成PDF签名流程
            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("签名完成,文件路径: " + System.getProperty("user.dir") + "/" + DEST);
            System.out.println("------------------操作结束--------------------------");
            System.exit(0);
        } catch (GeneralSecurityException e) {
            throw new IOException(e);
        } catch (DocumentException e) {
            throw new IOException(e);
        }
    }
}

核心修复逻辑

之前的错误在于:直接将文档哈希发送给硬件签名,但对于我们使用的PKCS#7 detached签名(代码中PdfName.ADBE_PKCS7_DETACHED类型),硬件需要签名的不是原始文档哈希,而是PKCS#7包内的认证属性集合哈希值(也就是代码中的signedAttributesHash变量)。

这个认证属性集合包含了文档哈希、签名算法、签名时间戳等关键元数据,只有对这个哈希进行签名,生成的PKCS#7包结构才合法,PDF阅读器才能正确验证签名有效性。之前跳过这一步直接签名文档哈希,导致签名包结构不符合规范,阅读器就会判定文件已被修改。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:38:47