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

使用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与实际文件内容不匹配,验证时自然判定“文档已被更改”。

修正步骤

  1. 复用tempFile作为签名嵌入的基础文件,不要重新创建基于原始PDF的文件。
  2. 修正增量保存LTV的方式,确保使用原始文件流作为增量保存的输入,避免覆盖签名数据。
  3. 移除手动设置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 10:05:33