使用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()); } }
问题原因
- API版本混用:代码同时使用iText 5和iText 7的API,两个版本对PDF文件结构的处理逻辑完全不同,混用会篡改原有签名的字节范围或破坏PDF结构,导致第一个签名验证失败。
- 签名位置计算错误:当
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
相关产品推荐
相关产品推荐

