javapdfitextdigital-signature

Itext pdf deferred signing with invalid signature


I encountered the problem of signature verification failure when using itext to sign pdf. I took it as a reference 'itext-pdf-deferred-signing-results-in-pdf-with-invalid-signature' Modifications were made, but the problem was still not solved

 public static void emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        appearance.setVisibleSignature(new Rectangle(50, 780, 144, 780), 1, fieldname);
        appearance.setCertificate(chain[0]);
        ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        MakeSignature.signExternalContainer(appearance, external, 8192);
        InputStream inp = appearance.getRangeStream();
        BouncyCastleDigest digest = new BouncyCastleDigest();
        byte[] hash = DigestAlgorithms.digest(inp, digest.getMessageDigest("SHA256"));
//I need to pass this hash value to a third-party system and provide them with the generated signed hash for verification.
        System.out.println("Hash: " + Base64.getEncoder().encodeToString(hash));
    }

    public static void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        ExternalSignatureContainer external = new MyExternalSignatureContainer(pk, chain);
        MakeSignature.signDeferred(reader, fieldname, os, external);
    }

main

 public static void main(String[] args) throws DocumentException, GeneralSecurityException, IOException {
        //1.get temp empty file
        emptySignature("mypdf.pdf", "temp.pdf", "", null);//before deferred signing I can not get chain? How can I use this argument?
        //2.get cert from third party system
        Certificate[] cert = getCert();
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(new FileInputStream("aaa.p12"), "ogcio".toCharArray());
        String alias = ks.aliases().nextElement();
        PrivateKey pk = (PrivateKey) ks.getKey(alias, "ogcio".toCharArray());
        createSignature("temp.pdf", "result.pdf", "", pk, cert);
    }

MyExternalSignatureContainer

public class MyExternalSignatureContainer implements ExternalSignatureContainer {
    protected PrivateKey pk;
    protected Certificate[] chain;

    public MyExternalSignatureContainer(PrivateKey pk, Certificate[] chain) {
        this.pk = pk;
        this.chain = chain;
    }

    public byte[] sign(InputStream is) throws GeneralSecurityException {
        try {
            Security.addProvider(new BouncyCastleProvider());
            PrivateKeySignature signature = new PrivateKeySignature(pk, "SHA256", "BC");
            String hashAlgorithm = signature.getHashAlgorithm();
            BouncyCastleDigest digest = new BouncyCastleDigest();
            PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
            byte[] hash = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
            byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null, MakeSignature.CryptoStandard.CMS);
            byte[] extSignature = signature.sign(sh);
            sgn.setExternalDigest(extSignature, null, signature.getEncryptionAlgorithm());
            return sgn.getEncodedPKCS7(hash, null, null, null, MakeSignature.CryptoStandard.CMS);
        } catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
    }

    public void modifySigningDictionary(PdfDictionary signDic) {
        //signDic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
        //signDic.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
    }
}

I used some code above to do. But the signature is invalid with the message "The document has been altered or corrupted since the signatures was applied." my pdf:"https://drive.google.com/file/d/1jcisJPvfMEXoLFP9Ku-p6nFXuNe7ICWr/view?usp=sharing"


Solution

  • The existing code couldn't successfully sign the PDF as it needed the signer certificate before signing while the iAM Smart signing service (for which the code eventually shall be used) provides the certificate only together with the signature.

    Such signing services that return the signer certificate only together with the signature bytes are difficult to use for PDF signatures in general because most CMS signature container generators expect to have access to all data (except obviously the signature bytes) before starting to assemble the to-be-signed attributes and requesting a signature for them. For the special case of PAdES PDF signatures they even cannot be used at all as PAdES baseline signatures require information about the signer certificate in the to-be-signed attributes.

    In comments it fortunately turned out that the iAM Smart service also provides the functionality of returning a full PKCS#7 object (i.e. CMS signature container) explicitly profiled for PDF signing. If one uses this service functionality, one can simplify the existing code and use it to sign PDFs with iText as follows:

    File origFile = new File("deferredContainerSigningOriginal.pdf");
    File tempFile = new File("deferredContainerSigningTemp.pdf");
    File resultFile = new File("deferredContainerSigningResult.pdf");
    String fieldName = "Signature";
    
    byte[] hash = null;
    
    try (
        OutputStream tempOutput = new FileOutputStream(tempFile)
    ) {
        PdfReader pdfReader = new PdfReader(origFile.getPath());
        hash = prepareAndHashForSigning(pdfReader, tempOutput, fieldName);
    }
    
    // execute your signature container request for the calculated hash here 
    byte[] signatureContainer = retrievePkcs7ContainerForHash(hash);
    
    try (
        OutputStream resultOutput = new FileOutputStream(resultFile)
    ) {
        PdfReader pdfReader = new PdfReader(tempFile.getPath());
        injectPkcs7Container(pdfReader, resultOutput, fieldName, signatureContainer);
    }
    

    using the following helper methods and class:

    /**
     * This method adds a signature field to the given PDF and sets its value signature
     * dictionary. The placeholder for the signature container therein is filled with 0s.
     * 
     * @return the hash of the signed byte ranges of the added signature.
     * @see #testExternalSignatureContainer()
     */
    byte[] prepareAndHashForSigning(PdfReader pdfReader, OutputStream outputStream, String fieldName) throws DocumentException, IOException, GeneralSecurityException {
        PdfStamper pdfStamper = PdfStamper.createSignature(pdfReader, outputStream, (char) 0);
        PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance();
        pdfSignatureAppearance.setVisibleSignature(new Rectangle(50, 680, 144, 780), 1, fieldName);
    
        ExternalSignatureContainer blankContainer = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        MakeSignature.signExternalContainer(pdfSignatureAppearance, blankContainer, 12000);
        InputStream rangeStream = pdfSignatureAppearance.getRangeStream();
        BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
        byte[] hash = DigestAlgorithms.digest(rangeStream, bouncyCastleDigest.getMessageDigest("SHA256"));
        return hash;
    }
    
    /**
     * This method sets the signature container placeholder in the signature dictionary
     * value of the signature field with the given name.
     * 
     * @see #testExternalSignatureContainer()
     */
    void injectPkcs7Container(PdfReader pdfReader, OutputStream outputStream, String fieldName, byte[] signatureContainer) throws DocumentException, IOException, GeneralSecurityException {
        ExternalSignatureContainer injectingContainer = new InjectingSignatureContainer(signatureContainer);
        MakeSignature.signDeferred(pdfReader, fieldName, outputStream, injectingContainer);
    }
    
    /**
     * This is a dummy method that returns the given hash itself instead of a signature
     * container for it. Obviously, it is not for production purposes.
     * 
     * @see #testExternalSignatureContainer()
     */
    byte[] retrievePkcs7ContainerForHash(byte[] hash) {
        return hash;
    }
    
    /**
     * This {@link ExternalSignatureContainer} implementation returns a pre-generated
     * byte array in its {@link #sign(InputStream)} method.
     */
    static class InjectingSignatureContainer implements ExternalSignatureContainer {
        final byte[] signatureContainer;
    
        public InjectingSignatureContainer(byte[] signatureContainer) {
            this.signatureContainer = signatureContainer;
        }
    
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            return signatureContainer;
        }
    
        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
        }
    }
    

    (CreateDeferredSignature test)

    This all is pretty similar to your code which already implemented a number of correct concepts but which suffered from the background of eventually having to use an inappropriate signing service functionality.

    Beware, I additionally adapted some constants, in particular I increased the size reserved for the signature container from your 8192 to 12000 (because the example signature container you provided already is 10462 large) and I changed the coordinates of the rectangle as yours had height 0.