Signing PDFs with iText

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Signing a PDF means computing a digest over almost the whole file, signing that digest with a private key, and storing the resulting CMS container in a placeholder that was reserved inside the file for it. iText 9 offers two ways to do it: the high-level PdfPadesSigner, which produces PAdES baseline signatures at the four levels B-B, B-T, B-LT and B-LTA, and the low-level PdfSigner, which exposes every step (field, appearance, certification level, external digest, deferred signing) at the cost of more code. This page shows both with the Bookshelf Billing invoice, plus visible signatures, several signatures in one file, certification, and the case where the PDF was produced by JasperReports and the signature has to be added afterwards. Versions are stated as of 2026-10-09: iText Core 9.8.0 with bouncy-castle-adapter 9.8.0 (Bouncy Castle 1.86) on Java 21, and JasperReports 7.0.8 for the last example. Every Java example on this page was compiled against exactly those versions and run, and the results were read back with SignatureUtil. What was and was not exercised against network services is stated in the section on the baseline levels, because it matters.

What a signature is, why the file is signed in an incremental update, and how to read trust and validity, are in Digital Signatures: Fundamentals, which also owns the complete script that creates the test certificates used here. The four PAdES levels and the standards behind them are explained in PAdES Baseline Levels. Validating what this page produces, and keeping a signature valid for years, is in Signature Validation and Long-Term Validity. Signing with Apache PDFBox and the EU DSS framework instead of iText is in Signing with PDFBox and EU DSS, and keeping the private key in an HSM, a remote signing service or a key management service is in Remote Signing, HSMs and Deferred Signing.

What changed since PdfStamper.createSignature

Books written for iText 1 and 2, including the 2006 text iText in Action that was consulted for the concept order of this page and is not reproduced, sign a document with PdfStamper.createSignature(...), configure it through a PdfSignatureAppearance obtained from the stamper, and select the signature type with constants such as SELF_SIGNED, the Adobe.PPKLite filter and the adbe.pkcs7.sha1 sub-filter, with SHA-1 digests produced by the caller. That is finding O11 of this section’s source analysis, and it is outdated in four ways.

  • The API is gone. In iText 7 and later there is no PdfStamper; signing uses PdfPadesSigner or PdfSigner, and the appearance is a layout element, SignatureFieldAppearance, that is set on a SignerProperties object with setSignatureAppearance (the iText Knowledge Base article iText 9 Signing Improvements is the place to read about what iText 9 changed in signing).

  • The signature format changed. New signatures use the sub-filter ETSI.CAdES.detached, which is the PAdES format, or adbe.pkcs7.detached when the older CMS flavour is requested. The sub-filter adbe.pkcs7.sha1 is deprecated in PDF 2.0, and every signature written for this page reads back as /ETSI.CAdES.detached.

  • SHA-1 is not an option for new signatures. ETSI TS 119 312 V2.1.1 treats SHA-1 as acceptable only for verifying old signatures. Use SHA-256 or stronger, as in every example here.

  • Timestamps and long-term validation are part of the baseline. A signature that was good the day it was made is not necessarily verifiable in ten years, which is what B-T, B-LT and B-LTA are for.

The licence is also different from the old library. iText 7 and later, including the sign module and the signing add-ons used here, is released under the AGPLv3 or under a commercial licence. The AGPLv3 has a network clause that matters for a signing service offered to users over a network, so read the iText licensing pages, and ask your legal department, before you build a closed product on this page. The sign module is part of that licence, and no signing example on this page needs a commercial-only add-on.

Setup: coordinates, test certificates and helper classes

Signing needs iText Core as a POM aggregator and exactly one Bouncy Castle adapter, which is the same pair used for encryption. Use the regular adapter unless a FIPS 140 module is mandatory, in which case you swap it for bouncy-castle-fips-adapter (see iText Architecture and Modules).

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>9.8.0</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>9.8.0</version>
</dependency>

The examples need a signer certificate. They use the shared test PKI of this section: a self-signed test root CA in billing-test-ca.p12 (alias billing-test-ca), a signer key pair in billing-signer.p12 (alias billing-signer) whose certificate was issued by that CA, and the environment variable BILLING_KEYSTORE_PASSWORD for both stores. The script, with the explanation of why the CA signs the signer certificate with keytool -gencert, belongs to Digital Signatures: Fundamentals. As a reminder, it is repeated below unchanged; it was run in an empty directory with the JDK 21 keytool for this page. The password is read from the terminal and passed to keytool with -storepass:env, so it never appears on a command line; nothing here may be reused outside a test, and the generated files must never be committed.

# 0. Store password for both keystores, taken from the terminal and exported for keytool.
read -rs BILLING_KEYSTORE_PASSWORD && export BILLING_KEYSTORE_PASSWORD

# 1. Self-signed root CA (basicConstraints CA:true, may sign certificates and CRLs).
keytool -genkeypair -alias billing-test-ca -keyalg RSA -keysize 3072 -sigalg SHA256withRSA \
  -validity 3650 -dname "CN=Bookshelf Billing Test Root CA, O=Bookshelf Test, C=ES" \
  -ext bc:c=ca:true -ext ku:c=keyCertSign,cRLSign \
  -keystore billing-test-ca.p12 -storetype PKCS12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -keypass:env BILLING_KEYSTORE_PASSWORD

# 2. The signer's own key pair (with a temporary self-signed certificate that step 5 replaces).
keytool -genkeypair -alias billing-signer -keyalg RSA -keysize 3072 -sigalg SHA256withRSA \
  -validity 825 -dname "CN=Bookshelf Billing Test Signer, O=Bookshelf Test, C=ES" \
  -keystore billing-signer.p12 -storetype PKCS12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -keypass:env BILLING_KEYSTORE_PASSWORD

# 3. A certificate signing request for the signer.
keytool -certreq -alias billing-signer -sigalg SHA256withRSA \
  -keystore billing-signer.p12 -storepass:env BILLING_KEYSTORE_PASSWORD \
  -file billing-signer.csr

# 4. The CA issues the signer certificate: an end-entity certificate for document signing.
keytool -gencert -alias billing-test-ca -sigalg SHA256withRSA -validity 825 -rfc \
  -ext bc:c=ca:false -ext ku:c=digitalSignature,nonRepudiation -ext eku=emailProtection \
  -keystore billing-test-ca.p12 -storepass:env BILLING_KEYSTORE_PASSWORD \
  -infile billing-signer.csr -outfile billing-signer.pem

