javapdfpdfboxdigital-signature

external pdf signing with pdfbox 2.0.34 - The document has been altered or corrupted since the Signature was applied


I am using pdfbox 2.0.34, and i am trying to sign a pdf using an external api that returns the signature in der format.

here is my code:

private static String signPdf(String inputPdfPath, String outputPdfPath, String reason, String location,
        SignatureDetails signatureDetails, String sessionId)
        throws IOException, GeneralSecurityException, Exception {

    File inputFile = new File(inputPdfPath);
    File outputFile = new File(outputPdfPath);

    X509Certificate signerCertificate;
    try {
        if (signatureDetails == null || signatureDetails.getCertificate() == null
                || signatureDetails.getCertificate().isEmpty()) {
            throw new GeneralSecurityException("Signer certificate details missing from the first API response.");
        }
        byte[] certBytes = Base64.decodeBase64(signatureDetails.getCertificate());
        CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
                BouncyCastleProvider.PROVIDER_NAME);
        try (InputStream certInputStream = new ByteArrayInputStream(certBytes)) {
            signerCertificate = (X509Certificate) certFactory.generateCertificate(certInputStream);
        }

        signerCertificate.checkValidity();

        System.out.println("  Successfully loaded signer certificate provided by API.");
        System.out.println("    Cert Subject: " + signerCertificate.getSubjectX500Principal());
        System.out.println("    Cert Issuer: " + signerCertificate.getIssuerX500Principal());
        System.out.println("    Cert Valid Until: " + signerCertificate.getNotAfter());

    } catch (CertificateException | IllegalArgumentException e) {
        System.err.println("  Error decoding/parsing signer certificate from API details: " + e.getMessage());
        throw new GeneralSecurityException("Failed to process signer certificate from API.", e);
    }

    System.out.println("  Loading base PDF document for signing: " + inputFile.getAbsolutePath());

    try (PDDocument document = PDDocument.load(inputFile);
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        System.out.println("  Base PDF loaded.");

        PDSignature signatureDict = new PDSignature();
        signatureDict.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signatureDict.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signatureDict.setName("Ahmad Bin Abu");
        signatureDict.setLocation(location);
        signatureDict.setReason(reason);
        signatureDict.setSignDate(Calendar.getInstance());

        System.out.println("  Adding signature dictionary structure to the document...");
        document.addSignature(signatureDict);

        System.out.println("  Saving signed document incrementally (this triggers the actual signing call)...");
        ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(baos);

        InputStream content = externalSigning.getContent();
        byte[] contentBytes = IOUtils.toByteArray(content);

        String hashHex = DigestUtils.sha256Hex(contentBytes);

        String signedData2 = signExternal(sessionId, hashHex);

        System.out.println("signedData2:" + signedData2);

        byte[] derSignature = Base64.decodeBase64(signedData2);

        ContentSigner contentSigner2 = new ContentSigner() {
            private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

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

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

            @Override
            public AlgorithmIdentifier getAlgorithmIdentifier() {
                return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA");
            }
        };

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        JcaCertStore certStore = new JcaCertStore(Collections.singletonList(signerCertificate));
        gen.addCertificates(certStore);

        DigestCalculatorProvider digestProvider = new JcaDigestCalculatorProviderBuilder()
                .setProvider(new BouncyCastleProvider()).build();

        JcaX509CertificateHolder jcaX509CertificateHolder = new JcaX509CertificateHolder(signerCertificate);

        SignerInfoGenerator signerInfoGen = new JcaSignerInfoGeneratorBuilder(digestProvider).build(contentSigner2,
                jcaX509CertificateHolder);

        gen.addSignerInfoGenerator(signerInfoGen);

        CMSTypedData msg = new CMSProcessableByteArray(contentBytes);
        CMSSignedData signedData = gen.generate(msg, false);

        externalSigning.setSignature(signedData.getEncoded());

        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            baos.writeTo(fos);
        }

        return outputPdfPath;
    }
}

when i open the pdf inside adobe reader, the signature is invalid and the error message is "The document has been altered or corrupted since the Signature was applied. ". Somehow, the certificate appears to be correctly inserted, and I suspect the hash of the document is invalid.

[pdf-error1

I tried to refer to https://github.com/apache/pdfbox/blob/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java#L134 but it looks like their ContentSigner requires a private key, which I do not store.

In this scenario, signExternal() returns a base-64 encoded, DER-encoded ECDSA signature (example: MEUCIQDHBX5csJBX1pD618QA7kv/5uHJ2Tzxq2RMBKYCsqrbdQIgI6louYnhajZsUaRoh+GHhVLpAa5sq9a9SpmLFynat7I=)

Here is the sample pdf.

Would be greatly appreciated if anyone could point me in the right direction. Thanks in advance!


Solution

  • You sign the wrong piece of data with your signExternal call: You sign the PDF contents directly with it but you force the resulting derSignature bytes into a CMS signature container SignerInfo with signed attributes. Thus, you should have applied your signExternal call to the signed attributes instead.


    In detail:

    You create a ContentSigner contentSigner2 in which you completely ignore the data to sign (the ByteArrayOutputStream outputStream contents) and statically return derSignature as signature:

    ContentSigner contentSigner2 = new ContentSigner() {
        private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    
        @Override
        public OutputStream getOutputStream() {
            return outputStream;
        }
    
        @Override
        public byte[] getSignature() {
            return derSignature;
        }
    
        @Override
        public AlgorithmIdentifier getAlgorithmIdentifier() {
            return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA");
        }
    };
    

    You should consider signing the ByteArrayOutputStream outputStream contents instead here, e.g. like this:

    ContentSigner contentSigner2 = new ContentSigner() {
        private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    
        @Override
        public OutputStream getOutputStream() {
            return outputStream;
        }
    
        @Override
        public byte[] getSignature() {
            String hashHex = DigestUtils.sha256Hex(outputStream.toByteArray());
            String signedData2 = signExternal(sessionId, hashHex);
            System.out.println("signedData2:" + signedData2);
            byte[] derSignature = Base64.decodeBase64(signedData2);
            return derSignature;
        }
    
        @Override
        public AlgorithmIdentifier getAlgorithmIdentifier() {
            return new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withECDSA");
        }
    };
    

    (I don't know the details of your signExternal methods, so some adaptions may be necessary.)