XinDHA
XinDHA

Reputation: 43

Adding a new page to PDF and create signature with iText 7

For a project I have to digitally sign PDFs on an additionally created page by multiple people in a workflow. To realize this we're using the iText 7 libraries with following code, based on Bruno Lowagie's examples:

public static void main(String[] args) throws IOException, GeneralSecurityException, XMPException {
    String path = "F:/Java/keystores/testPdfSign";
    char[] pass = "test".toCharArray();

    KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
    ks.load(new FileInputStream(path), pass);
    String alias = "";
    Enumeration<String> aliases = ks.aliases();
    while (alias.equals("tester")==false && aliases.hasMoreElements()) 
    {
        alias = aliases.nextElement();
    }
    PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
    Certificate[] chain = ks.getCertificateChain(alias);
    PDFSign app = new PDFSign();
    app.sign(SRC, DEST, chain, pk, DigestAlgorithms.SHA1, "SunJSSE", PdfSigner.CryptoStandard.CMS, "Test", "Test", null, null, null, 0);
}

public void sign(String src, String dest,
                 Certificate[] chain, PrivateKey pk,
                 String digestAlgorithm, String provider, PdfSigner.CryptoStandard subfilter,
                 String reason, String location,
                 Collection<ICrlClient> crlList,
                 IOcspClient ocspClient,
                 ITSAClient tsaClient,
                 int estimatedSize)
        throws GeneralSecurityException, IOException, XMPException {
    // Creating the reader and the signer

    PdfDocument document = new PdfDocument(new PdfReader(SRC), new PdfWriter(DEST+"_temp"));
    if (initial == true)
    {
        document.addNewPage();
    }
    int pageCount = document.getNumberOfPages();
    document.close();
    PdfSigner signer = new PdfSigner(new PdfReader(DEST+"_temp"), new FileOutputStream(DEST), true); 
    // Creating the appearance
    if (initial == true)
    {
        signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
    }
    PdfSignatureAppearance appearance = signer.getSignatureAppearance()
            .setReason(reason)
            .setLocation(location)
            .setReuseAppearance(false);
    Rectangle rect = new Rectangle(10, 400, 100, 100);
    appearance
            .setPageRect(rect)
            .setPageNumber(pageCount);
    appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
    signer.setFieldName(signer.getNewSigFieldName());
    // Creating the signature
    IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
    ProviderDigest digest = new ProviderDigest(provider);
    signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);

}

This results in an invalid signature in the new signed version of the PDF, as Adobe Acrobat Reader says it has been edited after signing. Surprisingly when I open the file in Foxit Reader it says it hasn't been modified and is valid.

Also what I tried was to leave out the first step of adding a new page and just sign on the last page of the original document, then the signature is valid in Adobe Reader, but no solution for my situation, as an extra page is a must have. The other thing I tried was not setting the certificationLevel to CERTIFIED_FORM_FILLING_AND_ANNOTATIONS, but just leaving it at the default NOT_CERTIFIED, this way I also had a valid signature on a new page, but this is not a solution either, because it won't let me add any additional signatures later on.

Does someone have an idea what could be the reason for Adobe Reader rating the signature as invalid and/or has a solution to this problem?

Thanks in Advance

David

Upvotes: 0

Views: 3956

Answers (1)

mkl
mkl

Reputation: 95918

In short

I could not reproduce the OP's issue. Running his code (with slight adaptions to local circumstances) resulted in a java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE. Having replaced the provider argument "SunJSSE" to the sign call with "BC", on the other hand, the code creates a properly certified PDF.

The adapted code

I usually examine code from stackoverflow in the form of a JUnit test; this implies a few changes. Furthermore, the OP's code contained a number of variables which were referenced but not defined; these had to be given a definition. Finally i load the file to sign from a resource as stream, not from the file system as file.

Thus:

final static File RESULT_FOLDER = new File("target/test-outputs", "signature");

@BeforeClass
public static void setUpBeforeClass() throws Exception
{
    RESULT_FOLDER.mkdirs();
    BouncyCastleProvider provider = new BouncyCastleProvider();
    Security.addProvider(provider);
}