# 5. Export the CA certificate (the trust anchor), then import it and the issued certificate
#    into the signer keystore; the second import replaces the temporary certificate and stores
#    the full chain [signer, CA] under the alias billing-signer.
keytool -exportcert -rfc -alias billing-test-ca \
  -keystore billing-test-ca.p12 -storepass:env BILLING_KEYSTORE_PASSWORD \
  -file billing-test-ca.pem
keytool -importcert -noprompt -alias billing-test-ca -file billing-test-ca.pem \
  -keystore billing-signer.p12 -storepass:env BILLING_KEYSTORE_PASSWORD
keytool -importcert -noprompt -alias billing-signer -file billing-signer.pem \
  -keystore billing-signer.p12 -storepass:env BILLING_KEYSTORE_PASSWORD

# 6. Check: the signer entry must report a chain of length 2, issued by the test CA.
keytool -J-Duser.language=en -list -v -alias billing-signer -keystore billing-signer.p12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD | grep -E "Certificate chain length|Owner|Issuer"

The last import replaces the self-signed certificate of the signer alias with the two-certificate chain, signer first and CA second, and the check in step 6 reports a certificate chain length of 2. Because the CA is a throwaway, no real validator trusts it: the signatures below are cryptographically correct, and a viewer will report an unknown signer until the exported billing-test-ca.pem is added to its trust store. In production the key and the chain come from a qualified trust service provider or from your company PKI, and the private key lives in an HSM or a key management service (AWS Secrets Manager and KMS, Azure Key Vault, Google Cloud Secret Manager and KMS) instead of a file.

Two small classes are shared by the examples. The first loads the private key and the chain from the keystore, reading the store password from the environment, and the second builds the unsigned invoice with iText, in memory.

package com.example.billing;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;

/** Private key and certificate chain of the billing signer, read from a PKCS#12 keystore. */
public record BillingCredentials(PrivateKey privateKey, Certificate[] chain) {

    public static BillingCredentials load(Path keystore, String alias) throws IOException, GeneralSecurityException {
        String storePassword = System.getenv("BILLING_KEYSTORE_PASSWORD");
        if (storePassword == null || storePassword.isBlank()) {
            throw new IllegalStateException("Environment variable BILLING_KEYSTORE_PASSWORD is not set");
        }
        char[] password = storePassword.toCharArray();
        KeyStore store = KeyStore.getInstance("PKCS12");
        try (InputStream in = Files.newInputStream(keystore)) {
            store.load(in, password);
        }
        PrivateKey privateKey = (PrivateKey) store.getKey(alias, password);
        Certificate[] chain = store.getCertificateChain(alias);
        if (privateKey == null || chain == null) {
            throw new IllegalStateException("No private key and chain under alias " + alias);
        }
        return new BillingCredentials(privateKey, chain);
    }
}
package com.example.billing;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;

import java.io.ByteArrayOutputStream;

public final class InvoiceFactory {

    private InvoiceFactory() {
    }

    /** Builds the unsigned invoice. Signing never happens while the document is being written. */
    public static byte[] unsignedInvoice(String number) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (PdfDocument pdf = new PdfDocument(new PdfWriter(out));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Bookshelf Billing - invoice " + number));
            document.add(new Paragraph("Total: 42.50 EUR"));
        }
        return out.toByteArray();
    }
}

These helpers derive from the keystore handling of the iText Knowledge Base article PAdES Signing High Level API and of the repository of official examples, https://github.com/itext/itext-publications-signing-examples-java, which load a PKCS#12 file the same way.

The signing flow: hash, sign, timestamp, embed

Every signature in iText, whatever API is used, goes through the same four steps. Knowing them explains the options and the error messages that follow.

sequenceDiagram participant A as Application participant S as PdfSigner or PdfPadesSigner participant K as Private key participant T as TSA participant F as PDF file A->>S: unsigned PDF bytes, chain, key S->>F: append signature field, reserve a Contents placeholder S->>S: hash the byte ranges before and after the placeholder S->>K: sign the signed attributes digest K-->>S: signature value opt B-T and above S->>T: RFC 3161 request for the signature value T-->>S: timestamp token end S->>F: embed the CMS container in the placeholder opt B-LT and above S->>F: append DSS with certificates, OCSP and CRL end opt B-LTA S->>T: request for a document timestamp T-->>S: timestamp token S->>F: append a document timestamp signature end S-->>A: signed PDF bytes

In words: the signer first appends the signature field and a /Contents placeholder of a fixed size, filled with zeros, and records a /ByteRange array that excludes exactly that placeholder. It then computes the digest of the two byte ranges, builds the CMS signed attributes around it (content type, message digest and the signing-certificate reference that PAdES requires, but no signing-time attribute, which PAdES forbids: the claimed time is the /M entry of the signature dictionary), and asks the private key to sign them. For a signature timestamp it sends a digest of that signature value to a time stamping authority over HTTP, as defined by RFC 3161, and adds the returned token to the CMS container as an unsigned attribute. Finally the CMS bytes are written into the placeholder, which is why the placeholder size matters: a container that does not fit is an error, and one that is far too large wastes space. Everything after the first signature is added as an incremental update, as explained in How a PDF File Works, so the bytes that an earlier signature covers are never rewritten.

The private key never enters the document. In the middle step the key receives only a few hundred bytes to sign, which is what makes it possible to put the key in a token or a remote service (see the IExternalSignature section below).

High-level signing with PdfPadesSigner

PdfPadesSigner is the class to start with. Its constructor takes a PdfReader over the unsigned file and an OutputStream for the result, and four methods produce the PAdES baseline levels: signWithBaselineBProfile, signWithBaselineTProfile, signWithBaselineLTProfile and signWithBaselineLTAProfile. A fifth, prolongSignatures, extends a file that already carries signatures. Each signing method exists in two overloads that differ in the third argument: an IExternalSignature (shown here) or a bare PrivateKey. All of them take a SignerProperties, which holds the field name, reason, location and the claimed signing time, and the certificate chain with the signer certificate first. The methods always write a PAdES signature, sub-filter ETSI.CAdES.detached, so there is no CMS-versus-CAdES switch at this level.

PrivateKeySignature(privateKey, "SHA256", "BC") is the iText IExternalSignature that signs with a JCA private key: the first argument is the key, the second the digest algorithm name and the third the name of the JCA provider. The BC provider is registered once with Security.addProvider(new BouncyCastleProvider()), which is also how the class below does it.

The class collects the four levels as methods of one service. It is the only signing class a typical invoicing service needs.

package com.example.billing;

