diff --git a/design-doc/3.1-engine-dss.md b/design-doc/3.1-engine-dss.md
index e4ac339f..3c850b86 100644
--- a/design-doc/3.1-engine-dss.md
+++ b/design-doc/3.1-engine-dss.md
@@ -381,8 +381,16 @@ Read by the engine through the phase-1 `EngineConfig` view (which already maps
# Required for producing LT/LTA. Default false to keep B/T fully offline.
engine.dss.online.enabled=false
-# Trusted-list (LOTL) support for building the trust anchor set.
-engine.dss.trust.useDefaultLotl=false
+# EU LOTL support for building the trust anchor set. trust.eu.enabled wires the
+# bundled European LOTL and validates its signature against the bundled Official
+# Journal (OJ) keystore; the eu.* overrides are optional (effective when enabled).
+engine.dss.trust.eu.enabled=false
+engine.dss.trust.eu.lotlUrl=
+engine.dss.trust.eu.ojUrl=
+engine.dss.trust.eu.ojKeystoreFile=
+engine.dss.trust.eu.ojKeystorePassword=
+
+# Generic / advanced trust material.
engine.dss.trust.lotlUrls=
engine.dss.trust.certFiles=
engine.dss.trust.certUrls=
@@ -391,6 +399,20 @@ engine.dss.trust.truststoreType=
engine.dss.trust.truststorePassword=
```
+EU LOTL keys live under the `trust.eu.*` sub-namespace (the turnkey path that
+validates the LOTL signature against the bundled OJ keystore and follows the OJ
+pivot chain); `trust.lotlUrls` and the cert/truststore keys stay un-prefixed as
+the generic "bring your own trust" path. The bundled OJ keystore
+(`engines/dss/src/main/resources/.../eu-oj-keystore.p12`) must be refreshed when
+the OJ re-issues the LOTL signing certificates — see the keystore README.
+
+**LT/LTA preflight (issue #432).** Because LT/LTA only embed revocation data for a
+*trusted* chain reachable *online*, a missing trust source or `online.enabled`
+makes DSS fail deep with an opaque untrusted-chain alert. `DssLtTrustPreflight`
+checks `online.enabled AND (trust.eu.enabled ∨ truststoreFile ∨ certFiles ∨
+certUrls ∨ lotlUrls)` up front: the CLI fails fast with the exact keys to set, and
+the GUI offers to enable the prerequisites before signing.
+
`EngineConfig` currently exposes `getString` / `getBoolean` / `getInt`; list-
valued keys (`lotlUrls`, `certFiles`, `certUrls`) are parsed by the engine from
a delimiter-separated string, so no `EngineConfig` API change is required. (If a
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 2f08acdd..b8db8977 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -46,6 +46,12 @@
.sh.cmd
+
+ --add-exports=jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED --add-exports=jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED --add-exports=java.base/sun.security.action=ALL-UNNAMED --add-exports=java.base/sun.security.rsa=ALL-UNNAMED --add-opens=java.base/sun.security.util=ALL-UNNAMEDunixwindows
@@ -240,6 +246,18 @@
${project.version}
+
+
+ org.slf4j
+ slf4j-jdk14
+ 2.0.17
+ runtime
+
+
+
+ eu.europa.ec.joinup.sd-dss
+ specs-trusted-list-v211
+ eu.europa.ec.joinup.sd-dssdss-crl-parser-x509crl
@@ -79,17 +86,16 @@
test
-
+
eu.europa.ec.joinup.sd-dssdss-validation
- testeu.europa.ec.joinup.sd-dssdss-policy-jaxb
- test
diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java
index d2df1027..47016c20 100644
--- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java
+++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java
@@ -322,6 +322,12 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC
LOGGER.info(RES.get("console.closeStream"));
}
finished = true;
+ } catch (eu.europa.esig.dss.alert.exception.AlertException e) {
+ // LT/LTA: DSS refused because revocation data could not be collected for the signer chain — it is
+ // not anchored by the configured trust material (its CA is not in the truststore / cert files /
+ // LOTL, or an MRA LOTL needs engine.dss.trust.lotlMraSupport=true). Surface that instead of an
+ // opaque stack trace.
+ LOGGER.log(Level.SEVERE, RES.get("console.dss.untrustedChain"), e);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, RES.get("console.exception"), e);
} catch (OutOfMemoryError e) {
diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java
index d94aa573..17185e89 100644
--- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java
+++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java
@@ -26,10 +26,10 @@
import eu.europa.esig.dss.spi.tsl.TrustedListsCertificateSource;
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.spi.x509.CertificateSource;
-import eu.europa.esig.dss.spi.x509.CommonCertificateSource;
import eu.europa.esig.dss.spi.x509.CommonTrustedCertificateSource;
import eu.europa.esig.dss.spi.x509.KeyStoreCertificateSource;
import eu.europa.esig.dss.spi.x509.aia.DefaultAIASource;
+import eu.europa.esig.dss.tsl.function.OfficialJournalSchemeInformationURI;
import eu.europa.esig.dss.tsl.job.TLValidationJob;
import eu.europa.esig.dss.tsl.source.LOTLSource;
@@ -50,14 +50,51 @@
final class DssTrustConfigurer {
static final String KEY_ONLINE_ENABLED = "online.enabled";
- static final String KEY_USE_DEFAULT_LOTL = "trust.useDefaultLotl";
+
+ // --- EU LOTL machinery (trust.eu.* sub-namespace) ---
+ /** Enable the bundled European LOTL (with the bundled OJ keystore). */
+ static final String KEY_EU_ENABLED = "trust.eu.enabled";
+ /** Override the default EU LOTL URL (only effective when {@link #KEY_EU_ENABLED} is set). */
+ static final String KEY_EU_LOTL_URL = "trust.eu.lotlUrl";
+ /** Override the Official Journal scheme-information URL used by the announcement predicate. */
+ static final String KEY_EU_OJ_URL = "trust.eu.ojUrl";
+ /** Override the bundled OJ keystore with an external file. */
+ static final String KEY_EU_OJ_KEYSTORE_FILE = "trust.eu.ojKeystoreFile";
+ /** Password for the OJ keystore override file. */
+ static final String KEY_EU_OJ_KEYSTORE_PASSWORD = "trust.eu.ojKeystorePassword";
+
+ // --- generic / advanced trust material ---
static final String KEY_LOTL_URLS = "trust.lotlUrls";
+ /**
+ * Enable Mutual Recognition Agreement processing for the {@link #KEY_LOTL_URLS} sources, so DSS grants
+ * trust to third-country trust services recognised via an MRA LOTL (e.g. the eIDAS international pilot's
+ * {@code mra_lotl.xml}). Off by default; only MRA LOTLs need it.
+ */
+ static final String KEY_LOTL_MRA_SUPPORT = "trust.lotlMraSupport";
static final String KEY_CERT_FILES = "trust.certFiles";
static final String KEY_CERT_URLS = "trust.certUrls";
static final String KEY_TRUSTSTORE_FILE = "trust.truststoreFile";
static final String KEY_TRUSTSTORE_TYPE = "trust.truststoreType";
static final String KEY_TRUSTSTORE_PASSWORD = "trust.truststorePassword";
+ /** Canonical EU List of Trusted Lists location. */
+ static final String DEFAULT_EU_LOTL_URL = "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
+
+ /**
+ * Default Official Journal scheme-information URL announcing the certificates allowed to sign the EU LOTL.
+ * Must stay in sync with the bundled OJ keystore ({@link #OJ_KEYSTORE_RESOURCE}); both were rotated by
+ * OJ C/2026/1944 (April 2026). Override via {@link #KEY_EU_OJ_URL} when pointing at a newer OJ notice.
+ */
+ static final String DEFAULT_OJ_URL =
+ "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=OJ:C_202601944";
+
+ /** Classpath location of the bundled OJ keystore (validates the EU LOTL's own signature). */
+ static final String OJ_KEYSTORE_RESOURCE = "/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12";
+
+ /** Keystore type / password of the bundled OJ keystore (matches the DSS demonstrations keystore). */
+ private static final String OJ_KEYSTORE_TYPE = "PKCS12";
+ private static final String OJ_KEYSTORE_PASSWORD = "dss-password";
+
/** Separator for the list-valued keys (lotlUrls / certFiles / certUrls). */
private static final String LIST_SEPARATOR = "[,;]+";
@@ -125,7 +162,15 @@ private CertificateSource[] createTrustedCertSources() throws Exception {
tlValidationJob.setListOfTrustedListSources(lotlSources);
TrustedListsCertificateSource trustedListsCertificateSource = new TrustedListsCertificateSource();
tlValidationJob.setTrustedListCertificateSource(trustedListsCertificateSource);
- tlValidationJob.onlineRefresh();
+ try {
+ tlValidationJob.onlineRefresh();
+ } catch (Exception e) {
+ // Surface an actionable cause instead of an opaque DSS stack trace; the caller logs this via
+ // console.dss.trustConfigFailed and aborts signing. Common causes: offline / proxy not
+ // configured, or a stale OJ keystore that can no longer validate the LOTL signature.
+ throw new IllegalStateException("Failed to refresh the EU LOTL / trusted lists (check network /"
+ + " proxy, or update the OJ keystore via " + KEY_EU_OJ_KEYSTORE_FILE + ")", e);
+ }
trustedSources.add(trustedListsCertificateSource);
}
@@ -173,20 +218,71 @@ private static File tlCacheDirectory() {
return cacheDir;
}
- private LOTLSource[] getLotlSources() {
+ LOTLSource[] getLotlSources() throws Exception {
List lotlSources = new ArrayList<>();
- if (config.getBoolean(KEY_USE_DEFAULT_LOTL, false)) {
- lotlSources.add(new LOTLSource());
+ CertificateSource ojCertificateSource = null;
+
+ if (config.getBoolean(KEY_EU_ENABLED, false)) {
+ ojCertificateSource = ojKeystoreCertificateSource();
+ lotlSources.add(europeanLotlSource(ojCertificateSource));
+ }
+
+ List customLotlUrls = splitList(config.getString(KEY_LOTL_URLS));
+ if (!customLotlUrls.isEmpty() && ojCertificateSource == null) {
+ ojCertificateSource = ojKeystoreCertificateSource();
}
- for (String url : splitList(config.getString(KEY_LOTL_URLS))) {
+ boolean mraSupport = config.getBoolean(KEY_LOTL_MRA_SUPPORT, false);
+ for (String url : customLotlUrls) {
+ // Advanced / "bring your own trust": signed by the (bundled or overridden) OJ certs, pivot
+ // support on, but no OJ announcement predicate (a custom LOTL may not announce the EU OJ URL).
+ // MRA support is opt-in for third-country mutual-recognition LOTLs.
LOTLSource lotlSource = new LOTLSource();
lotlSource.setUrl(url);
- lotlSource.setCertificateSource(new CommonCertificateSource());
+ lotlSource.setCertificateSource(ojCertificateSource);
+ lotlSource.setPivotSupport(true);
+ lotlSource.setMraSupport(mraSupport);
lotlSources.add(lotlSource);
}
return lotlSources.toArray(new LOTLSource[0]);
}
+ /**
+ * Builds the European LOTL source wired so DSS can validate the LOTL's own signature against the OJ
+ * keystore and follow the pivot chain to the current trust anchors. The URL and OJ scheme-information URL
+ * default to the canonical EU values and are overridable via {@link #KEY_EU_LOTL_URL} / {@link #KEY_EU_OJ_URL}.
+ */
+ private LOTLSource europeanLotlSource(CertificateSource ojCertificateSource) {
+ LOTLSource lotl = new LOTLSource();
+ lotl.setUrl(config.getString(KEY_EU_LOTL_URL, DEFAULT_EU_LOTL_URL));
+ lotl.setCertificateSource(ojCertificateSource);
+ lotl.setSigningCertificatesAnnouncementPredicate(
+ new OfficialJournalSchemeInformationURI(config.getString(KEY_EU_OJ_URL, DEFAULT_OJ_URL)));
+ lotl.setPivotSupport(true);
+ return lotl;
+ }
+
+ /**
+ * Loads the certificate source that validates the LOTL signature: an external keystore when
+ * {@link #KEY_EU_OJ_KEYSTORE_FILE} is set, otherwise the keystore bundled on the classpath. Unlike
+ * {@link #KEY_TRUSTSTORE_FILE} (trust anchors for the document signer), these certs are consumed only by
+ * the {@link TLValidationJob} to decide whether to accept the LOTL.
+ */
+ private CertificateSource ojKeystoreCertificateSource() throws Exception {
+ final String overrideFile = config.getString(KEY_EU_OJ_KEYSTORE_FILE);
+ if (StringUtils.isNotEmpty(overrideFile)) {
+ final String pwd = config.getString(KEY_EU_OJ_KEYSTORE_PASSWORD, "");
+ return new KeyStoreCertificateSource(new File(overrideFile), KeyStore.getDefaultType(),
+ pwd != null ? pwd.toCharArray() : null);
+ }
+ try (InputStream is = DssTrustConfigurer.class.getResourceAsStream(OJ_KEYSTORE_RESOURCE)) {
+ if (is == null) {
+ throw new IllegalStateException("Bundled OJ keystore resource not found on the classpath: "
+ + OJ_KEYSTORE_RESOURCE);
+ }
+ return new KeyStoreCertificateSource(is, OJ_KEYSTORE_TYPE, OJ_KEYSTORE_PASSWORD.toCharArray());
+ }
+ }
+
private static List splitList(String value) {
List out = new ArrayList<>();
if (StringUtils.isNotBlank(value)) {
diff --git a/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/README-oj-keystore.md b/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/README-oj-keystore.md
new file mode 100644
index 00000000..dfbbcc5f
--- /dev/null
+++ b/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/README-oj-keystore.md
@@ -0,0 +1,27 @@
+# EU LOTL Official Journal (OJ) keystore
+
+`eu-oj-keystore.p12` validates the **signature of the EU List of Trusted Lists (LOTL)** itself
+(`LOTLSource.setCertificateSource(...)`). It is *not* a trust anchor for document signers — that is
+`engine.dss.trust.truststoreFile`.
+
+- **Type:** PKCS12 **Password:** `dss-password` (matches the DSS demonstrations keystore convention)
+- **Loaded by:** `DssTrustConfigurer.ojKeystoreCertificateSource()` (constant `OJ_KEYSTORE_RESOURCE`)
+- **Override at runtime:** `engine.dss.trust.eu.ojKeystoreFile` / `engine.dss.trust.eu.ojKeystorePassword`
+
+## Current file
+
+Sourced from the `dss-demonstrations` repository, tag `6.4+20260415` (post the April-2026 OJ rotation). It
+holds the EUROPEAN COMMISSION qualified signer certificates (organization + named statutory staff) that sign
+the EU LOTL — all `trustedCertEntry`, no private keys. DSS follows the OJ pivot chain from these anchors, so
+expired/legacy entries may legitimately remain.
+
+## Refreshing (release checklist)
+
+Refresh when the OJ re-issues the LOTL signing certificates (roughly every few years; pivot support absorbs
+the more frequent changes). Helper scripts live in `engines/dss/src/main/scripts/`:
+
+1. Run `fetch-oj-keystore-from-dss-demos.sh ` (recommended) to copy the keystore from a matching
+ `dss-demonstrations` tag, or `build-oj-keystore-from-live-lotl.sh` to bootstrap from the live LOTL signer.
+2. Verify the contained signer certificates (subjects / validity) against the OJ notice (the script prints them).
+3. Keep the path / type PKCS12 / password `dss-password`, or update the `OJ_KEYSTORE_*` constants.
+4. Update `DssTrustConfigurer.DEFAULT_OJ_URL` to the matching OJ notice URL.
diff --git a/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12 b/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12
new file mode 100644
index 00000000..82c673a6
Binary files /dev/null and b/engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12 differ
diff --git a/engines/dss/src/main/scripts/build-oj-keystore-from-live-lotl.sh b/engines/dss/src/main/scripts/build-oj-keystore-from-live-lotl.sh
new file mode 100755
index 00000000..718fa163
--- /dev/null
+++ b/engines/dss/src/main/scripts/build-oj-keystore-from-live-lotl.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# bootstrap the OJ keystore from the certificate(s) embedded in the live EU LOTL's
+# XML signature. TOFU (trust on first use) — cross-check the printed certs against the OJ notice
+# before committing. Produces a PKCS12 with trustedCertEntry entries, password dss-password.
+set -euo pipefail
+
+LOTL_URL="${1:-https://ec.europa.eu/tools/lotl/eu-lotl.xml}"
+PASS="dss-password"
+DEST="engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12"
+
+[ -f pom.xml ] || { echo "Run from the jsignpdf repo root." >&2; exit 1; }
+command -v python3 >/dev/null || { echo "python3 required." >&2; exit 1; }
+
+tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
+echo ">> Downloading $LOTL_URL ..."
+curl -fsSL "$LOTL_URL" -o "$tmp/eu-lotl.xml"
+
+echo ">> Extracting X509 certificate(s) from the LOTL signature ..."
+python3 - "$tmp/eu-lotl.xml" "$tmp" <<'PY'
+import re, base64, sys, os
+xml = open(sys.argv[1], encoding="utf-8").read()
+outdir = sys.argv[2]
+b64s = re.findall(r'<(?:\w+:)?X509Certificate>\s*([A-Za-z0-9+/=\s]+?)\s*(?:\w+:)?X509Certificate>', xml)
+if not b64s:
+ print("No X509Certificate found in the LOTL signature.", file=sys.stderr); sys.exit(1)
+seen = set(); n = 0
+for b in b64s:
+ der = base64.b64decode("".join(b.split()))
+ if der in seen: # de-dup
+ continue
+ seen.add(der)
+ pem = "-----BEGIN CERTIFICATE-----\n" + "\n".join(
+ base64.b64encode(der).decode()[i:i+64] for i in range(0, len(base64.b64encode(der)), 64)
+ ) + "\n-----END CERTIFICATE-----\n"
+ open(os.path.join(outdir, f"oj-{n}.pem"), "w").write(pem)
+ n += 1
+print(n)
+PY
+
+rm -f "$DEST"
+i=0
+for pem in "$tmp"/oj-*.pem; do
+ keytool -importcert -noprompt -trustcacerts -alias "oj-signer-$i" \
+ -file "$pem" -keystore "$DEST" -storetype PKCS12 -storepass "$PASS"
+ i=$((i+1))
+done
+
+echo ">> Built $DEST with $i certificate(s):"
+keytool -list -v -keystore "$DEST" -storetype PKCS12 -storepass "$PASS" \
+ | grep -E "Alias name:|Owner:|Valid from:" || true
+echo ">> CROSS-CHECK the owners/validity above against the OJ notice before committing."
diff --git a/engines/dss/src/main/scripts/fetch-oj-keystore-from-dss-demos.sh b/engines/dss/src/main/scripts/fetch-oj-keystore-from-dss-demos.sh
new file mode 100755
index 00000000..d4e4360b
--- /dev/null
+++ b/engines/dss/src/main/scripts/fetch-oj-keystore-from-dss-demos.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# copy the OJ/LOTL trust keystore from the dss-demonstrations repo,
+# matching our DSS version, into the engine resources. Password convention: dss-password.
+set -euo pipefail
+
+TAG="${1:-6.4}" # dss-demonstrations tag == DSS version
+PASS="dss-password"
+DEST="engines/dss/src/main/resources/net/sf/jsignpdf/engine/dss/eu-oj-keystore.p12"
+REPO="https://github.com/esig/dss-demonstrations.git"
+
+[ -f pom.xml ] || { echo "Run from the jsignpdf repo root." >&2; exit 1; }
+
+tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
+echo ">> Cloning $REPO @ $TAG ..."
+git clone --depth 1 --branch "$TAG" "$REPO" "$tmp" 2>/dev/null \
+ || { echo ">> Tag $TAG not found, falling back to default branch."; git clone --depth 1 "$REPO" "$tmp"; }
+
+# Find candidate keystores that (a) open with dss-password and (b) contain EU trusted-list signer certs.
+echo ">> Searching for the OJ/LOTL keystore ..."
+candidate=""
+while IFS= read -r ks; do
+ if keytool -list -keystore "$ks" -storetype PKCS12 -storepass "$PASS" >/dev/null 2>&1; then
+ if keytool -list -v -keystore "$ks" -storetype PKCS12 -storepass "$PASS" 2>/dev/null \
+ | grep -qiE "European Commission|Trusted List|C=EU|LOTL"; then
+ candidate="$ks"; break
+ fi
+ [ -z "$candidate" ] && candidate="$ks" # remember a dss-password p12 even if the heuristic misses
+ fi
+done < <(find "$tmp" -name '*.p12' | sort)
+
+[ -n "$candidate" ] || { echo "No dss-password PKCS12 keystore found in the repo." >&2; exit 1; }
+
+echo ">> Using: ${candidate#$tmp/}"
+cp "$candidate" "$DEST"
+echo ">> Copied to $DEST"
+echo ">> Contents:"
+keytool -list -v -keystore "$DEST" -storetype PKCS12 -storepass "$PASS" \
+ | grep -E "Alias name:|Owner:|Valid from:" || true
+echo ">> Done. Review the certificate owners/validity above before committing."
diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurerTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurerTest.java
new file mode 100644
index 00000000..dbfe9e15
--- /dev/null
+++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurerTest.java
@@ -0,0 +1,138 @@
+package net.sf.jsignpdf.engine.dss;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import net.sf.jsignpdf.engine.EngineConfig;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Test;
+
+import eu.europa.esig.dss.tsl.function.OfficialJournalSchemeInformationURI;
+import eu.europa.esig.dss.tsl.source.LOTLSource;
+
+/**
+ * Offline wiring tests for {@link DssTrustConfigurer#getLotlSources()}. They assert the LOTL sources are
+ * built with a non-null URL, a certificate source, the OJ announcement predicate and pivot support —
+ * the regression guard for issue #432 (the bare {@code new LOTLSource()} with a null URL that NPE'd) and the
+ * RC3 fix (the custom {@code lotlUrls} branch left with an empty certificate source). No network is used.
+ *
+ * @author Josef Cacek
+ */
+public class DssTrustConfigurerTest {
+
+ @Test
+ public void euEnabledBuildsEuropeanLotlSource() throws Exception {
+ LOTLSource[] sources = lotlSources(Map.of(DssTrustConfigurer.KEY_EU_ENABLED, "true"));
+
+ assertEquals("trust.eu.enabled must yield exactly one LOTL source", 1, sources.length);
+ LOTLSource lotl = sources[0];
+ assertEquals("default EU LOTL URL", DssTrustConfigurer.DEFAULT_EU_LOTL_URL, lotl.getUrl());
+ assertNotNull("OJ keystore certificate source must be wired", lotl.getCertificateSource());
+ assertTrue("OJ announcement predicate must be the Official Journal one",
+ lotl.getSigningCertificatesAnnouncementPredicate() instanceof OfficialJournalSchemeInformationURI);
+ assertTrue("pivot support must be enabled", lotl.isPivotSupport());
+ }
+
+ @Test
+ public void euLotlUrlOverrideIsApplied() throws Exception {
+ String override = "https://example.test/eu-lotl.xml";
+ LOTLSource[] sources = lotlSources(Map.of(
+ DssTrustConfigurer.KEY_EU_ENABLED, "true",
+ DssTrustConfigurer.KEY_EU_LOTL_URL, override));
+
+ assertEquals(1, sources.length);
+ assertEquals("euLotlUrl must relocate the EU LOTL", override, sources[0].getUrl());
+ }
+
+ @Test
+ public void customLotlUrlsWiredWithOjCertSourceAndPivotNoPredicate() throws Exception {
+ LOTLSource[] sources = lotlSources(Map.of(
+ DssTrustConfigurer.KEY_LOTL_URLS, "https://a.test/lotl.xml, https://b.test/lotl.xml"));
+
+ assertEquals("two custom LOTL URLs -> two sources", 2, sources.length);
+ for (LOTLSource lotl : sources) {
+ assertTrue("custom LOTL must have a non-blank URL", StringUtils.isNotBlank(lotl.getUrl()));
+ assertNotNull("custom LOTL must reuse the OJ certificate source", lotl.getCertificateSource());
+ assertTrue("custom LOTL must keep pivot support", lotl.isPivotSupport());
+ assertNull("custom LOTL must not carry the EU OJ predicate",
+ lotl.getSigningCertificatesAnnouncementPredicate());
+ }
+ }
+
+ @Test
+ public void mraSupportIsOffByDefaultAndOptInForCustomLotlUrls() throws Exception {
+ LOTLSource[] defaultSources = lotlSources(Map.of(
+ DssTrustConfigurer.KEY_LOTL_URLS, "https://mra.test/mra_lotl.xml"));
+ assertEquals(1, defaultSources.length);
+ assertFalse("MRA support must be off by default", defaultSources[0].isMraSupport());
+
+ LOTLSource[] mraSources = lotlSources(Map.of(
+ DssTrustConfigurer.KEY_LOTL_URLS, "https://mra.test/mra_lotl.xml",
+ DssTrustConfigurer.KEY_LOTL_MRA_SUPPORT, "true"));
+ assertEquals(1, mraSources.length);
+ assertTrue("MRA support must be enabled when opted in", mraSources[0].isMraSupport());
+ }
+
+ @Test
+ public void blankAndGarbageLotlUrlEntriesAreSkipped() throws Exception {
+ // Empty / whitespace-only entries between separators must never become a LOTLSource with a null URL.
+ LOTLSource[] sources = lotlSources(Map.of(
+ DssTrustConfigurer.KEY_LOTL_URLS, "https://a.test/lotl.xml,, ; https://b.test/lotl.xml ,"));
+
+ assertEquals("only the two real URLs survive", 2, sources.length);
+ for (LOTLSource lotl : sources) {
+ assertTrue("no LOTL source may have a blank URL", StringUtils.isNotBlank(lotl.getUrl()));
+ }
+ }
+
+ @Test
+ public void noLotlConfigYieldsNoSources() throws Exception {
+ assertEquals("no LOTL config -> no sources", 0, lotlSources(Map.of()).length);
+ }
+
+ private static LOTLSource[] lotlSources(Map cfg) throws Exception {
+ return new DssTrustConfigurer(new MapEngineConfig(cfg)).getLotlSources();
+ }
+
+ /** Minimal in-memory {@link EngineConfig} backed by a map (keys already engine-relative). */
+ private static final class MapEngineConfig implements EngineConfig {
+ private final Map map;
+
+ MapEngineConfig(Map map) {
+ this.map = new HashMap<>(map);
+ }
+
+ @Override
+ public String getString(String key) {
+ return map.get(key);
+ }
+
+ @Override
+ public String getString(String key, String fallback) {
+ return map.getOrDefault(key, fallback);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean fallback) {
+ String v = map.get(key);
+ return v == null ? fallback : Boolean.parseBoolean(v);
+ }
+
+ @Override
+ public int getInt(String key, int fallback) {
+ String v = map.get(key);
+ try {
+ return v == null ? fallback : Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ return fallback;
+ }
+ }
+ }
+}
diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java
index 7a918db2..62f494db 100644
--- a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java
+++ b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerLogic.java
@@ -7,6 +7,7 @@
import java.util.List;
import java.util.logging.Level;
+import net.sf.jsignpdf.engine.DssLtTrustPreflight;
import net.sf.jsignpdf.engine.EngineConfig;
import net.sf.jsignpdf.engine.EngineMismatchValidator;
import net.sf.jsignpdf.engine.EngineMismatchValidator.Mismatch;
@@ -83,6 +84,22 @@ public boolean signFile() {
}
final EngineConfig engineConfig = AppConfig.engineConfigFor(engine.id());
+
+ // Fail fast on an LT/LTA request the engine isn't configured to satisfy (issue #432), before any
+ // key/PIN access or network round-trip, with the exact keys to set.
+ final DssLtTrustPreflight.Result preflight =
+ DssLtTrustPreflight.check(options, engine, engineConfig);
+ if (preflight.hasIssues()) {
+ LOGGER.severe(RES.get("console.dss.ltPreflightFailed"));
+ if (preflight.onlineMissing()) {
+ LOGGER.severe(RES.get("console.dss.ltPreflight.online"));
+ }
+ if (preflight.trustSourceMissing()) {
+ LOGGER.severe(RES.get("console.dss.ltPreflight.trust"));
+ }
+ return false;
+ }
+
finished = engine.sign(options, engineConfig);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, RES.get("console.exception"), e);
diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/DssLtTrustPreflight.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/DssLtTrustPreflight.java
new file mode 100644
index 00000000..57aca4eb
--- /dev/null
+++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/DssLtTrustPreflight.java
@@ -0,0 +1,75 @@
+package net.sf.jsignpdf.engine;
+
+import net.sf.jsignpdf.BasicSignerOptions;
+import net.sf.jsignpdf.types.PadesLevel;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Config preflight for the PAdES LT / LTA levels: those embed revocation data, which DSS only fetches for a
+ * trusted signer chain reachable online. Without both, signing fails deep inside DSS with an
+ * opaque {@code AlertException} ("Revocation data is skipped for untrusted certificate chain") — the
+ * defect behind issue #432. This validator detects the misconfiguration up front so the CLI can fail fast and
+ * the GUI can offer to fix it, before any expensive work (PDF load, key/PIN access, TSA round-trip).
+ *
+ *
+ * It is a config-level check, not a connectivity test: a configuration that passes can still fail at runtime
+ * (LOTL unreachable, proxy, stale OJ keystore), which the engine surfaces via {@code console.dss.trustConfigFailed}.
+ *
+ *
+ * @author Josef Cacek
+ */
+public final class DssLtTrustPreflight {
+
+ // Engine-relative keys (resolved under engine..* by EngineConfig); mirror DssTrustConfigurer's keys.
+ static final String KEY_ONLINE_ENABLED = "online.enabled";
+ static final String KEY_EU_ENABLED = "trust.eu.enabled";
+ static final String KEY_TRUSTSTORE_FILE = "trust.truststoreFile";
+ static final String KEY_CERT_FILES = "trust.certFiles";
+ static final String KEY_CERT_URLS = "trust.certUrls";
+ static final String KEY_LOTL_URLS = "trust.lotlUrls";
+
+ /**
+ * Outcome of the preflight.
+ *
+ * @param applicable whether the check applied at all (LT/LTA selected on an LT-capable engine)
+ * @param onlineMissing LT/LTA needs online fetching but {@code online.enabled} is off
+ * @param trustSourceMissing LT/LTA needs a trust anchor but none of the trust sources is configured
+ */
+ public record Result(boolean applicable, boolean onlineMissing, boolean trustSourceMissing) {
+ /** @return {@code true} when the configuration would make LT/LTA signing fail. */
+ public boolean hasIssues() {
+ return applicable && (onlineMissing || trustSourceMissing);
+ }
+ }
+
+ private DssLtTrustPreflight() {
+ }
+
+ /**
+ * Checks whether the engine is configured to satisfy an LT/LTA request.
+ *
+ * @param o the populated signing options
+ * @param engine the engine that would sign
+ * @param config the engine-scoped configuration view
+ * @return the preflight result; {@link Result#hasIssues()} is {@code false} when nothing is wrong or the
+ * check does not apply (non-LT/LTA level, or an engine that cannot produce LT)
+ */
+ public static Result check(BasicSignerOptions o, SigningEngine engine, EngineConfig config) {
+ if (o == null || engine == null || config == null) {
+ return new Result(false, false, false);
+ }
+ final PadesLevel level = o.getPadesLevel();
+ final boolean ltOrLta = level == PadesLevel.BASELINE_LT || level == PadesLevel.BASELINE_LTA;
+ if (!ltOrLta || !engine.capabilities().contains(Capability.PADES_BASELINE_LT)) {
+ return new Result(false, false, false);
+ }
+ final boolean online = config.getBoolean(KEY_ONLINE_ENABLED, false);
+ final boolean trustSource = config.getBoolean(KEY_EU_ENABLED, false)
+ || StringUtils.isNotBlank(config.getString(KEY_TRUSTSTORE_FILE))
+ || StringUtils.isNotBlank(config.getString(KEY_CERT_FILES))
+ || StringUtils.isNotBlank(config.getString(KEY_CERT_URLS))
+ || StringUtils.isNotBlank(config.getString(KEY_LOTL_URLS));
+ return new Result(true, !online, !trustSource);
+ }
+}
diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java
index 2ed6e3d2..0c5223ea 100644
--- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java
+++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java
@@ -95,7 +95,7 @@ public class PreferencesController {
@FXML private ComboBox cmbTsaHashAlgorithm;
@FXML private CheckBox chkDssOnlineEnabled;
- @FXML private CheckBox chkDssUseDefaultLotl;
+ @FXML private CheckBox chkDssEuEnabled;
@FXML private TextField txtDssLotlUrls;
@FXML private TextField txtDssCertFiles;
@FXML private TextField txtDssCertUrls;
@@ -231,7 +231,7 @@ void bind(PreferencesViewModel viewModel) {
cmbTsaHashAlgorithm.valueProperty().bindBidirectional(vm.tsaHashAlgorithmProperty());
chkDssOnlineEnabled.selectedProperty().bindBidirectional(vm.dssOnlineEnabledProperty());
- chkDssUseDefaultLotl.selectedProperty().bindBidirectional(vm.dssUseDefaultLotlProperty());
+ chkDssEuEnabled.selectedProperty().bindBidirectional(vm.dssEuEnabledProperty());
txtDssLotlUrls.textProperty().bindBidirectional(vm.dssLotlUrlsProperty());
txtDssCertFiles.textProperty().bindBidirectional(vm.dssCertFilesProperty());
txtDssCertUrls.textProperty().bindBidirectional(vm.dssCertUrlsProperty());
diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java
index 16bfbc5f..d00c59d4 100644
--- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java
+++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java
@@ -49,7 +49,7 @@ public class PreferencesViewModel {
// DSS engine (engine.dss.*) — trust material for PAdES LT/LTA
private final BooleanProperty dssOnlineEnabled = new SimpleBooleanProperty(false);
- private final BooleanProperty dssUseDefaultLotl = new SimpleBooleanProperty(false);
+ private final BooleanProperty dssEuEnabled = new SimpleBooleanProperty(false);
private final StringProperty dssLotlUrls = new SimpleStringProperty("");
private final StringProperty dssCertFiles = new SimpleStringProperty("");
private final StringProperty dssCertUrls = new SimpleStringProperty("");
@@ -77,7 +77,7 @@ public void loadFrom(AdvancedConfig cfg, String pkcs11FileBody) {
tsaHashAlgorithm.set(cfg.getNotEmptyProperty("tsa.hashAlgorithm", "SHA-256"));
dssOnlineEnabled.set(cfg.getAsBool("engine.dss.online.enabled", false));
- dssUseDefaultLotl.set(cfg.getAsBool("engine.dss.trust.useDefaultLotl", false));
+ dssEuEnabled.set(cfg.getAsBool("engine.dss.trust.eu.enabled", false));
dssLotlUrls.set(orEmpty(cfg.getProperty("engine.dss.trust.lotlUrls")));
dssCertFiles.set(orEmpty(cfg.getProperty("engine.dss.trust.certFiles")));
dssCertUrls.set(orEmpty(cfg.getProperty("engine.dss.trust.certUrls")));
@@ -102,7 +102,7 @@ public void writeTo(AdvancedConfig cfg) {
cfg.setProperty("tsa.hashAlgorithm", tsaHashAlgorithm.get());
cfg.setProperty("engine.dss.online.enabled", dssOnlineEnabled.get());
- cfg.setProperty("engine.dss.trust.useDefaultLotl", dssUseDefaultLotl.get());
+ cfg.setProperty("engine.dss.trust.eu.enabled", dssEuEnabled.get());
writeStringOrRemove(cfg, "engine.dss.trust.lotlUrls", dssLotlUrls.get());
writeStringOrRemove(cfg, "engine.dss.trust.certFiles", dssCertFiles.get());
writeStringOrRemove(cfg, "engine.dss.trust.certUrls", dssCertUrls.get());
@@ -152,7 +152,7 @@ public void applyTsaDefaults(AdvancedConfig defaults) {
public void applyDssDefaults(AdvancedConfig defaults) {
dssOnlineEnabled.set(parseBool(defaults.getBundledDefault("engine.dss.online.enabled"), false));
- dssUseDefaultLotl.set(parseBool(defaults.getBundledDefault("engine.dss.trust.useDefaultLotl"), false));
+ dssEuEnabled.set(parseBool(defaults.getBundledDefault("engine.dss.trust.eu.enabled"), false));
dssLotlUrls.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.lotlUrls")));
dssCertFiles.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.certFiles")));
dssCertUrls.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.certUrls")));
@@ -258,7 +258,7 @@ private static void writeStringOrRemove(AdvancedConfig cfg, String key, String v
public IntegerProperty pdfLibOpenpdfOrderProperty() { return pdfLibOpenpdfOrder; }
public StringProperty tsaHashAlgorithmProperty() { return tsaHashAlgorithm; }
public BooleanProperty dssOnlineEnabledProperty() { return dssOnlineEnabled; }
- public BooleanProperty dssUseDefaultLotlProperty() { return dssUseDefaultLotl; }
+ public BooleanProperty dssEuEnabledProperty() { return dssEuEnabled; }
public StringProperty dssLotlUrlsProperty() { return dssLotlUrls; }
public StringProperty dssCertFilesProperty() { return dssCertFiles; }
public StringProperty dssCertUrlsProperty() { return dssCertUrls; }
diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java
index 3a00bfa2..55458b13 100644
--- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java
+++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java
@@ -52,9 +52,12 @@
import net.sf.jsignpdf.Constants;
import net.sf.jsignpdf.PdfExtraInfo;
import net.sf.jsignpdf.engine.Capability;
+import net.sf.jsignpdf.engine.DssLtTrustPreflight;
+import net.sf.jsignpdf.engine.EngineConfig;
import net.sf.jsignpdf.engine.EngineRegistry;
import net.sf.jsignpdf.engine.SigningEngine;
import net.sf.jsignpdf.fx.EngineCapabilities;
+import net.sf.jsignpdf.utils.AdvancedConfig;
import net.sf.jsignpdf.utils.AppConfig;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
@@ -1085,6 +1088,12 @@ private void onSign() {
options.setOutFile(nameBase + AppConfig.defaultOutSuffix() + suffix);
}
+ // LT/LTA preflight (issue #432): warn before signing if the DSS engine isn't configured for the
+ // online fetching + trust anchor that LT/LTA require, and offer to enable them.
+ if (!confirmLtPreflight()) {
+ return;
+ }
+
// Start signing
signingService.cancel();
signingService.reset();
@@ -1095,6 +1104,55 @@ private void onSign() {
signingService.start();
}
+ /**
+ * Runs the LT/LTA trust preflight ({@link DssLtTrustPreflight}) against the active engine and its config.
+ * When the configuration would make LT/LTA signing fail (issue #432), shows a confirmation offering to
+ * enable the missing prerequisites ({@code engine.dss.online.enabled} and, if no trust source is set,
+ * {@code engine.dss.trust.eu.enabled}) and persist them, with a "sign anyway" escape.
+ *
+ * @return {@code true} to proceed with signing, {@code false} to abort
+ */
+ private boolean confirmLtPreflight() {
+ final SigningEngine engine = engineCapabilities.activeEngineProperty().get();
+ if (engine == null) {
+ return true;
+ }
+ final EngineConfig engineConfig = AppConfig.engineConfigFor(engine.id());
+ final DssLtTrustPreflight.Result preflight = DssLtTrustPreflight.check(options, engine, engineConfig);
+ if (!preflight.hasIssues()) {
+ return true;
+ }
+
+ final Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
+ confirm.setTitle(RES.get("jfx.gui.dialog.ltPreflight.title"));
+ confirm.setHeaderText(null);
+ confirm.setContentText(RES.get("jfx.gui.dialog.ltPreflight.text"));
+ confirm.initOwner(stage);
+ final ButtonType enableAndSign = new ButtonType(RES.get("jfx.gui.dialog.ltPreflight.enable"),
+ ButtonBar.ButtonData.YES);
+ final ButtonType signAnyway = new ButtonType(RES.get("jfx.gui.dialog.ltPreflight.anyway"),
+ ButtonBar.ButtonData.NO);
+ confirm.getButtonTypes().setAll(enableAndSign, signAnyway, ButtonType.CANCEL);
+
+ final Optional result = confirm.showAndWait();
+ if (result.isEmpty() || result.get() == ButtonType.CANCEL) {
+ return false;
+ }
+ if (result.get() == enableAndSign) {
+ final AdvancedConfig cfg = PropertyStoreFactory.getInstance().advancedConfig();
+ cfg.setProperty("engine.dss.online.enabled", true);
+ if (preflight.trustSourceMissing()) {
+ cfg.setProperty("engine.dss.trust.eu.enabled", true);
+ }
+ try {
+ cfg.save();
+ } catch (Exception e) {
+ LOGGER.log(Level.WARNING, "Could not persist DSS LT/LTA prerequisites", e);
+ }
+ }
+ return true;
+ }
+
@FXML
private void onZoomIn() {
documentVM.zoomIn();
diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml
index 6b0bf87c..81b2b661 100644
--- a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml
+++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml
@@ -103,7 +103,7 @@
-
+
diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/DssLtTrustPreflightTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/DssLtTrustPreflightTest.java
new file mode 100644
index 00000000..c22a9c16
--- /dev/null
+++ b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/DssLtTrustPreflightTest.java
@@ -0,0 +1,130 @@
+package net.sf.jsignpdf.engine;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import net.sf.jsignpdf.BasicSignerOptions;
+import net.sf.jsignpdf.engine.DssLtTrustPreflight.Result;
+import net.sf.jsignpdf.types.PadesLevel;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link DssLtTrustPreflight}: the config preflight that catches an LT/LTA request the DSS
+ * engine isn't configured to satisfy (issue #432) before any signing work happens.
+ *
+ * @author Josef Cacek
+ */
+public class DssLtTrustPreflightTest {
+
+ private static final SigningEngine LT_ENGINE =
+ new StubSigningEngine("dss", Capability.PADES_BASELINE_LT, Capability.PADES_BASELINE_LTA);
+
+ @Test
+ public void ltWithOnlineAndTrustHasNoIssues() {
+ Result r = check(PadesLevel.BASELINE_LT, LT_ENGINE, Map.of(
+ "online.enabled", "true", "trust.eu.enabled", "true"));
+ assertFalse("a fully configured LT request must pass", r.hasIssues());
+ }
+
+ @Test
+ public void ltMissingOnlineAndTrustFlagsBoth() {
+ Result r = check(PadesLevel.BASELINE_LT, LT_ENGINE, Map.of());
+ assertTrue(r.hasIssues());
+ assertTrue("online must be flagged", r.onlineMissing());
+ assertTrue("trust source must be flagged", r.trustSourceMissing());
+ }
+
+ @Test
+ public void ltMissingOnlyOnline() {
+ Result r = check(PadesLevel.BASELINE_LT, LT_ENGINE, Map.of("trust.eu.enabled", "true"));
+ assertTrue(r.hasIssues());
+ assertTrue(r.onlineMissing());
+ assertFalse(r.trustSourceMissing());
+ }
+
+ @Test
+ public void ltMissingOnlyTrust() {
+ Result r = check(PadesLevel.BASELINE_LT, LT_ENGINE, Map.of("online.enabled", "true"));
+ assertTrue(r.hasIssues());
+ assertFalse(r.onlineMissing());
+ assertTrue(r.trustSourceMissing());
+ }
+
+ @Test
+ public void truststoreFileCountsAsTrustSource() {
+ Result r = check(PadesLevel.BASELINE_LTA, LT_ENGINE, Map.of(
+ "online.enabled", "true", "trust.truststoreFile", "/path/to/ts.p12"));
+ assertFalse("a configured truststore satisfies the trust requirement", r.hasIssues());
+ }
+
+ @Test
+ public void lotlUrlsCountAsTrustSource() {
+ Result r = check(PadesLevel.BASELINE_LT, LT_ENGINE, Map.of(
+ "online.enabled", "true", "trust.lotlUrls", "https://a.test/lotl.xml"));
+ assertFalse("custom lotlUrls satisfy the trust requirement", r.hasIssues());
+ }
+
+ @Test
+ public void baselineBIsNotApplicable() {
+ Result r = check(PadesLevel.BASELINE_B, LT_ENGINE, Map.of());
+ assertFalse("B/T levels do not need the LT trust material", r.hasIssues());
+ }
+
+ @Test
+ public void nullLevelIsNotApplicable() {
+ Result r = check(null, LT_ENGINE, Map.of());
+ assertFalse(r.hasIssues());
+ }
+
+ @Test
+ public void engineWithoutLtCapabilityIsNotApplicable() {
+ SigningEngine noLt = new StubSigningEngine("openpdf", Capability.PADES_BASELINE_B);
+ Result r = check(PadesLevel.BASELINE_LT, noLt, Map.of());
+ assertFalse("an engine that cannot produce LT is out of scope here", r.hasIssues());
+ }
+
+ private static Result check(PadesLevel level, SigningEngine engine, Map cfg) {
+ BasicSignerOptions o = new BasicSignerOptions();
+ o.setPadesLevel(level);
+ return DssLtTrustPreflight.check(o, engine, new MapEngineConfig(cfg));
+ }
+
+ /** Minimal in-memory {@link EngineConfig} backed by a map (keys already engine-relative). */
+ private static final class MapEngineConfig implements EngineConfig {
+ private final Map map;
+
+ MapEngineConfig(Map map) {
+ this.map = new HashMap<>(map);
+ }
+
+ @Override
+ public String getString(String key) {
+ return map.get(key);
+ }
+
+ @Override
+ public String getString(String key, String fallback) {
+ return map.getOrDefault(key, fallback);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean fallback) {
+ String v = map.get(key);
+ return v == null ? fallback : Boolean.parseBoolean(v);
+ }
+
+ @Override
+ public int getInt(String key, int fallback) {
+ String v = map.get(key);
+ try {
+ return v == null ? fallback : Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ return fallback;
+ }
+ }
+ }
+}