Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create pkcs7 signature from file digest

Currently i have a client-server application that, given a PDF file, signs it (with the server certificate), attachs the signature with the original file and returns the output back to the client (all of this is achieved with PDFBox).
I have a Signature handler, which is my External Signing Support (where content is the PDF file)

    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);
    }
}

It works fine, but i was thinking - what if the PDF file is too big to be uploaded? ex: 100mb... it would take forever! Given that, i am trying to figure out, if instead of signing the PDF file, is it possible to just sign the Hash (ex SHA1) of that file and than the client puts it all together in the end?

Update:

I have been trying to figure this out, and now my signing method is:

    @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);
    }

}

And i am merging the signature with the PDF with pdfbox

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

The problem is that Adobe says the signature is invalid because the "document has been altered or corrupted since it was signed". Can anyone help?

like image 605
Snox Avatar asked Jan 20 '17 15:01

Snox


People also ask

What is a PKCS7 signature?

PKCS #7 is the specific standard used for generation and verification of digital signatures and certificates managed by a PKI (Public Key Infrastructure). This standard served as the basis for the S/MIME (Secure/Multipurpose Internet Mail Extensions) standard.

How do you open the signature on PKCS 7?

After you receive the certificate from the CA, double-click on the certificate to open it. Locate the path of the certificate on your computer and double-click on the certificate again to open it.

Is PKCS7 private key?

When used for distribution purposes, the PKCS #7 package as a whole is neither signed nor encrypted. As with the single binary certificate, the PKCS #7 package does not contain any private keys.

What is PKCS7 encryption?

In cryptography, "PKCS #7: Cryptographic Message Syntax" (a.k.a. "CMS") is a standard syntax for storing signed and/or encrypted data. PKCS #7 is one of the family of standards called Public-Key Cryptography Standards (PKCS) created by RSA Laboratories. The latest version, 1.5, is available as RFC 2315.


1 Answers

In his update the OP nearly has it right, there merely are two errors:

  • He tries to read the InputStream parameter content twice:

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

    Thus, all data had already been read from the stream before the second attempt which consequently returned an empty byte[]. So the message digest attribute contained a wrong hash value.

  • He creates the final CMS container in a convoluted way:

    return new CMSSignedData(msg, s.getEncoded()).getEncoded();
    

Reducing the latter to what is actually needed, it turns out that there is no need for the CMSTypedData msg anymore. Thus, the former is implicitly resolved.

After re-arranging the digest calculation to the top of the method and additionally switching to SHA256 (as SHA1 is deprecated in many contexts, I prefer to use a different hash algorithm) and allowing for a certificate chain instead of a single certificate, the method looks like this:

// 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 method signWithSeparatedHashing)

Used in a fairly minimal signing code frame

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 method sign)

like this

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 test method testSignWithSeparatedHashing)

results in properly signed PDFs, as proper at least as the certificates and private key in question are for the task at hand.


One remark:

The OP used IOUtils.toByteArray(content)) (and so do I in the code above). But considering the OP's starting remark

what if the PDF file is too big to be uploaded? ex: 100mb

doing so is not such a great idea as it loads a big file into memory at once only for hashing. If one really wants to consider the resource footprint of one's application, one should read the stream a few KB at a time and consecutively digest the data using MessageDigest.update and only use MessageDigest.digest at the end to get the result hash value.

like image 117
mkl Avatar answered Sep 20 '22 03:09

mkl