import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.CrlClientOnline;
import com.itextpdf.signatures.ICrlClient;
import com.itextpdf.signatures.IExternalSignature;
import com.itextpdf.signatures.ITSAClient;
import com.itextpdf.signatures.OcspClientBouncyCastle;
import com.itextpdf.signatures.PdfPadesSigner;
import com.itextpdf.signatures.PrivateKeySignature;
import com.itextpdf.signatures.SignerProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.util.Calendar;

public class PadesBaselineSigner {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private final BillingCredentials credentials;
    private final ICrlClient crlClient;

    public PadesBaselineSigner(BillingCredentials credentials) {
        this(credentials, new CrlClientOnline());
    }

    public PadesBaselineSigner(BillingCredentials credentials, ICrlClient crlClient) {
        this.credentials = credentials;
        this.crlClient = crlClient;
    }

    private SignerProperties properties(String fieldName) {
        return new SignerProperties()
                .setFieldName(fieldName)
                .setReason("Invoice issued by Bookshelf Billing")
                .setLocation("Madrid")
                .setClaimedSignDate(Calendar.getInstance());
    }

    private IExternalSignature signature() {
        return new PrivateKeySignature(credentials.privateKey(), "SHA256", BouncyCastleProvider.PROVIDER_NAME);
    }

    /** B-B: signing certificate and chain, no timestamp. */
    public byte[] signBaselineB(byte[] pdf) throws IOException, GeneralSecurityException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfPadesSigner signer = new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(pdf)), out);
        signer.signWithBaselineBProfile(properties("billing-signature"), credentials.chain(), signature());
        return out.toByteArray();
    }

    /** B-T: adds an RFC 3161 signature timestamp. Needs network access to the TSA. */
    public byte[] signBaselineT(byte[] pdf, ITSAClient tsa) throws IOException, GeneralSecurityException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfPadesSigner signer = new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(pdf)), out);
        signer.signWithBaselineTProfile(properties("billing-signature"), credentials.chain(), signature(), tsa);
        return out.toByteArray();
    }

    /** B-LT: B-T plus validation data (certificates, OCSP, CRL) in the DSS dictionary. */
    public byte[] signBaselineLT(byte[] pdf, ITSAClient tsa) throws IOException, GeneralSecurityException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfPadesSigner signer = new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(pdf)), out)
                .setOcspClient(new OcspClientBouncyCastle())
                .setCrlClient(crlClient);
        signer.signWithBaselineLTProfile(properties("billing-signature"), credentials.chain(), signature(), tsa);
        return out.toByteArray();
    }

    /** B-LTA: B-LT plus an archive document timestamp. */
    public byte[] signBaselineLTA(byte[] pdf, ITSAClient tsa) throws IOException, GeneralSecurityException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfPadesSigner signer = new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(pdf)), out)
                .setOcspClient(new OcspClientBouncyCastle())
                .setCrlClient(crlClient)
                .setTimestampSignatureName("billing-archive-timestamp");
        signer.signWithBaselineLTAProfile(properties("billing-signature"), credentials.chain(), signature(), tsa);
        return out.toByteArray();
    }

    /** Adds a fresh document timestamp (and refreshed validation data) to a B-LTA file. */
    public byte[] prolong(byte[] signedPdf, ITSAClient tsa) throws IOException, GeneralSecurityException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfPadesSigner signer = new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(signedPdf)), out)
                .setOcspClient(new OcspClientBouncyCastle())
                .setCrlClient(crlClient)
                .setTimestampSignatureName("billing-archive-timestamp-2");
        signer.prolongSignatures(tsa);
        return out.toByteArray();
    }
}

Some details of this class deserve a sentence each.

  • new PdfReader(new ByteArrayInputStream(pdf)) reads the unsigned file from memory. A PdfReader over a path or a stream works the same way.

  • The signer writes into the OutputStream that you pass. A ByteArrayOutputStream is the simplest choice for an invoice, and a FileOutputStream or the HTTP response stream suits large files. setTemporaryDirectoryPath sets the directory for the temporary files that the signer may use.

  • setClaimedSignDate sets the signing time that the signer claims. It is not trusted evidence, which is why B-T adds a timestamp from a third party. The class sets it to the current time.

  • The field name must be unique in the file. In the runs on this page iText created the field, invisible, with that name; the visible case is in the low-level section.

  • setOcspClient, setCrlClient, setTimestampSignatureName, setEstimatedSize and setTrustedCertificates are the fluent options. setEstimatedSize is the number of bytes reserved for the CMS container, setStampingProperties changes the stamping settings, setExternalDigest replaces the digest provider, and setIssuingCertificateRetriever controls how missing issuer certificates are fetched.

Official documentation: the Knowledge Base article PAdES Signing High Level API and the API pages for PdfPadesSigner and SignerProperties.

B-B: a basic signature

The B-B level is a signature with the signing certificate and its chain and no timestamp. It needs no network. A small main writes the B-B file and, as the next section explains, the B-T file as well:

package com.example.billing;

import com.itextpdf.signatures.TSAClientBouncyCastle;

import java.nio.file.Files;
import java.nio.file.Path;

public class SignInvoiceDemo {

    public static void main(String[] args) throws Exception {
        BillingCredentials credentials = BillingCredentials.load(Path.of("billing-signer.p12"), "billing-signer");
        PadesBaselineSigner signer = new PadesBaselineSigner(credentials);
        byte[] unsigned = InvoiceFactory.unsignedInvoice("INV-2026-0001");

        Files.write(Path.of("invoice-b-b.pdf"), signer.signBaselineB(unsigned));

        TSAClientBouncyCastle tsa = new TSAClientBouncyCastle("https://freetsa.org/tsr");
        Files.write(Path.of("invoice-b-t.pdf"), signer.signBaselineT(unsigned, tsa));
        System.out.println("B-B and B-T written");
    }
}

The B-B signature is the cheapest and the weakest. It proves that the file was not changed since the key owner signed it, as far as the signer’s own clock can be believed, and it stops being verifiable on its own as soon as the signer certificate expires or is revoked. Use B-B for internal documents and for the first step of a longer pipeline.

B-T: adding a signature timestamp

B-T adds an RFC 3161 timestamp token over the signature value, issued by a time stamping authority (TSA). The token is independent evidence that the signature existed at that time, so the signature stays valid after the signer certificate expires, as long as it was valid at the timestamp time. In iText the timestamp comes from an ITSAClient; the implementation to use is TSAClientBouncyCastle, constructed with the TSA URL. A TSA that requires credentials takes a user name and a password as the second and third constructor arguments, and another constructor takes the token size estimate and the digest algorithm name too. The free test TSA used in this section needs no credentials:

