【问题标题】:Create pkcs7 signature from file digest从文件摘要创建 pkcs7 签名
【发布时间】:2017-01-20 15:39:56
【问题描述】:

目前我有一个客户端-服务器应用程序,给定一个 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 文件,是否可以只签署该文件的哈希(前 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);
    }

}

我正在使用 pdfbox 将签名与 PDF 合并

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

问题在于 Adob​​e 说签名无效,因为“文档自签名后已被更改或损坏”。 有人可以帮忙吗?

【问题讨论】:

  • 这是可能的(并且是明智的)。如果没有人很快回答,我会在下周调查。
  • @mkl 非常感谢。在那之前我会继续我的研究(现在我知道这是可能的)
  • 关于您的更新:一个明显的问题是您试图阅读InputStream 两次。第二次尝试将始终导致一个空数组。我仍将对此进行调查。 ;)

标签: java pdfbox pkcs#7


【解决方案1】:

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

  • 他尝试读取InputStream参数内容两次:

    CMSTypedData msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));
    [...]
    Attribute attr = new Attribute(CMSAttributes.messageDigest,
            new DERSet(new DEROctetString(md.digest(IOUtils.toByteArray(content)))));
    

    因此,在第二次尝试之前已经从流中读取了所有数据,因此返回了一个空的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 以获得结果哈希值。

【讨论】:

  • 非常感谢! :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-13
  • 2011-01-10
  • 2016-05-08
相关资源
最近更新 更多