iText8两步签名PDF出现BER验证错误求助
iText5迁移至iText8.0.2两步签名BER错误排查
我正在将代码从iText5迁移至iText8.0.2,采用两步PDF签名流程:先生成哈希,通过CA API完成签名后再嵌入PDF。流程能正常执行,但生成的PDF验证时出现BER错误。已用PdfPKCS7实现,问题仍未解决,恳请帮忙排查。
代码实现
public class PDF_prepareHash { //global variable static Logger logger = LogManager.getLogger(PDF_prepareHash.class.getName()); static Timestamp timestamp = new Timestamp(System.currentTimeMillis()); static Instant instant = timestamp.toInstant(); static String txId=Long.toString(instant.toEpochMilli()); static String ORI="D:\\pdf"+File.separator+"files"+File.separator+txId+".pdf"; static String TEMP="D:\\pdf"+File.separator+"files"+File.separator+txId+"-TEMP.pdf"; static String DEST="D:\\pdf"+File.separator+"files"+File.separator+txId+"-SIGNED.pdf"; public static PdfPKCS7 sgn; //variable for get sign hash private static String signature=null; //function for prepare hash public static String prepareHash(String certx509,SignatureDetails sd,String name, String userId) { JSONObject obj = new JSONObject(); try{ BouncyCastleProvider providerBC = new BouncyCastleProvider(); Security.addProvider(providerBC); String x509b64 = certx509; String certString="-----BEGIN CERTIFICATE-----\n" +x509b64+"\n-----END CERTIFICATE-----"; byte[] certinByte= certString.getBytes(); X509Certificate x509 = fromByteArrayToX509Certificate(certinByte); Certificate cert = loadCertificate(certinByte); Certificate[] chain = new Certificate[1]; chain[0] = cert; FileBase64Converter fileB64 = new FileBase64Converter(); fileB64.base64ToFile(sd.getPdfb64(), ORI); PDF_prepareHash app = new PDF_prepareHash(); String json_hh = emptySignature_hash(chain, sd.getVisibility(), sd.getPage(), sd.getX1(), sd.getY1(), sd.getX2(), sd.getY2(), name, userId); if(!json_hh.isEmpty()){ obj.put("StatusCode", "000"); obj.put("StatusMsg", "Successfully prepare hash"); obj.put("Data", json_hh); logger.info("prepareHash: Status Code: 000"); logger.info("prepareHash: Status Msg: Successfully prepare hash"); return obj.toString(); } }catch(GeneralSecurityException e){ logger.fatal("GeneralSecurityException"); logger.fatal("GeneralSecurityException: " + e.getMessage()); }catch(JSONException e){ logger.fatal("JSONException"); logger.fatal("JSONException: " + e.getMessage()); } return null; } public static String emptySignature_hash(Certificate[] chain, Boolean visibility, Integer page, Integer x1, Integer y1, Integer x2, Integer y2,String name,String userId) { try { JSONObject obj = new JSONObject(); PdfReader reader = new PdfReader(ORI); FileOutputStream os = new FileOutputStream(TEMP); PdfSigner signer = new PdfSigner(reader, os, new StampingProperties()); String timeStamp = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss z").format(new Timestamp(System.currentTimeMillis())); Rectangle rect = new Rectangle(36, 748, 200, 100); PdfSignatureAppearance sap = signer.getSignatureAppearance(); sap .setLayer2Text("Digitally signed by\n"+name+", "+userId+"\nDate: "+timeStamp) .setReuseAppearance(false) .setPageRect(rect) .setPageNumber(page) .setCertificate(chain[0]); signer.setFieldName("signature"); // this field already exists BouncyCastleDigest digest = new BouncyCastleDigest(); sgn = new PdfPKCS7(null, chain, DigestAlgorithms.SHA256, null, digest,false); //IExternalSignatureContainer like BlankContainer PreSignatureContainer external = new PreSignatureContainer(PdfName.Adobe_PPKLite,PdfName.Adbe_pkcs7_detached) {}; signer.signExternalContainer( external, 8192); byte[] hash=external.getHash(); byte[] sh = sgn.getAuthenticatedAttributeBytes(hash,PdfSigner.CryptoStandard.CMS, null, null); MessageDigest messageDigest = MessageDigest.getInstance("SHA256"); messageDigest.update(sh); byte[] hash_byte = messageDigest.digest(); obj.put("digest", Base64.getEncoder().encodeToString(hash)); obj.put("hash", Base64.getEncoder().encodeToString(hash_byte)); os.close(); reader.close(); return obj.toString(); } catch (IOException ex) { logger.fatal("IOException"); logger.fatal("IOException: " + ex.getMessage()); } catch (InvalidKeyException ex) { logger.fatal("InvalidKeyException"); logger.fatal("InvalidKeyException: " + ex.getMessage()); } catch (NoSuchProviderException ex) { logger.fatal("NoSuchProviderException"); logger.fatal("NoSuchProviderException: " + ex.getMessage()); } catch (NoSuchAlgorithmException ex) { logger.fatal("NoSuchAlgorithmException"); logger.fatal("NoSuchAlgorithmException: " + ex.getMessage()); } catch (GeneralSecurityException ex) { logger.fatal("GeneralSecurityException"); logger.fatal("GeneralSecurityException: " + ex.getMessage()); } return null; } //function for get sign hash public static String serveSigning(String userId, String hash,String digest,String x509b64) { JSONObject obj = new JSONObject(); Scanner myObj = new Scanner(System.in); // Create a Scanner object System.out.println("Enter signature"); String casignature = myObj.nextLine(); // Read user input signature=casignature; byte[] extSignature = Base64.getDecoder().decode(signature); Certificate[] chain = null; try { String certString="-----BEGIN CERTIFICATE-----\n" +x509b64+"\n-----END CERTIFICATE-----"; byte[] certinByte= certString.getBytes(); X509Certificate x509 = fromByteArrayToX509Certificate(certinByte); Certificate cert = loadCertificate(certinByte); chain = new Certificate[1]; chain[0] = cert; } catch (CertificateException e) { logger.fatal("IOException"); logger.fatal("IOException: " + e.getMessage()); return null; } //embed signature String extSignature_b64 = Base64.getEncoder().encodeToString(extSignature); PDF_prepareHash.createSignature(digest, extSignature_b64, chain); obj.put("StatusCode", "000"); obj.put("StatusMsg", "Embed signature succesfully "); logger.info("[GetSignHash] Successfully embed prepared hash"); return obj.toString(); } //function for embed signature public static void createSignature(String hash, String extSignature, Certificate[] chain) { try { // BouncyCastleDigest digest = new BouncyCastleDigest(); // PdfPKCS7 sgn = new PdfPKCS7(null, chain, DigestAlgorithms.SHA256, null, digest, false); sgn.setExternalSignatureValue(Base64.getDecoder().decode(extSignature), null, "ECDSA"); ITSAClient tsc = null; Security.addProvider(new BouncyCastleProvider()); String TSA_URL="http://timestamp.entrust.net/TSS/RFC3161sha2TS"; String TSA_ACCNT=null; String TSA_PASSW=null; try { tsc = new TSAClientBouncyCastle(TSA_URL, TSA_ACCNT, TSA_PASSW); } catch(Exception e) { logger.error("Error timestamping services: "+e.toString()); } byte[] hh_sign = sgn.getEncodedPKCS7(Base64.getDecoder().decode(hash), PdfSigner.CryptoStandard.CMS, tsc, null, null); PdfReader reader = new PdfReader(TEMP); FileOutputStream os = new FileOutputStream(DEST); PdfSigner signer = new PdfSigner(reader, os, new StampingProperties()); MyExternalSignatureContainer external = new MyExternalSignatureContainer(hh_sign,chain); PdfSigner.signDeferred(signer.getDocument(), "signature", os, external); //close pdf files os.close(); reader.close(); } catch(IOException ex) { logger.fatal("IOException"); logger.fatal("IOException: " + ex.getMessage()); } catch (GeneralSecurityException ex) { logger.fatal("GeneralSecurityException"); logger.fatal("GeneralSecurityException: " + ex.getMessage()); } } //global function public static X509Certificate fromByteArrayToX509Certificate(byte[] bytes) throws CertificateException { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(bytes); X509Certificate x509cert = (X509Certificate)certFactory.generateCertificate(in); return x509cert; } public static Certificate loadCertificate(byte[] bytes) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(bytes); Certificate c=null; try { c =(Certificate)cf.generateCertificate(in); //c.checkValidity(); } catch (CertificateException ex) { logger.error("[PDF_prepareHash.java] Certificate loadCertificate: "+ex.getMessage()); } finally { try { in.close(); } catch (IOException ex) { logger.error("[PDF_prepareHash.java] Certificate loadCertificate: "+ex.getMessage()); } } try { in.close(); } catch (IOException ex) { logger.error("[PDF_prepareHash.java] Certificate loadCertificate: "+ex.getMessage()); } return c; } }
问题排查分析
从代码和BER错误表现来看,核心问题集中在签名容器实现、全局实例复用、哈希传递逻辑这几个点:
- 全局
PdfPKCS7实例风险:跨方法复用静态sgn实例,线程不安全且易导致状态混乱,破坏CMS签名结构。 PreSignatureContainer实现缺失:匿名实现未重写sign方法,无法正确捕获文档哈希,导致后续签名验证时数据不匹配。- 签名算法与哈希传递不匹配:
setExternalSignatureValue指定的"ECDSA"算法需与CA实际使用的一致;同时getEncodedPKCS7传入的哈希参数逻辑混淆了文档哈希与认证属性哈希的用途。
修复方案
1. 替换全局PdfPKCS7为局部状态传递
移除静态sgn变量,在emptySignature_hash中生成并序列化认证属性、签名算法等关键数据,传递给createSignature步骤,避免状态污染。
2. 正确实现PreSignatureContainer
重写sign方法确保捕获正确的文档哈希:
class PreSignatureContainer implements IExternalSignatureContainer { private final PdfName filter; private final PdfName subFilter; private byte[] documentHash; public PreSignatureContainer(PdfName filter, PdfName subFilter) { this.filter = filter; this.subFilter = subFilter; } @Override public byte[] sign(InputStream data) throws GeneralSecurityException { BouncyCastleDigest digest = new BouncyCastleDigest(); documentHash = DigestAlgorithms.digest(data, digest.getMessageDigest(DigestAlgorithms.SHA256)); return new byte[0]; // 两步签名返回空占位 } @Override public void modifySigningDictionary(PdfDictionary signDic) { signDic.put(PdfName.Filter, filter); signDic.put(PdfName.SubFilter, subFilter); } public byte[] getDocumentHash() { return documentHash; } }
3. 修正哈希传递与CMS构建逻辑
在emptySignature_hash中保存认证属性字节数组,传递给createSignature后直接用于构建CMS:
// emptySignature_hash中新增 obj.put("authAttributes", Base64.getEncoder().encodeToString(sh)); // createSignature中获取并使用 byte[] authAttrs = Base64.getDecoder().decode(authAttributesStr); PdfPKCS7 localSgn = new PdfPKCS7(null, chain, DigestAlgorithms.SHA256, null, new BouncyCastleDigest(), false); localSgn.setExternalSignatureValue(signatureBytes, null, "ECDSAwithSHA256"); // 匹配CA算法 byte[] pkcs7 = localSgn.getEncodedPKCS7(null, PdfSigner.CryptoStandard.CMS, tsc, authAttrs, null);
4. 确保signDeferred正确调用
验证临时文件路径与签名字段名称完全匹配,避免因文件读取错误导致的签名结构损坏。
内容的提问来源于stack exchange,提问作者Mace
相关产品推荐
相关产品推荐