TSAClientBouncyCastle tsa = new TSAClientBouncyCastle("https://freetsa.org/tsr");

The signBaselineT method of the class above passes that client to signWithBaselineTProfile, and the main class has already used it: the B-T file was written to invoice-b-t.pdf. The free service at freetsa.org is meant for tests and demonstrations; it is not a qualified TSA in the eIDAS sense, offers no availability guarantee, and the URL must be replaced by your trust service provider’s endpoint, with its credentials read from the environment or a secret store, before production. How to choose a TSA is in PAdES Baseline Levels.

B-LT: embedding the validation data

B-LT adds, on top of B-T, the data a verifier needs to check the signature offline: the certificates of the chain and the revocation information (OCSP responses or CRLs) in the document security store (the /DSS dictionary). iText collects the data for you. OcspClientBouncyCastle asks the OCSP responder named in the certificate’s Authority Information Access extension, and CrlClientOnline downloads the CRLs named in the certificate’s CRL distribution points. Both are passed to PdfPadesSigner with setOcspClient and setCrlClient, as in signBaselineLT above, and the TSA client is the same one as for B-T.

The throwaway certificates of this page have neither an OCSP address nor a CRL distribution point, because a test CA has no responder. Running signWithBaselineLTProfile with the default clients on them fails with a PdfException whose message says that neither OCSP nor CRL data are available for the signing certificate, which is the correct behaviour: iText refuses to claim B-LT without revocation data. To exercise the LT path in a test, the class below replaces the online CRL client. It builds an empty, freshly signed CRL for the test CA with Bouncy Castle and delegates every other issuer, such as the one of the TSA’s certificate, to the online client. It is a test fixture, never a model for production.

package com.example.billing;

import com.itextpdf.signatures.CrlClientOnline;
import com.itextpdf.signatures.ICrlClient;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * Test-only CRL source: issues an empty CRL for the throwaway billing test CA, because the test
 * certificates carry no CRL distribution point. Every other issuer is delegated to the online client.
 */
public class TestCaCrlClient implements ICrlClient {

    private final X509Certificate caCertificate;
    private final PrivateKey caKey;
    private final ICrlClient online = new CrlClientOnline();

    public TestCaCrlClient(X509Certificate caCertificate, PrivateKey caKey) {
        this.caCertificate = caCertificate;
        this.caKey = caKey;
    }

    @Override
    public Collection<byte[]> getEncoded(X509Certificate checkCert, String url) {
        try {
            if (!checkCert.getIssuerX500Principal().equals(caCertificate.getSubjectX500Principal())) {
                return online.getEncoded(checkCert, url);
            }
            Date now = new Date();
            X509v2CRLBuilder builder = new X509v2CRLBuilder(
                    new JcaX509CertificateHolder(caCertificate).getSubject(), now);
            builder.setNextUpdate(new Date(now.getTime() + 24L * 3600 * 1000));
            X509CRLHolder crl = builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(caKey));
            return List.of(new JcaX509CRLConverter().getCRL(crl).getEncoded());
        } catch (Exception e) {
            throw new IllegalStateException("Cannot build the test CRL", e);
        }
    }
}

The class implements ICrlClient, whose single method returns the encoded CRLs for a certificate. The demo below wires it into PadesBaselineSigner through the second constructor, then writes the B-LT file, the B-LTA file of the next section, and the prolonged file of the one after.

package com.example.billing;

import com.itextpdf.signatures.TSAClientBouncyCastle;

import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

/** Test-only wiring: the throwaway CA issues the CRL because the test certificates have no CRL endpoint. */
public class SignInvoiceLtaDemo {

    public static void main(String[] args) throws Exception {
        char[] password = System.getenv("BILLING_KEYSTORE_PASSWORD").toCharArray();
        KeyStore ca = KeyStore.getInstance("PKCS12");
        try (var in = Files.newInputStream(Path.of("billing-test-ca.p12"))) {
            ca.load(in, password);
        }
        TestCaCrlClient crlClient = new TestCaCrlClient(
                (X509Certificate) ca.getCertificate("billing-test-ca"),
                (PrivateKey) ca.getKey("billing-test-ca", password));

        BillingCredentials credentials = BillingCredentials.load(Path.of("billing-signer.p12"), "billing-signer");
        PadesBaselineSigner signer = new PadesBaselineSigner(credentials, crlClient);
        TSAClientBouncyCastle tsa = new TSAClientBouncyCastle("https://freetsa.org/tsr");
        byte[] unsigned = InvoiceFactory.unsignedInvoice("INV-2026-0001");

        byte[] lt = signer.signBaselineLT(unsigned, tsa);
        Files.write(Path.of("invoice-b-lt.pdf"), lt);
        byte[] lta = signer.signBaselineLTA(unsigned, tsa);
        Files.write(Path.of("invoice-b-lta.pdf"), lta);
        Files.write(Path.of("invoice-b-lta-prolonged.pdf"), signer.prolong(lta, tsa));
        System.out.println("B-LT, B-LTA and prolonged file written");
    }
}

In production none of this fixture exists: the certificates issued by a real CA carry AIA and CRL addresses, the default OcspClientBouncyCastle and CrlClientOnline find them, and the signing service needs outbound HTTP access to those responders. Put a timeout on them (CrlClientOnline.setConnectionTimeout), because an unreachable responder otherwise blocks the request thread, and decide whether a failure to reach a responder should fail the invoice or fall back to B-T. Whatever you decide, remember that revocation data is only evidence of the state at signing time: it must be fetched when you sign.

Official documentation: the Knowledge Base article How to enable LTV for a timestamp signature, the API page for TSAClientBouncyCastle, and the signing examples repository, https://github.com/itext/itext-publications-signing-examples-java.

B-LTA and prolongSignatures

B-LTA adds, after the DSS, a document timestamp: a second signature of type /ETSI.RFC3161 that covers the whole file including the validation data. It protects that data against the day the algorithms used in it are broken. The method is signWithBaselineLTAProfile, and setTimestampSignatureName names the field that holds the document timestamp (name it, so that the file can be inspected later by that name).

A B-LTA file is not finished for ever. The document timestamp has its own certificate and algorithms, and they age too. prolongSignatures(ITSAClient) appends another document timestamp, with refreshed validation data, on top of the existing ones, which is the renewal step that the standard describes. Call it on a schedule, long before the timestamp certificate expires, with a fresh PdfPadesSigner over the stored file, as the prolong method above does. prolongSignatures() without an argument exists as well. In the run on this page it appended a further revision to the B-LTA file (three revisions, still two signatures) and no new timestamp, so use the overload with the TSA client when the goal is a new timestamp.

