【问题标题】:Signing PDF with PDFBox and BouncyCastle使用 PDFBox 和 BouncyCastle 签署 PDF
【发布时间】:2015-08-04 17:08:41
【问题描述】:

我正在尝试使用 PDFBox 签署 PDF,它确实签署了,但是当我在 adobe 阅读器中打开文档时,我收到以下消息“文档自签署以来已被更改或损坏”有人可以帮我找到问题。

密钥库是使用“keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias test -validity 365 -v -keyalg RSA -keystore keystore.p12”创建的

使用 pdfbox-1.8.9 和 bcpkix-jdk15on-1.52

这是我的代码:

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.exceptions.SignatureException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Calendar;
import java.util.Collections;
import java.util.Enumeration;

public class CreateSignature implements SignatureInterface {
    private static PrivateKey privateKey;
    private static Certificate certificate;

    boolean signPdf(File pdfFile, File signedPdfFile) {

        try (
                FileInputStream fis1 = new FileInputStream(pdfFile);
                FileInputStream fis = new FileInputStream(pdfFile);
                FileOutputStream fos = new FileOutputStream(signedPdfFile);
                PDDocument doc = PDDocument.load(pdfFile)) {
            int readCount;
            byte[] buffer = new byte[8 * 1024];
            while ((readCount = fis1.read(buffer)) != -1) {
                fos.write(buffer, 0, readCount);
            }

            PDSignature signature = new PDSignature();
            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
            signature.setName("NAME");
            signature.setLocation("LOCATION");
            signature.setReason("REASON");
            signature.setSignDate(Calendar.getInstance());
            doc.addSignature(signature, this);
            doc.saveIncremental(fis, fos);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public byte[] sign(InputStream is) throws SignatureException, IOException {
        try {
            BouncyCastleProvider BC = new BouncyCastleProvider();
            Store certStore = new JcaCertStore(Collections.singletonList(certificate));

            CMSTypedDataInputStream input = new CMSTypedDataInputStream(is);
            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            ContentSigner sha512Signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(BC).build(privateKey);

            gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
                    new JcaDigestCalculatorProviderBuilder().setProvider(BC).build()).build(sha512Signer, new X509CertificateHolder(certificate.getEncoded())
            ));
            gen.addCertificates(certStore);
            CMSSignedData signedData = gen.generate(input, false);

            return signedData.getEncoded();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) throws IOException, GeneralSecurityException, SignatureException, COSVisitorException {
        char[] password = "123456".toCharArray();

        KeyStore keystore = KeyStore.getInstance("PKCS12");
        keystore.load(new FileInputStream("/home/user/Desktop/keystore.p12"), password);

        Enumeration<String> aliases = keystore.aliases();
        String alias;
        if (aliases.hasMoreElements()) {
            alias = aliases.nextElement();
        } else {
            throw new KeyStoreException("Keystore is empty");
        }
        privateKey = (PrivateKey) keystore.getKey(alias, password);
        Certificate[] certificateChain = keystore.getCertificateChain(alias);
        certificate = certificateChain[0];

        File inFile = new File("/home/user/Desktop/sign.pdf");
        File outFile = new File("/home/user/Desktop/sign_signed.pdf");
        new CreateSignature().signPdf(inFile, outFile);
    }
}


class CMSTypedDataInputStream implements CMSTypedData {
    InputStream in;

    public CMSTypedDataInputStream(InputStream is) {
        in = is;
    }

    @Override
    public ASN1ObjectIdentifier getContentType() {
        return PKCSObjectIdentifiers.data;
    }

    @Override
    public Object getContent() {
        return in;
    }

    @Override
    public void write(OutputStream out) throws IOException,
            CMSException {
        byte[] buffer = new byte[8 * 1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
        in.close();
    }
}

【问题讨论】:

    标签: java pdf pdfbox


    【解决方案1】:

    修复“文档已被更改或损坏”

    错误是你用InputStream 调用PDDocument.saveIncremental 只是覆盖了原始PDF:

    FileInputStream fis1 = new FileInputStream(pdfFile);
    FileInputStream fis = new FileInputStream(pdfFile);
    FileOutputStream fos = new FileOutputStream(signedPdfFile);
    ...
    doc.saveIncremental(fis, fos);
    

    但该方法希望 InputStream 覆盖原始文件以及为准备签名所做的更改。

    因此fis也需要指向signedPdfFile,由于该文件之前可能不存在,所以fisfos的创建顺序必须互换>

    FileInputStream fis1 = new FileInputStream(pdfFile);
    FileOutputStream fos = new FileOutputStream(signedPdfFile);
    FileInputStream fis = new FileInputStream(signedPdfFile);
    ...
    doc.saveIncremental(fis, fos);
    

    很遗憾,JavaDocs 没有指出这一点。

    另一个问题

    生成的签名还有另一个问题。如果您查看示例结果的 ASN.1 转储,您会看到以下内容:

        <30 80>
       0 NDEF: SEQUENCE {
        <06 09>
       2    9:   OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
             :     (PKCS #7)
        <A0 80>
      13 NDEF:   [0] {
        <30 80>
      15 NDEF:     SEQUENCE {
        <02 01>
      17    1:       INTEGER 1
        <31 0F>
      20   15:       SET {
    

    NDEF 长度指示表明不定长度方法 用于对签名容器的这些外层进行编码。 基本编码规则 (BER) 允许使用此方法,但更严格的可分辨编码规则 (DER) 则不允许使用此方法。虽然对于通用 PKCS#7/CMS 签名允许将 BER 用于外层,但 PDF 规范明确要求:

    当使用 PKCS#7 签名时,Contents 的值应为包含签名的 DER 编码的 PKCS#7 二进制数据对象。

    (第 12.8.3.3.1 节“ISO 32000 中使用的 PKCS#7 签名”/ISO 32000-1 中的“常规”)

    因此,严格来说,您的签名在结构上甚至是无效的。但通常情况下,PDF 签名验证服务不会检测到这一点,因为它们中的大多数使用标准 PKCS#7/CMS 库或方法来验证签名容器。

    如果您想确保您的签名是真正有效的 PDF 签名,您可以通过替换来实现此目的

    return signedData.getEncoded();
    

    类似

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DEROutputStream dos = new DEROutputStream(baos);
    dos.writeObject(signedData.toASN1Structure());
    return baos.toByteArray();
    

    现在整个签名对象都是 DER 编码的。

    (您可以在此处找到使用原始代码和固定代码创建签名的测试,无论是否改进编码:SignLikeLoneWolf.java

    【讨论】:

    • 非常有帮助!
    • 我可以使用上面相同的代码生成带有可见数字符号的 PDF 吗?
    • 这个问题已经超过五年了。相反,您应该将新的签名代码基于 PDFBox 示例 CreateVisibleSignatureCreateVisibleSignature2 的当前版本。
    猜你喜欢
    • 1970-01-01
    • 2018-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-27
    • 2019-12-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多