使用现有证书、中间文件和远程创建的签名,使用 itextpdf 对 Java 的 PDF 进行两步签名

Two-steps signing of PDF with itextpdf for Java using existing certificate, intermediary file and remote created signature

用例如下:本地应用程序必须使用远程服务器上生成的完整 PKCS#7 分离签名签署 PDF 文档,这需要一次性- e-mail/SMS 发送的密码;远程服务器根据关联的文档哈希生成分离签名,生成的签名是“外部 signature”,在CMS RFC (RFC5652), section 5.2中定义,例如生成的签名文件是一个单独的文件。


几天来我一直在努力为这个用例(使用 itextpdf 5.5.13.1)实施一个可行的解决方案,但最终签名的文件有错误的签名消息 "Certification by is invalid" -- 请参阅附件中的 "signed_with_error" 文件。 概念性 "two-steps signing" 实施过程是:

第一步:从原始文档文件--见"original"附件--我创建一个中间文件--见"intermediary"附加文件——我在其中插入了一个空签名,相关文档散列是基于该签名创建的。这一步保存签名摘要,以备下一步使用

第 2 步: 远程服务器被调用,分离的签名文件被接收,我通过在预签名文件的签名中插入内容来创建签名文件上一步的签名摘要和从远程服务器接收到的分离签名内容。

原始、中间和签名错误样本文件为:
original
intermediary
signed_with_error

有人知道我下面的代码可能有什么问题吗?
相关代码部分如下:

第一步:

        ByteArrayOutputStream preSignedDocument = new ByteArrayOutputStream();
        Path customerPathInDataStorage = storageService.resolveCustomerPathInDataStorage(customerExternalId);
        PdfReader pdfReader = new PdfReader(originalDocumentContent);
        PdfStamper stamper = PdfStamper.createSignature(pdfReader, preSignedDocument, '[=11=]', customerPathInDataStorage.toFile(), true);

        // create certificate chain using certificate received from remote server system
        byte[] certificateContent = certificateInfo.getData(); // this is the customer certificate received one time from the remote server and used for every document signing initialization
        X509Certificate certificate = SigningUtils.buildCertificateFromCertificateContent(certificateContent);
        java.security.cert.Certificate[] certificatesChain = CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertPath(
                Collections.singletonList(certificate)).getCertificates().toArray(new java.security.cert.Certificate[0]);

        // create empty digital signature inside pre-signed document
        PdfSignatureAppearance signatureAppearance = stamper.getSignatureAppearance();
        signatureAppearance.setVisibleSignature(new Rectangle(72, 750, 400, 770), 1, "Signature_" + customerExternalId);
        signatureAppearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
        signatureAppearance.setCertificate(certificate);
        CustomPreSignExternalSignature externalSignatureContainer =
                new CustomPreSignExternalSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        MakeSignature.signExternalContainer(signatureAppearance, externalSignatureContainer, 8192);

        ExternalDigest digest = new SignExternalDigest();
        PdfPKCS7 pdfPKCS7 = new PdfPKCS7(null, certificatesChain, DOCUMENT_HASHING_ALGORITHM, null, digest, false);
        byte[] signatureDigest = externalSignatureContainer.getSignatureDigest();
        byte[] authAttributes = pdfPKCS7.getAuthenticatedAttributeBytes(signatureDigest, null, null,
                MakeSignature.CryptoStandard.CMS);

        pdfReader.close();

        documentDetails.setPreSignedContent(preSignedDocument.toByteArray()); // this is the intermediary document content used in 2nd step in the line with the comment ***PRESIGNED_CONTENT****
        documentDetails.setSignatureDigest(signatureDigest); // this is the signature digest used in 2nd step in the line with comment ****SIGNATURE_DIGEST****
        byte[] hashForSigning = DigestAlgorithms.digest(new ByteArrayInputStream(authAttributes),
                digest.getMessageDigest(DOCUMENT_HASHING_ALGORITHM));
        documentDetails.setSigningHash(hashForSigning); // this is the hash sent to remote server for signing

第二步:

        // create certificate chain from detached signature
        byte[] detachedSignatureContent = documentDetachedSignature.getSignature(); // this is the detached signature file content received from the remote server which contains also customer the certificate
        X509Certificate certificate = SigningUtils.extractCertificateFromDetachedSignatureContent(detachedSignatureContent);
        java.security.cert.Certificate[] certificatesChain = CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertPath(
                    Collections.singletonList(certificate)).getCertificates().toArray(new java.security.cert.Certificate[0]);

        // create digital signature from detached signature
        ExternalDigest digest = new SignExternalDigest();
        PdfPKCS7 pdfPKCS7 = new PdfPKCS7(null, certificatesChain, DOCUMENT_HASHING_ALGORITHM, null, digest, false);

        pdfPKCS7.setExternalDigest(detachedSignatureContent, null, SIGNATURE_ENCRYPTION_ALGORITHM);
        byte[] signatureDigest = documentVersion.getSignatureDigest(); // this is the value from 1st step for ****SIGNATURE_DIGEST****
        byte[] encodedSignature = pdfPKCS7.getEncodedPKCS7(signatureDigest, null, null, null, MakeSignature.CryptoStandard.CMS);
        ExternalSignatureContainer externalSignatureContainer = new CustomExternalSignature(encodedSignature);

        // add signature content to existing signature container of the intermediary PDF document
        PdfReader pdfReader = new PdfReader(preSignedDocumentContent);// this is the value from 1st step for ***PRESIGNED_CONTENT****
        ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream();
        MakeSignature.signDeferred(pdfReader, "Signature_" + customerExternalId, signedPdfOutput, externalSignatureContainer);

        return signedPdfOutput.toByteArray();