What was run, and what was not

The B-B signature needs no network and was produced and verified locally. On 2026-10-09 the sandbox that built this page could reach https://freetsa.org/tsr, and signWithBaselineTProfile, signWithBaselineLTProfile, signWithBaselineLTAProfile and prolongSignatures were all run against it and read back. The results come from the SignatureInspector class shown later and are summarised here. Online services come and go: a HEAD request to freetsa.org returned HTTP 403 while time stamp requests were served, so a curl -I is not a health check, and the TSA may be unreachable from your build server. Treat the table as a record of one run, not a guarantee.

File What was observed DSS present Signatures reported

B-B

Signature covers the whole document, digest SHA-256, no timestamp

No

billing-signature

B-T

Same, plus a signature timestamp read back from the CMS container

No

billing-signature

B-LT

Timestamp present; the signature no longer covers the whole file because the DSS was appended after it

Yes

billing-signature

B-LTA

Adds a document timestamp that covers the whole file

Yes

billing-signature and billing-archive-timestamp

Prolonged

Third revision with a second document timestamp

Yes

billing-signature, billing-archive-timestamp and billing-archive-timestamp-2

Two limits of this run must be stated. First, the OCSP path was configured (OcspClientBouncyCastle) but not exercised for the test certificates, because they have no OCSP address: their revocation data in the B-LT file came from the test CRL fixture. This run did not itemise the DSS dictionary; the separate lab run of PAdES Baseline Levels did, and the OCSP responses it counts can only concern the TSA’s own chain. Second, the files were verified for integrity and structure with iText and rendered once with PyMuPDF for the visible examples, but they were not opened in Acrobat, in the EU DSS demo validator or in veraPDF, and nobody trusts the test CA. A real B-LT or B-LTA claim needs a real certificate chain and a validation with a tool that checks trust and revocation, which is the subject of Signature Validation and Long-Term Validity.

Reading back what was signed

A signing service should read its own output before it returns it. The class below opens a file with SignatureUtil, lists the signatures, and for each one prints the sub-filter, the revision, whether the signature covers the whole document, whether the CMS container verifies against the bytes (verifySignatureIntegrityAndAuthenticity) and the timestamp it carries. It is how the table in the previous section was produced. It checks the cryptography of each signature only. It does not build a trust path, does not look at revocation and does not apply any policy, so it is a smoke test and not a validator.

package com.example.billing;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.PdfPKCS7;
import com.itextpdf.signatures.SignatureUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.IOException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.Security;

/** Prints what each signature of a file claims. It does not validate trust, revocation or policy. */
public class SignatureInspector {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public static void main(String[] args) throws IOException, GeneralSecurityException {
        try (PdfDocument pdf = new PdfDocument(new PdfReader(Path.of(args[0]).toString()))) {
            SignatureUtil util = new SignatureUtil(pdf);
            boolean hasDss = pdf.getCatalog().getPdfObject().containsKey(PdfName.DSS);
            System.out.println("DSS dictionary present: " + hasDss);
            for (String name : util.getSignatureNames()) {
                PdfPKCS7 pkcs7 = util.readSignatureData(name);
                System.out.printf("%s | subFilter=%s | revision %d of %d | whole document=%b | integrity=%b%n",
                        name,
                        util.getSignature(name).getSubFilter(),
                        util.getRevision(name), util.getTotalRevisions(),
                        util.signatureCoversWholeDocument(name),
                        pkcs7.verifySignatureIntegrityAndAuthenticity());
                if (pkcs7.isTsp()) {
                    System.out.println("   document timestamp at " + pkcs7.getTimeStampDate().getTime());
                } else {
                    System.out.println("   signer " + pkcs7.getSigningCertificate().getSubjectX500Principal()
                            + ", digest " + pkcs7.getDigestAlgorithmName()
                            + ", signature timestamp " + (pkcs7.getTimeStampDate() != null
                            ? pkcs7.getTimeStampDate().getTime() : "none"));
                }
            }
        }
    }
}

The class derives from the signature utilities of iText as described in the API pages for SignatureUtil and PdfPKCS7. For real validation iText 9 has a SignatureValidator in com.itextpdf.signatures.validation, covered in the validation page of this section.

Low-level signing with PdfSigner

PdfPadesSigner covers the common case. When you need a visible appearance, a certification signature, a particular field, an existing signature field in a form, a non-default digest or signature algorithm, or a CMS signature instead of CAdES, use PdfSigner. Its three-argument constructor takes the reader, the output stream and the StampingProperties; a second constructor takes a temporary-file path and the SignerProperties too. A signature is always an incremental change to an existing file, so the stamping properties normally contain useAppendMode(), which is mandatory when the file already carries a signature, because rewriting the file would destroy the earlier signature.

SignerProperties is the bag of options, and iText 9 uses it for both signers.

Method Meaning

setFieldName

Name of the signature field. Created when missing; must be unique.

setReason, setLocation, setContact

Optional strings stored in the signature dictionary.

setClaimedSignDate

The signing time the signer claims, a Calendar.

setSignatureCreator

Name of the application that created the signature.

setPageNumber, setPageRect

Page and rectangle of a visible signature.

setSignatureAppearance

The SignatureFieldAppearance that draws the visible signature.

setCertificationLevel

An AccessPermissions value that turns the first signature into a certification signature.

setFieldLockDict

A PdfSigFieldLock that locks form fields after signing.

The signing call is signDetached, in several overloads. The one used below takes an IExternalDigest, an IExternalSignature, the certificate chain, the CRL clients, the OCSP client, the TSA client, the estimated size of the container, and a PdfSigner.CryptoStandard. Passing null for the CRL clients, the OCSP client and the TSA client produces a B-B-like signature; passing the clients embeds the data and the timestamp, which is how the low-level API reaches B-T and B-LT without PdfPadesSigner. The value CryptoStandard.CADES writes the PAdES sub-filter ETSI.CAdES.detached, and CryptoStandard.CMS selects the older adbe.pkcs7.detached (not run here); prefer CADES, because it is the format the PAdES levels and the European validators expect. An estimated size of 0 worked for every signature on this page. BouncyCastleDigest is the IExternalDigest that computes digests through Bouncy Castle.

Visible signatures and certification

The class below signs the invoice with a visible appearance in a rectangle of the first page, and with a certification level. It does both in one method so that the same code can sign a second time.

