Signature Validation & Long-term Validity

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 is only half of the story. A signature protects nobody until somebody checks it, and the check has to answer more questions than "does the cryptography add up". This page explains what a validator really verifies, shows how to run the check from Java with iText 9.8 and with the EU Digital Signature Service (DSS) 6.5, how to read the answers, how Adobe Acrobat decides whom to trust, and what you must do so that a signature made today can still be verified in 2046. Versions are stated as of 2026-10-09: iText Core 9.8.0, EU DSS 6.5, Apache PDFBox 3.0.8 (used by DSS) and Bouncy Castle 1.86 on Java 21.

The examples validate the invoice of the Bookshelf Billing scenario that is created and signed on the page Signing with iText. The signing code shown here is only a small helper to obtain a test file; how signing works, which options exist and how to put the key in a hardware module are explained there and in Remote Signing, HSMs & Deferred Signing. The vocabulary (digest, certificate chain, CRL, OCSP, timestamp) is introduced in Digital Signatures Fundamentals, and the profile names used below (B-B, B-T, B-LT, B-LTA) are defined in PAdES Baseline Levels. Signing with PDFBox and DSS is in Signing with PDFBox & EU DSS.

Every example on this page was compiled and run against the exact versions above, with one exception that is stated where it appears (the DSS trusted-list job, which was compiled only). The licences matter: iText Core is AGPLv3 or commercial, Apache PDFBox is Apache-2.0, and EU DSS is LGPL-2.1. A validation service that you offer to third parties over a network is exactly the use that the AGPL obliges you to publish the source of, unless you buy the commercial licence, so check this before building one on iText.

What a validator checks

"Valid" is not one question but a stack of them, and a validator reports a different answer when a different layer fails. Following the order used by ETSI EN 319 102-1 (the standard that both iText and DSS implement), the layers are:

  1. Format. The signature container is well formed: the /ByteRange is plausible, /Contents holds a parseable CMS (PKCS#7) structure, and mandatory signed attributes such as the content type and message digest are present.

  2. Integrity. The digest of the bytes named by /ByteRange equals the digest signed in the CMS, and the signature value verifies with the public key of the signing certificate. Any change inside the covered range ends here with a definite negative: the document was altered, or the signature was not produced over this file.

  3. Identification of the signer. The signing certificate is found, and where the CMS carries a reference to it (the signing-certificate-v2 attribute, required for PAdES) it matches.

  4. Certificate path. A chain is built from the signer to a trust anchor that the validator has been told to trust. Each certificate must be inside its validity period at the relevant time, carry the extensions that its role needs (CA basic constraints, key usage) and be signed with an algorithm that is still acceptable.

  5. Revocation. For each certificate of the chain the validator wants evidence, from a CRL or an OCSP response, that it had not been revoked, and that the evidence is fresh enough.

  6. Time. The validator needs to know when the signature existed. Without a trusted timestamp the only time is the claimed signing time written by the signer, which proves nothing. With a signature timestamp (RFC 3161) or a document timestamp, there is a proof of existence, and the certificate path is judged at that moment instead of today.

  7. Modifications after signing. The signature covers the bytes up to its own revision. A later incremental update, the mechanism described in How a PDF File Works, leaves those bytes intact, so the signature stays mathematically correct, but the validator still has to decide whether the added revision is an allowed change (for example, a second signature, or filling a form field when the author’s rules permit it) or a forbidden one (new page content, changed text). This is the layer that most often surprises developers.

  8. Policy. Finally the answers are combined according to a validation policy: which algorithms, key sizes and revocation freshness are acceptable, and whether a missing piece makes the result a failure, a warning or nothing.

Three outcomes come out of this, and the terms are the same in both libraries. Valid (iText VALID, DSS TOTAL_PASSED) means every check that the policy requires passed. Invalid (iText INVALID, DSS TOTAL_FAILED) means that a definite negative was found, typically a broken digest. Indeterminate (iText INDETERMINATE, DSS INDETERMINATE with a sub-indication) means that no negative was found but something needed to conclude is missing: a trust anchor, a revocation answer, a timestamp. Indeterminate is the answer you will get most often in practice, it is not a polite way of saying valid, and treating it as valid (or as invalid) in application code is a design decision to make consciously.

A last conceptual point: a validator does not decide whether the signer is who they claim to be in a legal sense. It decides whether the certificate chains to something that the validating party has chosen to trust. Trust is configuration, never a property of the PDF.

The diagram summarises the decision flow that both libraries walk through.

flowchart TD A[Signed PDF] --> B{Signature container well formed?} B -- no --> X1[Invalid: format failure] B -- yes --> C{Digest over ByteRange and signature value match?} C -- no --> X2[Invalid: document altered or wrong key] C -- yes --> D{Certificate chain reaches a trust anchor?} D -- no --> I1[Indeterminate: untrusted chain] D -- yes --> E{Revocation evidence available and fresh?} E -- no --> I2[Indeterminate: revocation unknown] E -- revoked --> X3[Invalid: revoked before signing time] E -- yes --> F{Trusted timestamp present?} F -- no --> G[Judge the chain today, with the claimed time] F -- yes --> H[Judge the chain at the timestamp time] G --> J{Changes after the signature allowed?} H --> J J -- no --> X4[Invalid: forbidden modification] J -- yes --> K{Algorithms acceptable at that time?} K -- no --> I3[Indeterminate or invalid by policy] K -- yes --> V[Valid]

Revocation and chain conditions are shown before the time check for readability; in the libraries the time is chosen first and the other checks are then evaluated against it.

A file to validate

To keep this page self-contained, the examples need a signed file, a trust anchor and, for the long-term part, a timestamp authority. The test PKI follows the convention used throughout this section: a self-signed test root billing-test-ca.p12 (alias billing-test-ca) that issues the signer certificate stored with its chain in billing-signer.p12 (alias billing-signer), the password read from the environment variable BILLING_KEYSTORE_PASSWORD (any throwaway value, such as changeit-placeholder, for this experiment), and the CA certificate exported to billing-test-ca.pem. The complete script is explained in Digital Signatures Fundamentals; this is the condensed version used for this page. It creates the keys at run time, and nothing generated is ever committed.

export BILLING_KEYSTORE_PASSWORD=changeit-placeholder
mkdir -p pki && cd pki

keytool -genkeypair -alias billing-test-ca -keyalg RSA -keysize 3072 -sigalg SHA256withRSA \
  -dname "CN=Bookshelf Billing Test CA, O=Bookshelf Test, C=ES" -validity 3650 \
  -ext bc:c -ext ku:c=keyCertSign,cRLSign -keystore billing-test-ca.p12 -storetype PKCS12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -keypass:env BILLING_KEYSTORE_PASSWORD

keytool -genkeypair -alias billing-signer -keyalg RSA -keysize 3072 -sigalg SHA256withRSA \
  -dname "CN=Bookshelf Billing Signer, O=Bookshelf Test, C=ES" -validity 730 \
  -keystore billing-signer.p12 -storetype PKCS12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -keypass:env BILLING_KEYSTORE_PASSWORD
keytool -certreq -alias billing-signer -keystore billing-signer.p12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -file billing-signer.csr
keytool -gencert -alias billing-test-ca -keystore billing-test-ca.p12 \
  -storepass:env BILLING_KEYSTORE_PASSWORD -sigalg SHA256withRSA -validity 730 -rfc \
  -ext ku:c=digitalSignature,nonRepudiation -ext eku=emailProtection \
  -infile billing-signer.csr -outfile billing-signer.pem

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 -alias billing-signer -file billing-signer.pem \
  -keystore billing-signer.p12 -storepass:env BILLING_KEYSTORE_PASSWORD
rm -f billing-signer.csr billing-signer.pem
cd ..

The script was run with the keytool of JDK 26 for this page and produces the two keystores and the PEM file; every option it uses is also in JDK 21, the release the Maven project below compiles for. The Maven project uses these dependencies; the first three are the same as in Getting Started with Spring Boot and the DSS ones are explained by the failures they prevent (see the section on DSS below).

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <itext.version>9.8.0</itext.version>
  <dss.version>6.5</dss.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>${itext.version}</version>
    <type>pom</type>
  </dependency>
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>${itext.version}</version>
  </dependency>
  <!-- EU DSS: PDFBox flavour of PAdES, validation, reports, and the two mandatory implementations -->
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-pades-pdfbox</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-validation</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-policy-jaxb</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-cms-object</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-crl-parser-x509crl</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-utils-apache-commons</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>eu.europa.ec.joinup.sd-dss</groupId>
    <artifactId>dss-service</artifactId>
    <version>${dss.version}</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.16</version>
  </dependency>
</dependencies>

The helper below builds a two-line invoice and signs it with a detached CAdES signature in append mode. It is deliberately minimal: no timestamp, no revocation data, so the result is a plain B-B signature. The reasons for each line are in the signing page.

package example;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
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 java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public final class SampleSigner {

    public static void createInvoice(Path target) throws Exception {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter(target.toString()));
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Bookshelf Billing - invoice 2026-0001"));
            document.add(new Paragraph("Total: 42.00 EUR"));
        }
    }

    public static void sign(Path unsigned, Path signed, String fieldName) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        char[] storePassword = System.getenv("BILLING_KEYSTORE_PASSWORD").toCharArray();
        KeyStore store = KeyStore.getInstance("PKCS12");
        try (var in = Files.newInputStream(Path.of("pki/billing-signer.p12"))) {
            store.load(in, storePassword);
        }
        PrivateKey privateKey = (PrivateKey) store.getKey("billing-signer", storePassword);
        Certificate[] chain = store.getCertificateChain("billing-signer");

        IExternalSignature signature =
                new PrivateKeySignature(privateKey, "SHA-256", BouncyCastleProvider.PROVIDER_NAME);
        try (PdfReader reader = new PdfReader(unsigned.toString());
             OutputStream out = Files.newOutputStream(signed)) {
            PdfSigner signer = new PdfSigner(reader, out, new StampingProperties().useAppendMode());
            signer.setSignerProperties(new SignerProperties().setFieldName(fieldName));
            signer.signDetached(new BouncyCastleDigest(), signature, chain, null, null, null, 0,
                    PdfSigner.CryptoStandard.CADES);
        }
    }

    public static void main(String[] args) throws Exception {
        Files.createDirectories(Path.of("out"));
        createInvoice(Path.of("out/invoice.pdf"));
        sign(Path.of("out/invoice.pdf"), Path.of("out/invoice-signed.pdf"), "billing-signature");
    }
}

