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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 11:59:52