package com.example.billing;

import com.itextpdf.forms.fields.properties.SignedAppearanceText;
import com.itextpdf.forms.form.element.SignatureFieldAppearance;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.signatures.AccessPermissions;
import com.itextpdf.signatures.BouncyCastleDigest;
import com.itextpdf.signatures.IExternalSignature;
import com.itextpdf.signatures.PdfSigner;
import com.itextpdf.signatures.PrivateKeySignature;
import com.itextpdf.signatures.SignerProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.util.Calendar;

public class LowLevelSigner {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private final BillingCredentials credentials;

    public LowLevelSigner(BillingCredentials credentials) {
        this.credentials = credentials;
    }

    /**
     * Signs with a visible appearance on page 1. The first signature of a file is the only one that may
     * certify it. Subsequent signatures must use append mode, which the second argument of the stamping
     * properties selects.
     */
    public byte[] sign(byte[] pdf, String fieldName, Rectangle rect, AccessPermissions certification,
                       IExternalSignature externalSignature)
            throws IOException, GeneralSecurityException {
        Calendar now = Calendar.getInstance();

        SignedAppearanceText text = new SignedAppearanceText()
                .setSignedBy("Bookshelf Billing Test Signer")
                .setReasonLine("Reason: invoice issued")
                .setLocationLine("Location: Madrid")
                .setSignDate(now);
        SignatureFieldAppearance appearance = new SignatureFieldAppearance(SignerProperties.IGNORED_ID)
                .setContent(text)
                .setFontSize(10);

        SignerProperties properties = new SignerProperties()
                .setFieldName(fieldName)
                .setReason("Invoice issued by Bookshelf Billing")
                .setLocation("Madrid")
                .setClaimedSignDate(now)
                .setCertificationLevel(certification)
                .setPageNumber(1)
                .setPageRect(rect)
                .setSignatureAppearance(appearance);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfSigner signer = new PdfSigner(new PdfReader(new ByteArrayInputStream(pdf)), out,
                new StampingProperties().useAppendMode());
        signer.setSignerProperties(properties);
        signer.signDetached(new BouncyCastleDigest(), externalSignature, credentials.chain(),
                null, null, null, 0, PdfSigner.CryptoStandard.CADES);
        return out.toByteArray();
    }

    public IExternalSignature rsaSha256() {
        return new PrivateKeySignature(credentials.privateKey(), "SHA256", BouncyCastleProvider.PROVIDER_NAME);
    }
}

Three points need explanation.

  • SignatureFieldAppearance is the iText 9 layout element for the look of a signature field. The constructor takes an identifier; SignerProperties.IGNORED_ID is the constant to pass when PdfSigner assigns the field name itself from setFieldName. setContent has overloads for a plain string, for a SignedAppearanceText, for an image, for a string and an image, for a SignedAppearanceText and an image, and for a Div, which is how you build an arbitrary layout. SignedAppearanceText carries the four lines that iText composes: "Digitally signed by", the signing date, the reason and the location.

  • The rectangle must be large enough for the text. The first attempt on this page used a rectangle of 200 by 60 points and the rendered signature showed only the signer and the date, with the reason and the location silently cut off, because the longer first line wrapped. A rectangle of 240 by 90 points showed all four lines. Size the rectangle for the longest line you expect, or set the font size to match, and render the result once.

  • setCertificationLevel(AccessPermissions.FORM_FIELDS_MODIFICATION) makes the first signature a certification (DocMDP) signature. The four values are UNSPECIFIED (an ordinary approval signature), NO_CHANGES_PERMITTED (value 1 in the DocMDP transform), FORM_FIELDS_MODIFICATION (2, filling in form fields and signing are allowed) and ANNOTATION_MODIFICATION (3, additionally annotations). Reading the output of the run showed the DocMDP transform with /P 2 for the form-fields level. A certification signature must be the first signature of the file, and a viewer is expected to warn when later changes exceed the allowed level.

Here is the same class used for two signatures, a certification and an approval, written into a file that carries both.

package com.example.billing;

import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.signatures.AccessPermissions;

import java.nio.file.Files;
import java.nio.file.Path;

public class SignTwiceDemo {
    public static void main(String[] args) throws Exception {
        BillingCredentials creds = BillingCredentials.load(Path.of("billing-signer.p12"), "billing-signer");
        LowLevelSigner signer = new LowLevelSigner(creds);
        byte[] unsigned = InvoiceFactory.unsignedInvoice("INV-2026-0001");
        byte[] first = signer.sign(unsigned, "billing-certification", new Rectangle(36, 650, 240, 90),
                AccessPermissions.FORM_FIELDS_MODIFICATION, signer.rsaSha256());
        Files.write(Path.of("invoice-certified.pdf"), first);
        byte[] second = signer.sign(first, "billing-approval", new Rectangle(300, 650, 240, 90),
                AccessPermissions.UNSPECIFIED, signer.rsaSha256());
        Files.write(Path.of("invoice-two-signatures.pdf"), second);
        System.out.println("done");
    }
}

The inspector reports two signatures: billing-certification at revision 1 of 2, which no longer covers the whole file (it is followed by the second signature, which is expected), and billing-approval at revision 2 of 2, which covers the whole document. Both have valid integrity. The second signature must be made in append mode, which the StampingProperties in LowLevelSigner always requests. The order matters: you can add an approval signature to a certified file, but you cannot certify a file that is already signed.

A certification level is a promise that later changes are limited. iText does not enforce it when you sign again. In the run on this page a second signature was added without an exception to a file certified with NO_CHANGES_PERMITTED; it is the validator in the reader that decides whether that second signature violates the certification. Choose the level to match what your workflow does after signing, and for an invoice that is archived without further edits NO_CHANGES_PERMITTED is the honest value, which means that nobody should add an approval signature on top.

An image and text in the appearance

A logo in the signature box is a common requirement. setContent(SignedAppearanceText, ImageData) draws the image on the left and the text on the right, as in the rendering of this page’s run, where an orange test rectangle stands in for the logo. The demo generates the image with BufferedImage, so no binary file is needed, and certifies the file with NO_CHANGES_PERMITTED.

package com.example.billing;

import com.itextpdf.forms.fields.properties.SignedAppearanceText;
import com.itextpdf.forms.form.element.SignatureFieldAppearance;
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.signatures.AccessPermissions;
import com.itextpdf.signatures.BouncyCastleDigest;
import com.itextpdf.signatures.PdfSigner;
import com.itextpdf.signatures.SignerProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Security;

public class LogoSignatureDemo {

