0

The use-case is as following: a local application must sign a PDF document using a full PKCS#7 detached signature generated on a remote server which requires a one-time-password sent by e-mail/SMS; 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, such as the resulting signature file is a separate file.


I'm struggling for days now to implement a working solution for this use-case (using itextpdf 5.5.13.1), but the final signed file has the signature in error with the message "Certification by is invalid" -- see the "signed_with_error" file attached. The conceptual "two-steps signing" implemented process is:

1st step: from the original document file -- see the "original" attached file -- I create an intermediary file -- see the "intermediary" file attached -- in which I've inserted an empty signature based on which the associated document hash is created. In this step, the signature digest is saved to be used in the next step

2nd step: remote server is called, the detached signature file is received and I'm creating the signed file by inserting content in the signature from the pre-signed file using the signature digest from previous step and detached signature content received from the remote server.

Original, intermediary and signed with error sample files are:
original
intermediary
signed_with_error

Anyone has any idea of what could be wrong with my code beloow?
Relevant code parts are as following:

1st step:

        ByteArrayOutputStream preSignedDocument = new ByteArrayOutputStream();
        Path customerPathInDataStorage = storageService.resolveCustomerPathInDataStorage(customerExternalId);
        PdfReader pdfReader = new PdfReader(originalDocumentContent);
        PdfStamper stamper = PdfStamper.createSignature(pdfReader, preSignedDocument, '\0', 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

2nd step:

        // 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();

dependencies:

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

}

Later comment:

Even if it is not correct, I've observed that if, in 2nd step, instead of line:

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

I'll use:

byte[] signatureDigest = new byte[0];

than the signed file will be generated with a visible certificate which can be validate but the PDF document is invalid with error "Document certification is INVALID". --- "The document has been altered or corrupted since Certification was applied." - see attached file signed_certification_invalid. It looks legit to be invalid, but for me is strange why in this case certification is displayed in document but it is broken when using - what I consider - "the right" signatureDigest value.

1 Answers1

0

You say

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

thus, what you retrieve here in step 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

already is a CMS signature container. Consequentially you must not insert this into a PKCS#7 / CMS signature container anymore as you do in the following lines but can immediately inject it into the PDF.

So your second step should just be

// 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();

Furthermore, the wrong hash gets signed. Indeed, in step 1 you should not play around with the PdfPKCS7 class either but simply use

documentDetails.setSigningHash(externalSignatureContainer.getSignatureDigest()); // this is the hash sent to remote server for signing
mkl
  • 90,588
  • 15
  • 125
  • 265