Skip to content

DSS Validator

The {{ product_name }} DSS validator (alpine-dss-validator) is a central, multi-tenant service that validates electronic signatures. It checks a signed document against a merged trust store holding both the EU List of Trusted Lists (LOTL) and the Swiss Trusted List, runs the full ETSI EN 319 102-1 validation process once, and reports a verdict per signature — for EU eIDAS and Swiss ZertES / VZertES.

  • All AdES levels: B, T, LT, LTA.
  • All formats: PAdES, CAdES, XAdES, JAdES, and ASiC containers.
  • Two surfaces: a browser UI and a JSON REST API.

The framework is detected, not requested

The validator decides EU eIDAS vs Swiss ZertES automatically, per signature, from the trusted-list anchor of the signing certificate in the document. Callers never pass a framework hint — the only thing they supply is their tenant identity for authentication and licensing. See Validation modes.

Deployment

Prerequisites

  • Java 25 only.
  • A read-only control database (PostgreSQL 18) for tenant OIDC providers and license entitlements. Leave alpine.dss.control-db.url blank only for local dev with permit-all.
  • Trust-list authentication assets (not shipped — supply per deployment):
    • the EU Official-Journal keystore (tl/eu-oj-keystore.p12) holding the certificates the EU publishes as authorised to sign the LOTL;
    • the Swiss TL signer certificate (certs/CH-TL-cert-DER.cer).
  • A pre-seeded tl-cache/ directory only for a deployment with no outbound access — on any machine that can reach the publishers, the startup refresh fetches the lists itself.
  • Outbound network egress for AIA / OCSP / CRL revocation checks and the daily trusted-list refresh.
  • The dss.validator feature license assigned to each tenant that will validate (tenant license).

The validator starts without these assets — that is the dangerous case, not the safe one

Nothing in the trusted-list configuration throws. A missing Official-Journal keystore or Swiss signer certificate logs a warning and the validator starts anyway, holding trusted lists whose own signatures it cannot verify — it is then taking trust anchors from lists it cannot prove are genuine. Every page consequently carries a persistent danger banner that names the file to supply, and anchorDataAuthenticated is false on every affected verdict.

The lists themselves need no seeding where there is egress: the refresh that runs shortly after startup fetches them into tl-cache-dir, and the daily job keeps them current, re-downloading a list only when its digest changes. The bundled tests run fully offline.

Build and run

# Build this module and its dependencies (from the repository root)
mvn -pl 07_dss/alpine-dss-validator -am package

# Run
java -jar 07_dss/alpine-dss-validator/target/alpine-dss-validator-<version>.jar \
  --alpine.dss.control-db.url=jdbc:postgresql://db-host:5432/alpine_control \
  --alpine.dss.control-db.username=dss_ro \
  --alpine.dss.control-db.password="$ALPINE_DSS_CONTROL_DB_PASSWORD"

The validator listens on port 8090 by default.

Configuration

Configuration is in 07_dss/alpine-dss-validator/src/main/resources/application.yml; every key has an environment-variable override.

Core

Key Env override Default Description
server.port ALPINE_DSS_VALIDATOR_PORT 8090 HTTP listening port.
alpine.dss.validator.base-url ALPINE_DSS_VALIDATOR_BASE_URL http://localhost:8090 External base URL, used to build per-tenant OIDC redirect URIs for the UI login.
alpine.dss.validator.tl-cache-dir ALPINE_DSS_TL_CACHE ./target/tl-cache On-disk EU LOTL + Swiss TL cache (pre-seeded for offline first boot).
alpine.dss.validator.refresh-cron ALPINE_DSS_VALIDATOR_REFRESH_CRON 0 0 3 * * * Cron for the daily online trusted-list refresh.
alpine.dss.validator.max-request-bytes ALPINE_DSS_VALIDATOR_MAX_REQUEST_BYTES 41943040 Max accepted upload size (bytes) for the API surface.
alpine.dss.validator.security.permit-all ALPINE_DSS_VALIDATOR_PERMIT_ALL false Dev only — bypass the tenant OIDC authentication filters.
spring.servlet.multipart.max-file-size (relaxed binding) 20MB Max single-file upload size.
spring.servlet.multipart.max-request-size (relaxed binding) 40MB Max total multipart request size.