Derived from the iText Knowledge Base article Digital Signing with iText. The file invoice-signed.pdf is the input of every example below.

Validating with iText 9

Until iText Core 8.0.5 there was no validator in the library; developers combined SignatureUtil, PdfPKCS7 and their own chain code. Since 8.0.5 the sign module contains a validation API, and iText Core 9.0 finalised it in the package com.itextpdf.signatures.validation of that same module, with a policy that follows RFC 5280, RFC 3161 and ETSI EN 319 102-1, according to the iText Knowledge Base. The pieces are:

  • ValidatorChainBuilder assembles the validators and their configuration: trusted certificates, extra known certificates, revocation clients, resource retrievers, the EU list of trusted lists and the report generators.

  • SignatureValidator, obtained from buildSignatureValidator(PdfDocument), validates all signatures (validateSignatures()) or one by name (validateSignature(String)), newest first, because the timestamp of a later signature fixes what an earlier one was covering.

  • ValidationReport is the result. getValidationResult() returns VALID, INVALID or INDETERMINATE; getFailures() and getCertificateFailures() list what went wrong; getLogs() and getCertificateLogs() list everything that was examined.

  • SignatureValidationProperties is the validation policy, described below.

Licence: the validation API is part of the sign module of iText Core, under the same AGPLv3 or commercial licence as the rest of iText Core; it is not a separate add-on.

The first run: three honest answers

The class below validates the same file three times and prints the report. The first run has no trust anchor, the second trusts the test CA, the third also supplies a CRL issued by that CA. A CRL is the simplest revocation evidence to fabricate for a test PKI: the helper TestCrl signs an empty list with the CA key at run time and hands it to the validator through ICrlClient, so that the experiment needs no network and no certificate carries a distribution point.

package example;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.validation.ValidatorChainBuilder;
import com.itextpdf.signatures.validation.report.CertificateReportItem;
import com.itextpdf.signatures.validation.report.ReportItem;
import com.itextpdf.signatures.validation.report.ValidationReport;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;

public final class ItextValidate {

    static X509Certificate loadCertificate(Path pem) throws Exception {
        try (InputStream in = Files.newInputStream(pem)) {
            return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
        }
    }

    static ValidationReport validate(Path pdf, List<X509Certificate> trustAnchors, boolean withCrl)
            throws Exception {
        try (PdfDocument document = new PdfDocument(new PdfReader(pdf.toString()))) {
            ValidatorChainBuilder builder = new ValidatorChainBuilder()
                    .withTrustedCertificates(List.copyOf(trustAnchors));
            if (withCrl) {
                builder.getProperties().addCrlClient(new TestCrl());
            }
            return builder.buildSignatureValidator(document).validateSignatures();
        }
    }