依赖关系:

public static final String DOCUMENT_HASHING_ALGORITHM = "SHA256";
public static final String CERTIFICATE_TYPE = "X.509";
public static final String SIGNATURE_ENCRYPTION_ALGORITHM = "RSA";

public class CustomPreSignExternalSignature implements ExternalSignatureContainer {

    private static final Logger logger = LoggerFactory.getLogger(CustomPreSignExternalSignature.class);

    private PdfDictionary dictionary;
    private byte[] signatureDigest;

    public CustomPreSignExternalSignature(PdfName filter, PdfName subFilter) {
        dictionary = new PdfDictionary();
        dictionary.put(PdfName.FILTER, filter);
        dictionary.put(PdfName.SUBFILTER, subFilter);
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            ExternalDigest digest = new SignExternalDigest();
            signatureDigest = DigestAlgorithms.digest(data, digest.getMessageDigest(DOCUMENT_HASHING_ALGORITHM));
        } catch (IOException e) {
            logger.error("CustomSignExternalSignature - can not create hash to be signed", e);
            throw new GeneralSecurityException("CustomPreSignExternalSignature - can not create hash to be signed", e);
        }

        return new byte[0];
    }

    @Override
    public void modifySigningDictionary(PdfDictionary pdfDictionary) {
        pdfDictionary.putAll(dictionary);
    }

    public byte[] getSignatureDigest() {
        return signatureDigest;
    }
}

public class CustomExternalSignature implements ExternalSignatureContainer {

        private byte[] signatureContent;

        public CustomExternalSignature(byte[] signatureContent) {
            this.signatureContent = signatureContent;
        }

        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            return signatureContent;
        }

        @Override
        public void modifySigningDictionary(PdfDictionary pdfDictionary) {
        }
    }

public class SignExternalDigest implements ExternalDigest {

    @Override
    public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException {
        return DigestAlgorithms.getMessageDigest(hashAlgorithm.toUpperCase(), null);
    }

}

稍后评论:

即使它不正确,我也观察到如果在第二步中,而不是行:

byte[] signatureDigest = documentVersion.getSignatureDigest(); // this is the value from 1st step for ****SIGNATURE_DIGEST****

我将使用:

byte[] signatureDigest = new byte[0];

生成的签名文件带有可以验证的可见证书,但 PDF 文档无效,错误 "Document certification is INVALID"。 --- "The document has been altered or corrupted since Certification was applied." - 见附件 signed_certification_invalid。看起来无效是合法的,但对我来说很奇怪为什么在这种情况下证书显示在文档中但在使用时它被破坏了 - 我认为 - "the right" signatureDigest 值。

你说

the remote server generates the detached signature based on the associated document hash and the resulting signature is an “external signature”, as defined in the CMS RFC (RFC5652), section 5.2

因此,您在第 2 步中在这里检索到的内容:

// create certificate chain from detached signature
byte[] detachedSignatureContent = documentDetachedSignature.getSignature(); // this is the detached signature file content received from the remote server which contains also customer the certificate

已经是CMS签名容器。因此,您不能再像在以下几行中那样将其插入 PKCS#7 / CMS 签名容器中,但可以立即将其插入 PDF 中。

所以你的第二步应该是

// create certificate chain from detached signature
byte[] detachedSignatureContent = documentDetachedSignature.getSignature(); // this is the detached signature file content received from the remote server which contains also customer the certificate
ExternalSignatureContainer externalSignatureContainer = new CustomExternalSignature(detachedSignatureContent);

// add signature content to existing signature container of the intermediary PDF document
PdfReader pdfReader = new PdfReader(preSignedDocumentContent);// this is the value from 1st step for ***PRESIGNED_CONTENT****
ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream();
MakeSignature.signDeferred(pdfReader, "Signature_" + customerExternalId, signedPdfOutput, externalSignatureContainer);

return signedPdfOutput.toByteArray();

此外,错误的散列被签名。事实上,在第 1 步中你不应该玩弄 PdfPKCS7 class 而是简单地使用

documentDetails.setSigningHash(externalSignatureContainer.getSignatureDigest()); // this is the hash sent to remote server for signing