iText7对接外部签名服务签署PDF后Adobe验证提示文档被篡改
iText7对接外部签名服务PDF签署验证失败解决方案
核心问题定位
你的实现存在4个致命逻辑错误,直接导致签名验证失败:
- 哈希计算逻辑完全不符合PDF签名规范:你提前对磁盘上的完整临时PDF文件计算全文件哈希,但PDF签名实际的签名范围是iText在签名流程中动态划定的、排除签名字段预留写入空间的字节区间,提前计算的全文件哈希和实际待签名内容完全不匹配,必然触发“文档被篡改或损坏”的校验错误。
- 签名接口实现逻辑错误:自定义
IExternalSignature时,完全忽略sign(byte[] message)方法的入参message——这个参数才是iText按照规范计算出的、真正需要提交给外部签名服务的待签名数据,你直接返回提前获取的签名值,签名内容和待签内容不匹配。 - CMS容器构造参数错误:自行封装CMS时,错误传入提前计算的全文件哈希作为已认证属性参数,没有使用iText在签名流程中生成的标准已认证属性结构,导致CMS格式不符合规范,触发签名格式错误。
- 多签逻辑缺失:签名时没有开启追加模式,写入签名时会重写原有PDF结构,多签场景下会直接破坏前序签名的有效性。
正确实现方案
首先需要和外部签名服务确认返回值类型,分两类场景适配:
场景1:外部服务返回原始裸签名值(最常见)
这类场景下服务端仅返回对传入哈希的原始签名结果(RSA/SM2/ECDSA签名值),CMS容器由iText本地构造,流程如下:
- 提前生成带所有签名字段的临时PDF,该部分你的现有逻辑可正常使用,注意生成时不要加密、不要携带多余增量更新内容。
- 禁止提前计算文件哈希,所有待签名数据必须在PdfSigner签名流程中由iText生成。
- 自定义
IExternalSignature实现,在sign(byte[] message)方法内部,将iText传入的message(即符合PDF规范的待签名数据)做Base64编码提交给签名服务,拿到返回的签名值解码后直接返回,不要做额外哈希或包装。 - 签名时必须开启追加模式,避免修改原有文件结构破坏签名有效性。
- 多签场景下,每次签署都以上一次签署完成的PDF作为源文件,始终保持追加模式开启。
场景2:外部服务返回完整CMS签名容器
这类场景下服务端直接封装好符合规范的PKCS#7/CMS结构(包含证书链、签名值、已认证属性),不需要iText本地构造CMS,需要使用signExternalContainer方法对接,自定义IExternalSignatureContainer实现即可。
修正后核心代码示例
裸签名值场景实现
// 自定义外部签名实现 public class ExternalServiceSignature implements IExternalSignature { private final Certificate[] chain; private final SignServiceClient signServiceClient; public ExternalServiceSignature(Certificate[] chain, SignServiceClient client) { this.chain = chain; this.signServiceClient = client; } @Override public String getHashAlgorithm() { // 按实际签名算法调整,国密场景对应填SM3 return DigestAlgorithms.SHA256; } @Override public String getEncryptionAlgorithm() { // 按证书类型调整,RSA填RSA,SM2填SM2 return "RSA"; } @Override public byte[] sign(byte[] message) throws GeneralSecurityException { // message为iText生成的标准待签名数据,直接提交给服务端 String base64ToSign = Base64.getEncoder().encodeToString(message); String base64SignResult = signServiceClient.getRawSign(base64ToSign); return Base64.getDecoder().decode(base64SignResult); } } // 签名调用逻辑 public void sign(String src, String dest, Certificate[] chain, String fieldName) throws GeneralSecurityException, IOException { try (FileOutputStream os = new FileOutputStream(dest); InputStream is = new FileInputStream(src)) { PdfReader reader = new PdfReader(is); // 关键:开启追加模式,多签不破坏前序签名 StampingProperties properties = new StampingProperties().useAppendMode(); PdfSigner signer = new PdfSigner(reader, os, properties); signer.setFieldName(fieldName); IExternalDigest digest = new BouncyCastleDigest(); IExternalSignature signature = new ExternalServiceSignature(chain, new SignServiceClient()); // 预留签名空间修正为8192(原代码8096为笔误),证书链较长/带时间戳可适当调大 signer.signDetached(digest, signature, chain, null, null, null, 8192, PdfSigner.CryptoStandard.CMS); } }
完整CMS容器场景实现
public class ExternalCmsContainer implements IExternalSignatureContainer { private final SignServiceClient signServiceClient; public ExternalCmsContainer(SignServiceClient client) { this.signServiceClient = client; } @Override public byte[] sign(InputStream data) throws GeneralSecurityException { try { byte[] docRangeBytes = StreamUtil.inputStreamToArray(data); // 按服务端要求计算哈希,部分服务会直接处理原始字节无需本地算哈希 byte[] hash = DigestAlgorithms.digest(new ByteArrayInputStream(docRangeBytes), DigestAlgorithms.SHA256, null); String base64Hash = Base64.getEncoder().encodeToString(hash); String base64Cms = signServiceClient.getCmsSign(base64Hash); return Base64.getDecoder().decode(base64Cms); } catch (IOException e) { throw new GeneralSecurityException("签名流程异常", e); } } @Override public void modifySigningDictionary(PdfDictionary signDic) { signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite); signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached); } } // 调用逻辑 public void signWithCms(String src, String dest, String fieldName) throws GeneralSecurityException, IOException { try (FileOutputStream os = new FileOutputStream(dest); InputStream is = new FileInputStream(src)) { PdfReader reader = new PdfReader(is); StampingProperties properties = new StampingProperties().useAppendMode(); PdfSigner signer = new PdfSigner(reader, os, properties); signer.setFieldName(fieldName); // CMS容器预留空间建议设为16384以上,避免空间不足截断 signer.signExternalContainer(new ExternalCmsContainer(new SignServiceClient()), 16384); } }
避坑说明
- 对接前必须和签名服务确认待签数据要求:部分服务会要求传入原始文档字节,服务端自行做哈希计算,这类场景不要本地提前计算哈希,严格按服务端接口要求传参,保证服务端签名的内容和iText传入的待签内容完全一致。
- 预留签名空间大小要匹配实际签名内容长度,空间不足会导致签名值被截断,触发格式错误。
- 如果需要添加签名外观、时间戳、OCSP/CRL吊销信息,直接在signDetached对应参数位置传入即可,不影响外部签名核心逻辑。
内容的提问来源于stack exchange,提问作者papi_pl
相关产品推荐
相关产品推荐