    static void print(String title, ValidationReport report) {
        System.out.println("== " + title + ": " + report.getValidationResult());
        for (ReportItem failure : report.getFailures()) {
            System.out.println("  failure [" + failure.getCheckName() + "] " + failure.getMessage());
        }
        for (CertificateReportItem failure : report.getCertificateFailures()) {
            System.out.println("  certificate failure: " + failure.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        Path signed = Path.of(args[0]);
        X509Certificate ca = loadCertificate(Path.of("pki/billing-test-ca.pem"));
        print("without trust anchor", validate(signed, List.of(), false));
        print("with test CA as trust anchor", validate(signed, List.of(ca), false));
        print("with trust anchor and a CRL", validate(signed, List.of(ca), true));
    }
}

TestCrl needs Bouncy Castle’s certificate API, which bouncy-castle-adapter already brings in:

package example;

import com.itextpdf.signatures.ICrlClient;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/** Issues an empty CRL (nothing revoked) from the test CA and serves it to the validator. */
public final class TestCrl implements ICrlClient {

    private final byte[] encodedCrl;

    public TestCrl() throws Exception {
        char[] storePassword = System.getenv("BILLING_KEYSTORE_PASSWORD").toCharArray();
        KeyStore store = KeyStore.getInstance("PKCS12");
        try (InputStream in = Files.newInputStream(Path.of("pki/billing-test-ca.p12"))) {
            store.load(in, storePassword);
        }
        PrivateKey caKey = (PrivateKey) store.getKey("billing-test-ca", storePassword);
        X509Certificate ca = (X509Certificate) store.getCertificate("billing-test-ca");

        long now = System.currentTimeMillis();
        X509v2CRLBuilder builder = new X509v2CRLBuilder(
                new JcaX509CertificateHolder(ca).getSubject(), new Date(now - 60_000));
        builder.setNextUpdate(new Date(now + 7L * 24 * 3600 * 1000));
        X509CRLHolder crl = builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(caKey));
        this.encodedCrl = crl.getEncoded();
    }

    @Override
    public Collection<byte[]> getEncoded(X509Certificate checkCert, String url) {
        return List.of(encodedCrl);
    }
}

Output of the run (the messages are those of iText 9.8.0; paths and times differ on your machine):

== without trust anchor: INDETERMINATE
  failure [Revocation data check.] Certificate revocation status cannot be checked: no revocation data available or the status cannot be determined.
  failure [Certificate check.] Certificate CN=Bookshelf Billing Signer, O=Bookshelf Test, C=ES isn't trusted and issuer certificate isn't provided.
  certificate failure: (the same two messages again, as certificate-level items)
== with test CA as trust anchor: INDETERMINATE
  failure [Revocation data check.] Certificate revocation status cannot be checked: no revocation data available or the status cannot be determined.
  certificate failure: (the same revocation message)
== with trust anchor and a CRL: VALID

This is the honest answer for a self-signed test CA, and it is worth reading slowly. The signature is cryptographically perfect in all three runs. The first run is INDETERMINATE because the validator has never heard of the issuer: the chain ends in a certificate that nobody told it to trust, which is exactly what happens when an unknown Acrobat user opens a document signed with a private CA. The second run trusts the CA but still cannot conclude, because the file contains no revocation evidence and the test certificate has no CRL distribution point or OCSP address that iText could fetch. Only when evidence is supplied does the result become VALID. Nothing was "fixed" by the last run: the three results differ only in the information given to the validator. In production the second and third runs are replaced by a real CA that publishes revocation endpoints (or by embedding revocation data in the file, see the long-term section), and the first by a trust list that you control or by the EU trusted lists described below.

Failures that are not indeterminate

The next example makes two different alterations of the signed invoice and validates both with the trust anchor and the CRL from before. The first replaces one digit of the page width inside the signed bytes (the same number of characters, so the /ByteRange offsets stay valid); the second appends a paragraph to the document in append mode, a new revision that leaves every signed byte untouched.

package example;

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

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.List;

public final class TamperDemo {

    public static void main(String[] args) throws Exception {
        Path signed = Path.of("out/invoice-signed.pdf");
        X509Certificate ca = ItextValidate.loadCertificate(Path.of("pki/billing-test-ca.pem"));

        // 1. Change one byte inside the signed range: page width 595 becomes 596.
        String text = new String(Files.readAllBytes(signed), StandardCharsets.ISO_8859_1);
        Files.write(Path.of("out/invoice-edited-bytes.pdf"),
                text.replaceFirst("MediaBox\\[0 0 595 842]", "MediaBox[0 0 596 842]")
                        .getBytes(StandardCharsets.ISO_8859_1));

        // 2. Append a new revision after the signature.
        try (PdfDocument pdf = new PdfDocument(new PdfReader(signed.toString()),
                new PdfWriter("out/invoice-appended.pdf"), new StampingProperties().useAppendMode());
             Document document = new Document(pdf)) {
            document.add(new Paragraph("Added after signing"));
        }

        for (String name : List.of("invoice-edited-bytes.pdf", "invoice-appended.pdf")) {
            ItextValidate.print(name, ItextValidate.validate(Path.of("out/" + name), List.of(ca), true));
        }
    }
}
== invoice-edited-bytes.pdf: INVALID
  failure [Signature verification check.] Signature billing-signature cannot be mathematically verified.
== invoice-appended.pdf: INVALID
  failure [DocMDP check.] Page was unexpectedly modified.

The first result is the textbook integrity failure. The second is more instructive: the original signature is still mathematically correct, since the bytes it covers did not change, but the document revisions validator (DocumentRevisionsValidator, which SignatureValidator calls internally) compared the later revision with the signed one and found a change to a page that the document’s modification rules (DocMDP, "document modification detection and prevention") do not allow. An uncertified signature allows little beyond adding signatures and filling form fields, and a certification signature can tighten or relax this. A consequence for the Bookshelf Billing workflow: if a stamp, a page number or an archiving annotation has to be added to an invoice, it must be added before the signature, or the document must be signed in a way that explicitly permits it.

The validation policy

SignatureValidationProperties holds the policy, and it is set per combination of three dimensions, each with a small enum of constants in com.itextpdf.signatures.validation.context: the validator that is running (for example CRL_VALIDATOR, OCSP_VALIDATOR, REVOCATION_DATA_VALIDATOR, CERTIFICATE_CHAIN_VALIDATOR, SIGNATURE_VALIDATOR, DOCUMENT_REVISIONS_VALIDATOR), the role of the certificate being examined (SIGNER_CERT, CERT_ISSUER, TRUSTED, TIMESTAMP, OCSP_ISSUER, CRL_ISSUER, LOTL_CERT), and whether the evidence is HISTORICAL (covered by a timestamp) or PRESENT (judged now). The factory classes ValidatorContexts, CertificateSources and TimeBasedContexts build the sets (of(...), all(), complementOf(...)).

Three settings matter most:

  • Freshness. The maximum accepted age of a CRL or OCSP response when the validation happens. The defaults, from the iText Knowledge Base article on the policy, are one minute for historical revocation data (data stored in the document and covered by a timestamp) and 30 days for present data (data not covered by any timestamp).

  • Online fetching. ALWAYS_FETCH, FETCH_IF_NO_OTHER_DATA_AVAILABLE or NEVER_FETCH decide whether the library may open network connections to get CRLs, OCSP responses and issuer certificates. In a locked-down service the right choice is often NEVER_FETCH plus clients that read from your own store.

