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

使用iText添加PAdES签名时,新增签名破坏已有签名的问题

已有签名PDF添加新签名时破坏原有签名的问题解决

问题描述

我编写了一段Java代码,流程为:添加签名占位符生成预签名文件→提取哈希→将哈希发送至存储私钥的客户端签名→把签名后的哈希添加到PDF。这段代码处理无签名的PDF完全正常,但给已有他人签名的PDF加新签名时,会破坏第一个签名。在Adobe Acrobat Reader中显示第二个签名正常,但第一个有效签名已损坏。

问题代码

public PadesDigestRes digest(PadesDigestReq req) throws Exception {
    try {
        // create result object
        PadesDigestRes result = new PadesDigestRes();

        // generate id for pdf document
        String fileId = UUID.randomUUID().toString();

        // read pdf from base64 document
        com.itextpdf.text.pdf.PdfReader pdfReader = new com.itextpdf.text.pdf.PdfReader(new ByteArrayInputStream(Base64Utils.decodeFromString(req.getBase64Document())));

        // create destination file output stream
        File file = new File(TEMP_DIR + fileId + ".pdf");

        // create stamper to add properties to the pdf document
        PdfStamper pdfStamper = PdfStamper.createSignature(pdfReader, null, '\0', file, true);

        // read previous signatures from pdf document
        ArrayList<String> signatureNames = pdfStamper.getAcroFields().getSignatureNames();

        // check if there is no signature field on the pdf document
        // then add new page at the end of the pdf document to add new signature field
        if (signatureNames.size() == 0) {
            com.itextpdf.text.pdf.PdfReader tempReader = pdfStamper.getReader();
            int numberOfPages = tempReader.getNumberOfPages();
            pdfStamper.insertPage(numberOfPages + 1, tempReader.getPageSize(1));
        }

        // get signature appearance && set required properties
        com.itextpdf.text.pdf.PdfSignatureAppearance signatureAppearance = pdfStamper.getSignatureAppearance();
        //read the user certificate 
        X509Certificate certificate = PadesUtil.getX509CertificateFromBase64String(req.getBase64Certificate());
        signatureAppearance.setCertificate(certificate);
        signatureAppearance.setReason("test");
        signatureAppearance.setLocation("Berlin");

        //read user data from certificate
        X500Name certificateName = PadesUtil.getX500NameByCertificate(certificate);
        RDN nationalCode = PadesUtil.getRDN(certificateName);
        String signDate = PadesUtil.generateSignDate();

        String signatureInfo = PadesUtil.generateSignatureStringInfo(signatureNames.size(), certificate, nationalCode, signDate);
        
        signatureAppearance.setLayer2Text(signatureInfo);
        signatureAppearance.setLayer4Text(signatureInfo);

        //building ui for the signature
        float width = pdfStamper.getReader().getPageSizeWithRotation(1).getWidth();
        int whoMany = (int) Math.floor(width / (150 + 25));
        int signatureIndex = signatureNames.size() + 1;
        float lowerLeftX = (((signatureIndex % whoMany) - 1) * 150 + 25) * signatureIndex;
        float lowerLeftY = (signatureIndex / whoMany) * 100 + 50;
        Rectangle rectangle = new Rectangle(lowerLeftX, lowerLeftY, 150, 100);
        
        
        String fieldName = PadesUtil.generateUserInfo(certificate);
        signatureAppearance.setVisibleSignature(rectangle, pdfStamper.getReader().getNumberOfPages(), fieldName);
        
        // set both digital and image signature on render mode
        signatureAppearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);

        // create signature container
        com.itextpdf.text.pdf.security.ExternalBlankSignatureContainer external = new com.itextpdf.text.pdf.security.ExternalBlankSignatureContainer(com.itextpdf.text.pdf.PdfName.ADOBE_PPKLITE, com.itextpdf.text.pdf.PdfName.ADBE_PKCS7_DETACHED);
        MakeSignature.signExternalContainer(signatureAppearance, external, 10000);

        // read generated document(pre-signed pdf with placeholder) from file and get hashable byte range
        PdfReader pdfReaderForHash = new PdfReader(TEMP_DIR + fileId + ".pdf");
        PdfDocument pdfDocument = new PdfDocument(pdfReaderForHash);
        SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
        PdfSignature pdfSignature = signatureUtil.getSignature(fieldName);

        if (pdfSignature == null) {
            throw (new com.itextpdf.kernel.PdfException("There is no field in the document with such name: {0}."))
                    .setMessageParams(new Object[]{fieldName});
        } else if (!signatureUtil.signatureCoversWholeDocument(fieldName)) {
            throw (new PdfException("Signature with name {0} is not the last. It doesn't cover the whole document."))
                    .setMessageParams(new Object[]{fieldName});
        } else {
            PdfArray byteRange = pdfSignature.getByteRange();
            long[] gaps = byteRange.toLongArray();
            IRandomAccessSource readerSource = pdfDocument.getReader().getSafeFile().createSourceView();
            InputStream data = new RASInputStream((new RandomAccessSourceFactory()).createRanged(readerSource, gaps));

            byte[] bytes = Files.readAllBytes(Paths.get("./path/to/chain.p7b"));
            Certificate[] certificates = readCertificatesFromPKCS7(bytes);

            // create hash from whole document
            ExternalDigest digestBC = new com.itextpdf.text.pdf.security.BouncyCastleDigest();
            String algorithm = DigestAlgorithms.SHA256;
            com.itextpdf.text.pdf.security.PdfPKCS7 pkcs7 = new com.itextpdf.text.pdf.security.PdfPKCS7(null, certificates, algorithm, null, digestBC, false);
            byte[] documentHash = DigestAlgorithms.digest(data, digestBC.getMessageDigest(algorithm));
            byte[] authedAttrBytes = pkcs7.getAuthenticatedAttributeBytes(documentHash, null, null, MakeSignature.CryptoStandard.CMS);

            // set response fields
            result.setFileId(fileId);
            result.setBase64Pkcs7(Base64Utils.encodeToString(authedAttrBytes));
            result.setBase64DocumentDigest(Base64Utils.encodeToString(documentHash));

            return result;
        }
    } catch (IOException e) {
        log.warn("Could not read document with base64document value : " + e);
        throw new IOException(e.getMessage());
    } catch (DocumentException e) {
        log.warn("Could not create signature using pdfStamper : " + e);
        throw new DocumentException(e.getMessage());
    } catch (PdfException e) {
        log.warn(e.getMessage());
        throw new PdfException(e.getMessage());
    } catch (Exception e) {
        log.warn("Internal error : " + e);
        throw new Exception(e.getMessage());
    }
}

