从文件摘要创建pkcs7签名


问题内容

目前,我有一个客户端-服务器应用程序,给定一个PDF文件,使用服务器证书对其进行签名,将签名与原始文件一起附加,然后将输出返回给客户端(所有这些操作均通过PDFBox实现)。
我有一个签名处理程序,这是我的外部签名支持(其中内容为PDF文件)

    public byte[] sign(InputStream content) throws IOException {
    try {
        System.out.println("Generating CMS signed data");
        CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
        ContentSigner sha1Signer = new JcaContentSignerBuilder("Sha1WithRSA").build(privateKey);
        generator.addSignerInfoGenerator(
                new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build())
                        .build(sha1Signer, new X509CertificateHolder(certificate.getEncoded())));
        CMSTypedData cmsData = new CMSProcessableByteArray(IOUtils.toByteArray(content));
        CMSSignedData signedData = generator.generate(cmsData, false);

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

它工作正常,但我在想-如果PDF文件太大而无法上传怎么办?例如:100mb
…这将永远!鉴于此,我想弄清楚,如果不对PDF文件签名,是否可以仅对文件的哈希(ex SHA1)签名,然后客户端将它们放到最后?

更新:

我一直在试图找出答案,现在我的签名方法是:

    @Override
public byte[] sign(InputStream content) throws IOException {
    // testSHA1WithRSAAndAttributeTable
    try {
        MessageDigest md = MessageDigest.getInstance("SHA1", "BC");
        List<Certificate> certList = new ArrayList<Certificate>();
        CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));

        certList.add(certificate);

        Store certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        Attribute attr = new Attribute(CMSAttributes.messageDigest,
                new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));

        ASN1EncodableVector v = new ASN1EncodableVector();

        v.add(attr);

        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

        AlgorithmIdentifier sha1withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA");

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        InputStream in = new ByteArrayInputStream(certificate.getEncoded());
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

        gen.addSignerInfoGenerator(builder.build(
                new BcRSAContentSignerBuilder(sha1withRSA,
                        new DefaultDigestAlgorithmIdentifierFinder().find(sha1withRSA))
                                .build(PrivateKeyFactory.createKey(privateKey.getEncoded())),
                new JcaX509CertificateHolder(cert)));

        gen.addCertificates(certs);

        CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
        return new CMSSignedData(msg, s.getEncoded()).getEncoded();

    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        throw new IOException(e);
    }

}

我将签名与PDF和pdfbox合并

            ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);
        byte[] cmsSignature = sign(externalSigning.getContent());
        externalSigning.setSignature(cmsSignature);

问题在于Adobe表示签名无效,因为“自签名以来,文档已被更改或破坏”。有人可以帮忙吗?


问题答案:

在他的更新中,OP几乎正确,只有两个错误:

  • 他尝试InputStream两次读取参数内容:
        CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));
    [...]
    Attribute attr = new Attribute(CMSAttributes.messageDigest,
            new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));

因此,在第二次尝试之前已经从流中读取了所有数据,因此返回了empty byte[]。因此,消息摘要属性包含错误的哈希值。

  • 他以复杂的方式创建了最终的CMS容器:
    return new CMSSignedData(msg, s.getEncoded()).getEncoded();
    

将后者减少到实际需要的数量,事实证明不再需要CMSTypedData msg。因此,前者被隐式解决。

在将摘要计算重新安排到方法的顶部并另外切换到SHA256后(由于在许多情况下不赞成使用SHA1,我更喜欢使用其他哈希算法)并允许使用证书chain而不是单个证书certificate,该方法看起来像这个:

    // Digest generation step
    MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
    byte[] digest = md.digest(IOUtils.toByteArray(content));

    // Separate signature container creation step
    List<Certificate> certList = Arrays.asList(chain);
    JcaCertStore certs = new JcaCertStore(certList);

    CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

    Attribute attr = new Attribute(CMSAttributes.messageDigest,
            new DERSet(new DEROctetString(digest)));

    ASN1EncodableVector v = new ASN1EncodableVector();

    v.add(attr);

    SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
            .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

    AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");

    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
    InputStream in = new ByteArrayInputStream(chain[0].getEncoded());
    X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

    gen.addSignerInfoGenerator(builder.build(
            new BcRSAContentSignerBuilder(sha256withRSA,
                    new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
                            .build(PrivateKeyFactory.createKey(pk.getEncoded())),
            new JcaX509CertificateHolder(cert)));

    gen.addCertificates(certs);

    CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
    return s.getEncoded();

CreateSignature方法signWithSeparatedHashing

用在相当少的签名代码框架中

    void sign(PDDocument document, OutputStream output, SignatureInterface signatureInterface) throws IOException
    {
        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");
        signature.setSignDate(Calendar.getInstance());
        document.addSignature(signature);
        ExternalSigningSupport externalSigning =
                document.saveIncrementalForExternalSigning(output);
        byte[] cmsSignature = signatureInterface.sign(externalSigning.getContent());
        externalSigning.setSignature(cmsSignature);
    }

CreateSignature方法sign

像这样

    try (   InputStream resource = getClass().getResourceAsStream("test.pdf");
            OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "testSignedWithSeparatedHashing.pdf"));
            PDDocument pdDocument = PDDocument.load(resource)   )
    {
        sign(pdDocument, result, data -> signWithSeparatedHashing(data));
    }

CreateSignature测试方法testSignWithSeparatedHashing

导致正确签名的PDF,至少与所讨论的证书和私钥适合当前任务一样适当。


一句话:

使用的OP IOUtils.toByteArray(content))(我在上面的代码中也是如此)。但是考虑到OP的开场白

如果PDF文件太大而无法上传怎么办?例如:100mb

这样做并不是一个好主意,因为它一次仅将一个大文件加载到内存中以进行哈希处理。如果确实要考虑一个应用程序的资源占用量,则应一次读取该流几个KB,然后连续使用来消化数据,MessageDigest.update并且仅MessageDigest.digest在最后使用来获得结果哈希值。