EU trusted lists

Key Env override Default Description
alpine.dss.validator.eu.lotl-url ALPINE_DSS_VALIDATOR_LOTL_URL https://ec.europa.eu/tools/lotl/eu-lotl.xml EU List of Trusted Lists URL.
alpine.dss.validator.eu.oj-keystore ALPINE_DSS_VALIDATOR_OJ_KEYSTORE classpath:tl/eu-oj-keystore.p12 Keystore (classpath:/file:) with the Official-Journal LOTL signer certificates.
alpine.dss.validator.eu.oj-keystore-type ALPINE_DSS_VALIDATOR_OJ_KEYSTORE_TYPE PKCS12 Keystore type.
alpine.dss.validator.eu.oj-keystore-password ALPINE_DSS_VALIDATOR_OJ_KEYSTORE_PASSWORD (empty) Keystore password (may be blank).

Swiss trusted list

Key Env override Default Description
alpine.dss.validator.swiss.enabled ALPINE_DSS_VALIDATOR_SWISS_ENABLED true Load the Swiss TL and perform Swiss ZertES assessment.
alpine.dss.validator.swiss.ch-tl-url ALPINE_DSS_VALIDATOR_CH_TL_URL https://trustedlist.tsl-switzerland.ch/tsl-ch.xml Swiss Trusted List XML URL.
alpine.dss.validator.swiss.ch-tl-signer-cert ALPINE_DSS_VALIDATOR_CH_TL_SIGNER_CERT classpath:certs/CH-TL-cert-DER.cer DER/PEM certificate that signs the Swiss TL.

Trust-list freshness

A stale trust list does not fail — it answers confidently and wrongly. The validator therefore tracks how current each list is and says so wherever a verdict is shown. These thresholds tune when it says it.

The strongest signal is not configured here: it is each list's own declared NextUpdate date, which its publisher asserted. A list past that date is reported stale whatever these values say. The thresholds below cover only what a NextUpdate cannot — a list well inside its months-long validity whose contents changed (a trust service withdrawn) without this validator noticing.

Key Env override Default Description
alpine.dss.validator.trust.warn-after ALPINE_DSS_VALIDATOR_TRUST_WARN_AFTER P7D Report a list stale this long after its last successful online refresh — or, for a copy never confirmed online, after its issue date; and this long after its own NextUpdate lapses.
alpine.dss.validator.trust.fail-after ALPINE_DSS_VALIDATOR_TRUST_FAIL_AFTER P30D Report a list expired this long past either reference — the last online confirmation (or issue date), or the list's own NextUpdate.
alpine.dss.validator.trust.failure-threshold ALPINE_DSS_VALIDATOR_TRUST_FAILURE_THRESHOLD 3 Consecutive failed attempts before a list is reported as failing.
alpine.dss.validator.trust.startup-catch-up ALPINE_DSS_VALIDATOR_TRUST_STARTUP_CATCH_UP true Refresh once shortly after startup when the cached lists are already behind. false suppresses only this startup refresh — the scheduled refresh above still runs.
alpine.dss.validator.trust.startup-catch-up-delay ALPINE_DSS_VALIDATOR_TRUST_STARTUP_CATCH_UP_DELAY PT30S How long after readiness the catch-up runs (keeps it off the startup path).
alpine.dss.validator.trust.startup-catch-up-after ALPINE_DSS_VALIDATOR_TRUST_STARTUP_CATCH_UP_AFTER P1D How far behind the cached lists must be to trigger a catch-up.
alpine.dss.validator.trust.health-down-when-never-loaded ALPINE_DSS_VALIDATOR_TRUST_HEALTH_DOWN_WHEN_NEVER_LOADED false Whether an empty trust store fails GET /actuator/health. Default false because trust-list assets are supplied per deployment.

A scheduled refresh is not the same as a successful one

EU DSS does not raise an error when an individual trusted list fails to download, parse or validate — it records the outcome and returns normally. The validator reads that record after every pass, so a refresh that reached nothing is reported as such rather than logged as "completed".