问题原因

  1. API版本混用:代码同时使用iText 5和iText 7的API,两个版本对PDF文件结构的处理逻辑完全不同,混用会篡改原有签名的字节范围或破坏PDF结构,导致第一个签名验证失败。
  2. 签名位置计算错误:当signatureIndex >=2时,lowerLeftX会出现负数,导致签名框超出页面范围,虽不直接破坏原有签名,但属于无效UI布局。

解决方案

1. 统一使用iText 7 API

iText 7对多签名场景支持更完善,严格遵循PDF追加模式规则,不会破坏原有签名。

2. 修正签名位置计算逻辑

确保签名框始终在页面范围内。

修改后的代码示例

public PadesDigestRes digest(PadesDigestReq req) throws Exception {
    PadesDigestRes result = new PadesDigestRes();
    String fileId = UUID.randomUUID().toString();
    File tempFile = new File(TEMP_DIR + fileId + ".pdf");

    // 使用iText 7读取PDF并开启追加模式
    PdfReader reader = new PdfReader(new ByteArrayInputStream(Base64Utils.decodeFromString(req.getBase64Document())));
    PdfWriter writer = new PdfWriter(tempFile);
    PdfDocument pdfDoc = new PdfDocument(reader, writer, new StampingProperties().useAppendMode());

    SignatureUtil signatureUtil = new SignatureUtil(pdfDoc);
    List<String> existingSignatures = signatureUtil.getSignatureNames();

    // 无签名时追加新页面
    if (existingSignatures.isEmpty()) {
        PageSize pageSize = pdfDoc.getPage(1).getPageSize();
        pdfDoc.addNewPage(pageSize);
    }

    // 准备签名外观
    PdfSignatureAppearance appearance = new PdfSignatureAppearance(pdfDoc);
    X509Certificate certificate = PadesUtil.getX509CertificateFromBase64String(req.getBase64Certificate());
    appearance.setCertificate(certificate);
    appearance.setReason("test");
    appearance.setLocation("Berlin");

    // 读取证书信息并生成签名描述
    X500Name certificateName = PadesUtil.getX500NameByCertificate(certificate);
    RDN nationalCode = PadesUtil.getRDN(certificateName);
    String signDate = PadesUtil.generateSignDate();
    String signatureInfo = PadesUtil.generateSignatureStringInfo(existingSignatures.size(), certificate, nationalCode, signDate);
    appearance.setLayer2Text(signatureInfo);

    // 修正签名位置计算,确保在页面内
    int signatureIndex = existingSignatures.size() + 1;
    PageSize pageSize = pdfDoc.getPage(pdfDoc.getNumberOfPages()).getPageSize();
    float widthPerSignature = 150;
    float margin = 25;
    float spacing = 25;
    int cols = (int) Math.floor((pageSize.getWidth() - 2 * margin) / (widthPerSignature + spacing));
    int col = (signatureIndex - 1) % cols;
    int row = (signatureIndex - 1) / cols;
    float lowerLeftX = margin + col * (widthPerSignature + spacing);
    float lowerLeftY = margin + row * 100;
    Rectangle rect = new Rectangle(lowerLeftX, lowerLeftY, widthPerSignature, 100);

    String fieldName = PadesUtil.generateUserInfo(certificate);
    appearance.setVisibleSignature(rect, pdfDoc.getNumberOfPages(), fieldName);
    appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);

    // 创建空白签名容器
    IExternalSignatureContainer externalContainer = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
    signatureUtil.signExternalContainer(appearance, externalContainer, 10000);

    // 关闭文档后重新读取,提取哈希
    pdfDoc.close();

    PdfReader hashReader = new PdfReader(tempFile);
    PdfDocument hashDoc = new PdfDocument(hashReader);
    SignatureUtil hashSignatureUtil = new SignatureUtil(hashDoc);
    PdfSignature pdfSignature = hashSignatureUtil.getSignature(fieldName);

    if (pdfSignature == null) {
        throw new PdfException(String.format("不存在名为%s的签名域", fieldName));
    }
    if (!hashSignatureUtil.signatureCoversWholeDocument(fieldName)) {
        throw new PdfException(String.format("签名%s不是最后一个,未覆盖整个文档", fieldName));
    }

    // 提取待哈希的字节范围
    PdfArray byteRange = pdfSignature.getByteRange();
    long[] ranges = byteRange.toLongArray();
    IRandomAccessSource source = hashDoc.getReader().getSafeFile().createSourceView();
    RASInputStream dataStream = new RASInputStream(new RandomAccessSourceFactory().createRanged(source, ranges));

    // 计算文档哈希和认证属性哈希
    byte[] chainBytes = Files.readAllBytes(Paths.get("./path/to/chain.p7b"));
    Certificate[] certificates = readCertificatesFromPKCS7(chainBytes);

    BouncyCastleDigest digestBC = new BouncyCastleDigest();
    String algorithm = DigestAlgorithms.SHA256;
    PdfPKCS7 pkcs7 = new PdfPKCS7(null, certificates, algorithm, null, digestBC, false);
    byte[] documentHash = DigestAlgorithms.digest(dataStream, digestBC.getMessageDigest(algorithm));
    byte[] authedAttrBytes = pkcs7.getAuthenticatedAttributeBytes(documentHash, null, null, PdfSigner.CryptoStandard.CMS);

    // 设置返回参数
    result.setFileId(fileId);
    result.setBase64Pkcs7(Base64Utils.encodeToString(authedAttrBytes));
    result.setBase64DocumentDigest(Base64Utils.encodeToString(documentHash));

    hashDoc.close();
    return result;
}

关键修改点

  • 全程使用iText 7 API,避免版本混用
  • 开启PDF追加模式new StampingProperties().useAppendMode(),确保原有签名的字节范围不被修改
  • 修正签名位置计算逻辑,保证签名框在页面可视范围内
  • 统一异常处理的字符串格式化方式,提升可读性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 02:39:58