@Test
public void testSignLikeXinDHA() throws GeneralSecurityException, IOException, XMPException
{
    String path = "keystores/demo-rsa2048.p12";
    char[] pass = "demo-rsa2048".toCharArray();

    KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
    ks.load(new FileInputStream(path), pass);
    String alias = "";
    Enumeration<String> aliases = ks.aliases();
    while (alias.equals("demo") == false && aliases.hasMoreElements())
    {
        alias = aliases.nextElement();
    }
    PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
    Certificate[] chain = ks.getCertificateChain(alias);

    try ( InputStream resource = getClass().getResourceAsStream("/mkl/testarea/itext7/content/test.pdf"))
    {
        sign(resource, new File(RESULT_FOLDER, "test_XinDHA_signed_initial.pdf").getAbsolutePath(),
                chain, pk, DigestAlgorithms.SHA1, /*"SunJSSE"*/"BC", PdfSigner.CryptoStandard.CMS, "Test", "Test",
                null, null, null, 0, true);
    }
}

public void sign(InputStream src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm,
        String provider, PdfSigner.CryptoStandard subfilter, String reason, String location,
        Collection<ICrlClient> crlList, IOcspClient ocspClient, ITSAClient tsaClient, int estimatedSize,
        boolean initial)
        throws GeneralSecurityException, IOException, XMPException
{
    // Creating the reader and the signer

    PdfDocument document = new PdfDocument(new PdfReader(src), new PdfWriter(dest + "_temp"));
    if (initial == true)
    {
        document.addNewPage();
    }
    int pageCount = document.getNumberOfPages();
    document.close();
    PdfSigner signer = new PdfSigner(new PdfReader(dest + "_temp"), new FileOutputStream(dest), true);
    // Creating the appearance
    if (initial == true)
    {
        signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
    }
    PdfSignatureAppearance appearance = signer.getSignatureAppearance().setReason(reason).setLocation(location)
            .setReuseAppearance(false);
    Rectangle rect = new Rectangle(10, 400, 100, 100);
    appearance.setPageRect(rect).setPageNumber(pageCount);
    appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
    signer.setFieldName(signer.getNewSigFieldName());
    // Creating the signature
    IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
    ProviderDigest digest = new ProviderDigest(provider);
    signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}

(AddPageAndSign.java)

Running the code

I ran the code using a fairly recent Oracle Java 8 with Unlimited Strength JavaTM Cryptography Extension Policy Files, BouncyCastle 1.49, and iText either in version 7.0.0 or 7.0.1-SNAPSHOT (the current development branch).

(Definitively use an Oracle Java as downloaded from their web site, some variants of the Oracle JDK (supplied by some Linux distributions) contains changes in the security providers which can break your code.)

Running the code using the provider argument "SunJSSE" to the sign call results in

java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE
    at sun.security.jca.GetInstance.getService(GetInstance.java:87)
    at sun.security.jca.GetInstance.getInstance(GetInstance.java:206)
    at java.security.Security.getImpl(Security.java:698)
    at java.security.MessageDigest.getInstance(MessageDigest.java:227)
    at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:134)
    at com.itextpdf.signatures.DigestAlgorithms.getMessageDigest(DigestAlgorithms.java:182)
    at com.itextpdf.signatures.ProviderDigest.getMessageDigest(ProviderDigest.java:69)
    at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:127)
    at com.itextpdf.signatures.PdfSigner.signDetached(PdfSigner.java:528)
    at mkl.testarea.itext7.signature.AddPageAndSign.sign(AddPageAndSign.java:125)
    at mkl.testarea.itext7.signature.AddPageAndSign.testSignLikeXinDHA(AddPageAndSign.java:81)

Running the code using the provider argument "BC" to the sign call results in a properly certified PDF with the signature visualization on an extra page:

Screenshot of signature panel

Why using SunJSSE doesn't make sense

The exception I get with the "SunJSSE" provider actually is not surprising as that provider does not provide a SHA1 algorithm.

According to its documentation by Oracle, it provides no MessageDigest algorithm as such at all, merely in combination as a signature algorithm (SHA1withRSA).

Thus, the IExternalSignature defined in sign as

IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);

will work because here SHA1withRSA will be used, but the ProviderDigest defined there as

ProviderDigest digest = new ProviderDigest(provider);

will fail because it attempts to use the message digest algorithm SHA1.

As an aside

You use SHA1. As this message digest algorithm is less and less trusted in the context of signature creation, that is not a good idea. I would advise switching to an algorithm of the SHA2 famaily.

Upvotes: 1

Related Questions