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 usesPdfPadesSignerorPdfSigner, and the appearance is a layout element,SignatureFieldAppearance, that is set on aSignerPropertiesobject withsetSignatureAppearance(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, oradbe.pkcs7.detachedwhen the older CMS flavour is requested. The sub-filteradbe.pkcs7.sha1is 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.
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. APdfReaderover a path or a stream works the same way. -
The signer writes into the
OutputStreamthat you pass. AByteArrayOutputStreamis the simplest choice for an invoice, and aFileOutputStreamor the HTTP response stream suits large files.setTemporaryDirectoryPathsets the directory for the temporary files that the signer may use. -
setClaimedSignDatesets 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,setEstimatedSizeandsetTrustedCertificatesare the fluent options.setEstimatedSizeis the number of bytes reserved for the CMS container,setStampingPropertieschanges the stamping settings,setExternalDigestreplaces the digest provider, andsetIssuingCertificateRetrievercontrols 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 |
|
B-T |
Same, plus a signature timestamp read back from the CMS container |
No |
|
B-LT |
Timestamp present; the signature no longer covers the whole file because the DSS was appended after it |
Yes |
|
B-LTA |
Adds a document timestamp that covers the whole file |
Yes |
|
Prolonged |
Third revision with a second document timestamp |
Yes |
|
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 |
|---|---|
|
Name of the signature field. Created when missing; must be unique. |
|
Optional strings stored in the signature dictionary. |
|
The signing time the signer claims, a |
|
Name of the application that created the signature. |
|
Page and rectangle of a visible signature. |
|
The |
|
An |
|
A |
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.
-
SignatureFieldAppearanceis the iText 9 layout element for the look of a signature field. The constructor takes an identifier;SignerProperties.IGNORED_IDis the constant to pass whenPdfSignerassigns the field name itself fromsetFieldName.setContenthas overloads for a plain string, for aSignedAppearanceText, for an image, for a string and an image, for aSignedAppearanceTextand an image, and for aDiv, which is how you build an arbitrary layout.SignedAppearanceTextcarries 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 areUNSPECIFIED(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) andANNOTATION_MODIFICATION(3, additionally annotations). Reading the output of the run showed the DocMDP transform with/P 2for 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.
Official documentation: the Knowledge Base articles iText Core signature appearance improvements and Advanced examples for SignatureFieldAppearance, and the API pages for PdfSigner, AccessPermissions and SignatureFieldAppearance.
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().PdfPadesSignerdoes this on its own. -
The field names must differ. Use a meaningful scheme (
billing-certification,billing-approval) rather than the defaultSignature1,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
SignatureUtilreportssignatureCoversWholeDocumentas 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
PdfReaderand 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
IExternalSignatureabove 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
BillingCredentialsand the TSA client, create a newPdfPadesSignerfor every document (the class is not reusable), and return the bytes from a controller or aStreamingResponseBodyas 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
-
PdfStamper.createSignature,PdfSignatureAppearance,SELF_SIGNEDand theAdobe.PPKLitefilter: replaced byPdfPadesSignerorPdfSignerwithSignerPropertiesandSignatureFieldAppearance. -
The sub-filter
adbe.pkcs7.sha1and any SHA-1 digest for new signatures: useETSI.CAdES.detachedwith SHA-256 or stronger. -
PdfSigner.signDetachedcalls that pass the appearance settings throughPdfSignersetters from iText 7: in iText 9 put the field name, reason, location, rectangle, certification level and appearance onSignerProperties. -
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.
-
Signing and then modifying the file, including encrypting it: the signature breaks. Sign last.
-
Treating a visible signature box as the signature: the evidence is the CMS container.
-
Assuming JasperReports can sign: it cannot, in any version; export the bytes and sign them.
-
Hard-coded keystore passwords and key files in the repository: use the environment, a secret store or an HSM.
References
-
iText Knowledge Base, PAdES Signing High Level API.
-
iText Knowledge Base, iText 9 Signing Improvements.
-
iText Knowledge Base, iText Core signature appearance improvements.
-
iText Knowledge Base, Advanced examples for SignatureFieldAppearance.
-
iText Knowledge Base, How to enable LTV for a timestamp signature.
-
iText Java API, PdfPadesSigner, PdfSigner, SignerProperties, TSAClientBouncyCastle, AccessPermissions, PrivateKeySignature, IExternalSignature and SignatureFieldAppearance.
-
iText, signing examples repository (GitHub).
-
iText licensing, AGPLv3 licence.
-
RFC 3161, Time-Stamp Protocol (TSP).
-
ETSI EN 319 142-1, PAdES digital signatures, Part 1: building blocks and PAdES baseline signatures.
-
ETSI TS 119 312, Cryptographic suites (V2.1.1).
-
freetsa.org, free time stamp authority (test use only).
-
Oracle, keytool in JDK 21.
-
JasperReports, Spring Boot sample.
-
In this section: Digital Signatures: Fundamentals, PAdES Baseline Levels, Signing with PDFBox and EU DSS, Remote Signing, HSMs and Deferred Signing, Signature Validation and Long-Term Validity, How a PDF File Works, Encryption and Permissions, PDF Exporter Configuration and Getting Started with Spring Boot.