将签名哈希集成至原始PDF后签名有效性异常求助
解决PDF签名后提示“PDF在签名后已被修改”的问题
我帮你排查了代码里的问题,核心问题在于签名数据的格式和填充逻辑不符合PDF分离式签名的规范,导致最终的PDF签名结构无效,被验证器判定为“签名后已修改”。咱们来一步步修正:
问题分析
- 签名数据格式错误:你使用的是
ADBE_PKCS7_DETACHED签名类型,这种类型要求填充到CONTENTS字段的是完整的PKCS#7签名数据(包含签名值、证书链、算法信息等),而不是单纯的哈希签名值。你的外部签名工具可能只返回了哈希的签名结果,而非完整的PKCS#7容器。 - CONTENTS字段填充逻辑错误:你手动构造的
paddedSig填充方式不符合PDF规范,需要确保填充的字节长度与preClose时声明的CONTENTS长度一致,并且正确处理PKCS#7数据的编码。 - 不必要的设置:
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(); } } }
关键修改说明
- 修正签名数据格式:确保你的外部bat脚本返回的是完整的PKCS#7签名数据(通常是DER编码的字节流),而不是仅哈希的签名值。如果你的签名工具目前只返回哈希签名值,需要修改工具逻辑,生成包含证书链和算法信息的PKCS#7容器。
- 优化CONTENTS填充逻辑:
- 增大了
contentEstimated的长度(设为16384),确保足够容纳PKCS#7数据; - 填充时只复制PKCS#7数据到
paddedSig,剩余部分用0填充,符合PDF规范;
- 增大了
- 清理不必要的设置:移除了
reader.unethicalreading = true和reader.setAppendable(true),因为PdfStamper.createSignature默认会以正确的方式处理PDF; - 资源管理优化:使用try-with-resources自动关闭流,避免资源泄漏;
- 明确签名字段名称:给
setVisibleSignature指定了字段名称"MySignature",便于后续识别。
额外检查点
- 确认你的外部签名工具使用的哈希算法与代码中的
SHA256一致; - 验证PKCS#7数据的有效性:可以用
openssl pkcs7 -inform DER -in doc_signed.hash -print_certs命令检查是否包含正确的证书链; - 确保生成的PDF没有被其他工具修改,比如签名后不要用编辑器打开保存。
内容的提问来源于stack exchange,提问作者Mehdi
相关产品推荐
相关产品推荐