    public static void main(String[] args) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        BillingCredentials credentials = BillingCredentials.load(Path.of("billing-signer.p12"), "billing-signer");
        byte[] unsigned = InvoiceFactory.unsignedInvoice("INV-2026-0001");

        // A small generated image stands in for the company logo.
        BufferedImage image = new BufferedImage(120, 60, BufferedImage.TYPE_INT_RGB);
        var graphics = image.createGraphics();
        graphics.setColor(Color.ORANGE);
        graphics.fillRect(0, 0, 120, 60);
        graphics.dispose();
        ByteArrayOutputStream png = new ByteArrayOutputStream();
        ImageIO.write(image, "png", png);
        ImageData logo = ImageDataFactory.create(png.toByteArray());

        SignedAppearanceText text = new SignedAppearanceText()
                .setSignedBy("Bookshelf Billing Test Signer")
                .setReasonLine("Reason: invoice issued");
        SignatureFieldAppearance appearance = new SignatureFieldAppearance(SignerProperties.IGNORED_ID)
                .setContent(text, logo)
                .setFontSize(10);

        SignerProperties properties = new SignerProperties()
                .setFieldName("billing-visible")
                .setPageNumber(1)
                .setPageRect(new Rectangle(36, 600, 300, 100))
                .setSignatureAppearance(appearance)
                .setCertificationLevel(AccessPermissions.NO_CHANGES_PERMITTED);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PdfSigner signer = new PdfSigner(new PdfReader(new ByteArrayInputStream(unsigned)), out,
                new StampingProperties().useAppendMode());
        signer.setSignerProperties(properties);
        signer.signDetached(new BouncyCastleDigest(),
                new JcaExternalSignature(credentials.privateKey(), BouncyCastleProvider.PROVIDER_NAME),
                credentials.chain(), null, null, null, 0, PdfSigner.CryptoStandard.CADES);
        Files.write(Path.of("invoice-logo.pdf"), out.toByteArray());
    }
}

For another layout, setContent(Div) takes any layout element, so a table or a paragraph with a custom font is possible, and iText needs its fonts embedded as for any other content (see Fonts and Unicode). The invisible signatures of PadesBaselineSigner are often better for machine-processed invoices, because they overlap nothing in the layout; for the person reading the paper copy, a visible signature box is useful. A visible signature that is only an appearance and has no cryptographic part is a picture and not a signature, which is the most common misunderstanding of non-technical users: the evidence is in the CMS container, not in the box.

IExternalSignature: keeping the key where it is

PrivateKeySignature needs the private key as a java.security.PrivateKey. When the key is on a smart card, behind a PKCS#11 provider or in a key management service that exposes a JCA provider, that object is still a PrivateKey, and the signature happens in the device. When the key cannot be exposed at all, the interface to implement is IExternalSignature. It has four methods: the digest algorithm name, the signature algorithm name, the signature mechanism parameters (for RSA-PSS) and sign(byte[]), which receives the bytes to sign, the DER-encoded signed attributes, and returns the signature value. The class below delegates to any JCA Signature, with an optional provider name, and is the starting point for a PKCS#11 token or for a wrapper around a cloud KMS call.

package com.example.billing;

import com.itextpdf.signatures.IExternalSignature;
import com.itextpdf.signatures.ISignatureMechanismParams;

import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;

/**
 * An IExternalSignature that delegates to any JCA Signature. The private key may live in a PKCS#11 token
 * or a key management service behind a JCA provider; iText only ever sees the bytes to sign.
 */
public class JcaExternalSignature implements IExternalSignature {

    private final PrivateKey privateKey;
    private final String provider;

    public JcaExternalSignature(PrivateKey privateKey, String provider) {
        this.privateKey = privateKey;
        this.provider = provider;
    }

    @Override
    public String getDigestAlgorithmName() {
        return "SHA256";
    }

    @Override
    public String getSignatureAlgorithmName() {
        return "RSA";
    }

    @Override
    public ISignatureMechanismParams getSignatureMechanismParameters() {
        return null;
    }

    @Override
    public byte[] sign(byte[] message) throws GeneralSecurityException {
        Signature signature = provider == null
                ? Signature.getInstance("SHA256withRSA")
                : Signature.getInstance("SHA256withRSA", provider);
        signature.initSign(privateKey);
        signature.update(message);
        return signature.sign();
    }
}

It was used for the signature in the logo example above, with BC as the provider. A few remarks on algorithms. The digest SHA256 with RSA produces SHA256withRSA, a PKCS#1 v1.5 signature that every PDF reader accepts. ETSI TS 119 312 V2.1.1 recommends RSA-PSS, ECDSA and EdDSA, and treats PKCS#1 v1.5 as legacy; PrivateKeySignature has a constructor that takes applicable signature parameters, which is the hook for RSA-PSS, and IExternalSignature has a getSignatureMechanismParameters method for the same purpose; neither was exercised on this page, and you should check that the validators of your recipients accept the algorithm before you switch. The shared test PKI allows an EC P-256 signer key as an alternative to RSA 3072, but only RSA was run here.

When the document hash must be signed in a different process or at a different time, for example by a signing service that returns a value minutes later or by a remote HSM that signs a hash without ever seeing the file, iText offers PdfSigner.signExternalContainer and the static signDeferred, which split signing in two phases. That mechanism has its own page, Remote Signing, HSMs and Deferred Signing.

Several signatures in one file

A PDF can carry any number of signatures, each one a separate incremental update. The rules are short.

  • Every signature after the first needs StampingProperties.useAppendMode(). PdfPadesSigner does this on its own.

  • The field names must differ. Use a meaningful scheme (billing-certification, billing-approval) rather than the default Signature1, Signature2, because the names appear in the validation report.

  • Only the first signature can be a certification signature, and a certification level limits what the later signatures and edits may do.

  • The first signature no longer covers the whole file once another is added. That is normal, and SignatureUtil reports signatureCoversWholeDocument as false for it. Only the last signature covers everything, and a signature that covers everything up to a point and is followed by an unexpected incremental update is the symptom to investigate.

  • A timestamp (B-T) is inside each signature. A document timestamp (B-LTA) is a signature of its own, and it counts as one of the signatures in the file.

A common workflow is "the system signs, then a person approves". Make the system’s signature the certification signature with FORM_FIELDS_MODIFICATION if the approver has to fill in a field, or sign with UNSPECIFIED in both steps when there is no form. Do not mix signing with encryption without testing: a signature covers the bytes of the file, so encrypting a signed file, or decrypting it, invalidates the signature. Decide the order early and test the combination you ship; encryption is covered in Encryption and Permissions.

