1

I am trying to digitally sign document using pdfbox. I am using filter as FILTER_ADOBE_PPKLITE and subfilter as SUBFILTER_ETSI_CADES_DETACHED. For ETSI_CADES_Detached, a signing attribute is needs to be added. I am fetching signed hash and certificates from CSC> But after adding signing attribute, it is making the document corrupted. Sharing the screenshot for the reference

error image

1

Seems like hash is getting chagned. Sharing code for the reference.

PDDocument document = PDDocument.load(inputStream);

            outFile = File.createTempFile("signedFIle", ".pdf");

            Certificate[] certificateChain = retrieveCertificates(requestId, providerId, credentialId, accessToken);//Retrieve certificates from CSC.


            setCertificateChain(certificateChain);

            // sign
            FileOutputStream output = new FileOutputStream(outFile);
            IOUtils.copy(inputStream, output);

            // create signature dictionary
            PDSignature signature = new PDSignature();
//            signature.setType(COSName.SIG);


//        PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(null);

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

            

            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);
            signature.setName("Test Name");
//            signature.setLocation("Bucharest, RO");
//            signature.setReason("PDFBox Signing");
            signature.setSignDate(Calendar.getInstance());
            Rectangle2D humanRect = new Rectangle2D.Float(location.getLeft(), location.getBottom(), location.getRight(), location.getTop());

            PDRectangle rect = createSignatureRectangle(document, humanRect);

            SignatureOptions signatureOptions = new SignatureOptions();
            signatureOptions.setVisualSignature(createVisualSignatureTemplate(document, 0, rect, signature));
            signatureOptions.setPage(0);
            document.addSignature(signature, signatureOptions);


            ExternalSigningSupport externalSigning =
                    document.saveIncrementalForExternalSigning(output);

            InputStream content = externalSigning.getContent();



            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            X509Certificate cert = (X509Certificate) certificateChain[0];
            gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));

            MessageDigest digest = MessageDigest.getInstance("SHA-256");

            // Use a buffer to read the input stream in chunks
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = content.read(buffer)) != -1) {
                digest.update(buffer, 0, bytesRead);
            }

            byte[] hashBytes = digest.digest();

            ESSCertIDv2 certid = new ESSCertIDv2(
                    new AlgorithmIdentifier(new ASN1ObjectIdentifier("*****")),
                    MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
            );

            SigningCertificateV2 sigcert = new SigningCertificateV2(certid);

            final DERSet attrValues = new DERSet(sigcert);
            Attribute attr = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, attrValues);
            ASN1EncodableVector v = new ASN1EncodableVector();
            v.add(attr);
            v.add(new Attribute(CMSAttributes.messageDigest, new DERSet(new DEROctetString(hashBytes))));

            AttributeTable atttributeTable = new AttributeTable(v);
            //Create a standard attribute table from the passed in parameters - certhash
            CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(atttributeTable);


            final byte[] signedHash = signHash(requestId, providerId, accessToken, hashBytes); //Retrieve signed hash from CSC.

            ContentSigner contentSigner = new ContentSigner() {

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

                @Override
                public OutputStream getOutputStream() {
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    byteArrayOutputStream.writeBytes(hashBytes);
                    return byteArrayOutputStream;
                }

                @Override
                public AlgorithmIdentifier getAlgorithmIdentifier() {
                    return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
                }
            };



            org.bouncycastle.asn1.x509.Certificate cert2 = org.bouncycastle.asn1.x509.Certificate.getInstance(ASN1Primitive.fromByteArray(cert.getEncoded()));
            JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build());

//            RevocationInfoResponse revocationInfoResponse = sealingService.getRevocationInfo(requestId, accessToken, revocationInfoRequest);



            sigb.setSignedAttributeGenerator(attrGen);


//            sigb.setDirectSignature( true );
            gen.addSignerInfoGenerator(sigb.build(contentSigner, new X509CertificateHolder(cert2)));
            CMSTypedData msg = new CMSProcessableInputStream( inputStream);

            CMSSignedData signedData = gen.generate((CMSTypedData)msg, false);
            byte[] cmsSignature = signedData.getEncoded();

            inputStream.close();

            externalSigning.setSignature(cmsSignature);
            IOUtils.closeQuietly(signatureOptions);

            return new FileInputStream(outFile);