What the states mean

State Meaning Anchors available?
CURRENT An online refresh succeeded recently and the list's own NextUpdate is in the future. Yes
CACHE_ONLY Loaded from the on-disk cache; no online refresh has succeeded in this process. Content is known; its currency is not. Yes
STALE Behind on either axis: past warn-after since the last successful online refresh (or, for a copy never confirmed online, since its issue date), or past the list's own NextUpdate. Yes
EXPIRED Past fail-after on either axis: since the last successful online refresh (or issue date), or since the list's own NextUpdate lapsed — so a list its publisher has abandoned expires even while refreshes keep succeeding. Yes
NEVER_LOADED No usable copy of the list, ever. No

The aggregate state is worst-wins across lists, so one unusable member-state list shows as NEVER_LOADED at the aggregate while the rest of the store is intact. Health and the banner wording distinguish the two: "no trusted list is loaded" is only said when nothing has anchors.

Freshness is evaluated when it is read, not when a refresh runs, and on two independent axes — how long since this validator confirmed the list online, and how long past the date the publisher itself declared. The worse of the two wins, so a validator that loses its route out drifts from CURRENT to STALE to EXPIRED on its own, and a list its publisher has abandoned expires even while every refresh succeeds. A cached copy that has never been confirmed online ages the same way, measured from the date its publisher stamped on it, so an air-gapped install does not rest at CACHE_ONLY indefinitely.

Authenticity is a separate question from freshness

A trusted list also has to be genuine, and that is tracked independently of how old it is. DSS reports three outcomes for a list's own signature, not two: valid, invalid (the document was tampered with), and indeterminate — the signer did not chain to the anchor this deployment configured. The last is what you get from a substituted list, and also from an anchor you simply have not configured.

Anything short of a positive confirmation means the validator is taking anchors from a list it cannot prove is genuine. That makes the trust store degraded regardless of its freshness state, raises its own banner ranked above any staleness message, and sets trustData.current to false.

The trust-list signing certificates are supplied per deployment

alpine.dss.validator.eu.oj-keystore and alpine.dss.validator.swiss.ch-tl-signer-cert default to classpath locations that are not shipped in this repository. Until a deployment supplies them, the corresponding lists load but their signatures are never confirmed, and the validator says so wherever a verdict is shown.

What a degraded validator reports

It annotates; it does not refuse, and it does not silently downgrade.

The evidence in a cached trusted list is real. Discarding it because of a fact about this validator's network would throw away a genuine result and make the product useless in the on-premises deployment it is meant to serve. Equally, turning a QESIG into "not qualified" would assert something the evidence does not support. What is actually known is a statement about the evidence's currency, so that is what is said:

As of 28 August 2026 this anchor was listed as a qualified trust service on the German trusted list. This validator has not been able to re-check that list since. Trust services withdrawn in the last 21 days are not reflected in this result.

Two cases are treated differently:

  • NEVER_LOADED — no trust store at all. Every signature reports as reaching no anchor, and the page says plainly that this is a configuration problem with the validator, not a property of the document.
  • Dropped anchors — a parse failure (unlike a download failure) makes DSS rebuild its trust store without that list, silently removing its anchors. That is reported in its own words, because the resulting verdict says nothing about the signature.

Staleness is tracked and reported per list: a stale EU list does not qualify a fully-evidenced Swiss verdict.

The Swiss TL and the EU lists are handled identically. The Swiss list ages through the same states against the same thresholds, counts refresh failures the same way, and produces the same banner and the same API flags — only the list's name differs. In particular, a Swiss ZertES verdict reached against a Swiss TL this validator could not re-fetch is annotated, never withheld: no setting makes an out-of-date trusted list block a verdict. Where a qualified verdict cannot be attributed to any tracked list at all, it is reported as unconfirmed rather than assumed current — for Swiss and eIDAS signatures alike.

Where it is surfaced

