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

将签名哈希集成至原始PDF后签名有效性异常求助

解决PDF签名后提示“PDF在签名后已被修改”的问题

我帮你排查了代码里的问题,核心问题在于签名数据的格式和填充逻辑不符合PDF分离式签名的规范,导致最终的PDF签名结构无效,被验证器判定为“签名后已修改”。咱们来一步步修正:

问题分析

  1. 签名数据格式错误:你使用的是ADBE_PKCS7_DETACHED签名类型,这种类型要求填充到CONTENTS字段的是完整的PKCS#7签名数据(包含签名值、证书链、算法信息等),而不是单纯的哈希签名值。你的外部签名工具可能只返回了哈希的签名结果,而非完整的PKCS#7容器。
  2. CONTENTS字段填充逻辑错误:你手动构造的paddedSig填充方式不符合PDF规范,需要确保填充的字节长度与preClose时声明的CONTENTS长度一致,并且正确处理PKCS#7数据的编码。
  3. 不必要的设置:reader.unethicalreading = true和reader.setAppendable(true)在签名场景下不是必须的,反而可能干扰签名流程。

修正后的代码

package com.example.hashdocument;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import com.lexpersona.commons.utils.ProcessLauncher;
import org.bouncycastle.util.encoders.Hex;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Calendar;
import java.util.HashMap;

public class Test2 {
    private static final String SRC = "B:/Hash-et-Reconstitution/tmp/Doc_test.pdf";
    private static final String DST = "B:/Hash-et-Reconstitution/tmp/Doc_test_DST.pdf";
    private static final String HASH = "B:/Hash-et-Reconstitution/tmp/Doc_test_hashed.hash";
    private static final String PATH_BAT = "C:/Repo_LP7/lpcommand.bat";
    private static final String PIN = "123456";
    private static final String CERTIFICATE = "C:/lp7command/tools/certificate.p12";
    private static final String SIGNED_HASH = "B:/Hash-et-Reconstitution/tmp/doc_signed.hash";

    private static byte[] readFileToByteArray(File file) {
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] bArray = new byte[(int) file.length()];
            fis.read(bArray);
            return bArray;
        } catch (IOException ioExp) {
            ioExp.printStackTrace();
            return new byte[0];
        }
    }

    public static File bytesToFile(byte[] fileByte, String pathFile) {
        File file = new File(pathFile);
        try (OutputStream os = new FileOutputStream(file)) {
            os.write(fileByte);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return file;
    }

    public static byte[] signDocument() throws IOException {
        ProcessLauncher p = new ProcessLauncher(System.out, System.err);
        // 注意:确保你的bat脚本返回的是完整的PKCS#7签名数据,而非仅哈希签名值
        int exec = p.exec("cmd.exe /c " + PATH_BAT + " <nul " + SIGNED_HASH + " " + PIN + " " + HASH + " " + CERTIFICATE, null, null);
        return readFileToByteArray(new File(SIGNED_HASH));
    }

    public static void main(String[] args) throws IOException, GeneralSecurityException, DocumentException {
        PdfReader reader = new PdfReader(SRC);
        int pdfPageNumber = reader.getNumberOfPages();

        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            // 创建签名Stamper,指定append模式为true(默认就是true,这里显式声明更清晰)
            PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0', null, true);
            PdfSignatureAppearance appearance = stamper.getSignatureAppearance();

            // 设置签名属性
            Calendar cal = Calendar.getInstance();
            appearance.setSignDate(cal);
            appearance.setReason("Signature de contrat");
            appearance.setLocation("MAROC");
            appearance.setVisibleSignature(new Rectangle(20, 20, 300, 300), pdfPageNumber, "MySignature");
            appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);

            // 配置签名字典
            PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
            dic.setReason(appearance.getReason());
            dic.setLocation(appearance.getLocation());
            dic.setDate(new PdfDate(appearance.getSignDate()));
            appearance.setCryptoDictionary(dic);

            // 预估CONTENTS字段长度,PKCS#7数据通常需要足够的空间,这里设为16384(2*8192)
            int contentEstimated = 16384;
            HashMap<PdfName, Integer> exc = new HashMap<>();
            exc.put(PdfName.CONTENTS, contentEstimated * 2 + 2);
            appearance.preClose(exc);

            // 计算待签名数据的哈希
            InputStream data = appearance.getRangeStream();
            MessageDigest messageDigest = MessageDigest.getInstance(DigestAlgorithms.SHA256);
            byte[] buff = new byte[8192];
            int read;
            while ((read = data.read(buff)) > 0) {
                messageDigest.update(buff, 0, read);
            }
            byte[] hashDigest = messageDigest.digest();

            // 将哈希写入文件,发送给外部签名工具
            try (PrintStream out = new PrintStream(new FileOutputStream(HASH))) {
                out.print(new String(Hex.encode(hashDigest), "UTF-8"));
            }

            // 获取外部签名后的PKCS#7数据
            byte[] pkcs7Signature = signDocument();

            // 填充CONTENTS字段:将PKCS#7数据填充到指定长度,剩余部分用0填充
            byte[] paddedSig = new byte[contentEstimated];
            System.arraycopy(pkcs7Signature, 0, paddedSig, 0, Math.min(pkcs7Signature.length, contentEstimated));

            PdfDictionary dic2 = new PdfDictionary();
            dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
            appearance.close(dic2);

            // 写入最终PDF
            try (OutputStream outputStream = new FileOutputStream(DST)) {
                os.writeTo(outputStream);
            }
        } finally {
            reader.close();
        }
    }
}

关键修改说明

  1. 修正签名数据格式:确保你的外部bat脚本返回的是完整的PKCS#7签名数据(通常是DER编码的字节流),而不是仅哈希的签名值。如果你的签名工具目前只返回哈希签名值,需要修改工具逻辑,生成包含证书链和算法信息的PKCS#7容器。
  2. 优化CONTENTS填充逻辑:
    • 增大了contentEstimated的长度(设为16384),确保足够容纳PKCS#7数据;
    • 填充时只复制PKCS#7数据到paddedSig,剩余部分用0填充,符合PDF规范;
  3. 清理不必要的设置:移除了reader.unethicalreading = true和reader.setAppendable(true),因为PdfStamper.createSignature默认会以正确的方式处理PDF;
  4. 资源管理优化:使用try-with-resources自动关闭流,避免资源泄漏;
  5. 明确签名字段名称:给setVisibleSignature指定了字段名称"MySignature",便于后续识别。

额外检查点

  • 确认你的外部签名工具使用的哈希算法与代码中的SHA256一致;
  • 验证PKCS#7数据的有效性:可以用openssl pkcs7 -inform DER -in doc_signed.hash -print_certs命令检查是否包含正确的证书链;
  • 确保生成的PDF没有被其他工具修改,比如签名后不要用编辑器打开保存。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 13:17:31