Java使用IText7签名PDF后Adobe提示签署后文档被篡改损坏求助
PDF签名后Adobe打开报文档篡改错误排查
对PDF文件进行签名后,使用Adobe打开已签署PDF时遇到「Document has been altered or corrupted since it was signed」报错,报错截图如下:
该报错无详细说明,排查方向不明确,代码逻辑看起来正常但实际运行报错,所用代码如下:
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
相关产品推荐
相关产品推荐