Surface What it shows
The upload form and the result page A persistent, non-dismissible banner when the trust data is not confirmed current. The public tenant-gateway page is deliberately excluded — an anonymous visitor is not told which lists are missing — and the error page cannot carry it, because model attributes are not contributed to error-handled views.
The result page An always-present Trust data at validation row taken from the verdict's own snapshot, plus an "as of" marker on any signature whose own anchor list is behind. The banner above it reports the validator's condition now, which is a different question.
POST /api/validate A trustData object on the verdict itself, so a consumer cannot keep the verdict and drop the caveat.
GET /api/trust-status The same status without uploading a document (authenticated, and bound to the tenant like every other call on that surface).
GET /actuator/health Roll-up only, no details — an anonymous caller is not told which lists are missing.
Logs One line per refresh naming each list's outcome, at WARN when degraded. Raw DSS and HTTP failure text is written here only — the banner and the API carry a short category instead, since the underlying message can contain internal host names and egress-proxy addresses.

A degraded validator stays in service

CACHE_ONLY, STALE, EXPIRED and repeated refresh failures all report health UP. Removing a degraded validator from a load balancer would turn a qualified answer into no answer.

Private CA trust anchors

For a closed ecosystem — an internal CA whose certificates will never appear on a public trusted list — an administrator can supply trust anchors that are trusted alongside the EU and Swiss lists.

Key Env override Default Description
alpine.dss.validator.private-ca.enabled ALPINE_DSS_VALIDATOR_PRIVATE_CA_ENABLED false Whether administrator-supplied anchors are loaded at all.
alpine.dss.validator.private-ca.directory ALPINE_DSS_VALIDATOR_PRIVATE_CA_DIR file:/etc/alpine/trust-anchors A file: URI or absolute path to a directory. Every .pem, .crt, .cer and .der file in it is loaded, and a file holding several certificates contributes all of them.

A directory rather than a list of files, because that is the shape of a Kubernetes ConfigMap or Secret mount, and because adding a CA should not mean editing configuration. A concatenated bundle works: a ca-bundle.pem holding a root and its issuing CA contributes both.

The anchors apply to every validation, whichever list the signature would otherwise have been judged against. They are wired once — beside the single merged EU + Swiss trusted-list source, on the one certificate verifier every request shares — rather than per framework, so they behave the same whether swiss.enabled is true or false, and they remain trusted even when neither public list could be loaded.

classpath: is deliberately not accepted. A directory inside the packaged executable jar cannot be enumerated, so accepting it would accept a setting that silently loads nothing.

Loading is fail-soft per file: one unreadable file is logged with its name and skipped, never fatal — a validator that will not boot over a stray file in a mounted directory is worse than one that boots and reports what it loaded. A configuration that is switched on and loaded nothing is reported on the trust surfaces rather than only in the boot log, because that state and "the feature is off" look identical otherwise and need completely different fixes.

An anchor is identified by its certificate subject, not the file name, so two installs trusting different CAs cannot look alike in a diff. Its own notAfter is evaluated on every read, so an anchor that lapses while the validator runs is reported as expired rather than frozen at whatever it was at boot.

A signature anchored on a private CA is not qualified, and the validator will say so

DSS reports a certificate as having reached a trusted list only when the anchor actually came from one. A private CA is not a trusted list, so such a signature is classified UNKNOWN and carries no eIDAS and no ZertES qualification.

That is the correct answer rather than a limitation. The signature may be perfectly valid and its chain may verify cleanly; what this validator has no basis for is calling it qualified under either regime, because qualification is a statement about a trust service appearing on a supervised list.

A private CA has no freshness

It has no publisher, no issue date, no next-update date and no refresh, so it takes no part in the freshness model: it can be neither stale nor current, and forcing it into one of those states would report something the validator invented. Anchors are therefore excluded from the aggregate and never make the trust store look degraded — or look healthy when the public lists are not.

They are named on every surface that reports trust data, because a verdict can rest on one and the reader should know which anchors are in play.

Unlike a trusted list — whose own signature this validator checks against a configured signing certificate — a private CA is the anchor. Its authenticity rests on the administrator having placed the file there, which is a deployment fact rather than something the validator can re-derive, so it is not reported as unauthenticated.

Egress control (SSRF hardening)