If I use subfilter as SUBFILTER_ADBE_PKCS7_DETACHED and don't add addtibutesTable, then it works fine. But for SUBFILTER_ETSI_CADES_DETACHED, attributes needs to be added.

  • In your code you completely ignore that the _signed attributes_ shall be _**signed** attributes_. Your code simply signs the plain document hash. To fix this, you first need to make sure that the `attrGen` creates a message-digest attribute with the value of `hashBytes` and that you replace your `ContentSigner nonSigner` by a `ContentSigner` that actually does sign the bytes it retrieves to sign. Reading [RFC 5652](https://www.rfc-editor.org/rfc/rfc5652) may help you understand what needs to be done... – mkl Feb 20 '23 at 22:05
  • @mkl, I have updated the code, added message-digest attribute with the value of hashBytes and also made some changes while initialising contentSigner. But as told in description we fetch certificates and signedHash from some external provide (CSC). We don't have privateKey. So, after fetching signedHash from CSC, we are directly overriding the method getSignature of ContentSignature and returning that signedHash. ContentSigner cannot actually sign the bytes. Can you please provide way how I can resolve this? – Tanmay Sharma Feb 21 '23 at 11:51
  • @mkl Is there any source/documentation for PDFBox which can help in better in-depth understanding of the library? – Himanshu Jindal Feb 27 '23 at 12:56
  • @HimanshuJindal *"Is there any source/documentation for PDFBox which can help in better in-depth understanding of the library?"* - Well, in my opinion looking at the source code gives the best understanding, and PDFBox is open source. So simply look there. Furthermore, The PDFBox API often expects you to know the format PDF internally. Thus, have a look at ISO 32000. A part 1 equivalent can be downloaded at https://Adobe.com/go/pdfreference – mkl Feb 27 '23 at 13:27

1 Answers1

2

In a comment you explained

we fetch certificates and signedHash from some external provide (CSC). We don't have privateKey. So, after fetching signedHash from CSC, we are directly overriding the method getSignature of ContentSignature and returning that signedHash. ContentSigner cannot actually sign the bytes.

ContentSigner can actually sign the bytes. You merely have to call CSC signing from within it.

Simplifying your code a bit you can do that as follows:

try (
    OutputStream output = new FileOutputStream(outFile);
    PDDocument document = PDDocument.load(resource)   
) {
    PDSignature signature = new PDSignature();

    signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
    signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);
    signature.setName("Test Name");
    signature.setSignDate(Calendar.getInstance());

    SignatureOptions signatureOptions = new SignatureOptions();
    signatureOptions.setPage(0);

    document.addSignature(signature, signatureOptions);
    ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);

    // retrieve signer certificate and its chain
    Certificate[] certificateChain = retrieveCertificates(requestId, providerId, credentialId, accessToken);
    X509Certificate cert = (X509Certificate) certificateChain[0];

    // build signed attribute table generator and SignerInfo generator builder
    ESSCertIDv2 certid = new ESSCertIDv2(
            new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
            MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
    );
    SigningCertificateV2 sigcert = new SigningCertificateV2(certid);
    Attribute attr = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(sigcert));

    ASN1EncodableVector v = new ASN1EncodableVector();
    v.add(attr);
    AttributeTable atttributeTable = new AttributeTable(v);
    CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(atttributeTable);

    org.bouncycastle.asn1.x509.Certificate cert2 = org.bouncycastle.asn1.x509.Certificate.getInstance(ASN1Primitive.fromByteArray(cert.getEncoded()));
    JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build());
    sigb.setSignedAttributeGenerator(attrGen);

    // create ContentSigner that signs by calling the CSC endpoint
    ContentSigner contentSigner = new ContentSigner() {
        private MessageDigest digest = MessageDigest.getInstance("SHA-256");
        private OutputStream stream = OutputStreamFactory.createStream(digest);

        @Override
        public byte[] getSignature() {
            try {
                byte[] hash = digest.digest();
                byte[] signedHash = signHash(requestId, providerId, accessToken, hash);
                return signedHash;
            } catch (Exception e) {
                throw new RuntimeException("Exception while signing", e);
            }
        }

        @Override
        public OutputStream getOutputStream() {
            return stream;
        }

        @Override
        public AlgorithmIdentifier getAlgorithmIdentifier() {
            return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
        }
    };

    // create the SignedData generator and execute
    CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
    gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
    gen.addSignerInfoGenerator(sigb.build(contentSigner, new X509CertificateHolder(cert2)));

    CMSTypedData msg = new CMSTypedDataInputStream(externalSigning.getContent());
    CMSSignedData signedData = gen.generate(msg, false);

    byte[] cmsSignature = signedData.getEncoded();
    externalSigning.setSignature(cmsSignature);
}

(RemoteSigning test method testSignLikeSkdjksDfkslImproved)

mkl
  • 90,588
  • 15
  • 125
  • 265