Signing a JasperReports PDF

JasperReports has no native signing. Its PDF exporter, which writes the file through OpenPDF since version 6.20.1 and lives in the separate jasperreports-pdf module since 7.0, produces a PDF, and there is no exporter parameter that adds a signature (finding O27 of this section’s analysis: the books on JasperReports that were consulted do not mention signing at all). The correct design is a post-processing step: export the report to bytes and give the bytes to a signer, which can be iText, as here, or PDFBox with the EU DSS framework (Signing with PDFBox and EU DSS). The signer never touches the Jasper objects. iText 9 and OpenPDF live in different packages (com.itextpdf and com.lowagie, or org.openpdf from OpenPDF 3), so having both on the classpath is fine.

The template is the v7 JRXML of the other pages of this section, with a single parameter:

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport name="billing-invoice" language="java" pageWidth="595" pageHeight="842"
              columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
    <parameter name="number" class="java.lang.String"/>
    <title height="60">
        <element kind="staticText" x="0" y="0" width="555" height="30" fontSize="18.0">
            <text><![CDATA[Bookshelf Billing invoice]]></text>
        </element>
        <element kind="textField" x="0" y="30" width="555" height="20">
            <expression><![CDATA["Number: " + $P{number}]]></expression>
        </element>
    </title>
</jasperReport>

The Maven coordinates are the JasperReports ones of Getting Started with Spring Boot, here at 7.0.8: net.sf.jasperreports:jasperreports, jasperreports-pdf, jasperreports-fonts and jasperreports-jdt. JasperReports is LGPL-3.0, so the licence of the signing step stays the iText one. The class compiles the template once, fills it, exports the bytes with JasperExportManager.exportReportToPdf, and signs them with PdfPadesSigner:

package com.example.billing;

import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.PdfPadesSigner;
import com.itextpdf.signatures.PrivateKeySignature;
import com.itextpdf.signatures.SignerProperties;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;

/** JasperReports renders the invoice; iText signs the exported bytes. The two libraries never share objects. */
public class JasperInvoiceSigner {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private final JasperReport report;
    private final BillingCredentials credentials;

    public JasperInvoiceSigner(BillingCredentials credentials) throws IOException, JRException {
        this.credentials = credentials;
        try (InputStream template = getClass().getResourceAsStream("/reports/billing-invoice.jrxml")) {
            this.report = JasperCompileManager.compileReport(template);
        }
    }

    public byte[] renderAndSign(String number) throws JRException, IOException, GeneralSecurityException {
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("number", number);
        JasperPrint print = JasperFillManager.fillReport(report, parameters);
        byte[] unsigned = JasperExportManager.exportReportToPdf(print);

        ByteArrayOutputStream signed = new ByteArrayOutputStream();
        new PdfPadesSigner(new PdfReader(new ByteArrayInputStream(unsigned)), signed)
                .signWithBaselineBProfile(
                        new SignerProperties().setFieldName("billing-signature"),
                        credentials.chain(),
                        new PrivateKeySignature(credentials.privateKey(), "SHA256", BouncyCastleProvider.PROVIDER_NAME));
        return signed.toByteArray();
    }
}

The signed file was produced and read back: one signature, ETSI.CAdES.detached, covering the whole document, with a valid integrity check. Three constraints apply to this pattern.

  • Sign last. Everything that changes the bytes, such as tagging, encryption, metadata updates from the exporter’s configuration (see PDF Exporter Configuration) or merging several reports, must happen before the signature. The signature is the last thing the document receives.

  • PDF/A and tagging. Signing is an incremental update that adds a field, an appearance and a signature, and a PDF/A or PDF/UA file has to stay conformant afterwards. If the report is exported as PDF/A or with tags (the tagged PDF output needs the Professional edition of JasperReports 7.0.8), verify the signed file with a validator such as veraPDF. That combination was not exercised on this page.

  • Large reports. The example keeps the exported PDF in a byte array. For a report of hundreds of megabytes, export to a temporary file and give PdfReader and the output stream files instead of byte arrays.

Official documentation for the JasperReports side: the Spring Boot sample. The iText side is the Knowledge Base article PAdES Signing High Level API.

Production notes

  • Where the key lives. The examples read a PKCS#12 file because it is the easiest thing to reproduce. A production service should sign with a key that cannot be exported: an HSM through PKCS#11, a cloud KMS, or a remote signing service. The IExternalSignature above is the extension point. Never log the password, the keystore path of a real key or the signed bytes of a document that contains personal data.

  • Spring Boot. Make the signer a stateless singleton that holds the BillingCredentials and the TSA client, create a new PdfPadesSigner for every document (the class is not reusable), and return the bytes from a controller or a StreamingResponseBody as described in Getting Started with Spring Boot. Load the keystore password from Spring configuration or the secret store. The TSA URL and credentials belong in configuration too.

  • Outbound traffic. B-T and above call the TSA, and B-LT also calls OCSP and CRL endpoints. Plan timeouts, retries and a circuit breaker, and decide what an invoice does when the TSA is down: delay and retry is usually right, because a B-B file is not equivalent.

  • Clock. A signing time or a timestamp from a host with a wrong clock is evidence of nothing. Keep NTP running on signing hosts.

  • Test the readers. Open the result in the viewers your recipients use. A test CA is reported as untrusted by every one of them, which is the expected result, and the signature panel should still show an intact, unmodified document.

Outdated points to avoid

  1. PdfStamper.createSignature, PdfSignatureAppearance, SELF_SIGNED and the Adobe.PPKLite filter: replaced by PdfPadesSigner or PdfSigner with SignerProperties and SignatureFieldAppearance.

  2. The sub-filter adbe.pkcs7.sha1 and any SHA-1 digest for new signatures: use ETSI.CAdES.detached with SHA-256 or stronger.

  3. PdfSigner.signDetached calls that pass the appearance settings through PdfSigner setters from iText 7: in iText 9 put the field name, reason, location, rectangle, certification level and appearance on SignerProperties.

  4. A B-B signature presented as long-term proof: it has no timestamp. Use B-T at least, B-LT or B-LTA when the document must be verifiable after the certificate expires.

  5. Signing and then modifying the file, including encrypting it: the signature breaks. Sign last.

  6. Treating a visible signature box as the signature: the evidence is the CMS container.

  7. Assuming JasperReports can sign: it cannot, in any version; export the bytes and sign them.

  8. Hard-coded keystore passwords and key files in the repository: use the environment, a secret store or an HSM.