Java Apache PDFBox - 生成外部签名的哈希值或将其合并回来的问题 - 将公钥证书链添加到 PDF

问题描述 投票:0回答:1

问题描述

我们有一个特定的业务/技术案例 - 我们需要对 PDF 进行电子签名,为此,我们正在与一家处理 PDF 哈希电子签名的公司合作。

我们或多或少知道如何从 PDF 中获取哈希值,并在外部服务通过 Rest API 签名后将其合并回来 - 但此过程中的某些内容会导致 PDF 显示“文档自签名以来已被更改或损坏” .

Resulting PDF with broken signature

我们是如何做的

我们正在使用库 Apache PDFBox 版本 3.0.0(我们也尝试了不同的版本),我们的解决方案基于官方 Apache 示例:

https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/

  • 创建签名
  • 创建签名库

我们在 stackoverlow 上阅读了许多描述与我们类似的问题的帖子,例如:

我们决定采用此处描述的解决方案Java PdfBox - PDF 签名问题 - 外部签名 - 无效签名 此签名中包含的格式或信息存在错误

我们的主类 - CreateSignature

package TOTPSignPDF.Service;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Base64;
import java.util.Calendar;

/*
    This solution was based on official examples from Apache:
    https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignature.java?revision=1899086&view=markup
    https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java?view=markup
 */

@Service
public class CreateSignature {

    private static final Logger LOG = LoggerFactory.getLogger(CreateSignature.class);

    //@Autowired
    //ServerSignature serverSignature;

    @Autowired
    ExternalSignature externalSignature;

    private Certificate[] certificateChain;

    /**
     * Signs the given PDF file.
     *
     * @param inFile the original PDF file
     */
    public void signDocument(File inFile) throws IOException {

        // we're being given the certificate chain with public key
        setCertificateChain(externalSignature.getCertificateChain());

        String name = inFile.getName();
        String substring = name.substring(0, name.lastIndexOf('.'));

        File outFile = new File(inFile.getParent(), substring + "_signed.pdf");
        loadPDFAndSign(inFile, outFile);
    }

    private void setCertificateChain(final Certificate[] certificateChain) {
        this.certificateChain = certificateChain;
    }

    /**
     * Signs the given PDF file.
     *
     * @param inFile  input PDF file
     * @param outFile output PDF file
     * @throws IOException if the input file could not be read
     */

    private void loadPDFAndSign(File inFile, File outFile) throws IOException {
        if (inFile == null || !inFile.exists()) {
            throw new FileNotFoundException("Document for signing does not exist");
        }

        // sign
        try (FileOutputStream fileOutputStream = new FileOutputStream(outFile);
             PDDocument doc = Loader.loadPDF(inFile)) {
            addSignatureDictionaryAndSignExternally(doc, fileOutputStream);
        }
    }

    private void addSignatureDictionaryAndSignExternally(PDDocument document, OutputStream output)
        throws IOException {

        int accessPermissions = SigUtils.getMDPPermission(document);
        if (accessPermissions == 1) {
            throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
        }

        // create signature dictionary
        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setName("Example User");
        signature.setLocation("Los Angeles, CA");
        signature.setReason("Testing");

        // the signing date, needed for valid signature
        signature.setSignDate(Calendar.getInstance());

        // Optional: certify 
        if (accessPermissions == 0) {
            SigUtils.setMDPPermission(document, signature, 2);
        }

        // it was if(isExternalSigning()) {
        document.addSignature(signature);
        ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
        // invoke external signature service - passing the document content with the "empty" signature added
        byte[] cmsSignature = sign(externalSigning.getContent());
        // set signature bytes received from the service
        externalSigning.setSignature(cmsSignature);
        //}

        // call SigUtils.checkCrossReferenceTable(document) if Adobe complains - we're doing it only because I ran out of ideas
        // and read https://stackoverflow.com/a/71293901/535646
        // and https://issues.apache.org/jira/browse/PDFBOX-5382
        SigUtils.checkCrossReferenceTable(document); // no errors here

        //document.close(); not needed because document is defined as auto-closeable resource in the function above
    }