  • Continue after failure. Whether the validator stops at the first failing check or keeps collecting failures, which is what you want for a diagnostic report and not what you want for a fast reject.

The policy also states which certificate extensions are required. Per the same article, a signing certificate needs no key usage extension, which technically contradicts ETSI but matches Adobe Acrobat and most other tools; a CA certificate must have the basic constraints extension with the CA flag, and keyCertSign if key usage is present; certificates that sign OCSP responses need the ocsp_signing extended key usage, and those that sign timestamps need time_stamping. For certificate validity, iText uses only the "shell" model: the whole chain must have been valid at the time of signing, so an intermediate CA’s validity must contain that of every certificate it issued. The "chain" model, which only asks that each certificate was valid when it signed the next one, is not implemented in iText (DSS’s bundled policy also declares the shell model, shown below).

package example;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.validation.SignatureValidationProperties;
import com.itextpdf.signatures.validation.ValidatorChainBuilder;
import com.itextpdf.signatures.validation.context.CertificateSources;
import com.itextpdf.signatures.validation.context.TimeBasedContexts;
import com.itextpdf.signatures.validation.context.ValidatorContexts;
import com.itextpdf.signatures.validation.report.ValidationReport;
import com.itextpdf.signatures.validation.report.pades.PAdESLevelReportGenerator;
import com.itextpdf.signatures.validation.report.xml.PadesValidationReport;
import com.itextpdf.signatures.validation.report.xml.XmlReportAggregator;
import com.itextpdf.signatures.validation.report.xml.XmlReportGenerator;
import com.itextpdf.signatures.validation.report.xml.XmlReportOptions;

import java.io.StringWriter;
import java.nio.file.Path;
import java.security.cert.Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

import static com.itextpdf.signatures.validation.SignatureValidationProperties.OnlineFetching.NEVER_FETCH;
import static com.itextpdf.signatures.validation.context.CertificateSource.SIGNER_CERT;
import static com.itextpdf.signatures.validation.context.TimeBasedContext.PRESENT;
import static com.itextpdf.signatures.validation.context.ValidatorContext.CRL_VALIDATOR;
import static com.itextpdf.signatures.validation.context.ValidatorContext.OCSP_VALIDATOR;
import static com.itextpdf.signatures.validation.context.ValidatorContext.REVOCATION_DATA_VALIDATOR;

public final class ItextPolicy {

