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

使用PDFBox+HSM签名PDF后Adobe提示签名无效(文档已篡改)的解决方法

解决PDFBox结合HSM外部签名后Adobe Reader提示签名无效的问题

核心问题分析

你的代码存在三个关键错误,导致Adobe验证签名时判定文档被篡改:

  1. 重复读取输入流导致数据丢失
    在sign方法中,你两次调用content.readAllBytes():第一次传给HSM签名,第二次用于构造签名属性。InputStream只能读取一次,第二次读取会得到空字节数组,导致签名属性中的消息摘要完全错误,Adobe验证时自然不匹配。

  2. CMS签名结构构造逻辑错误
    你错误地将HSM返回的签名值作为CMS的待处理内容,同时还尝试用本地导出的私钥重新签名——这完全不符合外部签名的逻辑。外部签名中,HSM已经对文档哈希完成签名,你需要做的是把这个签名值嵌入到CMS结构的SignerInfo中,而不是重新签名。

  3. 签名属性OID使用错误
    构造签名属性时,你使用了NISTObjectIdentifiers.id_sha256作为属性OID,正确的消息摘要属性OID应该是PKCSObjectIdentifiers.id_messageDigest,属性值是文档的SHA256哈希值。

修复后的代码

public void signPdfExternally() throws Exception {
    PDDocument document = null;
    File pdfToBeSigned;
    File signedPdfFile;
    FileOutputStream output = null;
    byte[] cmsSignature;
    ExternalSigningSupport externalSigningSupport;
    try {
        pdfToBeSigned = new File("testPdf.pdf");
        signedPdfFile = new File(pdfToBeSigned.getParent(), pdfToBeSigned.getName().substring(0, pdfToBeSigned.getName().lastIndexOf('.')) + "_signed.pdf");
        document = Loader.loadPDF(pdfToBeSigned);
        output = new FileOutputStream(signedPdfFile);
        loadExternalCert(eSignCertProvider.getCertificateChain());
        
        document.addSignature(signature());
        externalSigningSupport = document.saveIncrementalForExternalSigning(output);
        
        // 提前读取待签名内容到字节数组,避免重复读取流
        byte[] contentBytes = IOUtils.toByteArray(externalSigningSupport.getContent());
        cmsSignature = sign(contentBytes);
        externalSigningSupport.setSignature(cmsSignature);
    } catch (Exception ex) {
        ex.printStackTrace();
        throw ex;
    } finally {
        if (output != null) {
            try {
                output.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (document != null) {
            document.close();
        }
    }
}

private PDSignature signature() {
    PDSignature signature = new PDSignature();
    signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
    signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
    signature.setName("Test User");
    signature.setLocation("Some location");
    signature.setReason("Testing");
    signature.setSignDate(Calendar.getInstance());
    // 指定签名算法,与HSM使用的算法保持一致
    signature.setSignatureAlgorithm(COSName.getPDFName("SHA256-RSA"));
    return signature;
}

// 签名方法:接收待签名的字节数组,构造正确的CMS签名
public byte[] sign(byte[] contentBytes) throws IOException {
    try {
        Security.addProvider(new BouncyCastleProvider());
        X509Certificate[] certChain = eSignCertProvider.getCertificateChain();
        if (certChain == null || certChain.length == 0) {
            throw new IllegalArgumentException("No certificate chain available");
        }

        // 计算文档SHA256哈希(如果HSM要求直接传入文档字节而非哈希,可直接传contentBytes给signContent)
        byte[] documentHash = MessageDigest.getInstance("SHA-256").digest(contentBytes);
        // 调用HSM对文档哈希签名
        byte[] hsmSignature = HsmServiceProvider.signContent(documentHash, "Testing1234");

        // 构造CMS签名结构
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        // 添加证书链到CMS
        List<Certificate> certList = Arrays.asList(certChain);
        JcaCertStore certs = new JcaCertStore(certList);
        gen.addCertificates(certs);

        // 构造签名属性:消息摘要+签名时间
        ASN1EncodableVector signedAttrs = new ASN1EncodableVector();
        signedAttrs.add(new Attribute(PKCSObjectIdentifiers.id_messageDigest, new DERSet(new DEROctetString(documentHash))));
        signedAttrs.add(new Attribute(PKCSObjectIdentifiers.id_signingTime, new DERSet(new DERUTCTime(new Date()))));

        // 构造SignerInfo,直接使用HSM返回的签名值
        X509Certificate signingCert = certChain[0];
        JcaX509CertificateHolder certHolder = new JcaX509CertificateHolder(signingCert);
        
        AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");
        AlgorithmIdentifier digestAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
        
        // 使用NullContentSigner嵌入HSM签名,避免本地重新签名
        ContentSigner nullSigner = new ContentSigner() {
            @Override
            public AlgorithmIdentifier getAlgorithmIdentifier() {
                return sigAlgId;
            }

            @Override
            public OutputStream getOutputStream() {
                return new ByteArrayOutputStream();
            }

            @Override
            public byte[] getSignature() {
                return hsmSignature;
            }
        };
        
        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(signedAttrs)));
        
        SignerInfoGenerator signerInfoGen = builder.build(nullSigner, certHolder);
        gen.addSignerInfoGenerator(signerInfoGen);

        // 生成分离式CMS签名(传入空内容,因为内容不在CMS中)
        CMSSignedData signedData = gen.generate(new CMSProcessableByteArray(new byte[0]), true);
        return signedData.getEncoded();
    } catch (Exception e) {
        e.printStackTrace();
        throw new IOException(e);
    }
}

关键修复点说明

  • 避免重复读取流:提前将待签名内容读取到字节数组,确保后续所有操作使用的是完整数据。
  • 修正CMS构造逻辑:使用NullContentSigner直接嵌入HSM返回的签名值,不再尝试本地签名,完全符合外部签名流程(私钥保留在HSM中)。
  • 修复签名属性:使用正确的消息摘要属性OID,传入文档的SHA256哈希值,同时添加签名时间属性提升Adobe兼容性。
  • 移除本地私钥依赖:删除extractPrivateKey()调用,外部签名不需要导出HSM中的私钥。

内容的提问来源于stack exchange,提问作者Roshan Pal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 13:19:52