    /**
     * SignatureInterface sample implementation.
     * Use your favorite cryptographic library to implement PKCS #7 signature creation.
     * If you want to create the hash and the signature separately (e.g. to transfer only the hash
     * to an external application), read <a href="https://stackoverflow.com/questions/41767351">this
     * answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
     *
     * @throws IOException
     */
    private byte[] sign(InputStream content) throws IOException {
        try {

            // get the hash of the document with additional signature field
            MessageDigest digest = MessageDigest.getInstance("SHA256", new BouncyCastleProvider());

            byte[] hashBytes = digest.digest(content.readAllBytes());
            String hashBase64 = new String(Base64.getEncoder().encode(hashBytes));
            LOG.info("Digest in Base64: " + hashBase64);

            // call External API to sign the hash - hash of the document with added field for signature
            //byte[] signedHashBytes = serverSignature.sign(hashBase64);
            byte[] signedHashBytes = externalSignature.sign(hashBytes);

            // this lower part of the code is based on this answer:
            // https://stackoverflow.com/questions/69676156/java-pdfbox-pdf-sign-problem-external-signature-invalid-signature-there-are
            //
            // In the standalone application this ContentSigner would be an implementation of signing process,
            // but we are given the signed hash from the External Api and we just have to return it - the same situation
            // as in this stackoverflow post
            ContentSigner nonSigner_signedHashProvided = new ContentSigner() {

                @Override
                public byte[] getSignature() {
                    return signedHashBytes;
                }

                @Override
                public OutputStream getOutputStream() {
                    return new ByteArrayOutputStream();
                }

                @Override
                public AlgorithmIdentifier getAlgorithmIdentifier() {
                    return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSA");
                }
            };

            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            X509Certificate certificateRepresentingTheSigner = (X509Certificate) certificateChain[0];

            gen.addSignerInfoGenerator(
                new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
                    .build(
                        nonSigner_signedHashProvided,
                        certificateRepresentingTheSigner
                    )
            );
            gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));

            //CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
            //these don't seem to change anything
            //CMSTypedData msg = new CMSProcessableByteArray(signedHashBytes);
            CMSTypedData msg = new CMSProcessableInputStream(new ByteArrayInputStream("not used".getBytes()));
            CMSSignedData signedData = gen.generate(msg, false);