    public static void main(String[] args) throws Exception {
        List<Certificate> anchors = new ArrayList<>();
        for (int i = 1; i < args.length; i++) {
            anchors.add(ItextValidate.loadCertificate(Path.of(args[i])));
        }

        SignatureValidationProperties properties = new SignatureValidationProperties()
                // Revocation data of the signer certificate may be at most one day old, not 30 days.
                .setFreshness(ValidatorContexts.of(CRL_VALIDATOR, OCSP_VALIDATOR),
                        CertificateSources.of(SIGNER_CERT), TimeBasedContexts.of(PRESENT),
                        Duration.ofDays(1))
                // Never call the network: use only what is in the file or what we supply.
                .setRevocationOnlineFetching(ValidatorContexts.of(REVOCATION_DATA_VALIDATOR),
                        CertificateSources.all(), TimeBasedContexts.all(), NEVER_FETCH);
        properties.addCrlClient(new TestCrl());

        PAdESLevelReportGenerator padesLevels = new PAdESLevelReportGenerator();
        XmlReportAggregator xmlReport = new XmlReportAggregator();

        try (PdfDocument document = new PdfDocument(new PdfReader(args[0]))) {
            ValidatorChainBuilder builder = new ValidatorChainBuilder()
                    .withSignatureValidationProperties(properties)
                    .withTrustedCertificates(anchors)
                    .withPAdESLevelReportGenerator(padesLevels)
                    .withAdESLevelReportGenerator(xmlReport);
            ValidationReport report = builder.buildSignatureValidator(document).validateSignatures();
            System.out.println("result: " + report.getValidationResult());
        }

        var levels = padesLevels.getReport();
        System.out.println("document level: " + levels.getDocumentLevel());
        levels.getSignatureReports().forEach((name, level) -> {
            System.out.println("  " + name + " -> " + level.getLevel());
            level.getNonConformaties().forEach((target, reasons) -> reasons.forEach(
                    r -> System.out.println("    not " + target + ": " + r.lines().findFirst().orElse(""))));
        });

        PadesValidationReport adesReport = xmlReport.getReport();
        StringWriter xml = new StringWriter();
        new XmlReportGenerator(new XmlReportOptions()).generate(adesReport, xml);
        System.out.println("ETSI XML report: " + xml.getBuffer().length() + " characters");
    }
}

Run with out/invoice-signed.pdf pki/billing-test-ca.pem:

result: VALID
document level: B_B
  billing-signature -> B_B
    not B_T: There must be a signature or document timestamp available
    not B_LT: Issuer for the following certificates is missing:
    not B_LT: A DSS dictionary is missing
    not B_LTA: Revocation data for the following certificates is not timestamped:
    not B_LTA: A document timestamp is missing
    not B_LTA: The DSS entry is not covered by a document timestamp
ETSI XML report: 3305 characters

Two additional reports appear in this example, and they answer questions that ValidationReport does not. PAdESLevelReportGenerator is an event handler registered with withPAdESLevelReportGenerator; after the validation it tells which PAdES baseline level each signature reaches (PAdESLevel.B_B, B_T, B_LT, B_LTA, or NONE/INDETERMINATE) and, for every higher level, why it was not reached. Here the file is a valid B-B signature, and the list reads like a to-do list for the upgrade: add a timestamp for B-T, a DSS dictionary with the revocation data for B-LT, and a document timestamp covering the DSS for B-LTA. XmlReportAggregator collects an ETSI TS 119 102-2 style validation report as XML (the root element is ValidationReport in the namespace http://uri.etsi.org/19102/v1.2.1#), which can be stored as an audit record of the validation. These reporting classes are present in the 9.8.0 jar, but the iText Knowledge Base did not document them at the time of writing; their API may change more readily than SignatureValidator.

Trusting the EU list of trusted lists

Maintaining your own set of trust anchors is fine for a closed ecosystem, but a Spanish invoice that was signed with a qualified certificate must be checked against the trusted lists that every EU member state publishes under eIDAS. The List of Trusted Lists (LOTL) is a signed XML file at https://ec.europa.eu/tools/lotl/eu-lotl.xml that points to those national lists; each national list names the trust service providers and the certificates of their services, with a service type that tells in which role a certificate can be trusted (issuing qualified certificates, timestamps, OCSP). A careful validator must not just download the certificates, it must verify the signatures of the LOTL and of every national list. iText does this, per the Knowledge Base article on the subject.

Switching it on is one call on the builder, trustEuropeanLotl(true), plus one rule: the data is large and slow to fetch, so it lives in a static cache that you must initialise once, at application start, before the first validation, with LotlService.initializeGlobalCache(LotlFetchingProperties). Initialising it twice is refused on purpose. The cache is refreshed in the background by a timer; by default the data is refreshed about every 5.5 hours (23% of the staleness) and is usable for 24 hours if refresh fails. LotlFetchingProperties takes a mandatory strategy for what to do when one country’s list cannot be retrieved (RemoveOnFailingCountryData ignores that country, ThrowExceptionOnFailingCountryData fails), and optional filters for the countries and for the service types. Because the LOTL’s own signature must be verified with certificates that are not in the LOTL, iText needs the certificates published in the Official Journal of the EU; they ship in the separate artifact com.itextpdf:eu-trusted-lists-resources (version 1.1.0 on Maven Central at the time of writing), which you add as a dependency and update when a new version appears. The article states that a transition period of about 15 days applies when the journal certificates change, during which iText logs a warning, and afterwards LOTL validation fails if the resources are outdated.

package example;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.validation.ValidatorChainBuilder;
import com.itextpdf.signatures.validation.lotl.LotlCountryCodeConstants;
import com.itextpdf.signatures.validation.lotl.LotlFetchingProperties;
import com.itextpdf.signatures.validation.lotl.LotlService;
import com.itextpdf.signatures.validation.lotl.RemoveOnFailingCountryData;
import com.itextpdf.signatures.validation.report.ValidationReport;

public final class ItextLotl {

    public static void main(String[] args) throws Exception {
        // Once per JVM, at start-up: downloads and validates the EU LOTL and the selected national lists.
        LotlService.initializeGlobalCache(
                new LotlFetchingProperties(new RemoveOnFailingCountryData())
                        .setCountryNames(LotlCountryCodeConstants.SPAIN));

        try (PdfDocument document = new PdfDocument(new PdfReader(args[0]))) {
            ValidatorChainBuilder builder = new ValidatorChainBuilder().trustEuropeanLotl(true);
            ValidationReport report = builder.buildSignatureValidator(document).validateSignatures();
            System.out.println("result: " + report.getValidationResult());
            report.getFailures().forEach(f -> System.out.println("  " + f.getMessage()));
        }
    }
}

Added to the Maven file:

<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>eu-trusted-lists-resources</artifactId>
  <version>1.1.0</version>
</dependency>

The example was run with the network available (the LOTL endpoint answered). The test file is signed by a private CA that is not in any national list, so the result is again INDETERMINATE with the two familiar messages (untrusted issuer, no revocation data): the lists were fetched and validated, they just do not know the test CA. That is the expected outcome and a good sanity test of the setup. Real qualified-certificate signatures were not available for this page, so the positive case against a genuine trust service provider was not exercised. For several configurations in one process (one country for one customer, another for the next) the builder accepts its own withLotlService(Supplier<LotlService>); each LotlService has its own cache and timer and implements AutoCloseable, so close it when done. The Knowledge Base lists known problems that you will meet on a real network: an SSLHandshakeException when the JDK is too old to trust a national server’s TLS certificate, servers that refuse an empty user agent (both fixed by supplying an IResourceRetriever with withCustomResourceRetriever), and the configuration needed under GraalVM native image. The LOTL endpoint can be unstable; the KB offers samples that read local copies first.

Validating with EU DSS

DSS is the European Commission’s open-source reference implementation of the AdES standards (PAdES for PDF, and also CAdES, XAdES, JAdES and ASiC). It is licensed under LGPL-2.1 and is the library behind the Commission’s own validation service. For PDF it parses documents with PDFBox (Apache-2.0), so it does not depend on iText. Where iText returns one report with a handful of messages, DSS produces four XML documents of increasing detail, which makes it the better tool when the answer must be explained to an auditor.

The entry point is SignedDocumentValidator.fromDocument(DSSDocument), which picks the right validator for the document type through the Java service loader, then a CertificateVerifier that carries the trust anchors and the sources of revocation data, and finally validateDocument(), which returns a Reports object.

DSS 6 split optional pieces into separate modules that must be on the classpath, and the first run tells you which in a message (three messages were hit while building the example, with the artifact list in the Maven section above as the cure): No ValidationPolicyFactory has been found means add dss-policy-jaxb; No implementation found for ICMSUtils means choose one of dss-cms-object or dss-cms-stream; No implementation found for ICRLUtils means choose dss-crl-parser-x509crl or dss-crl-parser-stream; and the class SignedDocumentValidator itself lives in dss-validation, which is not pulled in transitively by dss-pades-pdfbox.

package example;

import eu.europa.esig.dss.enumerations.Indication;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.model.FileDocument;
import eu.europa.esig.dss.simplereport.SimpleReport;
import eu.europa.esig.dss.spi.DSSUtils;
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.spi.x509.CommonTrustedCertificateSource;
import eu.europa.esig.dss.validation.SignedDocumentValidator;
import eu.europa.esig.dss.validation.reports.Reports;

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

public final class DssValidate {

    public static void main(String[] args) throws Exception {
        DSSDocument document = new FileDocument(new File(args[0]));

        CommonTrustedCertificateSource trusted = new CommonTrustedCertificateSource();
        for (int i = 1; i < args.length; i++) {
            trusted.addCertificate(DSSUtils.loadCertificate(new File(args[i])));
        }
        CommonCertificateVerifier verifier = new CommonCertificateVerifier();
        verifier.setTrustedCertSources(trusted);

        SignedDocumentValidator validator = SignedDocumentValidator.fromDocument(document);
        validator.setCertificateVerifier(verifier);

        String policy = System.getProperty("policy");
        Reports reports = policy == null
                ? validator.validateDocument()
                : validator.validateDocument(new File(policy));

        SimpleReport simple = reports.getSimpleReport();
        for (String id : simple.getSignatureIdList()) {
            Indication indication = simple.getIndication(id);
            System.out.println("signature: " + indication + " / " + simple.getSubIndication(id)
                    + " / format " + simple.getSignatureFormat(id));
            simple.getAdESValidationErrors(id).forEach(m -> System.out.println("  error: " + m.getValue()));
            simple.getAdESValidationWarnings(id).forEach(m -> System.out.println("  warning: " + m.getValue()));
        }

        Files.createDirectories(Path.of("out/reports"));
        Files.writeString(Path.of("out/reports/simple.xml"), reports.getXmlSimpleReport());
        Files.writeString(Path.of("out/reports/detailed.xml"), reports.getXmlDetailedReport());
        Files.writeString(Path.of("out/reports/diagnostic.xml"), reports.getXmlDiagnosticData());
        Files.writeString(Path.of("out/reports/etsi.xml"), reports.getXmlValidationReport());
    }
}

Derived from the validation chapters of the DSS documentation. Running it against the same files as before gives (identifiers shortened):

File and trust anchors Indication Sub-indication Main messages

invoice-signed.pdf, none

INDETERMINATE

NO_CERTIFICATE_CHAIN_FOUND

The certificate chain for signature is not trusted, it does not contain a trust anchor.

invoice-signed.pdf, test CA

INDETERMINATE

CERTIFICATE_CHAIN_GENERAL_FAILURE

No revocation data found for the certificate. Warnings: authority and revocation info access not present.

invoice-edited-bytes.pdf, test CA

TOTAL_FAILED

HASH_FAILURE

The reference data object is not intact!

invoice-appended.pdf, test CA

INDETERMINATE

CERTIFICATE_CHAIN_GENERAL_FAILURE

Same revocation error, plus warnings: visual difference detected on page 1, and undefined object modifications after the signature revision.

The two libraries agree on the facts and differ in presentation. The altered bytes are a definite failure in both. For the appended revision, iText returned INVALID through its DocMDP check, while DSS kept the indication at INDETERMINATE (for the revocation reason) and added the modification as a warning: DSS reports the visual difference and the unknown modifications as findings and leaves the decision to the policy and to the reader. Neither is wrong, but they are not interchangeable in an automated accept or reject rule, and a team that runs both should state which one decides.

The four reports

  • The simple report (SimpleReport) is the macroscopic view: for each signature an indication, a sub-indication, the detected format (here PAdES-BASELINE-B), the signer, the signing time, and the errors and warnings seen above. It is what a user interface shows. It also names the validation policy that was used; with the default one that is "QES AES/QC AES TL based", which expects the signer’s chain to end in a certificate from a trusted list.

  • The detailed report (DetailedReport) lists every constraint that the policy evaluated, in the building blocks of ETSI EN 319 102-1 (format checking, identification of the signing certificate, validation context initialisation, X.509 certificate validation, cryptographic verification, signature acceptance validation, and so on), each with its status and a message key. In the test above the detailed document for one signature contains about 66 constraint entries. It answers "which check, exactly, failed". The documentation chapter "Interpreting a detailed report" shows how to follow a failure back to its root.

  • The diagnostic data (DiagnosticData) is the raw, policy-independent material: every certificate with its extensions, every digest comparison, every revocation datum found, every timestamp. The detailed and simple reports are computed from it plus the policy, which makes it the right artefact to keep for a re-evaluation: you can run the same diagnostic data against a new policy later without having the original file or the network.

  • The ETSI validation report is the machine-readable report standardised in ETSI TS 119 102-2 (root element ValidationReport, here in the namespace http://uri.etsi.org/19102/v1.4.1#), meant to be exchanged between systems or archived. It carries the signature identifier with its digest, the validation status and the validation time. (The iText XML report above uses version 1.2.1 of the same schema.)

Sample documents for each report and a section on reading them are in the DSS documentation, chapter Inspection of validation reports. Stylesheets that turn the simple and detailed reports into HTML or PDF come with the library.

Policy and validation level in DSS

DSS ships its default policy as an XML file inside dss-policy-jaxb (policy/constraint.xml). Each rule has a level: FAIL, WARN, INFORM or IGNORE. That file is the policy you copy and edit to adapt the validator, and then pass to validateDocument(File), as the example above does when started with -Dpolicy=.... As an experiment, the file was copied and the two lines for the signing certificate, RevocationDataAvailable and AcceptableRevocationDataFound, were changed from FAIL to WARN. The same invoice-signed.pdf with the test CA then gave TOTAL_PASSED with an extra warning "No revocation data found for the certificate!". The lesson is double-edged: a policy is how you adapt the validator to an organisation that has no revocation infrastructure, and also how you can quietly turn a failure into a pass. A policy used in production must be reviewed, versioned and archived with the validation results.

The bundled policy also contains the cryptographic suitability rules, which matter for the long-term section. It lists the acceptable encryption and digest algorithms, minimum key sizes, and an AlgoExpirationDate table with the date from which an algorithm (or an algorithm with a key size below a given number) is no longer accepted, citing ETSI TS 119 312. In the DSS 6.5 file, for example, SHA-1 is dated 2012-08-01 and MD5 2004-08-01, SHA-224 2029-01-01, RSA keys shorter than 1900 bits 2027-01-01 and RSA keys shorter than 3000 bits 2035-01-01, while SHA-256 and larger have no end date. The signer key of the test PKI (RSA 3072, SHA-256) is therefore acceptable until the table is revised. The same file declares <Model Value="SHELL" />, the same certificate validity model that iText uses. Treat those dates as the state of the policy at the time of writing, not as law.

Two more knobs are worth knowing. The validator can be told how deep to go with setValidationLevel: BASIC_SIGNATURES, TIMESTAMPS, LONG_TERM_DATA or ARCHIVAL_DATA (the default), which correspond to sections 5.3 to 5.6 of ETSI EN 319 102-1. And revocation data can come from sources set on the CertificateVerifier (OnlineOCSPSource, OnlineCRLSource, offline sources), which are in dss-service; without them, only the data embedded in the file is used.

Trusted lists in DSS

DSS reads the same EU trusted lists with a TLValidationJob (module dss-tsl-validation). You describe the LOTL as a LOTLSource with its URL and the certificates of the Official Journal in a KeyStoreCertificateSource, give the job a cached data loader, run onlineRefresh() and pass the resulting TrustedListsCertificateSource to the CommonCertificateVerifier as a trusted source. The sketch below was compiled against DSS 6.5 but not run, because it needs a keystore with the Official Journal certificates that this page does not ship; its path and password come from environment variables, and the maintained way to get that keystore, and the application code that schedules the refresh, are in the DSS documentation chapter on TLValidationJob.

package example;

import eu.europa.esig.dss.service.http.commons.CommonsDataLoader;
import eu.europa.esig.dss.service.http.commons.FileCacheDataLoader;
import eu.europa.esig.dss.spi.tsl.TrustedListsCertificateSource;
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.spi.x509.KeyStoreCertificateSource;
import eu.europa.esig.dss.tsl.job.TLValidationJob;
import eu.europa.esig.dss.tsl.source.LOTLSource;

import java.io.File;

public final class DssLotl {

    public static CommonCertificateVerifier verifierWithEuTrustedLists() throws Exception {
        KeyStoreCertificateSource officialJournalCerts = new KeyStoreCertificateSource(
                new File(System.getenv("OJ_KEYSTORE_PATH")), "PKCS12",
                System.getenv("OJ_KEYSTORE_PASSWORD").toCharArray());

        LOTLSource lotl = new LOTLSource();
        lotl.setUrl("https://ec.europa.eu/tools/lotl/eu-lotl.xml");
        lotl.setCertificateSource(officialJournalCerts);
        lotl.setPivotSupport(true);

        FileCacheDataLoader loader = new FileCacheDataLoader(new CommonsDataLoader());
        loader.setCacheExpirationTime(24L * 3600 * 1000);
        loader.setFileCacheDirectory(new File("dss-tl-cache"));

        TrustedListsCertificateSource trustedLists = new TrustedListsCertificateSource();
        TLValidationJob job = new TLValidationJob();
        job.setTrustedListCertificateSource(trustedLists);
        job.setOnlineDataLoader(loader);
        job.setListOfTrustedListSources(lotl);
        job.onlineRefresh();

        CommonCertificateVerifier verifier = new CommonCertificateVerifier();
        verifier.setTrustedCertSources(trustedLists);
        return verifier;
    }
}

Dependency: eu.europa.ec.joinup.sd-dss:dss-tsl-validation:6.5. Derived from the DSS documentation (dss-documentation.html, sections on the trusted-list job).

The DSS demo webapp

The European Commission hosts a demonstration of DSS at the DSS webapp demo. You upload a signed document and get the simple, detailed, diagnostic and ETSI reports in the browser, which is the quickest way to get a second opinion on a file that one of the libraries rejects or to see what a report looks like before writing code. Its own documentation says that the demonstration is meant for testing and that the Commission accepts no liability for its use, so do not upload confidential invoices to it; for real documents, run the same library inside your infrastructure.

Adobe Acrobat and what the recipient sees

Whatever your library says, the customer will probably open the invoice in Adobe Acrobat or Acrobat Reader, and what they see depends on settings of their installation, not on your code. Acrobat keeps its own store of trusted certificates and uses three kinds of sources:

  • The Adobe Approved Trust List (AATL). Certificates of authorities that Adobe has approved are trusted globally, and a signature whose credential chains to a member of the list gets a green check. Acrobat downloads the list from an Adobe server automatically, which can be disabled in Preferences, Trust Manager, or by an administrator. See Adobe Approved Trust List Members.

  • The European Union Trusted Lists (EUTL). Acrobat has built-in support for them and refreshes them on a schedule (Preferences, Trust Manager, Automatic European Union Trust Lists updates, Update Now). The validation panel can show a qualified-status result, and Adobe states on its page that Acrobat does not query the authoritative list at the moment of validation and that legal decisions should use a qualified validation service or the EU Trusted List Browser at https://eidas.ec.europa.eu/efda/tl-browser/. See Adobe’s page European Union Trusted Lists.

  • Certificates the user trusts manually. A recipient can add the issuer certificate and choose what to trust it for (signatures, certified documents, dynamic content). Trusting a certificate as a root trusts everything issued under it, so this should be done with a certificate obtained through a channel other than the signed document. See Set certificate trust level in Acrobat.

For a closed ecosystem (your own customers, a private CA like the test CA), the practical procedure is to distribute the CA certificate (not the signer’s certificate) and tell recipients to add it as a trusted root for certified documents and signatures, or to push it by policy in a managed environment. Without that step the signature appears with a yellow warning or a question mark: the signature itself is intact, and the chain is not trusted, which is the INDETERMINATE of the first iText run seen from the user’s side.

The preferences under Preferences, Signatures (verifying signatures when a document is opened, using the time of an expired timestamp, requiring revocation checking) change how strictly Acrobat judges. Adobe’s help page Set digital signature validation preferences in Acrobat lists them. Two practical consequences: the same file can look different on two machines, so a support answer cannot be given without knowing the recipient’s settings; and "Acrobat says valid" is no substitute for server-side validation when you need an audit trail. Adobe’s internal definition of "LTV enabled", as recalled in the iText Knowledge Base article on timestamps and LTV, is that everything needed to validate the file except root certificates is contained in it, and that no timestamp is strictly required for that label.

The pages on Adobe Help block automated requests (a command line client received HTTP 403 for every address), so the links were checked through a web search that returned them, not by fetching them; open them in a browser.

Keeping signatures valid for decades

An invoice has to be kept for several years and some documents for much longer. The promise of a signature (this content existed at this time and was approved by this key holder) must outlast the certificates, the revocation services and the algorithms that supported it. Three things expire.

  • The signer certificate: typically two or three years. After that, the signature is still good if the validator can show it was made while the certificate was valid, which needs a trusted timestamp.

  • The revocation services: CRLs and OCSP responders are not required to keep answers for old certificates, so a validation in 2040 may not be able to find out what the status was in 2026 unless the answer was saved in the file.

  • The cryptography: a hash function or a key size that is safe today is broken or deprecated within the retention period. SHA-1 is the history lesson (see the policy table above, and finding O11 of this section’s source analysis: tutorials from the 2000s that sign with SHA-1 produce signatures that modern policies refuse for new signatures).

B-LT and B-LTA in practice

The PAdES baseline levels of PAdES Baseline Levels map to these three problems. B-T adds a signature timestamp, which fixes the time of signing. B-LT embeds the validation material (the certificates, and the CRL or OCSP answers for each) in a document security store, the /DSS dictionary, so that the revocation question can be answered offline. B-LTA adds a document timestamp over the whole file, including the DSS, so that the evidence itself has a proof of existence. Before the timestamp’s own certificate expires, or before its algorithm weakens, another document timestamp is added over everything, with fresh validation data for the previous timestamp. This is the renewal, and because each timestamp is an incremental update it does not disturb the earlier ones. A chain of document timestamps is what lets a validator in 2046 accept a signature from 2026: it sees that the file existed in 2026 (first timestamp), still existed in 2031 when the algorithm was still strong (second), and so on.

The Knowledge Base article on the relation of LTV and document timestamps, quoting a StackOverflow answer by mkl, adds a historical note worth knowing: the PAdES part that defined timestamps and the part that defined LTV information were written together, so some developers believe that adding a DSS requires a timestamp. Adobe’s own statement, quoted there, is that no timestamp is required for "LTV enabled", but that whenever a timestamp is added, validation information for the timestamp must be added too. In an archive you want both.

iText offers PdfPadesSigner for the whole sequence, signWithBaselineLTAProfile(...), and prolongSignatures(ITSAClient) for the renewal of an existing B-LTA file; the Java API page is PdfPadesSigner. The example below makes an LTA file with the test PKI and a public timestamp authority, then renews it. The timestamp server is https://freetsa.org/tsr, a free service that needs no credentials and is suitable for tests only.

package example;

import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.signatures.ITSAClient;
import com.itextpdf.signatures.PdfPadesSigner;
import com.itextpdf.signatures.SignerProperties;
import com.itextpdf.signatures.TSAClientBouncyCastle;

import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.List;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public final class LtaRenewal {

    public static void main(String[] args) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        char[] storePassword = System.getenv("BILLING_KEYSTORE_PASSWORD").toCharArray();
        KeyStore store = KeyStore.getInstance("PKCS12");
        try (var in = Files.newInputStream(Path.of("pki/billing-signer.p12"))) {
            store.load(in, storePassword);
        }
        PrivateKey privateKey = (PrivateKey) store.getKey("billing-signer", storePassword);
        Certificate[] chain = store.getCertificateChain("billing-signer");
        ITSAClient tsa = new TSAClientBouncyCastle("https://freetsa.org/tsr");
        X509Certificate ca = ItextValidate.loadCertificate(Path.of("pki/billing-test-ca.pem"));

        try (PdfReader reader = new PdfReader("out/invoice.pdf");
             OutputStream out = Files.newOutputStream(Path.of("out/invoice-lta.pdf"))) {
            PdfPadesSigner signer = new PdfPadesSigner(reader, out)
                    .setCrlClient(new TestCrl())
                    .setTrustedCertificates(List.of(ca));
            signer.signWithBaselineLTAProfile(new SignerProperties().setFieldName("billing-signature"),
                    chain, privateKey, tsa);
        }

        // Later - for instance from a yearly job: add a fresh document timestamp.
        try (PdfReader reader = new PdfReader("out/invoice-lta.pdf");
             OutputStream out = Files.newOutputStream(Path.of("out/invoice-lta-renewed.pdf"))) {
            new PdfPadesSigner(reader, out).prolongSignatures(tsa);
        }
    }
}

The TSA call succeeded from the test machine and the files were produced. Validating the results with the public CA certificate of the timestamp service added as a trust anchor (downloaded at run time from the service, not reproduced here) gave different answers in the two libraries, and the difference is worth understanding rather than hiding:

File iText level report DSS simple report

invoice-lta.pdf (after signWithBaselineLTAProfile)

B_T; B-LT not reached: issuers and revocation data missing

TOTAL_PASSED, format PAdES-BASELINE-T

invoice-lta-renewed.pdf (after prolongSignatures)

B_T; same reasons

TOTAL_PASSED, format PAdES-BASELINE-LTA

On this test PKI, signWithBaselineLTAProfile produced a file that DSS classifies as B-T, not B-LTA, because there was nothing to embed for B-LT: neither the test CA nor the free timestamp service publishes a revocation endpoint that the library can query, so the document security store could not be completed. After prolongSignatures, DSS recognises the structure as B-LTA and accepts it, while iText still reports B-T and names the missing issuer certificates and revocation data of the timestamp chain. Both reports are internally consistent; they apply different rules to a PKI that does not support the long-term profile. In production, with a CA and a TSA that publish CRLs or OCSP, the same two calls produce a file that both libraries should rate B-LTA, and the level report is how you check it in an automated test: assert on getDocumentLevel() rather than assuming. This was not verified against a real CA for this page. DSS can perform the same renewal with PAdESService.extendDocument and PAdES_BASELINE_LTA parameters plus an OnlineTSPSource (an extra document timestamp was added to the test file this way, the count of timestamps in the diagnostic data going from two to three); the details are in Signing with PDFBox & EU DSS.

A renewal schedule

When to add the next document timestamp is a policy decision, and the inputs are all measurable:

  • the notAfter date of the current timestamp authority’s certificate (renew comfortably before it);

  • the algorithm expiry dates in the policy you validate with (for example the digest of the last timestamp, and the key size of the timestamp authority);

  • the revocation data retention you can rely on.

A pragmatic scheme for the Bookshelf Billing archive is a yearly batch job (the structure is described in Generating Reports from Databases & Services and the Spring Batch and scheduling pages it links to) that selects the archived invoices whose last document timestamp is older than a year, or whose timestamp authority certificate expires within the next year, calls prolongSignatures or the DSS equivalent, validates the result, and stores the validation report next to the file. Because each renewal adds tens of kilobytes (the test file grew from about 63 KB to 96 KB), the storage cost of keeping thousands of invoices for a decade should be estimated before choosing the interval. A separate approach is an evidence record (RFC 4998, an Evidence Record Syntax) that renews many documents with one hash tree; DSS can validate those, but PDF readers do not understand them, so they are for the archive, not for the customer’s copy.

Algorithm expiry and migration

Algorithm expiry works through the same table that the policy uses. When a hash function reaches its end date, a signature made with it remains acceptable only if a timestamp, made with a still-acceptable algorithm before that end date, covers it. That is exactly why the document timestamp chain exists: each one lifts the protection of the previous ones to the current algorithms. A file signed with SHA-1 in 2010 and never timestamped cannot be saved afterwards, because there is no proof it existed before the algorithm was weak; this is the situation of every B-B signature without a timestamp. The default digest of the helper above is SHA-256, which the bundled DSS policy has no end date for.

Post-quantum signatures

The larger risk is that RSA and elliptic-curve signatures, which every PDF signature in the world uses today, would be breakable by a sufficiently powerful quantum computer, and that a signature that must stay credible for thirty years is exposed to it. The countermeasures are signature algorithms standardised by NIST: ML-DSA (FIPS 204, from the CRYSTALS-Dilithium family) and SLH-DSA (FIPS 205, based on SPHINCS+), with FN-DSA (Falcon) expected next. The iText Knowledge Base article on PQC support states the status plainly, and it is worth repeating without embellishment:

  • from iText Core 9.5.0, using Bouncy Castle, iText can generate and validate quantum-resistant signatures in a PDF, as a proof of concept;

  • this is a pre-standard implementation: ISO 32000 does not yet define how to embed PQC signatures, so a PDF signed this way is not a standards-compliant PDF for that feature, and other processors such as Adobe Reader will not validate it; it is useful for iText-to-iText workflows and experiments only;

  • it is not yet supported in the FIPS mode of Bouncy Castle;

  • ML-DSA is the recommended starting point (moderate size, about 2 to 4 KB per the article), SLH-DSA signatures are very large (8 to 50 KB), and the article notes that Rainbow, which was an NIST candidate, was broken and must not be used.

What this means for planning, from the article and from the structure of the problem: keep signing with RSA 3072 or ECDSA P-256 and SHA-256 for production today; make sure that every long-lived invoice has document timestamps so that a later algorithm change can be bridged by a new timestamp; do not use pre-standard PQC signatures for documents that customers must open in other tools; prepare the surrounding infrastructure (key storage, certificate authorities that can issue PQC certificates, larger /Contents placeholders because PQC signatures are bigger, validators that understand the new algorithms); and follow the PDF Association and ISO work to know when the format is final. This page does not show a PQC signing example because the pre-standard result cannot be validated by anything but iText itself, and it would give a false impression of readiness. The article links iText’s sample code on GitHub for those who want to experiment.

Derived from Post-Quantum Cryptography (PQC) support in iText. The NIST standards are FIPS 204 and FIPS 205.

Practical advice

  • Decide what each result means in your application. Write down what happens on VALID, INDETERMINATE and INVALID. Accepting INDETERMINATE silently and rejecting it silently are both bugs.

  • Validate on the server, with a policy you control. The recipient’s Acrobat is an opinion; your archive needs a repeatable verdict. Store the validation report (the DSS diagnostic data and the ETSI report, or the iText report) with the document and the validation time.

  • Keep trust anchors in configuration, not in the PDF. A certificate embedded in the file is evidence to be checked, never a reason to trust.

  • Provide revocation data when you sign. A file with no revocation data and a CA that will disappear is a file that will be INDETERMINATE forever. Use B-LT at least for documents with a retention period.

  • Add a document timestamp as the last step of anything that must be kept, and plan the renewal as a job, not as a hope.

  • Test the failure modes. The two tamper tests above are a few lines of code and should be in the test suite of any service that accepts or produces signed PDFs; see Testing & Performance with iText.

  • Mind the licences. iText Core is AGPLv3 or commercial. DSS (LGPL-2.1) and PDFBox (Apache-2.0) are alternatives when the AGPL does not fit.

  • Do not send confidential files to online demonstrations.

Outdated points to avoid

  1. Validating with PdfPKCS7.verify() alone: it checks the integrity layer and says nothing about the chain, revocation, timestamps or later modifications. Use SignatureValidator or DSS.

  2. PdfStamper.createSignature and adbe.pkcs7.sha1 signatures from the iText 1 and 2 era, and SHA-1 digests: policies reject them for new signatures (finding O11).

  3. Treating "the signature verifies" as "the document is valid": a verified signature with an untrusted chain, no revocation data or a forbidden later revision is not a valid document.

  4. Believing that an expired certificate invalidates earlier signatures. With a trusted timestamp (and a validator that uses it), signatures made before expiry stay valid, as the iText Knowledge Base explains.

  5. Assuming that "LTV enabled" requires a timestamp, or that a timestamp makes a file LTV enabled: they are different mechanisms that are best used together.

  6. Assuming Acrobat’s green check is a legal conclusion. Adobe’s own page on the EU trusted lists warns against it.

  7. Planning PQC around pre-standard signatures. They only interoperate with iText.