使用PDFBox添加签名时保留未签名字段的签名无效问题排查
问题:PDF添加新签名后提示“文档已被更改”签名无效
需要对包含签名字段的PDF添加新签名字段,同时保留现有未签名字段,但签名后新签名始终无效,提示“文档已被更改”。
现有代码
计算文档哈希的代码
private DocumentSignatureStructure createSignatureStructureAndComputeHash(byte[] inputFile, File tempFile, SignatureProperties sigProperties) throws IOException, NoSuchAlgorithmException { try (FileOutputStream fos = new FileOutputStream(tempFile); PDDocument doc = PDDocument.load(inputFile); SignatureOptions signatureOptions = new SignatureOptions();) { signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2); signatureOptions.setPage(sigProperties.getPage() - 1); if (sigProperties.isVisibleSignature()) { PDRectangle rect = createSignatureRectangle(doc, sigProperties); signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, rect, sigProperties)); } PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setSignDate(Calendar.getInstance()); doc.addSignature(signature, signatureOptions); ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos); MessageDigest digest = MessageDigest.getInstance(sigProperties.getHashAlgorithm().getAlgoName()); byte[] hashBytes = digest.digest(IOUtils.toByteArray(externalSigning.getContent())); String base64Hash = Base64.toBase64String(hashBytes); externalSigning.setSignature(new byte[0]); int offset = signature.getByteRange()[1] + 1; IOUtils.closeQuietly(signatureOptions); return DocumentSignatureStructure.builder().offset(offset) .hashValue(base64Hash) .build(); } }
嵌入签名代码
byte[] originalDocumentByte = docBlob.getBytes(1L, (int) docBlob.length()); File file = new File(getTempFolderPath(), getTempFileName("signed")); try (FileOutputStream fos = new FileOutputStream(file);) { fos.write(originalDocumentByte); } try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { raf.seek(documentSignatureStructure.getOffset()); raf.write(Hex.getBytes(Base64.decode(encodedSignature))); } Blob signedAndLtvBlob; try (PDDocument doc = PDDocument.load(file); FileOutputStream fos = new FileOutputStream(file); FileInputStream fis = new FileInputStream(file)) { if (createDss) { log.info("Adding revocation information to DSS dictionary of PDF"); makeLtv(doc, revocationData); } doc.saveIncremental(fos); }
尝试过的修改(无效)
在添加新签名字段前手动设置COSObject的NeedToBeUpdated标志为true:
//.. if (sigProperties.isVisibleSignature()) { PDRectangle rect = createSignatureRectangle(doc, sigProperties); signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, rect, sigProperties)); } PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm(); COSDictionary catalogDictionary = doc.getDocumentCatalog().getCOSObject(); catalogDictionary.setNeedToBeUpdated(true); COSDictionary acroFormDictionary = (COSDictionary) catalogDictionary.getDictionaryObject(COSName.ACRO_FORM); acroFormDictionary.setNeedToBeUpdated(true); COSArray array = (COSArray) acroFormDictionary.getDictionaryObject(COSName.FIELDS); array.setNeedToBeUpdated(true); for (PDField field : acroForm.getFieldTree()) { if (field instanceof PDSignatureField) { COSDictionary fieldDictionary = field.getCOSObject(); COSDictionary dictionary = (COSDictionary) fieldDictionary.getDictionaryObject(COSName.AP); dictionary.setNeedToBeUpdated(true); COSStream stream = (COSStream) dictionary.getDictionaryObject(COSName.N); stream.setNeedToBeUpdated(true); while (fieldDictionary != null) { fieldDictionary.setNeedToBeUpdated(true); fieldDictionary = (COSDictionary) fieldDictionary.getDictionaryObject(COSName.PARENT); } } } PDSignature signature = new PDSignature(); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); //..
问题根源与解决方案
核心错误
嵌入签名时错误使用了原始PDF文件,而非createSignatureStructureAndComputeHash方法中生成的tempFile。
createSignatureStructureAndComputeHash方法中,doc.saveIncrementalForExternalSigning(fos)已经对tempFile完成了关键修改:添加了新的签名字段、预留了签名字节位置、生成了对应签名的byteRange。但后续嵌入签名时,代码重新写入了原始PDF内容到新文件,完全忽略了前面的修改,导致签名的byteRange与实际文件内容不匹配,验证时自然判定“文档已被更改”。
修正步骤
- 复用
tempFile作为签名嵌入的基础文件,不要重新创建基于原始PDF的文件。 - 修正增量保存LTV的方式,确保使用原始文件流作为增量保存的输入,避免覆盖签名数据。
- 移除手动设置
NeedToBeUpdated的代码,PDFBox在添加签名字段时会自动处理对象更新标记,手动设置可能干扰正常流程。
修正后的嵌入签名代码示例
// 直接使用createSignatureStructureAndComputeHash生成的tempFile,无需重新写入原始PDF File file = tempFile; try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { raf.seek(documentSignatureStructure.getOffset()); raf.write(Hex.getBytes(Base64.decode(encodedSignature))); } Blob signedAndLtvBlob; // 增量保存LTV时,传入原始文件的输入流,确保增量更新正确 try (PDDocument doc = PDDocument.load(file); FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(file)) { if (createDss) { log.info("Adding revocation information to DSS dictionary of PDF"); makeLtv(doc, revocationData); } doc.saveIncremental(fis, fos); }
额外优化建议
移除createSignatureStructureAndComputeHash中的externalSigning.setSignature(new byte[0]);,saveIncrementalForExternalSigning已经预留了签名位置,设置空字节属于多余操作,可能影响后续签名嵌入。
内容的提问来源于stack exchange,提问作者Rajath R Joshi
相关产品推荐
相关产品推荐