            return signedData.getEncoded();
        } catch (GeneralSecurityException | CMSException | OperatorCreationException e) {
            throw new IOException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

模拟外部哈希签名过程的类 -ExternalSignature

我使用以下命令创建了带有自签名私钥的本地密钥库:

keytool -genkeypair -storepass 123456 -storetype pkcs12 -alias testKeystoreForPOC -validity 365 -keystore testKeystore.jks -keyalg RSA -sigalg SHA256withRSA

package TOTPSignPDF.Service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Enumeration;

@Service
public class ExternalSignature {
    private static final Logger LOG = LoggerFactory.getLogger(ExternalSignature.class);

    private Certificate[] certificateChain;

    private PrivateKey privateKey;

    public byte[] sign(byte[] bytesToSign) {
        LOG.info("Started signing process");
        try {
            Signature privateSignature = Signature.getInstance("SHA256withRSA");
            privateSignature.initSign(privateKey);
            privateSignature.update(bytesToSign);
            byte[] signature = privateSignature.sign();
            LOG.info("Finished signing process");
            return signature;
        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
            throw new RuntimeException(e);
        }
    }

    public Certificate[] getCertificateChain(){
        return this.certificateChain;
    }

    @PostConstruct
    private void initializePostConstruct() {
        File keystoreFile = new File("keystore/testKeystore.jks");

        try {
            KeyStore keystore = KeyStore.getInstance("PKCS12");

            char[] password = "123456".toCharArray();
            try (InputStream is = new FileInputStream(keystoreFile)) {
                keystore.load(is, password);
            }

            // grabs the first alias from the keystore and get the private key. An
            // alternative method or constructor could be used for setting a specific
            // alias that should be used.
            Enumeration<String> aliases = keystore.aliases();
            String alias;
            Certificate cert = null;
            while (cert == null && aliases.hasMoreElements()) {
                alias = aliases.nextElement();
                setPrivateKey((PrivateKey) keystore.getKey(alias, password));
                Certificate[] certChain = keystore.getCertificateChain(alias);
                if (certChain != null) {
                    setCertificateChain(certChain);
                    cert = certChain[0];
                    if (cert instanceof X509Certificate) {
                        // avoid expired certificate
                        ((X509Certificate) cert).checkValidity();

                        SigUtils.checkCertificateUsage((X509Certificate) cert);
                    }
                }
            }

            if (cert == null) {
                throw new IOException("Could not find certificate");
            }
        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public final void setPrivateKey(PrivateKey privateKey) {
        LOG.info("Set new private key");
        String base64OfPrivateKey = new String(Base64.getEncoder().encode(privateKey.getEncoded()));
        LOG.info("base64OfPrivateKey: " + base64OfPrivateKey);
        this.privateKey = privateKey;
    }

    public final void setCertificateChain(final Certificate[] certificateChain) {
        LOG.info("Set new certificate chain, size: " + certificateChain.length);
        try {
            String base64OfPublicKey = new String(Base64.getEncoder().encode(certificateChain[0].getEncoded()));
            LOG.info("base64OfPublicKey: " + base64OfPublicKey);
        } catch (CertificateEncodingException e) {
            throw new RuntimeException(e);
        }
        this.certificateChain = certificateChain;
    }
}

Apache 示例中使用的其他类:

  • CMSProcessableInputStream
  • SigUtils

请帮助我们

据我们了解该网站上围绕类似问题的其他帖子,一切都应该有效,但事实并非如此。 我们很可能错过了一些关键步骤。

我们期待任何回复,每一条信息都可以帮助我们。

我不明白的是为什么 PDF 的摘要(哈希)这么小:

LXl/LHCaxrf7lYlN8d8m7gDNp9DRqY+azvxCS/mB3uY=

看起来尺寸正确吗?

PDF 示例和公钥/私钥

  • 签名后的PDF

  • 签名前的PDF

  • 用于签名过程的密钥库

  • Base64 中的公钥:

    MIIDUTCCAjmgAwIBAgIEZPgGSjANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJURTENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVzdDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDEOMAwGA1UEAxMFSmFuIFMwHhcNMjMwODI0MTgzMTM2WhcNMjQwODIzMTgzMTM2WjBZMQswCQYDVQQGEwJURTENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVzdDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDEOMAwGA1UEAxMFSmFuIFMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEO5+UBxpzSI5vtmPvzlyz0d1yHph33VG9bFkqQ7bnmE3ghHiFyGTBrD7IPTrBg1NjhdC5jqpa/acY/oWUAwV5dlyD02nVBoy7VBLvz7QOc9QPaUQ2frVY4Qy05qgLZEGgnbpG2X44sqV8ogW1y2IaY+2F9NBKralp8r6M/sB4DKJmTWd+1UOlpnlfr3H5KvX+YXzfC+KVDIEoG7yzXZuw7I3x0VawVz/5gx3FAkagi7yHh9J58kmImpOFquxcK6SdMCoBsWmFmHmCpExVfxrVLMpIV5a5AHDWn84YHHRKC5kvhnyujXaIHFV+78TFxk8DGnPnKcgFRWKRVyfcYT7hAgMBAAGjITAfMB0GA1UdDgQWBBQFR8qbfk35MQdMqxQhZ2kkLoLPDDANBgkqhkiG9w0BAQsFAAOCAQEAI6aAn6zKcRieUkyHraRswYhxRjpc1fDaeDz01XanqxUIpNf31dU+f62oo5Dv4VAgd6MzLdFSpcERB9ScJzIIrz7mnqS0r8LhesDejs/mnDg2+E89XHy7yZtU3MqAXmESe7qms8AB1F68YZ+OOjWfn1AZfjaAzU0La49NaFbQE96vBLBGNX2Efavk7trv9PMCKMio19w/FBGwH8cU0erAi/WZhRlX3C1eFfmGSs5BWZL03yOmdeR/PVNut135dbk0EJ9rn6TJp1KgoNqcnsbPZcSxmUB/3U/hWUoFb6UN7g+1NbVh0PHCZ2Kbwyk6mO2viNbXka1WuopUxGh1RmJW9A==

  • Base64 格式的私钥:

    MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCEO5+UBxpzSI5vtmPvzlyz0d1yHph33VG9bFkqQ7bnmE3ghHiFyGTBrD7IPTrBg1NjhdC5jqpa/acY/oWUAwV5dlyD02nVBoy7VBLvz7QOc9QPaUQ2frVY4Qy05qgLZEGgnbpG2X44sqV8ogW1y2IaY+2F9NBKralp8r6M/sB4DKJmTWd+1UOlpnlfr3H5KvX+YXzfC+KVDIEoG7yzXZuw7I3x0VawVz/5gx3FAkagi7yHh9J58kmImpOFquxcK6SdMCoBsWmFmHmCpExVfxrVLMpIV5a5AHDWn84YHHRKC5kvhnyujXaIHFV+78TFxk8DGnPnKcgFRWKRVyfcYT7hAgMBAAECggEAdp8iBWHl6XsyQ6bDufFOmgVu+RvXPNfuptXWmyKJpvKrEfjkQWdGc7L30xuSZNxRZxs45ezEh8G6L6LL475eH9r9HUj/TJmGj9nY7wZNiRWBK54MEjLSrfudMX8lSqrScKpt23bqUyR3bfnO04my5Oe1wRCf9g4ZxzB6nfM+Z7Hq+HxJRk5KlkMCGCf5Q0kA0t2FCkEHyvLUKft/ba05/XNODnDbA3n7AkWCzKmU9ypRen2OjqWaamQAaD0eJvxXDMEsAbZ+xGzD68GKs9+glmF9iZUoDwFMa3aCqVTpskOUtYxKHXVwgbhZoPpgmC+NPjlwaWdntp8a77q/Z0Jg0QKBgQDEwxbwvUHG3vtUK27WMBW7UunwUHuyA5ECaZG6Spm1rq6OhGLaSOWDNUF9VVgLTEkW6M/stxEuTqzWT1lbUqGIYeOkP6e5tD9LIYoUb6Sozpa6f0cwEsxNKpVOxRIn9tIOx3dZS164+rT1d75+t6W9tHPGFaOZJOKwObn9eoJK7QKBgQCsCx7xQq+rawXoNPQBBABSvLe4alxCQ8MhBDhFYjWAh9scfocCDFHSYXzG1AhMG5dBMNWvkL3xuXVw1ba7FltSb1VHvjQqkr9AV+QNR4sCorVy1tNIuH63Jwtsn3UkYr0CUU6u/sWZW5pirs+fobFv2DZraPnB3j5z1d8RpjShRQKBgF3ilMSkGYmyBgxgeQ98fDIY2wVO8ea76upSwzU3uWZGhoX8R0rOs6zKsYgDO/KQIOPsjKHvrCQDaFcOH54CrI7t3ngV44sppXXM+BzONKxTfvpYFviqT4+WfQ3L3ODy1cI1jQ4vd3AeOFBUJbJDILOHMiLXWmuNfRkHQmbfmOH1AoGAFilQkQ9gBZrBpgm8LK1RRVcd61l4DOkhp40dmoJuFeJqLR93UKI5n/oC0rHZZ8ReFX2u6PCiJxMWt7Qv16WnmdTRjW5I1fsVO7qWm8dNdsdyzBo0GTf6yqjy5ckck9VMN5I1qoES/xA3sOKHyC5R5vBZAjkBgyGXteAk3eck/GkCgYEArm4brBpXj+Ox6Korw0Hk2bdk30R7BAm2+N+E1683wJBnWP3kiGGRDaGulUtoq2HthXr42ZyfwtuiS9UrAJFOWDLy3UYxeR1DYDFoq+mdy1fR8IniWXyGZQ6goJ0t/SNIN1NsRnh03Fc8iqRqYEs3anvuOr8xGvMsxXW0qT9OLcY=

java pdf hash pdfbox electronic-signature
1个回答
0
投票

分析您的示例文件,我们会发现签名中的哈希值都不正确。此外,人们发现 CMS 签名容器并不是您的黑客工作所需的简单形式。

创建一个简单的签名容器(无签名属性)

您希望创建一个简单的签名容器(没有签名属性,签名字节直接对数据进行签名),以便您的

ContentSigner
黑客工作。

不过,您的示例文件包含带有签名属性的签名容器。因此,签名属性和签名字节中的哈希都是错误的,前者是因为您使用

"not used".getBytes()
作为生成器输入,后者是因为它不是基于签名属性。

原因是您没有完全复制用作模板的 answer 中的代码:您在那里删除了

JcaSignerInfoGeneratorBuilder
方法
setDirectSignature
的调用:

    JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build());
    sigb.setDirectSignature(true);

但是,如果

JcaSignerInfoGeneratorBuilder
设置为
DirectSignature
,则
true
仅创建简单的签名容器结构。

因此,请填写该代码的副本。

签署正确的哈希值

即使切换到创建简单签名容器后,您也会签署错误的哈希值,因为在

ExternalSignature
开头使用
MessageDigest.getInstance("SHA256", new BouncyCastleProvider())
对数据进行哈希处理后,您的流程使用
CreateSignature.sign
测试替换进行哈希处理,然后使用
Signature.getInstance("SHA256withRSA") 再次进行哈希处理
ExternalSignature.sign

要使该流程正常工作,您必须删除其中一项哈希计算。

不过,我不知道您的

ExternalSignature
测试替换是否真正正确地代表了您的
ServerSignature
功能。如果后者不散列自身,但确实需要预先散列的数据,那么在切换到创建简单签名容器后,您的流程可能会毫不费力地工作。

顺便说一句

为什么坚持创建简单的签名容器?每个需要认真对待的签名配置文件都需要使用签名属性。

© www.soinside.com 2019 - 2024. All rights reserved.