Key Env override Default Description
alpine.dss.validator.egress.deny-private-networks ALPINE_DSS_VALIDATOR_EGRESS_DENY_PRIVATE true Block AIA/OCSP/CRL fetches to loopback, link-local (incl. 169.254.169.254), site-local and IPv6 unique-local / any / multicast addresses.
alpine.dss.validator.egress.allowed-hosts ALPINE_DSS_VALIDATOR_EGRESS_ALLOWED_HOSTS (empty = any public host) Optional strict allow-list of egress hostnames (exact, case-insensitive).
alpine.dss.validator.egress.proxy-host ALPINE_DSS_VALIDATOR_EGRESS_PROXY_HOST (empty = direct) Optional outbound HTTP(S) proxy host for all egress.
alpine.dss.validator.egress.proxy-port ALPINE_DSS_VALIDATOR_EGRESS_PROXY_PORT 0 Outbound proxy port (used only when a proxy host is set).

What the proxy covers, and what the guard covers

The proxy applies to all outbound HTTP: the certificate-driven fetches (AIA, OCSP, CRL) and the trusted-list refresh. On-premises or hosted is your choice, and an on-premises network commonly permits egress only through a corporate proxy — so set these two keys and the nightly trusted-list refresh will reach its publishers. Without them, on such a network, the refresh cannot connect and the validator reports the consequence as ageing trust data, which is true about the data but says nothing about the cause. The startup log names the proxy when one is in use.

deny-private-networks and allowed-hosts apply only to the certificate-driven fetches, and deliberately not to the trusted-list refresh. They exist because AIA/OCSP/CRL URLs are read out of the uploaded, untrusted certificate; trusted-list URLs come from alpine.dss.validator.eu.lotl-url and alpine.dss.validator.swiss.ch-tl-url, which you set. Applying the guard to them would break an internal mirror of a trusted list on a private address, and would break any deployment whose allow-list was written for certificate egress and so omits the LOTL host.

Control database

Key Env override Default Description
alpine.dss.control-db.url ALPINE_DSS_CONTROL_DB_URL (empty) Read-only control-DB JDBC URL. Blank disables control-DB-backed auth/license (dev with permit-all).
alpine.dss.control-db.username ALPINE_DSS_CONTROL_DB_USERNAME (empty) Control-DB user (read-only recommended).
alpine.dss.control-db.password ALPINE_DSS_CONTROL_DB_PASSWORD (empty) Control-DB password (supply via env / secrets manager).
alpine.dss.control-db.schema ALPINE_DSS_CONTROL_DB_SCHEMA public Control-DB schema.
alpine.dss.control-db.maximum-pool-size ALPINE_DSS_CONTROL_DB_POOL 4 HikariCP maximum pool size.

Table: the DSS validator's configuration keys, grouped by concern.

Keep private-network egress denied in production

During validation, DSS dereferences URLs embedded in the uploaded, untrusted signing certificate (AIA caIssuers, OCSP and CRL URIs). Without egress control this is a server-side request forgery (SSRF) vector into the internal network or the cloud metadata service. Keep egress.deny-private-networks=true, and optionally restrict to an exact allowed-hosts list.

permit-all is a development escape hatch

alpine.dss.validator.security.permit-all=true disables the tenant OIDC authentication filters and logs a warning. Never enable it in production.

Authentication

The tenant is identified from the request in both surfaces, then bound into the runtime authorization context where the dss.validator license is enforced.

Open the app with a ?tenantId=tnt-... query parameter. The validator resolves that tenant's OIDC provider from the control DB and runs an OIDC authorization-code login against it. A shared-issuer cross-tenant guard rejects an ID token whose urn::alpine:tenantId claim contradicts the tenant whose login was started. Opening the UI without a tenantId shows a "tenant required" page.

Send Authorization: Bearer <oidc-token> where the token carries the tenant UUID in the urn::alpine:tenantId claim. A bearer filter resolves that tenant's OIDC provider and verifies the token (signature, issuer, audience, tenant match) before the request is processed.

Either way, a tenant lacking the dss.validator feature license is rejected with an authorization error (tenant license).

Validation flows

Upload a signed document (and, for detached signatures, the original payload) and read the per-signature verdict.

  1. GET /validate?tenantId=tnt-... → redirected through your tenant's OIDC login → the upload form.
  2. POST /validate with file (the signature container / signed document) and optional detached originals → a result page showing the DSS-rendered Simple Report plus the per-signature summary.
  3. Download the full reports for the last validated document in your session:
    • GET /validate/report/detailed — the DSS Detailed Report (XML).
    • GET /validate/report/etsi — the ETSI EN 319 102-2 validation report (XML).

POST /api/validate (multipart file + optional detached) returns the JSON ValidationSummary.

curl -s \
  -H "Authorization: Bearer $TENANT_OIDC_TOKEN" \
  -F "file=@signed.pdf" \
  https://validator.example/api/validate
{
  "valid": true,
  "documentName": "signed.pdf",
  "documentSha256": "9f86d0818...",
  "signatureCount": 1,
  "signatures": [
    {
      "signatureId": "S-1",
      "signatureFormat": "PAdES_BASELINE_LTA",
      "indication": "TOTAL_PASSED",
      "subIndication": null,
      "trustFramework": "CH_ZERTES",
      "anchorCountry": "CH",
      "eidasQualification": null,
      "swissQualification": "QES",
      "handwrittenEquivalent": true,
      "errors": [],
      "warnings": []
    }
  ]
}

A failed signature is a normal result, not an error

A signature that does not validate is returned with HTTP 200 and an indication of TOTAL_FAILED or INDETERMINATE. HTTP error statuses (401, 403, 400, 413, 502) are reserved for operational failures — authentication, licensing, an unreadable/oversized upload, or infrastructure. Detached CAdES/XAdES signatures require the original payload uploaded as detached.

Validation modes

Each signature is projected into a SignatureReport (the array in ValidationSummary.signatures). The key fields:

Trust framework (auto-detected)

Value Meaning
EU_EIDAS The signing-certificate anchor is on an EU member-state trusted list (eIDAS, Regulation 910/2014).
CH_ZERTES The anchor is on the Swiss Trusted List (ZertES / VZertES, scheme territory CH).
UNKNOWN The certificate does not chain to any configured trusted list. Never defaulted to eIDAS.

The framework is read from the country/scheme of the trusted list that terminated the certificate path. A certificate that is dual-listed is assessed with Swiss precedence (the Swiss legal effect applies), so a dual-listed signature runs the Swiss branch.

AdES level and indication

Field Values
signatureFormat The AdES conformance level, e.g. PAdES_BASELINE_B / _T / _LT / _LTA (and the CAdES/XAdES/JAdES/ASiC equivalents).
indication ETSI status: TOTAL_PASSED, INDETERMINATE, or TOTAL_FAILED.
subIndication The ETSI sub-indication when not passed; null when TOTAL_PASSED.

Qualification

Field Applies to Values
eidasQualification EU_EIDAS only The DSS eIDAS qualification, e.g. QESIG, AdESig, AdESig-QC. null for non-EU.
swissQualification CH_ZERTES only QES, REGULATED_SIGNATURE, REGULATED_SEAL, ADVANCED, or NOT_APPLICABLE.
handwrittenEquivalent CH_ZERTES only true when the signature is equivalent to a handwritten signature under Swiss CO Art. 14 — a QES plus a qualified electronic timestamp from a recognized Swiss CSP. null for non-CH.
anchorCountry all Trusted-list country code of the anchor (e.g. CH, DE); null when untrusted.

Why Swiss qualification is computed separately

EU DSS's qualification implements the eIDAS matrix and cannot represent the Swiss regulated (geregelt) tier. The validator therefore derives the Swiss qualification itself from the CH trust service and certificate, in parallel with the eIDAS qualification (which stays authoritative for EU anchors only).

FAQ

How does the validator decide EU eIDAS vs Swiss ZertES?

Automatically, per signature, from the trusted list its signing-certificate anchor belongs to — CH maps to Swiss ZertES, an EU member-state list maps to eIDAS, and an untrusted anchor is UNKNOWN. The caller never supplies a framework hint. A dual-listed certificate is assessed with Swiss precedence.

Does an invalid signature return an HTTP error?

No. A signature that fails to validate is a normal HTTP 200 result with an indication of TOTAL_FAILED or INDETERMINATE. HTTP error statuses are reserved for operational failures — authentication, licensing, a bad or oversized upload, or infrastructure.

Which formats and levels are supported?

All AdES formats — PAdES, CAdES, XAdES, JAdES and ASiC containers — at all baseline levels: B, T, LT and LTA. The level is read from signatureFormat.

How do I authenticate to the REST API?

Send an Authorization: Bearer <oidc-token> header whose token carries the tenant UUID in the urn::alpine:tenantId claim. The validator resolves that tenant's OIDC provider and verifies the token. The tenant must hold the dss.validator feature license.

What is the CO Art. 14 handwritten-equivalence check?

Under the Swiss Code of Obligations Art. 14, a qualified electronic signature (QES) combined with a qualified electronic timestamp from a recognized Swiss provider is legally equivalent to a handwritten signature. When both hold for a valid CH signature, handwrittenEquivalent is true.

What does the validator report if it has not reached the EU LOTL in three weeks?

It still returns the qualification, and it refuses to present it as a current statement. The verdict carries a trustData object naming the state (STALE here) and the date the trust data was last confirmed, the upload form and the result page show a persistent banner, and the result page marks each signature whose own anchor list is behind. It does not refuse to answer, because the cached evidence is real; it does not silently downgrade the qualification, because the evidence does not support that either. Staleness is tracked per list, so a stale EU list does not qualify a fully-evidenced Swiss verdict.

Is the Swiss TL treated differently from the EU lists when it cannot be refreshed?

No. The Swiss Trusted List ages through the same states against the same thresholds, counts refresh failures the same way, and produces the same banner and the same API flags — only the list's name differs in the wording. A Swiss ZertES verdict reached against a Swiss TL this validator could not re-fetch is annotated, never withheld: no setting turns an out-of-date trusted list into a refusal to answer. The judgement is also made per list, in both directions — a stale EU list does not qualify a fully-evidenced Swiss verdict, and a stale Swiss list does not qualify an eIDAS one.

Can the validator run without outbound network access?

Yes. It boots from the pre-seeded on-disk cache in alpine.dss.validator.tl-cache-dir and never needs the network to answer. What it will do is say so: with no successful online refresh it reports CACHE_ONLY, and it drifts to STALE and then EXPIRED as the configured thresholds pass. Set alpine.dss.validator.trust.startup-catch-up=false to suppress the startup refresh; note that this alone does not silence the deployment, because the scheduled refresh still runs — set alpine.dss.validator.refresh-cron to a schedule that never fires as well. Note that an on-premises validator still needs the control database to resolve tenant OIDC configuration, so "on-premises" is not the same as "offline".

My network only allows egress through a proxy — will the trusted lists still refresh?

Yes, once you set alpine.dss.validator.egress.proxy-host and alpine.dss.validator.egress.proxy-port. Those two keys apply to all outbound HTTP, the nightly trusted-list refresh included, and the startup log names the proxy when one is in use. The other two egress keys — deny-private-networks and allowed-hosts — are an SSRF guard for URLs read out of uploaded certificates and are deliberately not applied to the trusted-list URLs you configure, so an internal mirror of a trusted list on a private address works, and an allow-list written for certificate egress will not strangle your refresh.

Can I trust my own internal CA?

Yes. Set alpine.dss.validator.private-ca.enabled and mount your certificates in the configured directory; they are trusted alongside the EU and Swiss lists. Be aware of what that does and does not buy you: a signature anchored on your CA will validate against it, but it is reported with trust framework UNKNOWN and no eIDAS or ZertES qualification, because qualification means appearing on a supervised trusted list and your CA does not. Private anchors also take no part in the freshness model — they have no publisher and no refresh — so they never make the trust store read as stale or as current.

They apply to every validation, whichever list the signature would otherwise be judged against: they are wired once, beside the single merged EU + Swiss trusted-list source, rather than per framework — so they behave the same whether alpine.dss.validator.swiss.enabled is true or false, and they stay trusted even when neither public list could be loaded.