From f324030d33d77eb34b10659e861b7bce97478262 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Wed, 17 Jun 2026 13:27:53 +0200 Subject: [PATCH 01/18] DSS engine support - design doc --- design-doc/3.1-engine-dss.md | 535 +++++++++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 design-doc/3.1-engine-dss.md diff --git a/design-doc/3.1-engine-dss.md b/design-doc/3.1-engine-dss.md new file mode 100644 index 00000000..8bdcfa8d --- /dev/null +++ b/design-doc/3.1-engine-dss.md @@ -0,0 +1,535 @@ +# Signing engine: EU DSS (PAdES) + +## Why this document exists + +`design-doc/3.1-signing-engines.md` introduced the pluggable signing-engine +abstraction (phase 1): the `SigningEngine` SPI, the `Capability` enum, the +`EngineConfig` view, `ServiceLoader`-based discovery off the `lib/*` classpath, +and the `EngineRegistry` / `EngineMismatchValidator` glue in the main module. +Phase 1 shipped exactly one engine, `openpdf`, as a pure refactor with +byte-identical output. + +This document is the design for the **first phase-2 engine: `dss`**, backed by +the EU **Digital Signature Service (DSS)** library and producing **PAdES** +signatures (`ETSI.CAdES.detached`), including the baseline levels **B / T / LT / +LTA** that the OpenPDF engine fundamentally cannot produce. + +PDFBox-as-a-signing-engine is **out of scope** for this document (it remains a +future `jsignpdf-engine-pdfbox`); PDFBox appears here only as an *internal* +dependency that DSS uses for PDF manipulation (and that JSignPdf already ships +for preview rendering). + +The wiring is not speculative: the sibling tool **`jsignpdf-pades`** already +implements DSS-based PAdES signing as a standalone CLI. This design lifts that +proven code path into a JSignPdf engine behind the phase-1 API. Concrete +references below point at +`jsignpdf-pades/jsignpdf-pades/src/main/java/com/github/intoolswetrust/jsignpdf/pades/`. + +## Goals and non-goals + +**Goals.** + +- Add a `dss` engine that signs PDFs as PAdES (B/T/LT/LTA), selectable by the + exact same mechanisms phase 1 built: `--engine dss`, `engine=dss` in + `advanced.properties`, and the FX toolbar dropdown. +- Reuse `jsignpdf-pades`' DSS wiring (`PAdESService`, `PAdESSignatureParameters`, + the in-memory `PrivateKeySignatureToken`, `OnlineTSPSource`, the visible- + signature appearance model) rather than re-deriving it. +- Map the existing `BasicSignerOptions` property bag onto DSS parameters so that + the *same* JSignPdf options (hash, TSA, visible signature, metadata, + certification level, encryption, permissions) work against `dss` wherever DSS + supports them. +- Add the **one** genuinely new per-document field DSS needs — the PAdES + baseline level — to `BasicSignerOptions`, persisted and presettable like every + other field, ignored by engines that don't support it (as phase 1 anticipated). +- Express DSS's capability differences from OpenPDF through the existing + `Capability` set so the CLI fails fast and the FX UI greys out controls DSS + can't honour — with **no new gating code**, only new table rows. +- Configure trust material (for LT/LTA revocation embedding) through + `EngineConfig` (`engine.dss.*`) and surface it in the Preferences dialog. + +**Non-goals.** + +- Byte-identical output. DSS output is not, and cannot be, byte-comparable with + OpenPDF output. Determinism across engines was explicitly never a goal + (phase-1 doc, "Testing"). The `dss` corpus is validated structurally, not by + byte equality. +- Making `dss` the default. The default stays `openpdf`; nothing changes for + existing users unless they opt in. +- CloudFoxy / external signing through DSS. Deferred (see "External signing"). +- A long-term-validation *refresh* workflow (re-timestamping an already-signed + document to extend LTA). This doc covers producing LT/LTA at signing time + only; LTA refresh is a separate feature. +- Certificate-based PDF encryption under DSS. DSS's encrypt-before-sign path + (via PDFBox) is password-only; certificate encryption stays OpenPDF-only. + +## Where this sits in the phase plan + +The phase-1 doc's "phase plan at a glance" already slots this in. Concretely +this phase delivers: + +| | **This doc (`dss`)** | +|---|---| +| Module added | `engines/dss` → `jsignpdf-engine-dss` | +| Default engine | `openpdf` (unchanged) | +| New capabilities *used* | `SUBFILTER_ETSI_CADES_DETACHED`, `PADES_BASELINE_B/T/LT/LTA`, `DSS_DICTIONARY` | +| `BasicSignerOptions` change | new `padesLevel` field (+ getter/setter, `createCopy`) | +| New value type | `net.sf.jsignpdf.types.PadesLevel` (backend-neutral, in `engine-api`) | +| CLI change | `-pl` / `--pades-level` | +| FX UI change | PAdES-level dropdown (visible only when the active engine supports it); DSS trust config in Preferences | +| Distribution | DSS jars land in `lib/` (~30 MB+); LGPL note in release notes | +| User-visible regressions | None (opt-in engine) | + +## Module layout + +The `engines/` aggregator created in phase 1 gains one module: + +``` +engines/ +├── pom.xml # aggregator +├── api/ # jsignpdf-engine-api (phase 1) +├── openpdf/ # jsignpdf-engine-openpdf (phase 1) +└── dss/ # jsignpdf-engine-dss (THIS doc) +``` + +`engines/dss/pom.xml` depends on `jsignpdf-engine-api` plus the DSS artifacts +`jsignpdf-pades` already uses. Take the dependency list and the `dss-bom` +import straight from `jsignpdf-pades`: + +```xml + +eu.europa.ec.joinup.sd-dssdss-pades-pdfbox +eu.europa.ec.joinup.sd-dssdss-cms-object +eu.europa.ec.joinup.sd-dssdss-token +eu.europa.ec.joinup.sd-dssdss-service +eu.europa.ec.joinup.sd-dssdss-utils-apache-commons +eu.europa.ec.joinup.sd-dssdss-tsl-validation +eu.europa.ec.joinup.sd-dssdss-crl-parser-x509crl +``` + +`dss-pades-pdfbox` pulls PDFBox 3 transitively; JSignPdf already bundles +PDFBox 3 for preview, so this is a version-alignment concern (one PDFBox version +on the `lib/*` classpath), not a net-new heavy dependency. + +The `dss-bom` import goes into the **root** `pom.xml`'s `dependencyManagement` +(mirroring how `jsignpdf-pades`' root pom imports it), and a managed entry for +`jsignpdf-engine-dss` is added next to the existing `jsignpdf-engine-api` / +`jsignpdf-engine-openpdf` entries. + +## Engine implementation + +A single class `net.sf.jsignpdf.engine.dss.DssSigningEngine implements +SigningEngine`, registered via +`engines/dss/src/main/resources/META-INF/services/net.sf.jsignpdf.engine.SigningEngine`. +It mirrors `OpenPdfSigningEngine`'s shape: + +```java +public class DssSigningEngine implements SigningEngine { + public static final String ID = "dss"; + + public String id() { return ID; } + public String displayName() { return "EU DSS (PAdES)"; } + public Set capabilities() { return CAPABILITIES; } + + public boolean sign(BasicSignerOptions options, EngineConfig engineConfig) { ... } +} +``` + +The body of `sign(...)` is `jsignpdf-pades`' +`SignerLogic.signFile(inFile, outFile)` +(`jsignpdf-pades/.../pades/SignerLogic.java`) adapted to: + +- read inputs from `BasicSignerOptions` (the JSignPdf model) instead of + `jsignpdf-pades`' `BasicConfig` (jcommander model); +- obtain the key/chain through JSignPdf's shared `KeyStoreUtils.getPkInfo(options)` + (already in `engine-api`) rather than `jsignpdf-pades`' own `KeyStoreUtils`; +- log through JSignPdf's `Constants.LOGGER` / `RES` bundle; +- read DSS-specific knobs (trust sources, online fetching) from `EngineConfig`. + +The dispatcher in the main module is unchanged from phase 1: it still does +input/output validation, engine resolution, `EngineMismatchValidator`, builds +the `engine..*` `EngineConfig` view, and fires the finished-event lifecycle. +`sign(...)` does the DSS work and returns success/failure, exactly like the +OpenPDF engine. + +### The signing flow (lifted from `jsignpdf-pades`) + +Reference: `jsignpdf-pades/.../pades/SignerLogic.java` lines 85–266. + +1. **Key material.** `PrivateKeyInfo pk = KeyStoreUtils.getPkInfo(options)` → + `PrivateKey` + `Certificate[] chain`. Wrap in the in-memory + `PrivateKeySignatureToken` (port of + `jsignpdf-pades/.../pades/utils/PrivateKeySignatureToken.java`) — an + `AbstractSignatureTokenConnection` that signs the DSS `ToBeSigned` with a + plain JCA `Signature`, choosing `SignatureAlgorithm` from the key's + `EncryptionAlgorithm` + the chosen `DigestAlgorithm`. This is the seam where + PKCS#11 keys work unchanged (the `PrivateKey` is a provider key) and where a + future external-signing token would plug in. +2. **Parameters.** `PAdESSignatureParameters`: + - `setDigestAlgorithm(map(options.getHashAlgorithmX()))` — see "Hash mapping". + - `setSigningCertificate` / `setCertificateChain` from the token key entry. + - `setSignatureLevel(...)` from `padesLevel` (with the TSA auto-upgrade, see + below). + - `bLevel().setSigningDate(now)`. + - `setReason` / `setLocation` / `setContactInfo` from the metadata fields. + - `setPermission(certLevel.toDssCertificationPermission())` for DocMDP. + - `setPasswordProtection(ownerPwd)` when signing an encrypted PDF. +3. **Encrypt-before-sign / blank page.** Reuse `jsignpdf-pades`' PDFBox + preprocessing (`SignerLogic.encryptPdf` / `addBlankPage`, lines 285–318): + load with `Loader.loadPDF`, apply `StandardProtectionPolicy` built from the + permission bitmask, or append a `PDPage`, write to a temp file, sign that. + Password-only (see non-goals). +4. **Visible signature.** Port `configureVisibleSignature` (lines 320–422): + `SignatureImageParameters` + `SignatureFieldParameters`. Note the coordinate + difference vs OpenPDF — DSS uses a **top-left origin** + (`originY = pageHeight - ury`), and `fixPosition` (negative = from the far + edge) is the same helper as the OpenPDF engine. Background image via + `setImage(new FileDocument(path))`; text via `SignatureImageTextParameters` + with a `DSSFont` from a ported `FontUtils.getVisibleSignatureFont(...)`. +5. **TSA.** When a TSA URL is set, build `OnlineTSPSource` over a + `TimestampDataLoader`, wire policy OID, basic auth (host/port/user/pwd), and + the TSA digest algorithm; `service.setTspSource(tsp)` (lines 213–239). +6. **Certificate verifier.** `CommonCertificateVerifier`, configured with the + trusted sources / online fetchers for LT/LTA (see "LT/LTA & trust"). +7. **Sign.** `getDataToSign` → `token.sign` → `signDocument` → write + `DSSDocument` to `options.getOutFileX()` (lines 241–251). + +### Capabilities + +```java +private static final Set CAPABILITIES = Set.copyOf(EnumSet.of( + Capability.SUBFILTER_ETSI_CADES_DETACHED, + Capability.PADES_BASELINE_B, Capability.PADES_BASELINE_T, + Capability.PADES_BASELINE_LT, Capability.PADES_BASELINE_LTA, + Capability.DSS_DICTIONARY, + + Capability.HASH_SHA256, Capability.HASH_SHA384, Capability.HASH_SHA512, + + Capability.APPEND_MODE, // DSS signs incrementally by default + Capability.CERTIFICATION_LEVEL, + Capability.ENCRYPTION_PASSWORD, Capability.PERMISSIONS_BITMASK, + + Capability.VISIBLE_SIGNATURE, Capability.VISIBLE_LAYER2_TEXT, + Capability.VISIBLE_BACKGROUND_IMAGE, Capability.VISIBLE_SIGNATURE_GRAPHIC, + Capability.VISIBLE_CUSTOM_FONT, + Capability.VISIBLE_RENDER_MODE_DESCRIPTION_ONLY, + Capability.VISIBLE_RENDER_MODE_GRAPHIC_AND_DESCRIPTION, + + Capability.TSA, Capability.TSA_POLICY_OID, Capability.TSA_BASIC_AUTH, + Capability.OCSP_EMBED, Capability.CRL_EMBED, + + Capability.PROXY_SUPPORT, + Capability.PKCS11_PROVIDER)); +``` + +What DSS deliberately **does not** declare, and why: + +| Absent capability | Reason | +|---|---| +| `SUBFILTER_ADBE_PKCS7_DETACHED` | DSS produces `ETSI.CAdES.detached` (PAdES), not the legacy Adobe subfilter. | +| `HASH_SHA1`, `HASH_RIPEMD160` | PAdES baseline disallows SHA-1; RIPEMD-160 is not a PAdES digest. The validator flags these when set. | +| `ENCRYPTION_CERTIFICATE` | The encrypt-before-sign path is password-only. | +| `VISIBLE_LAYER4_TEXT` | DSS's appearance model has a single text block; no separate layer-4 status text. | +| `VISIBLE_RENDER_MODE_NAME_AND_DESCRIPTION` | No equivalent in the DSS appearance model. (DESCRIPTION_ONLY and GRAPHIC_AND_DESCRIPTION map cleanly.) | +| `VISIBLE_BACKGROUND_IMAGE_SCALE` | DSS scales the image to the field; no OpenPDF-style scale factor. | +| `ACRO6_LAYERS` | OpenPDF/Acrobat-6 layered appearance is an iText-lineage concept. | +| `EXTERNAL_DIGEST` | CloudFoxy external signing is deferred (see "External signing"). | + +The set is static and identical across instances, per the phase-1 contract. +(The PAdES level is selected per-document via the new `padesLevel` field, not by +varying the capability set — dynamic capabilities remain out of scope.) + +## Model change: `padesLevel` + +This is the single new field on `BasicSignerOptions`, exactly as phase 1 +predicted ("DSS adds `padesLevel`"). + +- **New value type** `net.sf.jsignpdf.types.PadesLevel` in **`engine-api`**, + backend-neutral (no DSS import), following the phase-1 precedent of inlining + spec codes into the moved enums: + + ```java + public enum PadesLevel { BASELINE_B, BASELINE_T, BASELINE_LT, BASELINE_LTA } + ``` + + The DSS engine maps it to `eu.europa.esig.dss.enumerations.SignatureLevel` + internally (the mapping table currently in + `jsignpdf-pades/.../config/PadesLevel.java`). Keeping the JSignPdf enum + DSS-free preserves `engine-api`'s "no engine library leaks into the API" + invariant. + +- **`BasicSignerOptions`** gains `padesLevel` (default `null` = "engine + default"), its getter/setter, and a line in `createCopy()` — same mechanical + change as the phase-1 `engine` field. + +- **Default semantics.** `null` keeps existing behaviour: OpenPDF ignores it; + DSS treats `null` as `BASELINE_B`. So nothing changes unless the user picks a + level. + +- **TSA auto-upgrade.** Keep `jsignpdf-pades`' rule (`SignerLogic.java` + 137–143): if a TSA is configured and the level is `BASELINE_B`, sign as + `BASELINE_T` and log the upgrade. (A timestamp present but a B-level container + would otherwise drop the signature timestamp.) + +- **Persistence.** `padesLevel` is stored in `config.properties` / presets like + every other field — no special-casing (phase-1 doc, "Configuration"). + +## Hash mapping + +`BasicSignerOptions` carries a `HashAlgorithm` +(`net.sf.jsignpdf.types.HashAlgorithm`: SHA1/SHA256/SHA384/SHA512/RIPEMD160). +The engine maps the supported subset to DSS `DigestAlgorithm` +(`SHA256`/`SHA384`/`SHA512`). SHA-1 and RIPEMD-160 are not declared as +capabilities, so the validator rejects them before `sign(...)` runs — the engine +never sees an unmappable value, but it still defends with a clear error if it +does. JSignPdf's default hash is SHA-256, which maps cleanly, so the default +flow needs no user action. + +## CLI changes + +One new option, registered alongside the phase-1 `--engine` / `--list-engines` +pair in `Constants.java` + `SignerOptionsFromCmdLine.java`. The short/long names +match `jsignpdf-pades` for muscle-memory consistency: + +| Short | Long | Argument | Behaviour | +|---|---|---|---| +| `-pl` | `--pades-level` | `B` \| `T` \| `LT` \| `LTA` (case-insensitive) | Sets the PAdES baseline level. Honoured by engines that declare the matching `PADES_BASELINE_*` capability; the validator errors otherwise. | + +`--help` continues to list every option (phase-1 decision: no engine-filtered +help). Example mismatch when the option is used against the default engine: + +``` +$ jsignpdf --pades-level LTA contract.pdf +ERROR: The selected engine 'openpdf' does not support the following options: + --pades-level (capability PADES_BASELINE_LTA) +Use --list-engines to see available engines. +``` + +## Validation additions + +`EngineMismatchValidator.findMismatches(...)` gains one block, table-driven like +the existing hash/render maps: + +```java +private static final Map PADES_CAPS = new EnumMap<>(PadesLevel.class); +static { + PADES_CAPS.put(PadesLevel.BASELINE_B, Capability.PADES_BASELINE_B); + PADES_CAPS.put(PadesLevel.BASELINE_T, Capability.PADES_BASELINE_T); + PADES_CAPS.put(PadesLevel.BASELINE_LT, Capability.PADES_BASELINE_LT); + PADES_CAPS.put(PadesLevel.BASELINE_LTA, Capability.PADES_BASELINE_LTA); +} +// in findMismatches: +if (o.getPadesLevel() != null) { + Capability c = PADES_CAPS.get(o.getPadesLevel()); + if (c != null && !caps.contains(c)) out.add(new Mismatch("--pades-level", c)); +} +``` + +The existing hash check already produces the right error for SHA-1/RIPEMD-160 +against `dss` (DSS lacks `HASH_SHA1`/`HASH_RIPEMD160`), and the existing +encryption check already flags certificate encryption against `dss`. No other +new validator logic is needed — DSS's other gaps (layer-4 text, name-and- +description render mode, background scale, acro6 layers, CloudFoxy) are already +covered by the phase-1 table rows keyed on the capabilities DSS omits. + +## FX UI changes + +Phase 1 built the whole gating mechanism (`EngineCapabilities.gate(control, +caps...)` binding `disableProperty` + tooltip to the active engine). This phase +adds: + +1. **PAdES-level dropdown.** A `ChoiceBox` in the signature-options + area, bound to `options.padesLevel`. It is gated as a unit on + `PADES_BASELINE_B` (an engine that can't do even B is not a PAdES engine), so + it disables/greys for OpenPDF and enables for DSS — using the existing + `gate(...)` helper, no new pattern. Per-item gating of T/LT/LTA against their + individual capabilities can be added with the same per-item filter the hash + and render-mode dropdowns already use, if a future engine supports only a + subset. +2. **DSS trust configuration in Preferences.** Phase 1 explicitly left + `PreferencesController` as the place to surface `engine..*` keys when an + engine first needs them (phase-1 doc, "Open items"). DSS is that first engine: + a small "DSS engine" section binds the `engine.dss.*` keys below to + `advanced.properties`. It is only meaningful for LT/LTA; the section can stay + always-visible (plain config) since it is harmless when `dss` is unused. + +The existing shared `jfx.gui.engine.unsupported` tooltip covers the disabled +PAdES dropdown; no per-capability tooltip is introduced (phase-1 decision). + +## Engine configuration (`engine.dss.*`) + +Read by the engine through the phase-1 `EngineConfig` view (which already maps +`engine.dss.` → `getString("")`). These drive the +`CommonCertificateVerifier` and trust material for LT/LTA. They mirror +`jsignpdf-pades`' `TrustConfig` / `TrustedCertSourcesProvider` +(`jsignpdf-pades/common/.../TrustedCertSourcesProvider.java`): + +``` +# Online fetching of revocation data (OCSP/CRL) and intermediate certs (AIA). +# 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 +engine.dss.trust.lotlUrls= +engine.dss.trust.certFiles= +engine.dss.trust.certUrls= +engine.dss.trust.truststoreFile= +engine.dss.trust.truststoreType= +engine.dss.trust.truststorePassword= +``` + +`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 +typed list accessor proves cleaner across phase-2 engines, it can be added then — +the API is still young, per the phase-1 caveat.) + +## LT/LTA & trust + +- **B / T** need no trust configuration: B is the plain signature, T adds a + signature timestamp from the configured TSA. These work fully offline (T needs + only TSA connectivity). +- **LT / LTA** embed validation material (certificates + OCSP/CRL) and, for LTA, + an archive timestamp. DSS gathers that material through the + `CommonCertificateVerifier`. The engine builds the verifier from the + `engine.dss.*` trust config: trusted certificate sources + (`TrustedCertSourcesProvider`), an AIA source, and OCSP/CRL online sources + when `engine.dss.online.enabled=true`. Without reachable revocation data, + DSS cannot complete LT/LTA and the sign call fails with a logged error — the + engine surfaces this as a normal `sign(...) == false`. +- LTA *refresh* (extending an existing LTA over time) is out of scope here. + +## External signing (CloudFoxy) + +Phase 1 keeps CloudFoxy inside the OpenPDF flow and declares `EXTERNAL_DIGEST` +only on `openpdf`. DSS does **not** declare `EXTERNAL_DIGEST`, so selecting the +CloudFoxy keystore type with `dss` is rejected by the existing validator row — +the user is told to switch to OpenPDF. + +DSS's natural extension point is its `SignatureTokenConnection`: the +`PrivateKeySignatureToken` would be replaced by a CloudFoxy-backed token whose +`sign(ToBeSigned, ...)` calls the external CSP. This is the concrete case the +phase-1 doc reserved for "the cross-engine external-signing API, designed in +phase 2 with real implementations in front of it." It is **noted but not built +here**; wiring CloudFoxy across both OpenPDF (`byte[]→byte[]`) and DSS +(`SignatureToken`) is its own design once both shapes are in tree. + +## Distribution and build + +Per `design-doc/3.1-no-fat-jar.md`, the distribution is an appassembler `lib/` +of individual jars. Adding `dss`: + +- Add `jsignpdf-engine-dss` as a **runtime dependency of the `distribution` + module** (next to the existing `jsignpdf-engine-openpdf` runtime dep). Its jar + and all DSS / PDFBox transitive jars land in `lib/`; `ServiceLoader` discovers + it off `lib/*` with no merge step (no shading — phase-1 invariant). It then + appears in `--list-engines` and the FX dropdown automatically. +- **Size.** DSS adds ~30 MB+ to the bundle. Acknowledged in the release notes; + the trade-off (one download, all engines) follows the phase-1 decision. +- **License.** DSS is **LGPL-2.1**; JSignPdf is dual MPL-2.0 / LGPL-2.1, so it + is license-compatible, and the root pom already lists `LGPL-2.1` in + `includedLicenses` with the necessary `licenseMerges`. New transitive DSS + dependencies may surface license-name variants the `license-maven-plugin` + doesn't recognise; expect to add `licenseMerge` entries (the same fix applied + in phase 1 for the engine modules' own LGPL/MPL names). The *new* fact for + release notes: bundling DSS puts an LGPL dependency **on the active signing + path** when the user selects `dss`. +- **jpackage.** The jpackage images roll up `lib/`, so discovery holds inside + the installer image; a smoke check (`--list-engines` lists `dss`) is part of + this phase, as in phase 1. + +## i18n + +New keys in `messages.properties` (which lives in `engine-api` since phase 1): + +| Key | Purpose | +|---|---| +| `hlp.padesLevel` | CLI `--pades-level` help text | +| `jfx.gui.padesLevel.label` | Label next to the FX PAdES-level dropdown | +| `jfx.gui.dss.section.label` | Preferences "DSS engine" section header | +| `jfx.gui.dss.trust.*` | Labels/tooltips for the DSS trust fields | +| `console.dss.ltNoRevocation` | Error when LT/LTA is requested but revocation data is unreachable | +| `console.dss.tsaUpgrade` | Info: B→T auto-upgrade because a TSA is configured | + +English strings land in this PR; other languages are Weblate-synced (per +`AGENTS.md`). The new CLI option also needs a row + a "PAdES levels" note in +`website/docs/JSignPdf.adoc`. + +## Testing + +DSS output is non-deterministic and not byte-comparable, so unlike phase 1 +there is no replay-corpus equality test. Instead: + +- **Port the `jsignpdf-pades` signing tests.** That project already has a rich + signing suite (`.../pades/signing/`: `BasicSigningTest`, `PadesLevelSigningTest`, + `TimestampSigningTest`, `VisibleSignatureSigningTest`, `CertificationLevelSigningTest`, + `PasswordProtectedPdfSigningTest`, `DigestAlgorithmSigningTest`, …) plus an + embedded TSA (`signing/tsa/EmbeddedTsaServer`) and a DSS-based + `PdfSignatureValidator`. Adapt these to drive `DssSigningEngine` through the + JSignPdf `BasicSignerOptions` model and assert on the validation result. +- **Validate, don't byte-compare.** Each signed output is parsed back and + checked: signature present, PAdES level achieved (B/T/LT/LTA), TSA token + present when expected, visible signature on the right page, metadata fields + set. +- **`EngineMismatchValidatorTest`** gains cases for `--pades-level` against a + stub engine lacking the level, and the no-mismatch baseline against `dss`. +- **`EngineRegistryTest`** asserts `dss` is discovered and listed (default still + `openpdf`). +- **`SignerOptionsFromCmdLineTest`** gains `--pades-level` parsing cases. +- **`FxTranslationsTest`** covers the new i18n keys. +- **Distribution smoke test** asserts `--list-engines` includes `dss` in the + assembled `lib/`. + +LT/LTA tests that need live revocation/TSA endpoints run against the embedded +TSA and local trust material (mirroring `jsignpdf-pades`); no test depends on +the public LOTL. + +## Documentation updates + +Per `AGENTS.md` ("not done until it lands in the docs"): + +- `website/docs/JSignPdf.adoc` — `--pades-level` row; a "PAdES & the DSS engine" + subsection (levels B/T/LT/LTA, what LT/LTA need, the `engine.dss.*` trust + keys). +- `distribution/doc/release-notes/3.1.0.md` — bullet for the new `dss` engine, + the size bump, and the LGPL-on-signing-path note (this engine ships within the + 3.1 release alongside the phase-1 abstraction). +- `README.md` — phase 1 deferred the README mention to "when PDFBox + DSS bring + user-visible feature categories." DSS/PAdES is exactly that: add a short + "Signing engines (OpenPDF / PAdES via DSS)" line. +- `messages.properties` — keys above. +- This design doc. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| PDFBox version clash (preview PDFBox vs DSS's `dss-pades-pdfbox`) | Align on one PDFBox version in root `dependencyManagement`; CI build + the distribution smoke test catch a split classpath. | +| DSS bundle size / startup cost | Documented trade-off; discovery is lazy `ServiceLoader`, engine classes load only when `dss` is selected. | +| LT/LTA silently degrade when revocation data is unreachable | Engine fails the sign with a specific logged error (`console.dss.ltNoRevocation`) instead of emitting a weaker level. | +| Visible-signature placeholders differ (`${...}` in `jsignpdf-pades` vs `%placeholder` / `StrSubstitutor` in OpenPDF engine) | The DSS engine reuses JSignPdf's `Constants.L2TEXT_PLACEHOLDER_*` + `StrSubstitutor` so the *same* `--l2-text` template works across engines; covered by a visible-signature test. | +| New DSS transitive license names rejected by `license-maven-plugin` | Add `licenseMerge` entries as needed (same pattern as phase 1); the license check runs in CI. | +| Coordinate/appearance differences make visible signatures look different across engines | Accepted: engines are distinct backends; documented, and the appearance is validated structurally, not pixel-compared. | + +## Open items (not blocking this phase) + +- **Cross-engine external-signing API (CloudFoxy on DSS).** Deferred to its own + design once both the `byte[]→byte[]` (OpenPDF) and `SignatureToken` (DSS) + shapes are in tree. +- **LTA refresh** (extend-archive workflow) — separate feature. +- **Typed list accessor on `EngineConfig`** — only if a second phase-2 engine + also needs list-valued config; otherwise the engine parses delimited strings. +- **Per-level FX gating** (offer only the T/LT/LTA an engine supports) — trivial + to add with the existing per-item filter if a partial-support engine appears. + +## Out of scope (explicit non-goals) + +- Byte-identical / cross-engine deterministic output. +- Making `dss` the default engine. +- Certificate-based PDF encryption under DSS. +- CloudFoxy / external signing through DSS. +- A PDFBox *signing* engine (`jsignpdf-engine-pdfbox`). +- LTA refresh / re-timestamping of already-signed documents. + + From 65bef8d376963918825fb47bbf24b978cb4a8ed8 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Wed, 17 Jun 2026 15:06:58 +0200 Subject: [PATCH 02/18] design update & gating fix --- design-doc/3.1-engine-dss.md | 22 ++++++++++++++----- .../fx/view/MainWindowController.java | 22 +++++++++++++++---- .../net/sf/jsignpdf/fx/view/MainWindow.fxml | 3 ++- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/design-doc/3.1-engine-dss.md b/design-doc/3.1-engine-dss.md index 8bdcfa8d..31607d10 100644 --- a/design-doc/3.1-engine-dss.md +++ b/design-doc/3.1-engine-dss.md @@ -19,11 +19,23 @@ future `jsignpdf-engine-pdfbox`); PDFBox appears here only as an *internal* dependency that DSS uses for PDF manipulation (and that JSignPdf already ships for preview rendering). -The wiring is not speculative: the sibling tool **`jsignpdf-pades`** already -implements DSS-based PAdES signing as a standalone CLI. This design lifts that -proven code path into a JSignPdf engine behind the phase-1 API. Concrete -references below point at -`jsignpdf-pades/jsignpdf-pades/src/main/java/com/github/intoolswetrust/jsignpdf/pades/`. +The wiring is not speculative: the **`jsignpdf-pades`** project already +implements DSS-based PAdES signing as a standalone CLI. It is a **separate +repository**, not a module of this one: + +> **`jsignpdf-pades`** — +> (Maven coordinates `com.github.kwart.jsign:jsignpdf-pades`, currently +> `0.2.0-SNAPSHOT`; same author/org as JSignPdf, dual MPL-2.0 / LGPL-2.1.) + +This design lifts that proven code path into a JSignPdf engine behind the +phase-1 API. **All `jsignpdf-pades/...` paths in this document are relative to +the root of that separate repository**, not to the JSignPdf tree — e.g. +`jsignpdf-pades/jsignpdf-pades/src/main/java/com/github/intoolswetrust/jsignpdf/pades/SignerLogic.java` +maps to +. +The core signing logic to port is in the `jsignpdf-pades` **module** of that +repo (the repo is itself multi-module: `common`, `jsignpdf-pades`, `validator`, +`distribution`). ## Goals and non-goals 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 0f16c82f..c3164ba8 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 @@ -151,6 +151,7 @@ public class MainWindowController { // Content area @FXML private SplitPane splitPane; @FXML private Accordion sidePanelAccordion; + @FXML private TitledPane signatureAppearanceAccordionPane; @FXML private TitledPane tsaAccordionPane; @FXML private TitledPane encryptionAccordionPane; @FXML private ScrollPane scrollPane; @@ -454,11 +455,14 @@ public SigningEngine fromString(String s) { } }); - // Capability-driven section gating. These controls are not governed by the document-loaded - // disable logic, so binding their disableProperty here is safe. (With OpenPDF — the only - // engine in phase 1 — every capability is present, so nothing is disabled; the wiring exists - // for reduced-capability engines added in phase 2.) + // Capability-driven section gating at the umbrella (accordion-pane) granularity. These panes + // are not governed by the document-loaded disable logic, so binding their disableProperty here + // is safe. (With OpenPDF — the only engine in phase 1 — every capability is present, so nothing + // is disabled; the wiring exists for reduced-capability engines added in phase 2.) engineCapabilities.gate(btnTsa, Capability.TSA); + if (signatureAppearanceAccordionPane != null) { + engineCapabilities.gate(signatureAppearanceAccordionPane, Capability.VISIBLE_SIGNATURE); + } if (tsaAccordionPane != null) { engineCapabilities.gate(tsaAccordionPane, Capability.TSA); } @@ -466,6 +470,16 @@ public SigningEngine fromString(String s) { engineCapabilities.gate(encryptionAccordionPane, Capability.ENCRYPTION_PASSWORD, Capability.ENCRYPTION_CERTIFICATE); } + + // TODO(phase-2): field-level capability gating is still missing for controls that live inside + // the side-panel sub-controllers and already carry their own disable logic — hash algorithm, + // certification level, append mode, render-mode items, permission checkboxes, proxy fields, and + // the PKCS#11/CloudFoxy keystore-type items (see the control->capability table in + // design-doc/3.1-signing-engines.md). The CLI path is already comprehensive via + // EngineMismatchValidator, which is the authoritative table to mirror here. This asymmetry is + // invisible while OpenPDF (all capabilities) is the only engine; it must be closed when the + // first reduced-capability engine (DSS, see design-doc/3.1-engine-dss.md) lands so the GUI does + // not leave enabled options the engine will reject at sign time. } private void setupPresetCombo() { diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml index 75e816c3..36bb94f7 100644 --- a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml @@ -172,7 +172,8 @@ source="SignatureProperties.fxml"/> - + From 4e7d01a0608862294b91fdb8e5b9251511e5c69b Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Wed, 17 Jun 2026 15:50:03 +0200 Subject: [PATCH 03/18] DSS engine support initial implementation --- README.md | 1 + distribution/doc/release-notes/3.1.0.md | 18 +- distribution/pom.xml | 5 + .../net/sf/jsignpdf/BasicSignerOptions.java | 35 +- .../main/java/net/sf/jsignpdf/Constants.java | 4 + .../net/sf/jsignpdf/types/PadesLevel.java | 64 +++ .../jsignpdf/conf/advanced.default.properties | 18 + .../jsignpdf/translations/messages.properties | 17 + engines/dss/pom.xml | 63 +++ .../sf/jsignpdf/engine/dss/DssFontUtils.java | 48 ++ .../sf/jsignpdf/engine/dss/DssMappings.java | 93 ++++ .../jsignpdf/engine/dss/DssSigningEngine.java | 436 ++++++++++++++++++ .../engine/dss/DssTrustConfigurer.java | 161 +++++++ .../engine/dss/PrivateKeySignatureToken.java | 99 ++++ .../net.sf.jsignpdf.engine.SigningEngine | 1 + .../engine/dss/DssSigningEngineTest.java | 181 ++++++++ .../dss/src/test/resources/test-keystore.jks | Bin 0 -> 10524 bytes engines/pom.xml | 1 + jsignpdf/pom.xml | 5 + .../sf/jsignpdf/SignerOptionsFromCmdLine.java | 4 + .../engine/EngineMismatchValidator.java | 17 + .../fx/preferences/PreferencesController.java | 21 + .../fx/preferences/PreferencesViewModel.java | 48 ++ .../fx/view/MainWindowController.java | 33 ++ .../fx/viewmodel/SigningOptionsViewModel.java | 7 + .../net/sf/jsignpdf/fx/view/MainWindow.fxml | 6 + .../net/sf/jsignpdf/fx/view/Preferences.fxml | 24 + .../SignerOptionsFromCmdLineTest.java | 24 + .../engine/EngineMismatchValidatorTest.java | 45 ++ .../jsignpdf/engine/EngineRegistryTest.java | 17 + pom.xml | 29 +- website/docs/JSignPdf.adoc | 64 ++- 32 files changed, 1582 insertions(+), 7 deletions(-) create mode 100644 engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java create mode 100644 engines/dss/pom.xml create mode 100644 engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java create mode 100644 engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssMappings.java create mode 100644 engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java create mode 100644 engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java create mode 100644 engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/PrivateKeySignatureToken.java create mode 100644 engines/dss/src/main/resources/META-INF/services/net.sf.jsignpdf.engine.SigningEngine create mode 100644 engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java create mode 100644 engines/dss/src/test/resources/test-keystore.jks diff --git a/README.md b/README.md index 4f059ef3..9cb0cc86 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Project home page: [jsignpdf.eu](https://jsignpdf.eu/) - **Timestamping**: RFC 3161 TSA with optional user/password authentication. - **Revocation info**: CRL and OCSP embedding for LTV workflows. - **Visible signatures**: customizable layout with `${signer}`, `${timestamp}`, and other placeholders. +- **Signing engines (OpenPDF / PAdES via DSS)**: pluggable signing backend, selectable per run (`-eng` / toolbar). The default **OpenPDF** engine is unchanged; the bundled **EU DSS** engine produces PAdES signatures at baseline levels B / T / LT / LTA (`--pades-level`). - **Internationalization**: maintained via [Weblate](https://hosted.weblate.org/projects/jsignpdf/messages/) (15+ languages). ## Install diff --git a/distribution/doc/release-notes/3.1.0.md b/distribution/doc/release-notes/3.1.0.md index 768510b6..9a5ff6df 100644 --- a/distribution/doc/release-notes/3.1.0.md +++ b/distribution/doc/release-notes/3.1.0.md @@ -37,9 +37,23 @@ runtime. options, plus an *Engine* selector in the JavaFX toolbar, let you pick the engine; the choice is stored in `advanced.properties` (`engine=openpdf`). Engines are discovered via `ServiceLoader` from - `lib/`, so third-party engines can be dropped in. Additional engines - (PDFBox, EU DSS / PAdES) are planned for a follow-up. See + `lib/`, so third-party engines can be dropped in. See `design-doc/3.1-signing-engines.md`. +- **EU DSS / PAdES signing engine.** A second bundled engine (id `dss`), + built on the European Commission's Digital Signature Service library, + produces **PAdES** signatures (`ETSI.CAdES.detached`) at the ETSI + baseline levels **B / T / LT / LTA** — which the OpenPDF engine cannot + create. Select it with `-eng dss` / the *Engine* toolbar selector and + choose the level with the new `-pl` / `--pades-level` option (`B`, `T`, + `LT`, `LTA`); `LT`/`LTA` embed revocation data and are configured via + the `engine.dss.*` trust keys in `advanced.properties` (also on the new + *DSS engine* Preferences tab). The default engine stays `openpdf`, so + there is no change unless you opt in. Bundling DSS adds roughly 30 MB+ + of jars to the distribution and puts an **LGPL-2.1** dependency on the + active signing path when `dss` is selected (JSignPdf is dual + MPL-2.0 / LGPL-2.1, so this is license-compatible). See + `design-doc/3.1-engine-dss.md` and the *Signing engines* chapter of the + user guide. ## Notes diff --git a/distribution/pom.xml b/distribution/pom.xml index aa48fc05..2f08acdd 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -229,6 +229,11 @@ jsignpdf-engine-openpdf ${project.version} + + ${project.groupId} + jsignpdf-engine-dss + ${project.version} + ${project.groupId} installcert diff --git a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java index 3ad4e0a3..dbddb98b 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java @@ -8,6 +8,7 @@ import net.sf.jsignpdf.types.CertificationLevel; import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PadesLevel; import net.sf.jsignpdf.types.PDFEncryption; import net.sf.jsignpdf.types.PrintRight; import net.sf.jsignpdf.types.RenderMode; @@ -50,6 +51,8 @@ public class BasicSignerOptions { private String pdfEncryptionCertFile; private CertificationLevel certLevel; private HashAlgorithm hashAlgorithm; + // PAdES baseline level (DSS engine). null = engine default (OpenPDF ignores it; DSS treats null as BASELINE_B). + private PadesLevel padesLevel; protected boolean storePasswords = Constants.DEFVAL_STOREPWD; @@ -156,6 +159,7 @@ private void loadFromStore(PropertyProvider store, boolean includeAllConfig) { setPdfEncryptionCertFile(store.getProperty(Constants.PROPERTY_PDF_ENCRYPTION_CERT_FILE)); setCertLevel(store.getProperty(Constants.PROPERTY_CERT_LEVEL)); setHashAlgorithm(store.getProperty(Constants.PROPERTY_HASH_ALGORITHM)); + setPadesLevel(store.getProperty(Constants.PROPERTY_PADES_LEVEL)); setRightPrinting(store.getProperty(Constants.PROPERTY_RIGHT_PRINT)); setRightCopy(store.getAsBool(Constants.PROPERTY_RIGHT_COPY)); @@ -278,6 +282,7 @@ private void storeToStore(PropertyProvider store, boolean includeAllConfig) { store.setProperty(Constants.PROPERTY_PDF_ENCRYPTION_CERT_FILE, getPdfEncryptionCertFile()); store.setProperty(Constants.PROPERTY_CERT_LEVEL, getCertLevel().name()); store.setProperty(Constants.PROPERTY_HASH_ALGORITHM, getHashAlgorithm().name()); + store.setProperty(Constants.PROPERTY_PADES_LEVEL, getPadesLevel() == null ? null : getPadesLevel().name()); store.setProperty(Constants.PROPERTY_RIGHT_PRINT, getRightPrinting().name()); store.setProperty(Constants.PROPERTY_RIGHT_COPY, isRightCopy()); @@ -1173,6 +1178,30 @@ public void setHashAlgorithm(final String aValue) { setHashAlgorithm(hashAlg); } + /** + * @return the selected PAdES baseline level, or {@code null} for the engine default + */ + public PadesLevel getPadesLevel() { + return padesLevel; + } + + /** + * @param padesLevel the PAdES baseline level to set ({@code null} = engine default) + */ + public void setPadesLevel(final PadesLevel padesLevel) { + this.padesLevel = padesLevel; + } + + /** + * Sets the PAdES baseline level from a (short or full) string token. Unrecognised or empty values + * reset the level to {@code null} (engine default). + * + * @param aValue the level token ({@code B} / {@code T} / {@code LT} / {@code LTA} or full enum name) + */ + public void setPadesLevel(final String aValue) { + setPadesLevel(PadesLevel.fromString(aValue)); + } + public Proxy.Type getProxyType() { if (proxyType == null) { proxyType = Constants.DEFVAL_PROXY_TYPE; @@ -1277,6 +1306,7 @@ public BasicSignerOptions createCopy() { copy.setPdfEncryptionCertFile(getPdfEncryptionCertFile()); copy.setCertLevel(getCertLevel()); copy.setHashAlgorithm(getHashAlgorithm()); + copy.setPadesLevel(getPadesLevel()); copy.setStorePasswords(isStorePasswords()); copy.setRightPrinting(getRightPrinting()); copy.setRightCopy(isRightCopy()); @@ -1331,7 +1361,8 @@ public int hashCode() { result = prime * result + Objects.hash(acro6Layers, advanced, append, bgImgPath, bgImgScale, certLevel, contact, crlEnabled, encryptor, hashAlgorithm, imgPath, inFile, keyAlias, keyIndex, ksFile, ksType, l2Text, l2TextFontSize, l4Text, listener, location, ocspEnabled, ocspServerUrl, outFile, page, pdfEncryption, - pdfEncryptionCertFile, positionLLX, positionLLY, positionURX, positionURY, propertiesFilePath, props, proxyHost, + padesLevel, pdfEncryptionCertFile, positionLLX, positionLLY, positionURX, positionURY, propertiesFilePath, + props, proxyHost, proxyPort, proxyType, reason, renderMode, rightAssembly, rightCopy, rightFillIn, rightModifyAnnotations, rightModifyContents, rightPrinting, rightScreanReaders, signerName, storePasswords, timestamp, tsaCertFile, tsaCertFilePwd, tsaCertFileType, tsaHashAlg, tsaPasswd, tsaPolicy, tsaServerAuthn, tsaUrl, tsaUser, visible); @@ -1361,7 +1392,7 @@ public boolean equals(Object obj) { && Objects.equals(l4Text, other.l4Text) && Objects.equals(listener, other.listener) && Objects.equals(location, other.location) && ocspEnabled == other.ocspEnabled && Objects.equals(ocspServerUrl, other.ocspServerUrl) && Objects.equals(outFile, other.outFile) - && page == other.page && pdfEncryption == other.pdfEncryption + && page == other.page && padesLevel == other.padesLevel && pdfEncryption == other.pdfEncryption && Objects.equals(pdfEncryptionCertFile, other.pdfEncryptionCertFile) && Arrays.equals(pdfOwnerPwd, other.pdfOwnerPwd) && Arrays.equals(pdfUserPwd, other.pdfUserPwd) && Float.floatToIntBits(positionLLX) == Float.floatToIntBits(other.positionLLX) diff --git a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java index bd4e5897..9e79b309 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java @@ -129,6 +129,7 @@ public class Constants { public static final String PROPERTY_CERT_LEVEL = "certification.level"; public static final String PROPERTY_HASH_ALGORITHM = "hash.algorithm"; + public static final String PROPERTY_PADES_LEVEL = "pades.level"; public static final String PROPERTY_RIGHT_PRINT = "right.printing"; public static final String PROPERTY_RIGHT_COPY = "right.copy"; @@ -306,6 +307,9 @@ public class Constants { public static final String ARG_HASH_ALGORITHM = "ha"; public static final String ARG_HASH_ALGORITHM_LONG = "hash-algorithm"; + public static final String ARG_PADES_LEVEL = "pl"; + public static final String ARG_PADES_LEVEL_LONG = "pades-level"; + public static final String ARG_ENCRYPTED = "e"; public static final String ARG_ENCRYPTED_LONG = "encrypted"; diff --git a/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java b/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java new file mode 100644 index 00000000..afebb131 --- /dev/null +++ b/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java @@ -0,0 +1,64 @@ +package net.sf.jsignpdf.types; + +/** + * PAdES baseline signature levels (ETSI EN 319 142). This is the single new per-document field the + * DSS engine needs; it is selected by the user through {@code --pades-level} / the FX dropdown and + * honoured by engines that declare the matching {@code PADES_BASELINE_*} capability. + * + *

+ * The type is intentionally backend-neutral — it carries no DSS (or any other signing library) + * dependency, preserving the engine-api invariant that no engine library leaks into the shared model. + * The DSS engine maps these constants onto {@code eu.europa.esig.dss.enumerations.SignatureLevel} + * internally. + *

+ * + * @author Josef Cacek + */ +public enum PadesLevel { + + /** Basic signature. */ + BASELINE_B, + /** B + signature timestamp. */ + BASELINE_T, + /** T + validation material (certificates, OCSP/CRL) embedded for long-term validation. */ + BASELINE_LT, + /** LT + archive timestamp. */ + BASELINE_LTA; + + /** + * Parses a short, case-insensitive token ({@code B} / {@code T} / {@code LT} / {@code LTA}) or the + * full enum name ({@code BASELINE_B} ...) into a {@link PadesLevel}. + * + * @param value the token, may be {@code null} + * @return the matching level, or {@code null} when {@code value} is {@code null} or unrecognised + */ + public static PadesLevel fromString(String value) { + if (value == null) { + return null; + } + String v = value.trim().toUpperCase(java.util.Locale.ENGLISH); + switch (v) { + case "B": + return BASELINE_B; + case "T": + return BASELINE_T; + case "LT": + return BASELINE_LT; + case "LTA": + return BASELINE_LTA; + default: + try { + return PadesLevel.valueOf(v); + } catch (IllegalArgumentException e) { + return null; + } + } + } + + /** + * @return the short spec token for this level ({@code B} / {@code T} / {@code LT} / {@code LTA}) + */ + public String shortName() { + return name().substring("BASELINE_".length()); + } +} diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties b/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties index a8a961a1..3c04af52 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties @@ -52,3 +52,21 @@ pdf2image.libraries=jpedal,pdfbox,openpdf # Default hash algorithm requested when stamping a signature with a TSA # (used when the signer options don't override it). tsa.hashAlgorithm=SHA-256 + +# DSS signing engine (PAdES). These knobs only matter when the 'dss' engine +# is selected and the requested PAdES level is LT/LTA (which embed revocation +# data). B and T work fully offline and ignore them. +# +# Fetch revocation data (OCSP/CRL) and intermediate certs (AIA) online. +# Required to produce LT/LTA. Default false keeps B/T fully offline. +engine.dss.online.enabled=false +# Use the default EU List Of Trusted Lists (LOTL) to build the trust anchors. +engine.dss.trust.useDefaultLotl=false +# Comma-separated lists of custom trust material. +engine.dss.trust.lotlUrls= +engine.dss.trust.certFiles= +engine.dss.trust.certUrls= +# Truststore holding trust anchors (type defaults to the JVM default). +engine.dss.trust.truststoreFile= +engine.dss.trust.truststoreType= +engine.dss.trust.truststorePassword= diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 2de08b86..3708c3f3 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -34,6 +34,10 @@ console.engineMismatch=The selected engine ''{0}'' does not support the followin console.engineMismatch.option=\ {0} (capability {1}) console.engineNotFound=Unknown signing engine ''{0}''. Use --list-engines to see available engines. console.engines=Available signing engines: +console.dss.tsaUpgrade=A TSA is configured, upgrading the PAdES level B to T (so the signature timestamp is kept). +console.dss.ltNoRevocation=The PAdES level LT/LTA needs revocation data, but online fetching is disabled (engine.dss.online.enabled=false) or no trust material is reachable. Signing aborted. +console.dss.unsupportedHash=The DSS engine does not support the hash algorithm ''{0}'' for PAdES. Use SHA-256, SHA-384 or SHA-512. +console.dss.cannotEncryptSigned=Cannot encrypt a PDF that already contains signatures. console.getPrivateKey=Loading private key console.inFileNotFound.error=Input PDF was not found or it's not readable. console.keys=Key aliases in the keystore: @@ -212,6 +216,7 @@ hlp.ksType=sets KeyStore type (you can list possible values for this option -lkt hlp.l2Text=signature text, you can also use placeholders for signature properties (${signer}, ${certificate}, ${timestamp}, ${location}, ${reason}, ${contact}) hlp.l2TextFontSize=font size for visible signature text, default value is {0} hlp.l4Text=status text +hlp.padesLevel=PAdES baseline level for the PAdES signing engine (dss): B, T, LT or LTA (case-insensitive). Honoured by engines that support PAdES; ignored otherwise. LT/LTA require revocation data (see engine.dss.* in advanced.properties). hlp.engine=selects the signing engine for this invocation (overrides advanced.properties); use -le to list available engines hlp.listEngines=lists available signing engines, which can be used as values for the -eng option hlp.listKeys=lists keys in chosen keystore @@ -276,6 +281,18 @@ ssl.keymanager.init=Initializing key manager from keystore file {0}. jfx.gui.engine.label=Engine: jfx.gui.engine.tooltip=Signing engine used to sign the document. jfx.gui.engine.unsupported=This option is not supported by the selected engine. +jfx.gui.padesLevel.label=PAdES level: +jfx.gui.padesLevel.tooltip=PAdES baseline level (only for PAdES engines such as DSS). LT/LTA embed revocation data and require the DSS trust configuration. +jfx.gui.dss.section.label=DSS engine +jfx.gui.dss.online.enabled=Fetch revocation data online (OCSP/CRL, AIA) +jfx.gui.dss.online.enabled.tooltip=Required to produce LT/LTA. When off, the DSS engine works fully offline (B/T only). +jfx.gui.dss.trust.useDefaultLotl=Use the default EU List of Trusted Lists (LOTL) +jfx.gui.dss.trust.lotlUrls=LOTL URLs (comma-separated) +jfx.gui.dss.trust.certFiles=Trusted certificate files (comma-separated) +jfx.gui.dss.trust.certUrls=Trusted certificate URLs (comma-separated) +jfx.gui.dss.trust.truststoreFile=Truststore file +jfx.gui.dss.trust.truststoreType=Truststore type +jfx.gui.dss.trust.truststorePassword=Truststore password jfx.gui.menu.file=File jfx.gui.menu.file.open=Open PDF... jfx.gui.menu.file.close=Close diff --git a/engines/dss/pom.xml b/engines/dss/pom.xml new file mode 100644 index 00000000..c3f29a12 --- /dev/null +++ b/engines/dss/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + + jsignpdf-engine-dss + jar + ${project.artifactId} + JSignPdf EU-DSS-based PAdES signing engine (B/T/LT/LTA) + + + com.github.kwart.jsign + jsignpdf-root + 3.1.0-SNAPSHOT + ../../pom.xml + + + + + com.github.kwart.jsign + jsignpdf-engine-api + + + + + eu.europa.ec.joinup.sd-dss + dss-pades-pdfbox + + + eu.europa.ec.joinup.sd-dss + dss-cms-object + + + eu.europa.ec.joinup.sd-dss + dss-token + + + eu.europa.ec.joinup.sd-dss + dss-service + + + eu.europa.ec.joinup.sd-dss + dss-utils-apache-commons + + + eu.europa.ec.joinup.sd-dss + dss-tsl-validation + + + eu.europa.ec.joinup.sd-dss + dss-crl-parser-x509crl + + + + org.apache.commons + commons-lang3 + + + + junit + junit + test + + + diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java new file mode 100644 index 00000000..d06b2c27 --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java @@ -0,0 +1,48 @@ +package net.sf.jsignpdf.engine.dss; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.logging.Level; + +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.AppConfig; + +import org.apache.commons.lang3.StringUtils; + +import eu.europa.esig.dss.pades.DSSFileFont; +import eu.europa.esig.dss.pades.DSSFont; + +/** + * Resolves the {@link DSSFont} used for the visible-signature text. It honours the same + * {@code font.path} advanced-config knob the OpenPDF engine uses (capability + * {@code VISIBLE_CUSTOM_FONT}) and otherwise falls back to the DejaVuSans font bundled in + * {@code jsignpdf-engine-api} (so non-Latin text renders correctly). + * + * @author Josef Cacek + */ +final class DssFontUtils { + + /** DejaVuSans bundled as a resource in jsignpdf-engine-api. */ + private static final String DEFAULT_EMBEDDED_FONT_PATH = "/net/sf/jsignpdf/fonts/DejaVuSans.ttf"; + + private DssFontUtils() { + } + + /** + * @return a {@link DSSFont} for the visible-signature text, or {@code null} if no font could be + * loaded + */ + static DSSFont getVisibleSignatureFont() { + final String fontPath = AppConfig.fontPath(); + try (InputStream is = fontPath != null ? new FileInputStream(fontPath) + : DssFontUtils.class.getResourceAsStream(DEFAULT_EMBEDDED_FONT_PATH)) { + if (is != null) { + return new DSSFileFont(is); + } + } catch (Exception e) { + Constants.LOGGER.log(Level.SEVERE, "Font loading failed" + (StringUtils.isNotEmpty(fontPath) ? ": " + fontPath : ""), + e); + } + return null; + } +} diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssMappings.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssMappings.java new file mode 100644 index 00000000..ac077da8 --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssMappings.java @@ -0,0 +1,93 @@ +package net.sf.jsignpdf.engine.dss; + +import net.sf.jsignpdf.types.CertificationLevel; +import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PadesLevel; + +import eu.europa.esig.dss.enumerations.CertificationPermission; +import eu.europa.esig.dss.enumerations.DigestAlgorithm; +import eu.europa.esig.dss.enumerations.SignatureLevel; + +/** + * Translates JSignPdf's backend-neutral model enums onto their DSS counterparts. Keeping these maps in + * the DSS engine (rather than on the shared model types) preserves the engine-api invariant that no + * engine library leaks into {@code net.sf.jsignpdf.types.*}. + * + * @author Josef Cacek + */ +final class DssMappings { + + private DssMappings() { + } + + /** + * Maps a JSignPdf {@link PadesLevel} to a DSS {@link SignatureLevel}. {@code null} defaults to + * {@link SignatureLevel#PAdES_BASELINE_B}. + * + * @param level the JSignPdf level (may be {@code null}) + * @return the DSS signature level + */ + static SignatureLevel toSignatureLevel(PadesLevel level) { + if (level == null) { + return SignatureLevel.PAdES_BASELINE_B; + } + switch (level) { + case BASELINE_B: + return SignatureLevel.PAdES_BASELINE_B; + case BASELINE_T: + return SignatureLevel.PAdES_BASELINE_T; + case BASELINE_LT: + return SignatureLevel.PAdES_BASELINE_LT; + case BASELINE_LTA: + return SignatureLevel.PAdES_BASELINE_LTA; + default: + return SignatureLevel.PAdES_BASELINE_B; + } + } + + /** + * Maps a JSignPdf {@link HashAlgorithm} to a DSS {@link DigestAlgorithm}. + * + * @param hash the JSignPdf hash algorithm + * @return the DSS digest algorithm, or {@code null} when the algorithm is not a valid PAdES digest + * (SHA-1 / RIPEMD-160 are rejected by the capability validator before signing, so the engine + * should never actually see them) + */ + static DigestAlgorithm toDigestAlgorithm(HashAlgorithm hash) { + if (hash == null) { + return DigestAlgorithm.SHA256; + } + switch (hash) { + case SHA256: + return DigestAlgorithm.SHA256; + case SHA384: + return DigestAlgorithm.SHA384; + case SHA512: + return DigestAlgorithm.SHA512; + default: + return null; // SHA1 / RIPEMD160 are not PAdES digests + } + } + + /** + * Maps a JSignPdf {@link CertificationLevel} (DocMDP) to a DSS {@link CertificationPermission}. + * + * @param level the certification level + * @return the DSS permission, or {@code null} for a non-certifying (approval) signature + */ + static CertificationPermission toCertificationPermission(CertificationLevel level) { + if (level == null) { + return null; + } + switch (level) { + case CERTIFIED_NO_CHANGES_ALLOWED: + return CertificationPermission.NO_CHANGE_PERMITTED; + case CERTIFIED_FORM_FILLING: + return CertificationPermission.MINIMAL_CHANGES_PERMITTED; + case CERTIFIED_FORM_FILLING_AND_ANNOTATIONS: + return CertificationPermission.CHANGES_PERMITTED; + default: + return null; + } + } +} 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 new file mode 100644 index 00000000..552e616c --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssSigningEngine.java @@ -0,0 +1,436 @@ +package net.sf.jsignpdf.engine.dss; + +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_CONTACT; +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_LOCATION; +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_REASON; +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_SIGNER; +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_TIMESTAMP; +import static net.sf.jsignpdf.Constants.L2TEXT_PLACEHOLDER_CERTIFICATE; +import static net.sf.jsignpdf.Constants.LOGGER; +import static net.sf.jsignpdf.Constants.RES; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URI; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.PrivateKeyInfo; +import net.sf.jsignpdf.engine.Capability; +import net.sf.jsignpdf.engine.EngineConfig; +import net.sf.jsignpdf.engine.SigningEngine; +import net.sf.jsignpdf.types.CertificationLevel; +import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PadesLevel; +import net.sf.jsignpdf.types.PrintRight; +import net.sf.jsignpdf.types.RenderMode; +import net.sf.jsignpdf.types.ServerAuthentication; +import net.sf.jsignpdf.utils.KeyStoreUtils; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; +import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; + +import eu.europa.esig.dss.enumerations.CertificationPermission; +import eu.europa.esig.dss.enumerations.DigestAlgorithm; +import eu.europa.esig.dss.enumerations.SignatureLevel; +import eu.europa.esig.dss.model.DSSDocument; +import eu.europa.esig.dss.model.FileDocument; +import eu.europa.esig.dss.model.SignatureValue; +import eu.europa.esig.dss.model.ToBeSigned; +import eu.europa.esig.dss.pades.DSSFont; +import eu.europa.esig.dss.pades.PAdESSignatureParameters; +import eu.europa.esig.dss.pades.SignatureFieldParameters; +import eu.europa.esig.dss.pades.SignatureImageParameters; +import eu.europa.esig.dss.pades.SignatureImageTextParameters; +import eu.europa.esig.dss.pades.signature.PAdESService; +import eu.europa.esig.dss.service.http.commons.TimestampDataLoader; +import eu.europa.esig.dss.service.tsp.OnlineTSPSource; +import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier; + +/** + * The EU-DSS-based signing engine. It produces PAdES (ETSI.CAdES.detached) signatures at the baseline + * levels B / T / LT / LTA, the levels the OpenPDF engine cannot create. The signing flow is lifted from + * the standalone {@code jsignpdf-pades} project and adapted to read JSignPdf's {@link BasicSignerOptions} + * model, obtain key material through JSignPdf's shared {@link KeyStoreUtils#getPkInfo(BasicSignerOptions)}, + * and read DSS-specific trust knobs from the engine-scoped {@link EngineConfig}. + * + * @author Josef Cacek + */ +public class DssSigningEngine implements SigningEngine { + + /** Stable identifier used in config files and CLI args. */ + public static final String ID = "dss"; + + private static final Set CAPABILITIES = Set.copyOf(EnumSet.of( + Capability.SUBFILTER_ETSI_CADES_DETACHED, + Capability.PADES_BASELINE_B, Capability.PADES_BASELINE_T, + Capability.PADES_BASELINE_LT, Capability.PADES_BASELINE_LTA, + Capability.DSS_DICTIONARY, + + Capability.HASH_SHA256, Capability.HASH_SHA384, Capability.HASH_SHA512, + + Capability.APPEND_MODE, // DSS signs incrementally by default + Capability.CERTIFICATION_LEVEL, + Capability.ENCRYPTION_PASSWORD, Capability.PERMISSIONS_BITMASK, + + Capability.VISIBLE_SIGNATURE, Capability.VISIBLE_LAYER2_TEXT, + Capability.VISIBLE_BACKGROUND_IMAGE, Capability.VISIBLE_SIGNATURE_GRAPHIC, + Capability.VISIBLE_CUSTOM_FONT, + Capability.VISIBLE_RENDER_MODE_DESCRIPTION_ONLY, + Capability.VISIBLE_RENDER_MODE_GRAPHIC_AND_DESCRIPTION, + + Capability.TSA, Capability.TSA_POLICY_OID, Capability.TSA_BASIC_AUTH, + Capability.OCSP_EMBED, Capability.CRL_EMBED, + + Capability.PROXY_SUPPORT, + Capability.PKCS11_PROVIDER)); + + @Override + public String id() { + return ID; + } + + @Override + public String displayName() { + return "EU DSS (PAdES)"; + } + + @Override + public Set capabilities() { + return CAPABILITIES; + } + + @Override + public boolean sign(final BasicSignerOptions options, final EngineConfig engineConfig) { + final String outFile = options.getOutFileX(); + boolean finished = false; + File encryptedTempFile = null; + try { + final PrivateKeyInfo pkInfo = KeyStoreUtils.getPkInfo(options); + if (pkInfo == null) { + LOGGER.info(RES.get("console.certificateChainEmpty")); + return false; + } + final PrivateKey key = pkInfo.getKey(); + final Certificate[] chain = pkInfo.getChain(); + if (ArrayUtils.isEmpty(chain)) { + LOGGER.info(RES.get("console.certificateChainEmpty")); + return false; + } + + final HashAlgorithm hashAlgorithm = options.getHashAlgorithmX(); + final DigestAlgorithm digestAlgorithm = DssMappings.toDigestAlgorithm(hashAlgorithm); + if (digestAlgorithm == null) { + LOGGER.severe(RES.get("console.dss.unsupportedHash", hashAlgorithm.getAlgorithmName())); + return false; + } + + try (PrivateKeySignatureToken token = new PrivateKeySignatureToken(key, chain)) { + final PAdESSignatureParameters parameters = new PAdESSignatureParameters(); + parameters.setDigestAlgorithm(digestAlgorithm); + parameters.setSigningCertificate(token.getKeyEntry().getCertificate()); + parameters.setCertificateChain(token.getKeyEntry().getCertificateChain()); + + // PAdES level (+ TSA auto-upgrade B->T). + final boolean useTsa = options.isTimestampX() && StringUtils.isNotEmpty(options.getTsaUrl()); + final PadesLevel padesLevel = options.getPadesLevel(); + if (useTsa && (padesLevel == null || padesLevel == PadesLevel.BASELINE_B)) { + LOGGER.info(RES.get("console.dss.tsaUpgrade")); + parameters.setSignatureLevel(SignatureLevel.PAdES_BASELINE_T); + } else { + parameters.setSignatureLevel(DssMappings.toSignatureLevel(padesLevel)); + } + + final Calendar signingCal = Calendar.getInstance(); + parameters.bLevel().setSigningDate(signingCal.getTime()); + + final String reason = options.getReason(); + if (StringUtils.isNotEmpty(reason)) { + LOGGER.info(RES.get("console.setReason", reason)); + parameters.setReason(reason); + } + final String location = options.getLocation(); + if (StringUtils.isNotEmpty(location)) { + LOGGER.info(RES.get("console.setLocation", location)); + parameters.setLocation(location); + } + final String contact = options.getContact(); + if (StringUtils.isNotEmpty(contact)) { + LOGGER.info(RES.get("console.setContact", contact)); + parameters.setContactInfo(contact); + } + + // Certification level (DocMDP). + LOGGER.info(RES.get("console.setCertificationLevel")); + final CertificationLevel certLevel = options.getCertLevelX(); + final CertificationPermission permission = DssMappings.toCertificationPermission(certLevel); + if (permission != null) { + parameters.setPermission(permission); + } + + // Owner password to open an encrypted input PDF. + final char[] ownerPwd = options.getPdfOwnerPwd(); + if (ownerPwd != null && ownerPwd.length > 0) { + parameters.setPasswordProtection(ownerPwd); + } + + // Encrypt-before-sign (password-only). + File effectiveInFile = new File(options.getInFile()); + if (options.isAdvanced() && options.getPdfEncryption() == net.sf.jsignpdf.types.PDFEncryption.PASSWORD) { + LOGGER.info(RES.get("console.setEncryption")); + encryptedTempFile = encryptPdf(effectiveInFile, options); + if (encryptedTempFile == null) { + return false; + } + effectiveInFile = encryptedTempFile; + } + + final DSSDocument document = new FileDocument(effectiveInFile); + + if (options.isVisible()) { + LOGGER.info(RES.get("console.configureVisible")); + configureVisibleSignature(parameters, options, chain, signingCal, effectiveInFile); + } + + // Certificate verifier + trust material (LT/LTA). + final DssTrustConfigurer trustConfigurer = new DssTrustConfigurer(engineConfig); + final boolean ltOrLta = parameters.getSignatureLevel() == SignatureLevel.PAdES_BASELINE_LT + || parameters.getSignatureLevel() == SignatureLevel.PAdES_BASELINE_LTA; + if (ltOrLta && !trustConfigurer.isOnlineEnabled()) { + LOGGER.severe(RES.get("console.dss.ltNoRevocation")); + return false; + } + final CommonCertificateVerifier verifier = trustConfigurer.buildVerifier(); + final PAdESService service = new PAdESService(verifier); + + if (useTsa) { + LOGGER.info(RES.get("console.creatingTsaClient")); + service.setTspSource(buildTspSource(options, parameters, digestAlgorithm)); + } + + LOGGER.info(RES.get("console.processing")); + LOGGER.info(RES.get("console.createSignature")); + final ToBeSigned dataToSign = service.getDataToSign(document, parameters); + final SignatureValue signatureValue = token.sign(dataToSign, digestAlgorithm, null); + final DSSDocument signedDocument = service.signDocument(document, parameters, signatureValue); + + LOGGER.info(RES.get("console.createOutPdf", outFile)); + try (FileOutputStream fos = new FileOutputStream(outFile)) { + signedDocument.writeTo(fos); + } + LOGGER.info(RES.get("console.closeStream")); + } + finished = true; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, RES.get("console.exception"), e); + } catch (OutOfMemoryError e) { + LOGGER.log(Level.SEVERE, RES.get("console.memoryError"), e); + } finally { + if (encryptedTempFile != null) { + encryptedTempFile.delete(); + } + } + return finished; + } + + private OnlineTSPSource buildTspSource(BasicSignerOptions options, PAdESSignatureParameters parameters, + DigestAlgorithm digestAlgorithm) { + final String tsaUrl = options.getTsaUrl(); + final TimestampDataLoader tsDataLoader = new TimestampDataLoader(); + if (options.getTsaServerAuthn() == ServerAuthentication.PASSWORD) { + final URI tsaUri = URI.create(tsaUrl); + tsDataLoader.addAuthentication(tsaUri.getHost(), tsaUri.getPort(), null, + StringUtils.defaultString(options.getTsaUser()), + StringUtils.defaultString(options.getTsaPasswd()).toCharArray()); + } + final OnlineTSPSource tspSource = new OnlineTSPSource(tsaUrl, tsDataLoader); + final String policyOid = options.getTsaPolicy(); + if (StringUtils.isNotEmpty(policyOid)) { + LOGGER.info(RES.get("console.settingTsaPolicy", policyOid)); + tspSource.setPolicyOid(policyOid); + } + final String tsaHashAlg = options.getTsaHashAlgWithFallback(); + if (StringUtils.isNotEmpty(tsaHashAlg)) { + LOGGER.info(RES.get("console.settingTsaHashAlg", tsaHashAlg)); + parameters.getSignatureTimestampParameters().setDigestAlgorithm(DigestAlgorithm.forJavaName(tsaHashAlg)); + } + return tspSource; + } + + private File encryptPdf(File inFile, BasicSignerOptions options) throws Exception { + try (PDDocument doc = Loader.loadPDF(inFile)) { + if (!doc.getSignatureDictionaries().isEmpty()) { + LOGGER.info(RES.get("console.dss.cannotEncryptSigned")); + return null; + } + final AccessPermission ap = buildAccessPermission(options); + final String encOwnerPwd = StringUtils.defaultString(options.getPdfOwnerPwdStrX()); + final String encUserPwd = StringUtils.defaultString(options.getPdfUserPwdStr()); + final StandardProtectionPolicy policy = new StandardProtectionPolicy(encOwnerPwd, encUserPwd, ap); + policy.setEncryptionKeyLength(128); + doc.protect(policy); + + final File tempFile = File.createTempFile("jsignpdf-dss-enc-", ".pdf"); + tempFile.deleteOnExit(); + doc.save(tempFile); + return tempFile; + } + } + + private AccessPermission buildAccessPermission(BasicSignerOptions options) { + final AccessPermission ap = new AccessPermission(); + PrintRight printing = options.getRightPrinting(); + if (printing == null) { + printing = PrintRight.ALLOW_PRINTING; + } + ap.setCanPrint(printing != PrintRight.DISALLOW_PRINTING); + ap.setCanPrintFaithful(printing == PrintRight.ALLOW_PRINTING); + ap.setCanExtractContent(options.isRightCopy()); + ap.setCanAssembleDocument(options.isRightAssembly()); + ap.setCanFillInForm(options.isRightFillIn()); + ap.setCanExtractForAccessibility(options.isRightScreanReaders()); + ap.setCanModifyAnnotations(options.isRightModifyAnnotations()); + ap.setCanModify(options.isRightModifyContents()); + return ap; + } + + private void configureVisibleSignature(PAdESSignatureParameters parameters, BasicSignerOptions options, + Certificate[] chain, Calendar signingCal, File inFile) throws Exception { + final SignatureImageParameters imageParams = new SignatureImageParameters(); + + int page = options.getPage(); + float pageWidth; + float pageHeight; + try (PDDocument pdDoc = Loader.loadPDF(inFile)) { + final int totalPages = pdDoc.getNumberOfPages(); + if (page < 1 || page > totalPages) { + page = totalPages; + } + final PDPage pdPage = pdDoc.getPage(page - 1); + final PDRectangle mediaBox = pdPage.getMediaBox(); + final int rotation = pdPage.getRotation(); + if (rotation == 90 || rotation == 270) { + pageWidth = mediaBox.getHeight(); + pageHeight = mediaBox.getWidth(); + } else { + pageWidth = mediaBox.getWidth(); + pageHeight = mediaBox.getHeight(); + } + } + + final float llx = fixPosition(options.getPositionLLX(), pageWidth); + final float lly = fixPosition(options.getPositionLLY(), pageHeight); + final float urx = fixPosition(options.getPositionURX(), pageWidth); + final float ury = fixPosition(options.getPositionURY(), pageHeight); + + final SignatureFieldParameters fieldParams = new SignatureFieldParameters(); + fieldParams.setPage(page); + fieldParams.setOriginX(llx); + // DSS uses a top-left origin (PDF uses bottom-left), so flip the Y coordinate. + fieldParams.setOriginY(pageHeight - ury); + fieldParams.setWidth(urx - llx); + fieldParams.setHeight(ury - lly); + imageParams.setFieldParameters(fieldParams); + + final RenderMode renderMode = options.getRenderMode(); + final boolean withGraphic = renderMode == RenderMode.GRAPHIC_AND_DESCRIPTION; + + // Image: the signature graphic (render mode GRAPHIC_AND_DESCRIPTION) takes priority; otherwise a + // background image, if configured. + final String graphicPath = options.getImgPath(); + final String bgImgPath = options.getBgImgPath(); + final String imagePath = (withGraphic && graphicPath != null) ? graphicPath : bgImgPath; + if (imagePath != null) { + LOGGER.info(RES.get("console.createImage", imagePath)); + imageParams.setImage(new FileDocument(imagePath)); + } + + LOGGER.info(RES.get("console.setL2Text")); + final SignatureImageTextParameters textParams = new SignatureImageTextParameters(); + textParams.setText(buildSignatureText(options, chain, signingCal)); + final DSSFont font = DssFontUtils.getVisibleSignatureFont(); + if (font != null) { + float fontSize = options.getL2TextFontSize(); + if (fontSize <= 0f) { + fontSize = net.sf.jsignpdf.Constants.DEFVAL_L2_FONT_SIZE; + } + font.setSize(fontSize); + textParams.setFont(font); + } + imageParams.setTextParameters(textParams); + + parameters.setImageParameters(imageParams); + } + + private String buildSignatureText(BasicSignerOptions options, Certificate[] chain, Calendar signingCal) { + final X509Certificate signerCert = (X509Certificate) chain[0]; + String signer = extractCN(signerCert); + if (StringUtils.isNotEmpty(options.getSignerName())) { + signer = options.getSignerName(); + } + final String certificate = signerCert.getSubjectX500Principal().toString(); + final String timestamp = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(signingCal.getTime()); + final String reason = options.getReason(); + final String location = options.getLocation(); + + if (options.getL2Text() == null) { + final StringBuilder buf = new StringBuilder(); + buf.append(RES.get("default.l2text.signedBy")).append(' ').append(signer).append('\n'); + buf.append(RES.get("default.l2text.date")).append(' ').append(timestamp); + if (StringUtils.isNotEmpty(reason)) { + buf.append('\n').append(RES.get("default.l2text.reason")).append(' ').append(reason); + } + if (StringUtils.isNotEmpty(location)) { + buf.append('\n').append(RES.get("default.l2text.location")).append(' ').append(location); + } + return buf.toString(); + } + // Same placeholder template language (${...}) as the OpenPDF engine, so a single --l2-text works + // across engines. + final Map replacements = new HashMap<>(); + replacements.put(L2TEXT_PLACEHOLDER_SIGNER, StringUtils.defaultString(signer)); + replacements.put(L2TEXT_PLACEHOLDER_CERTIFICATE, certificate); + replacements.put(L2TEXT_PLACEHOLDER_TIMESTAMP, timestamp); + replacements.put(L2TEXT_PLACEHOLDER_LOCATION, StringUtils.defaultString(location)); + replacements.put(L2TEXT_PLACEHOLDER_REASON, StringUtils.defaultString(reason)); + replacements.put(L2TEXT_PLACEHOLDER_CONTACT, StringUtils.defaultString(options.getContact())); + return StrSubstitutor.replace(options.getL2Text(), replacements); + } + + private String extractCN(X509Certificate cert) { + try { + final LdapName ldapName = new LdapName(cert.getSubjectX500Principal().getName()); + for (Rdn rdn : ldapName.getRdns()) { + if ("CN".equalsIgnoreCase(rdn.getType())) { + return rdn.getValue().toString(); + } + } + } catch (Exception e) { + // fall through to the full DN + } + return cert.getSubjectX500Principal().toString(); + } + + private float fixPosition(float origPos, float base) { + return origPos >= 0 ? origPos : base + origPos; + } +} 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 new file mode 100644 index 00000000..067e65ea --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssTrustConfigurer.java @@ -0,0 +1,161 @@ +package net.sf.jsignpdf.engine.dss; + +import static net.sf.jsignpdf.Constants.LOGGER; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import net.sf.jsignpdf.engine.EngineConfig; + +import org.apache.commons.lang3.StringUtils; + +import eu.europa.esig.dss.service.crl.OnlineCRLSource; +import eu.europa.esig.dss.service.http.commons.CommonsDataLoader; +import eu.europa.esig.dss.service.http.commons.FileCacheDataLoader; +import eu.europa.esig.dss.service.ocsp.OnlineOCSPSource; +import eu.europa.esig.dss.spi.DSSUtils; +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.job.TLValidationJob; +import eu.europa.esig.dss.tsl.source.LOTLSource; + +/** + * Builds and configures the {@link CommonCertificateVerifier} that drives DSS revocation handling and + * trust-anchor resolution for the LT / LTA baseline levels. Trust material is read through the phase-1 + * {@link EngineConfig} view ({@code engine.dss.*}); the mapping mirrors {@code jsignpdf-pades}' + * {@code TrustConfig} / {@code TrustedCertSourcesProvider}. + * + *

+ * B and T need no trust configuration, so the verifier returned for them is the bare + * {@link CommonCertificateVerifier}. LT/LTA additionally need reachable revocation data; when + * {@code engine.dss.online.enabled=true} the AIA / OCSP / CRL online sources are wired in. + *

+ * + * @author Josef Cacek + */ +final class DssTrustConfigurer { + + static final String KEY_ONLINE_ENABLED = "online.enabled"; + static final String KEY_USE_DEFAULT_LOTL = "trust.useDefaultLotl"; + static final String KEY_LOTL_URLS = "trust.lotlUrls"; + 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"; + + /** Separator for the list-valued keys (lotlUrls / certFiles / certUrls). */ + private static final String LIST_SEPARATOR = "[,;]+"; + + private final EngineConfig config; + + DssTrustConfigurer(EngineConfig config) { + this.config = config; + } + + /** + * @return {@code true} when online fetching of revocation/AIA data is enabled + */ + boolean isOnlineEnabled() { + return config.getBoolean(KEY_ONLINE_ENABLED, false); + } + + /** + * Builds a verifier configured with the trusted certificate sources and (when online is enabled) the + * AIA / OCSP / CRL online sources required to embed validation material for LT/LTA. + * + * @return the configured certificate verifier + */ + CommonCertificateVerifier buildVerifier() { + CommonCertificateVerifier verifier = new CommonCertificateVerifier(); + try { + CertificateSource[] trustedSources = createTrustedCertSources(); + if (trustedSources.length > 0) { + verifier.setTrustedCertSources(trustedSources); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to configure DSS trusted certificate sources", e); + } + if (isOnlineEnabled()) { + verifier.setAIASource(new DefaultAIASource()); + verifier.setOcspSource(new OnlineOCSPSource()); + verifier.setCrlSource(new OnlineCRLSource()); + } + return verifier; + } + + private CertificateSource[] createTrustedCertSources() throws Exception { + List trustedSources = new ArrayList<>(); + + LOTLSource[] lotlSources = getLotlSources(); + if (lotlSources.length > 0) { + TLValidationJob tlValidationJob = new TLValidationJob(); + tlValidationJob.setOnlineDataLoader(new FileCacheDataLoader(new CommonsDataLoader())); + tlValidationJob.setListOfTrustedListSources(lotlSources); + TrustedListsCertificateSource trustedListsCertificateSource = new TrustedListsCertificateSource(); + tlValidationJob.setTrustedListCertificateSource(trustedListsCertificateSource); + tlValidationJob.onlineRefresh(); + trustedSources.add(trustedListsCertificateSource); + } + + for (String certFile : splitList(config.getString(KEY_CERT_FILES))) { + CommonTrustedCertificateSource source = new CommonTrustedCertificateSource(); + source.addCertificate(DSSUtils.loadCertificate(new File(certFile))); + trustedSources.add(source); + } + for (String certUrl : splitList(config.getString(KEY_CERT_URLS))) { + CommonTrustedCertificateSource source = new CommonTrustedCertificateSource(); + try (InputStream is = new URL(certUrl).openStream()) { + source.addCertificate(DSSUtils.loadCertificate(is)); + } + trustedSources.add(source); + } + + final String truststoreFile = config.getString(KEY_TRUSTSTORE_FILE); + if (StringUtils.isNotEmpty(truststoreFile)) { + final String type = config.getString(KEY_TRUSTSTORE_TYPE, KeyStore.getDefaultType()); + final String pwd = config.getString(KEY_TRUSTSTORE_PASSWORD, ""); + KeyStoreCertificateSource source = new KeyStoreCertificateSource(new File(truststoreFile), type, + pwd != null ? pwd.toCharArray() : null); + trustedSources.add(source); + } + return trustedSources.toArray(new CertificateSource[0]); + } + + private LOTLSource[] getLotlSources() { + List lotlSources = new ArrayList<>(); + if (config.getBoolean(KEY_USE_DEFAULT_LOTL, false)) { + lotlSources.add(new LOTLSource()); + } + for (String url : splitList(config.getString(KEY_LOTL_URLS))) { + LOTLSource lotlSource = new LOTLSource(); + lotlSource.setUrl(url); + lotlSource.setCertificateSource(new CommonCertificateSource()); + lotlSources.add(lotlSource); + } + return lotlSources.toArray(new LOTLSource[0]); + } + + private static List splitList(String value) { + List out = new ArrayList<>(); + if (StringUtils.isNotBlank(value)) { + for (String part : value.split(LIST_SEPARATOR)) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + out.add(trimmed); + } + } + } + return out; + } +} diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/PrivateKeySignatureToken.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/PrivateKeySignatureToken.java new file mode 100644 index 00000000..41234f9e --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/PrivateKeySignatureToken.java @@ -0,0 +1,99 @@ +package net.sf.jsignpdf.engine.dss; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +import eu.europa.esig.dss.enumerations.DigestAlgorithm; +import eu.europa.esig.dss.enumerations.EncryptionAlgorithm; +import eu.europa.esig.dss.enumerations.SignatureAlgorithm; +import eu.europa.esig.dss.model.DSSException; +import eu.europa.esig.dss.model.SignatureValue; +import eu.europa.esig.dss.model.ToBeSigned; +import eu.europa.esig.dss.model.x509.CertificateToken; +import eu.europa.esig.dss.token.AbstractSignatureTokenConnection; +import eu.europa.esig.dss.token.DSSPrivateKeyEntry; + +/** + * In-memory {@link eu.europa.esig.dss.token.SignatureTokenConnection} that signs the DSS + * {@link ToBeSigned} with a plain JCA {@link Signature}, deriving the {@link SignatureAlgorithm} from + * the key's {@link EncryptionAlgorithm} and the chosen {@link DigestAlgorithm}. + * + *

+ * This is the seam where PKCS#11 keys work unchanged (the {@link PrivateKey} is a provider key) and + * where a future external-signing token (e.g. CloudFoxy) would plug in. Ported from + * {@code jsignpdf-pades}. + *

+ * + * @author Josef Cacek + */ +final class PrivateKeySignatureToken extends AbstractSignatureTokenConnection { + + private final PrivateKey privateKey; + private final CertificateToken[] certificateChain; + private final DSSPrivateKeyEntry keyEntry; + + PrivateKeySignatureToken(PrivateKey key, Certificate[] chain) { + this.privateKey = key; + this.certificateChain = new CertificateToken[chain.length]; + for (int i = 0; i < chain.length; i++) { + this.certificateChain[i] = new CertificateToken((X509Certificate) chain[i]); + } + this.keyEntry = new PrivateKeyEntryImpl(); + } + + @Override + public List getKeys() throws DSSException { + return Collections.singletonList(keyEntry); + } + + DSSPrivateKeyEntry getKeyEntry() { + return keyEntry; + } + + @Override + public SignatureValue sign(ToBeSigned toBeSigned, DigestAlgorithm digestAlgorithm, DSSPrivateKeyEntry keyEntry) + throws DSSException { + try { + EncryptionAlgorithm encAlg = EncryptionAlgorithm.forKey(privateKey); + SignatureAlgorithm sigAlg = SignatureAlgorithm.getAlgorithm(encAlg, digestAlgorithm); + + Signature signature = Signature.getInstance(sigAlg.getJCEId()); + signature.initSign(privateKey); + signature.update(toBeSigned.getBytes()); + byte[] sigValue = signature.sign(); + + SignatureValue signatureValue = new SignatureValue(); + signatureValue.setAlgorithm(sigAlg); + signatureValue.setValue(sigValue); + return signatureValue; + } catch (GeneralSecurityException e) { + throw new DSSException("Unable to sign", e); + } + } + + @Override + public void close() { + } + + private class PrivateKeyEntryImpl implements DSSPrivateKeyEntry { + @Override + public CertificateToken getCertificate() { + return certificateChain[0]; + } + + @Override + public CertificateToken[] getCertificateChain() { + return certificateChain; + } + + @Override + public EncryptionAlgorithm getEncryptionAlgorithm() { + return EncryptionAlgorithm.forKey(privateKey); + } + } +} diff --git a/engines/dss/src/main/resources/META-INF/services/net.sf.jsignpdf.engine.SigningEngine b/engines/dss/src/main/resources/META-INF/services/net.sf.jsignpdf.engine.SigningEngine new file mode 100644 index 00000000..6c7fefe7 --- /dev/null +++ b/engines/dss/src/main/resources/META-INF/services/net.sf.jsignpdf.engine.SigningEngine @@ -0,0 +1 @@ +net.sf.jsignpdf.engine.dss.DssSigningEngine diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java new file mode 100644 index 00000000..33875845 --- /dev/null +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -0,0 +1,181 @@ +package net.sf.jsignpdf.engine.dss; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; + +import net.sf.jsignpdf.BasicSignerOptions; +import net.sf.jsignpdf.engine.Capability; +import net.sf.jsignpdf.engine.EngineConfig; +import net.sf.jsignpdf.types.PadesLevel; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Signing tests for {@link DssSigningEngine}, adapted from the {@code jsignpdf-pades} signing suite to + * drive the engine through the JSignPdf {@link BasicSignerOptions} model. Output is validated + * structurally (PAdES subfilter, signature presence) rather than by byte equality. + */ +public class DssSigningEngineTest { + + private static final char[] KS_PASSWD = "keystorepass".toCharArray(); + private static final String KS_FILE = "src/test/resources/test-keystore.jks"; + private static final String KEY_ALIAS = "rsa2048"; + private static final char[] KEY_PASSWD = "RSA2048pass".toCharArray(); + + private static final EngineConfig EMPTY_CONFIG = new MapEngineConfig(new HashMap<>()); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private File inputFile; + private File outputFile; + + @BeforeClass + public static void registerBc() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Before + public void createInputPdf() throws Exception { + inputFile = tmp.newFile("input.pdf"); + outputFile = tmp.newFile("output.pdf"); + outputFile.delete(); // engine writes it + try (PDDocument doc = new PDDocument()) { + doc.setVersion(1.7f); + PDPage page = new PDPage(); + doc.addPage(page); + try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { + cs.beginText(); + cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + cs.newLineAtOffset(100, 700); + cs.showText("Test PDF for DSS signing"); + cs.endText(); + } + doc.save(inputFile); + } + } + + private BasicSignerOptions baseOptions() { + BasicSignerOptions o = new BasicSignerOptions(); + // advanced mode so the dedicated key password (distinct from the keystore password) is used + o.setAdvanced(true); + o.setHashAlgorithm(net.sf.jsignpdf.types.HashAlgorithm.SHA256); + o.setKsType("JKS"); + o.setKsFile(KS_FILE); + o.setKsPasswd(KS_PASSWD); + o.setKeyAlias(KEY_ALIAS); + o.setKeyPasswd(KEY_PASSWD); + o.setInFile(inputFile.getAbsolutePath()); + o.setOutFile(outputFile.getAbsolutePath()); + return o; + } + + @Test + public void defaultLevelProducesPadesSignature() throws Exception { + BasicSignerOptions o = baseOptions(); // padesLevel == null -> BASELINE_B + boolean ok = new DssSigningEngine().sign(o, EMPTY_CONFIG); + assertTrue("signing should succeed", ok); + assertTrue("output should exist", outputFile.exists()); + assertPadesSignature(outputFile); + } + + @Test + public void explicitBaselineBProducesPadesSignature() throws Exception { + BasicSignerOptions o = baseOptions(); + o.setPadesLevel(PadesLevel.BASELINE_B); + assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertPadesSignature(outputFile); + } + + @Test + public void visibleSignatureIsPlacedAndSigned() throws Exception { + BasicSignerOptions o = baseOptions(); + o.setVisible(true); + o.setPage(1); + o.setReason("testing"); + o.setLocation("here"); + assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertPadesSignature(outputFile); + } + + @Test + public void ltWithoutOnlineFetchingFails() throws Exception { + BasicSignerOptions o = baseOptions(); + o.setPadesLevel(PadesLevel.BASELINE_LT); + // online fetching disabled -> the engine must refuse rather than emit a weaker level + boolean ok = new DssSigningEngine().sign(o, EMPTY_CONFIG); + assertFalse("LT without revocation data must fail", ok); + } + + @Test + public void capabilitiesAreStaticAndDeclarePades() { + DssSigningEngine engine = new DssSigningEngine(); + assertEquals("dss", engine.id()); + assertTrue(engine.capabilities().contains(Capability.PADES_BASELINE_B)); + assertTrue(engine.capabilities().contains(Capability.PADES_BASELINE_LTA)); + assertFalse("DSS must not declare the legacy Adobe subfilter", + engine.capabilities().contains(Capability.SUBFILTER_ADBE_PKCS7_DETACHED)); + assertFalse("PAdES disallows SHA-1", engine.capabilities().contains(Capability.HASH_SHA1)); + } + + private static void assertPadesSignature(File pdf) throws Exception { + try (PDDocument doc = Loader.loadPDF(pdf)) { + assertFalse("a signature must be present", doc.getSignatureDictionaries().isEmpty()); + PDSignature sig = doc.getSignatureDictionaries().get(0); + assertEquals("ETSI.CAdES.detached", sig.getSubFilter()); + } + } + + /** 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 = 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/engines/dss/src/test/resources/test-keystore.jks b/engines/dss/src/test/resources/test-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..415569943b0153a9a468f34e9583b6f1560a5689 GIT binary patch literal 10524 zcmcI~RaD$hx^2_AyE`;4Y3Rm1f#43oA-Dzz1WRxUPLSa4?(PJK;O-LKo#0$@X6`xj zKQn9Hhdc97YxP(CQ1bg~?{C+xr=zDM0001dY49%%27u_`XvhhM^1l3L`cNJC?xi0V z>FZgLRG-camxiXLo$z@a5NE-2W#k#4P}QE8*{ z*}I01gOKix)V2LH=HAC$g_=Wc4%Dkhfv)z@5%H{F(&OyRiQ&}-*@@##S@QdB5|^Hj zOun$}Gu)s!3+At1YmMIdgzh=&Q3)pQ#4#Zh~_hF4#n*a+$QdC(aqR}y3MA3dX212VUwX8 zR>l+(PQXxZk@Exl3ms$rj#Ue7`$c97!OTQRR_`fiJ20iP=3Y_=s_gnDs!oouDG0^8 zPye{b{ELbMn+qG?+zw5Kw!!MMwQRq-ibG+icCvUE{g+TB_C4p-ny|P2$WZsS)%eCs za{~Cbv2^wEuOG(zn=Y7iAlG08&BSI|nHeR)y7_sk)&Cp1XOJZE))szMf<*!R_ zMMgc2ovbIQ>6MBYzPJ3MPdaVc0DTDlPh$!zc0H86$noa)_Ma-MhL;@8cI-8E7L9Fh zgbxbilX#rwmCbbT4ADsXVC^J~6RlH6hmDUDKaS-|u8jr7piSXTp%EjnDEWGOPCr{? z3a=BlCtuo_$O?K{y0aSI%e@T|krk?ZrU?ncRsakhwu6e|ORLwUBFZaeX8YlQrX;3* zlyq10oz*2iZ`L-PQ(*OfO2JZFQw3XXjEMYpQObfg?k&v~Fx+?<9WPJvUY$fVH0#T5 zNp<;!CB0@>M%*a~On7~K+@~>A@ib8Y8}lF#Yg}+zu`w{?C})tmkcJ)ZDvyM4y;C<# z&q`y_wAi@x*e7bJ%O}B0FEz=k92`%XmL=L5*k#cEmZX$H8Mp z-)ldphq_;d?kz+&NSPoVM!I``#Ie6cjWfYJ0qXZ*Kxot2zmtWEoVGiTPQcqeQ-S8h zDTE)by4tYodhXQ>CYpjK(*+?_j~VxOdANW)Q9e5)#$*7syRkHm$gBb$yW*~LK5e*UipJ)(C4DSESUfypn5K7|8)W*1Ned+5%O2y69AcQ#5m6ho?u;xF5ut$-bp*3G zimp0E(7oYSA2%nb-VO-SRb3IUV}%rdtmRLaG8D-zDeW+qcr{!Gbu3{jmPC=WBNfF$NeIeO65&^_pC5p{Q(RFM+grd={8 zpTsjo4cIpr$6b#pW2Dapl<~`gQ2B>ls8;gs@B1`M0lvEW^x^;}uNuRhFjHlvPp<=e z1@M3K5OxZ12``<#+f6}ZaX>yGKgx`*gQ*6^^jXsSyC*}AuUn{Lr{TPEwP&zTW*-Arn8=PYZb7-ld6o#PyGZRi9YRH!Q;P~0%@Kv ziD3=ZVUfl*YVpQwcA80+^^_jbv}dcU+q6MCx{n7bHX4Xu;P3?-;WgR0zh!T^DAa-g?CU~~jz=C4v;f1|&^BRl}C#DNM%Mu1g^L%;>T(dHlm6Z{EK za4}_6EzNC}Ow6bp*&W#J*g5dPIDh^@#YI&Te@o@)WN&Zh@QDK(jQOV*78glHRgCHn zB7-Ri(K*0iZcZ*RH<%O3t4)Xw;o;!;)Ae8Be_%vk7@B`v)(h;z`odrWz`igjKwlUb zz)u&xo*MF`4Wa>JKU2@P;0wk zF#x99TM!=Ak{uiQL~-L{s=JwjqMWq$j}4X)XRP?1$}v|qCfh`AReNyuSMX0!D?>!S zlx+9N_9Hbn_xDyMZ8aUjz`pu5N6L2OME`@07$w5Z90#J(factFh&s+RwgH1BGMk5v zKdb-N+RNNp?pm|q;Rm8`j%9@?jE1&#;7G5w4V4DgQja3;w9YOY2Py05v8b}Z6nQ^D zQIz6SJepODh~bG``p^tl$QQEIy6-DV7i%v1X+>C`@F%AekbGk=yq5-6SqKE z7y!&a?$FDJf7u}z!0Sy-LU(z3C!=B$&!Kh{PG!a}4QOO}<=Qz4QOL+EEA-j*;S+48 zxwa=-KR1klYUW@8+8ER3Z0Bywz$GlR-B1*pTW9=+lLsulGb^e^q+CIX(0DF{`-D$b z-iPtLG#S0%{8&Vq?@%~G{_EV`VZ{p~=sA_0aFhbndc1nd3!TLQ*Eaus(EAjHK=Q&a zglgA}_xp64rC)3bIE51%Uv1B!rj5}s3v>nJU%i*1Nd4_(L%TzKiS06OEIQW8Ypzrh z0q;M*o&>F+wk|N5?04U9IXSj`;&r!EOtW>5$>vRbHdl5ysegTsdMW20QQQ!?_7%ji zeIyF}GZ?&u>QiJ9 zjY?&x;!Z7IN@l!2_v~#P?I`S=KwZ*1K>Vt7Pu&A?JusgXh2-eFCzfOA=-Kx{{SzC|`h4=lg-^d-Xs5zVe+0D1> zLM!~OGV?cu_Igl5@^|N1rh=>F=-1(t#3n0b1fZtiHA`E$QP7agU}UTvFnu`zFS$Ah z!xx>I3~w^Lni1m}vM{cPGd>}SAhYl8(uQCETZ|qa*nO<+q-18ahlAlxN-1w?lD_$Ha{*O<;P~o!f zz@L?4Vi_d!n=;WxG2p9NpCZ&NQ#B65<1BZE2yQLm3lW-g&S($XhMlj18$~p6Mm!j_ zpk1PrPu0L}w+L~bQVT8oP#D8^0`e#Ka_p8g7cFfp-f>xcJJU2F{kXl0CLV8+c3+)X z<}$h?DyNX~FE2l2m5FrW;+l%_EQ>6%x;E7uTBb9fmvG3;TDcp?lVb8-?(3ejNt|Lm z=uI<<*M%(J=YhMmwHRBx>0sCN@kVP$n@Nj1e}-5UV2DLsmYR%l0yrFhBrBF{iWgPTD=%j5b5Y+9|F?Pr7b_<3^G!_fo!s{T3nlNPHS)C@HMj;R#Rh`Bs8P7IDz@ zk_(jCm^F#7YIC3yRHC+sKadr=Mh)p*jh;0$CkfV)+$#)H$@%@|s{Kk4vzydVYtZh; zN@0~_j7m}GW`Y^WuQoyE42cZN#i5YTCOrEk_UUn`w1zzf?b&L&3B`%9?kBI#Ymt^P zKPr`4`?5nfoQUOMr+V=^_xl>QSv`r@1gxp&&#ne<(Xi&~2}--}VzX`xg*(cHU%yjA z8zlYQzop@fg(u|qTqZdNU*5SMPhXFt6n4a7Xw@uO%!eGEZwr>3?&0P-CLIIFW|tjW z96tJz(3ZDv5;tlI>9T?z`hO zqC+aGq-pk`fNJZ2@`D zfufLTUG?|kDCNEaHXI)!-+9QNa~v_$Y({U$XAt^Xj(?M>?O!_85$Ai-vr>uLMrDYh zPp$uyboZl0>Q*e8;xP5|AlK{+KI4A>a}l>CM99GfI4n+&Rbld$pk~@-In=pQY|q-V z-!p2x@6FEr?#;0LG9VncC{0aY&e1hjF~ka$BcHK8&!+#vT?578O1}m5CW*9B?dFh8 zQvxGZqlokhi_Dr7T9+2mqWY+-D>>DU+LQ`-mxP$hi&VruCu@In!)queoV& zTkE3dLmP7%2aZ^GcbqVU1Tm>XCJg>R~eq@=Cv$!yo^E8-k7 z-AdMbIq`IxaD=lRXeQw5%en3zbpMiR#Fi|XHT5Ek_u`NmqA>9uHAqQM&Q4OvkK*!q zxmQA5ljat>t1g6v>ad_Zk1JNQpQw_Q+jMQ2Ubmz3aQmeH#{p7+1HA6N(Bk2HVZuogYJ)T{UrMPfbiMd7vm^e2qC>^T_I4gL$mdie9)yq7Ef(!_iYmehi#@ zUS@aATxg9Yn#VOKtj)EHK+Eoxlwdqf{(5L($p5mn6}JxQO`mEH9x*W}4W1BtHUOJkm}y+YOfW)Dj|6zTuP3+&nW+i-!dur{LWA+cY}Rd1;O3l=+GGibZSl!Y%4n0 za%!}k3{1t81%b+&cSnLC?61h1ud?F<_l7N5Pdg0l_~$h=y-T2d&QLG6IS>BsM4s_q z&5}j%rxn2;ZFz||&V)s7VZ2hZ4NbxC+Sp8gy95K4mE3nFWqQLvzltS86|H|y3jbhmUzkU^^8KjLIw&gN%rKo`c3n5 zas{?I#?+Bm_ZN(!FuXdmopxW9#|YMz_Fzu9?aSpDYsh~d6^~DC?>|uc8mFdaQTiC8 z+50=o(D6B6R$YI##0YV=Py4sLR7dvtL7yK5{MFHgtYIX&j^tq*+j^>Rhrqw2^FMM* zWJt<|+x@04k&csdj7i(@KutC8j_9^kSSQAsWw~GX^c%|$XLeEJas!alaB_&Zwx@+b zHiIv%rQ?Vs@v@Hve3!)?-+H^w6lZaxtz@0a2T92aow-xYw&@jOqsz$@|&~LQDIFu*A_Q(*=of>NZNgEf@0*U=^G8#9n_ALRn_C7|Adu-u!b(BS`Pztu&c#sug>1WjxT*&Wh3`$mx z6}R(@!BzauyJwhxTa@C&r2C_7vMc@OyznLmsHamz&}N`$fPWVD`qfeP4jqyDuSR}V z6WKKHD5HR86{>IMM*~URj+sfy!0!k%4jhRh2ew{c7y8)w&}dcoOg(tS8hU^~?3c)s zo$v17xWHYF?!M|EZzc^P!j#%)Ms-1zu)z`fNULYP%*}-n|5!z^=A_EuF>kS?x|wut zqi|{#NoX7HTom2|zyKM>h(u)p^;IH(P|CF1iec1q)HhV_6j;-~i$z5o+g04#<&w|KK8EviBj zOtfoT>iuL+AqoGGJ7arJ#xuCTWi*6!#^X}Sut6aVnmEmoPT~rhn%bS(EhOuBdl4fL z8x~Whm9oI=6(E_G4_{T21@%{b3zwDX(mj6|qRO-i5BoIHtf_%ae|u)hP{)f)$gyGo z^xAC~@+AUsq1kYq$FcVXAT%WmR+H610BDS_;@<4rbIr*|BPYQ9!c$i;NMrb69`AP& zS*-(q15JgDJ^Cq2YqA1{PNCT+KN8m;QT#5cPkc0fTd&;Q`dB@ia;Ps4{Y0oj-3dyX z2Z`kNP3ibnxf<{-msDybeB||_-m0x$eeDNz{Pmoh7BIPLJL#8Z{3&CcV6XjL3DJ8` zk!Z?Lvm+XDJsX^K1TXIEvAZZDt5S;=WDp-@ALE|5yE%)wfOJ$0TQumTYDB+MIKPog z79&0IFn*AAgAt?v40~A1eg9RTImXk%Ri5}9A7n_KR(y_0`_+1oSL>Bk2(WSc?2MV!WTj!2H8g}mTC zwSKuV#N&sb?YfM)R-3q06Q=08D^nW(Au|L!(ui}}_Rv7*3EFu;gTNsMnym43U!Qr! zQUIKpmytw65vTf6m!XNs*~JaOp(TW9?U3O0RS9G>Og~pXPIZE41DcH@=Cn?qH^1o6 z_4&*kOUu;~pyZakM=6XOT?xprPqJA^Viy_uwe%3AI^1RYC}dXOqfeVcYG84Cj=oDv zX(`{qQ)1FV~5%6Yv1==|8F?Gaje7e1tCM#Luij>1P=uvcl?;j_KiR5e4Q zp*1%}dKu5$IkR3}>8N~U+)S{pK;r&Ye7%W6-LX!eDJLE}>q|PD+`q9 z_q#Js@qtFe5EarEmPSbk6XvgE%JuHKplpoO%t00QRV4c~$TfU_Q90qWJ4KQ1EK8uB z>Xcc>FpIPy&1tbI<#pbnFYzog$U)xG;B#o_y_k{Til&1n`w6jXe-&u#92>ri(ih!E z{_ArO+^)l^FzN;Fn^1wV)utSj_cf@KOOrvrFa_T3!VPvYdWW`_e7q?jQoQO3El#6u zweR)7pbL;3`qgz+8+9@A5%I`HCL8f5vfs%k{RwJc)*trU{q_7!2z$HV(kMkmLzL2W zt|@jHFj!;}TyNe?acKOe{-WwWoW=~73Lb(Zq^A>jP$K2>%fIU@GqlU~uB9>NtTHD1 zXhwt$@wB4QC<9^Gui1!L&VR(MioTp;oZ%*BNF|F1K7TLNN{qdJamgKm60xbPb<@f9=70(%gOu0e2 zEDB!?MIWHKRm{ieFR_`wFvB?A2kWv3!Y!0X4Mv<=e5^Kw+{O>CC_Pc4=ci*u6z_Ng zTFv~|RkUD&;W!TLP)^pH6>l08gfyjzdp=Pt=3n9Q$u$POtHDMOFP|R!aRbT3b%YJV zpD&NxIUH*Q-72nIcKi3Y^Ee{^B=1^wz`e5JxU&#uQj*`uSU3lsS~o5^Pdl?NaW%&8 zipSIRjhiRmu1VQR&|JM8B$!ApXMc{5%N&#x`!Z~k%Vk0#o1UxnpaNf>n1-U*c zXsU$qG_Z_rRMhy;r4&lDZ>Zc&F5!YYjZ08o%Rm}$LdNEU1&lEN z4%M=NA^z0|#Zyv`rjl>Y?_1^s_K`-cV8Wlh5GuJ#YUEjizdoyHO%M38U?NpBbdXES^4V{{1lchDZ#z z)9TTNC~-8lG;1a7xCu{G7S&n4^$m1WTbe_Zf9|NO8%@E1t~ z|Kbh7zj#Bi|KJU!{BKF~Z}o=$)P#Sz7leTS;tl-?|GoEu|23j7@SnXQ;2&=Y_~H!# zVE{=gzHn;twh8K>b3Td`+#NZ_s_$Vb82KI-1Dc}q=$D5B{7l-fzLywJvh8T7mFnwo zmo*Oo!oyyrhWdK)Lay99BD~0@n>tM(a)@EC(OH%{tWNBr2&_*zt!(sm5?po*HjQoZ zbzZ$`Eg_Mguzb&wbYe+WTVWo~LJ^g7+gmQ%bz#_hcYgr!+_4YG_kX0&VUvE9o9E5z z#*+jp9zwZj3GMuGdCQ6wkYYa16Y0r$w=LMhMT!$Dl>SDtXhl!IM&3A~I+}2q_tP%P zl)bcTNx$_3(Tc)vpvowaz;n^@;^(JNAV}>>t6cI#tS7S*NqDN&v$STeE1MZcxV7Ey zT&tRsd#ntWrg~ODL?@Fm$8Mjl4?ID2b&Qlx_1_0aTNa-^KtKjG|=%)Vx38k|7 zRCFQ=VPT`M%;7;uk^KcpG!j?^+lgx9zGm`X*(g_u{&GyUV&vOT`Zw z-}nQ_bk=3kgBMTu(cBq7 z+aeYD?@sXW9Z=>-XLEzZ5f*F+k<^8-Jhjg}-^>u;!N10M&xp_tMaZ(FD5_Np2{?C` zu77fElyuos=UDwDs*!^ylg2;G@y%RYqdCidD@)Pkxlntc5dnEL<-3xI#`@OG-!8{Z z91Z`+8@zrIV=&)d%#_f77PBxevee-&sv*+XNQ-0vhrg*~Wc&gzaH{9o__^t16LdHZ z)RXCGsi&|iniQzZ8j3d94DY}x*mK!JlorwZ$X!t%=fxQBCVF#sKS#>vEdn5dA9Y%@ zCspuogb@&arLmH5CyYReJz75C5!aT5xfP)GIo1rbO(E~%-?NHTCVhChP$cc@NWR&I=pn-I1b?nQ`tmlr&k2N|H!~#T8RE(aIzT^+Ai6x4GUOOY?Pb z*4EkK@PP;PZrtT)outLS(5cpLkX`Fo5GBn+&($p9UuX@u@&fD&FD>DJ?51?Y~F#g=u)nLe~GgESQ&o@{iH==%wmI z?u5gnvMQ6pez>`Fja{!Icp?tyq7d<}Q>M3+&=aLmzSj~8*Su@*xoQGm|4df5ne`Fy zCWqRco4v2MPE^XxNl)K}`+`N&sn}dN`&8dcdLy8*(_4^-j|q40cRkrd(FNpE!__8; z;=FdcC}T04Wj;BlfhA5ImxAd7;sfH}D}BSKMIEY&ZuHGM@JBt>B)j@EwcqO*0W--$ zD?eL`b-5cf8r{B^H{bE^``&8Ep$Z}Tnk#E*L^MrlDQ)&ne0_+Y9xMn#<_y-sm$7NMI=EtgS#fGPuBRbzOTa z9U(f@OyB#9I-LoOoWRqlG&8JE0N~F&YNP8AXD?B9%yP>4eF9%iAlfbB6T|f-s%86Y z82_XKb3RB?Wnu+PP0cVnY^WZaIU5)7!0B_$pRPD6V8x#G^nU>KaY^w2 literal 0 HcmV?d00001 diff --git a/engines/pom.xml b/engines/pom.xml index a8b331f2..46951999 100644 --- a/engines/pom.xml +++ b/engines/pom.xml @@ -16,5 +16,6 @@ api openpdf + dss diff --git a/jsignpdf/pom.xml b/jsignpdf/pom.xml index 8a05d4cf..8acd61a1 100644 --- a/jsignpdf/pom.xml +++ b/jsignpdf/pom.xml @@ -92,6 +92,11 @@ jsignpdf-engine-openpdf test
+ + com.github.kwart.jsign + jsignpdf-engine-dss + test + diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java index 8d6b145e..ce93061c 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java @@ -145,6 +145,8 @@ public void loadCmdLine() throws ParseException { setCertLevel(line.getOptionValue(ARG_CERT_LEVEL)); if (line.hasOption(ARG_HASH_ALGORITHM)) setHashAlgorithm(line.getOptionValue(ARG_HASH_ALGORITHM)); + if (line.hasOption(ARG_PADES_LEVEL)) + setPadesLevel(line.getOptionValue(ARG_PADES_LEVEL)); // encryption if (line.hasOption(ARG_ENCRYPTED)) @@ -377,6 +379,8 @@ private float getFloat(Object aVal, float aDefVal) { OPTS.addOption(OptionBuilder.withLongOpt(ARG_HASH_ALGORITHM_LONG) .withDescription(RES.get("hlp.hashAlgorithm", getEnumValues(HashAlgorithm.values()))).hasArg() .withArgName("algorithm").create(ARG_HASH_ALGORITHM)); + OPTS.addOption(OptionBuilder.withLongOpt(ARG_PADES_LEVEL_LONG).withDescription(RES.get("hlp.padesLevel")).hasArg() + .withArgName("level").create(ARG_PADES_LEVEL)); OPTS.addOption(OptionBuilder.withLongOpt(ARG_QUIET_LONG).withDescription(RES.get("hlp.quiet")).create(ARG_QUIET)); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java index 1ded8cd8..1243783a 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java @@ -8,6 +8,7 @@ import net.sf.jsignpdf.BasicSignerOptions; import net.sf.jsignpdf.Constants; import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PadesLevel; import net.sf.jsignpdf.types.PDFEncryption; import net.sf.jsignpdf.types.PrintRight; import net.sf.jsignpdf.types.RenderMode; @@ -58,6 +59,14 @@ public record Mismatch(String option, Capability capability) { RENDER_CAPS.put(RenderMode.SIGNAME_AND_DESCRIPTION, Capability.VISIBLE_RENDER_MODE_NAME_AND_DESCRIPTION); } + private static final Map PADES_CAPS = new EnumMap<>(PadesLevel.class); + static { + PADES_CAPS.put(PadesLevel.BASELINE_B, Capability.PADES_BASELINE_B); + PADES_CAPS.put(PadesLevel.BASELINE_T, Capability.PADES_BASELINE_T); + PADES_CAPS.put(PadesLevel.BASELINE_LT, Capability.PADES_BASELINE_LT); + PADES_CAPS.put(PadesLevel.BASELINE_LTA, Capability.PADES_BASELINE_LTA); + } + private EngineMismatchValidator() { } @@ -80,6 +89,14 @@ public static List findMismatches(BasicSignerOptions o, SigningEngine out.add(new Mismatch("--hash-algorithm", hashCap)); } + // PAdES baseline level + if (o.getPadesLevel() != null) { + final Capability padesCap = PADES_CAPS.get(o.getPadesLevel()); + if (padesCap != null && !caps.contains(padesCap)) { + out.add(new Mismatch("--pades-level", padesCap)); + } + } + // append mode if (o.isAppendX() && !caps.contains(Capability.APPEND_MODE)) { out.add(new Mismatch("--append", Capability.APPEND_MODE)); 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 2c2ac307..0195ade1 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 @@ -69,6 +69,7 @@ public class PreferencesController { @FXML private Tab tabNetwork; @FXML private Tab tabPdfRender; @FXML private Tab tabTsa; + @FXML private Tab tabDss; @FXML private Tab tabPkcs11; @FXML private TextField txtFontPath; @@ -86,6 +87,15 @@ public class PreferencesController { @FXML private ComboBox cmbTsaHashAlgorithm; + @FXML private CheckBox chkDssOnlineEnabled; + @FXML private CheckBox chkDssUseDefaultLotl; + @FXML private TextField txtDssLotlUrls; + @FXML private TextField txtDssCertFiles; + @FXML private TextField txtDssCertUrls; + @FXML private TextField txtDssTruststoreFile; + @FXML private TextField txtDssTruststoreType; + @FXML private TextField txtDssTruststorePassword; + @FXML private Label lblPkcs11Path; @FXML private TextArea txtPkcs11Body; @FXML private Label lblPkcs11EmptyHint; @@ -185,6 +195,15 @@ void bind(PreferencesViewModel viewModel) { cmbTsaHashAlgorithm.valueProperty().bindBidirectional(vm.tsaHashAlgorithmProperty()); + chkDssOnlineEnabled.selectedProperty().bindBidirectional(vm.dssOnlineEnabledProperty()); + chkDssUseDefaultLotl.selectedProperty().bindBidirectional(vm.dssUseDefaultLotlProperty()); + txtDssLotlUrls.textProperty().bindBidirectional(vm.dssLotlUrlsProperty()); + txtDssCertFiles.textProperty().bindBidirectional(vm.dssCertFilesProperty()); + txtDssCertUrls.textProperty().bindBidirectional(vm.dssCertUrlsProperty()); + txtDssTruststoreFile.textProperty().bindBidirectional(vm.dssTruststoreFileProperty()); + txtDssTruststoreType.textProperty().bindBidirectional(vm.dssTruststoreTypeProperty()); + txtDssTruststorePassword.textProperty().bindBidirectional(vm.dssTruststorePasswordProperty()); + txtPkcs11Body.textProperty().bindBidirectional(vm.pkcs11BodyProperty()); lblPkcs11EmptyHint.visibleProperty().bind( Bindings.createBooleanBinding(() -> txtPkcs11Body.getText() == null || txtPkcs11Body.getText().isEmpty(), @@ -291,6 +310,8 @@ private void resetActiveTabToDefaults() { vm.applyPdfRenderDefaults(defaults); } else if (active == tabTsa) { vm.applyTsaDefaults(defaults); + } else if (active == tabDss) { + vm.applyDssDefaults(defaults); } else if (active == tabPkcs11) { vm.pkcs11BodyProperty().set(PKCS11Utils.getSampleConfig()); } 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 091cddd8..147a0f84 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 @@ -44,6 +44,16 @@ public class PreferencesViewModel { private final StringProperty tsaHashAlgorithm = new SimpleStringProperty("SHA-256"); + // 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 StringProperty dssLotlUrls = new SimpleStringProperty(""); + private final StringProperty dssCertFiles = new SimpleStringProperty(""); + private final StringProperty dssCertUrls = new SimpleStringProperty(""); + private final StringProperty dssTruststoreFile = new SimpleStringProperty(""); + private final StringProperty dssTruststoreType = new SimpleStringProperty(""); + private final StringProperty dssTruststorePassword = new SimpleStringProperty(""); + private final StringProperty pkcs11Body = new SimpleStringProperty(""); /** Loads the VM from the given snapshot of {@link AdvancedConfig} and a pkcs11 file body. */ @@ -62,6 +72,15 @@ 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)); + 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"))); + dssTruststoreFile.set(orEmpty(cfg.getProperty("engine.dss.trust.truststoreFile"))); + dssTruststoreType.set(orEmpty(cfg.getProperty("engine.dss.trust.truststoreType"))); + dssTruststorePassword.set(orEmpty(cfg.getProperty("engine.dss.trust.truststorePassword"))); + pkcs11Body.set(pkcs11FileBody == null ? "" : pkcs11FileBody); } @@ -76,6 +95,15 @@ public void writeTo(AdvancedConfig cfg) { cfg.setProperty("relax.ssl.security", relaxSslSecurity.get()); cfg.setProperty("pdf2image.libraries", encodePdfLibraries()); cfg.setProperty("tsa.hashAlgorithm", tsaHashAlgorithm.get()); + + cfg.setProperty("engine.dss.online.enabled", dssOnlineEnabled.get()); + cfg.setProperty("engine.dss.trust.useDefaultLotl", dssUseDefaultLotl.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()); + writeStringOrRemove(cfg, "engine.dss.trust.truststoreFile", dssTruststoreFile.get()); + writeStringOrRemove(cfg, "engine.dss.trust.truststoreType", dssTruststoreType.get()); + writeStringOrRemove(cfg, "engine.dss.trust.truststorePassword", dssTruststorePassword.get()); } /** Resets every VM property to the bundled-default value (read from the given snapshot of bundled defaults). */ @@ -85,6 +113,7 @@ public void applyDefaults(AdvancedConfig defaults) { applyNetworkDefaults(defaults); applyPdfRenderDefaults(defaults); applyTsaDefaults(defaults); + applyDssDefaults(defaults); } public void applyFontDefaults(AdvancedConfig defaults) { @@ -111,6 +140,17 @@ public void applyTsaDefaults(AdvancedConfig defaults) { tsaHashAlgorithm.set(orFallback(defaults.getBundledDefault("tsa.hashAlgorithm"), "SHA-256")); } + 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)); + 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"))); + dssTruststoreFile.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.truststoreFile"))); + dssTruststoreType.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.truststoreType"))); + dssTruststorePassword.set(orEmpty(defaults.getBundledDefault("engine.dss.trust.truststorePassword"))); + } + public String encodePdfLibraries() { // Build (libname, order, enabled) tuples and emit the enabled ones in ascending order. record Lib(String name, int order, boolean enabled) {} @@ -206,6 +246,14 @@ private static void writeStringOrRemove(AdvancedConfig cfg, String key, String v public IntegerProperty pdfLibPdfboxOrderProperty() { return pdfLibPdfboxOrder; } public IntegerProperty pdfLibOpenpdfOrderProperty() { return pdfLibOpenpdfOrder; } public StringProperty tsaHashAlgorithmProperty() { return tsaHashAlgorithm; } + public BooleanProperty dssOnlineEnabledProperty() { return dssOnlineEnabled; } + public BooleanProperty dssUseDefaultLotlProperty() { return dssUseDefaultLotl; } + public StringProperty dssLotlUrlsProperty() { return dssLotlUrls; } + public StringProperty dssCertFilesProperty() { return dssCertFiles; } + public StringProperty dssCertUrlsProperty() { return dssCertUrls; } + public StringProperty dssTruststoreFileProperty() { return dssTruststoreFile; } + public StringProperty dssTruststoreTypeProperty() { return dssTruststoreType; } + public StringProperty dssTruststorePasswordProperty() { return dssTruststorePassword; } public StringProperty pkcs11BodyProperty() { return pkcs11Body; } /** Move the given library one slot up (order--), swapping with the lib currently at that order. */ 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 c3164ba8..dafd0a72 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 @@ -74,6 +74,7 @@ import net.sf.jsignpdf.fx.viewmodel.SignaturePlacementViewModel; import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; import net.sf.jsignpdf.fx.viewmodel.VisibleSignatureCoordinator; +import net.sf.jsignpdf.types.PadesLevel; import net.sf.jsignpdf.types.PageInfo; import static net.sf.jsignpdf.Constants.LOGGER; @@ -145,6 +146,8 @@ public class MainWindowController { @FXML private ToggleButton btnVisibleSig; @FXML private ToggleButton btnTsa; @FXML private ChoiceBox cmbEngine; + @FXML private Label lblPadesLevel; + @FXML private ChoiceBox cmbPadesLevel; @FXML private ComboBox cmbPresets; @FXML private Button btnSign; @@ -460,6 +463,7 @@ public SigningEngine fromString(String s) { // is safe. (With OpenPDF — the only engine in phase 1 — every capability is present, so nothing // is disabled; the wiring exists for reduced-capability engines added in phase 2.) engineCapabilities.gate(btnTsa, Capability.TSA); + setupPadesLevelSelector(); if (signatureAppearanceAccordionPane != null) { engineCapabilities.gate(signatureAppearanceAccordionPane, Capability.VISIBLE_SIGNATURE); } @@ -482,6 +486,35 @@ public SigningEngine fromString(String s) { // not leave enabled options the engine will reject at sign time. } + /** + * Populates and gates the PAdES-level dropdown. The control is bound to the signing ViewModel's + * {@code padesLevel} property and gated as a unit on {@link Capability#PADES_BASELINE_B}: an engine + * that cannot produce even B is not a PAdES engine, so the dropdown disables/greys for OpenPDF and + * enables for DSS. An empty selection means "engine default". + */ + private void setupPadesLevelSelector() { + if (cmbPadesLevel == null) { + return; + } + cmbPadesLevel.getItems().setAll(PadesLevel.values()); + cmbPadesLevel.setConverter(new StringConverter() { + @Override + public String toString(PadesLevel level) { + return level == null ? "" : level.shortName(); + } + + @Override + public PadesLevel fromString(String s) { + return PadesLevel.fromString(s); + } + }); + cmbPadesLevel.valueProperty().bindBidirectional(signingVM.padesLevelProperty()); + engineCapabilities.gate(cmbPadesLevel, Capability.PADES_BASELINE_B); + if (lblPadesLevel != null) { + engineCapabilities.gate(lblPadesLevel, Capability.PADES_BASELINE_B); + } + } + private void setupPresetCombo() { cmbPresets.setItems(presetManager.getPresets()); // JavaFX doesn't restore promptText in the button cell after clearSelection()+setValue(null); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java index 5e8be7de..8484d826 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java @@ -14,6 +14,7 @@ import net.sf.jsignpdf.Constants; import net.sf.jsignpdf.types.CertificationLevel; import net.sf.jsignpdf.types.HashAlgorithm; +import net.sf.jsignpdf.types.PadesLevel; import net.sf.jsignpdf.types.PDFEncryption; import net.sf.jsignpdf.types.PrintRight; import net.sf.jsignpdf.types.RenderMode; @@ -51,6 +52,8 @@ public class SigningOptionsViewModel { // Certification & hash private final ObjectProperty certLevel = new SimpleObjectProperty<>(); private final ObjectProperty hashAlgorithm = new SimpleObjectProperty<>(); + // PAdES baseline level (DSS engine); null = engine default + private final ObjectProperty padesLevel = new SimpleObjectProperty<>(); // Visible signature private final BooleanProperty visible = new SimpleBooleanProperty(false); @@ -127,6 +130,7 @@ public void syncToOptions(BasicSignerOptions opts) { opts.setContact(contact.get()); opts.setCertLevel(certLevel.get()); opts.setHashAlgorithm(hashAlgorithm.get()); + opts.setPadesLevel(padesLevel.get()); // Visible signature opts.setVisible(visible.get()); @@ -207,6 +211,7 @@ public void syncFromOptions(BasicSignerOptions opts) { contact.set(opts.getContact()); certLevel.set(opts.getCertLevelX()); hashAlgorithm.set(opts.getHashAlgorithmX()); + padesLevel.set(opts.getPadesLevel()); visible.set(opts.isVisible()); page.set(opts.getPage()); @@ -278,6 +283,7 @@ public void resetToDefaults() { contact.set(null); certLevel.set(CertificationLevel.NOT_CERTIFIED); hashAlgorithm.set(Constants.DEFVAL_HASH_ALGORITHM); + padesLevel.set(null); // Visible signature visible.set(false); @@ -381,6 +387,7 @@ private static String fromCharArray(char[] c) { public StringProperty contactProperty() { return contact; } public ObjectProperty certLevelProperty() { return certLevel; } public ObjectProperty hashAlgorithmProperty() { return hashAlgorithm; } + public ObjectProperty padesLevelProperty() { return padesLevel; } public BooleanProperty visibleProperty() { return visible; } public IntegerProperty pageProperty() { return page; } public FloatProperty positionLLXProperty() { return positionLLX; } diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml index 36bb94f7..6d61bf78 100644 --- a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml @@ -133,6 +133,12 @@ + + + com.github.kwart.jsign + jsignpdf-engine-dss + ${project.version} + + + + eu.europa.ec.joinup.sd-dss + dss-bom + ${dss.version} + pom + import + com.github.librepdf openpdf @@ -151,6 +165,8 @@ LGPL-2.1 MPL-2.0 + + BSD-3-Clause @@ -233,19 +249,28 @@ Apache-2.0 | - Apache License 2.0 + Apache License 2.0 | + Apache License, Version 2.0 | + The Apache License, Version 2.0 LGPL-2.1 | GNU Lesser General Public License (LGPL), Version 2.1 | Lesser General Public License (LGPL) | GNU Lesser General Public License, version 2.1 | - GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1 + GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1 | + GNU Lesser General Public License MPL-2.0 | Mozilla Public License Version 2.0 + + BSD-3-Clause | + EDL 1.0 | + Eclipse Distribution License - v 1.0 | + The BSD License + test,provided diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 0eb0c677..9752fac4 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -324,7 +324,7 @@ usage: jsignpdf [file1.pdf [file2.pdf ...]] [-a] [--bg-path [-lkt] [-llx ] [-lly ] [-lp] [-lpf ] [--ocsp] [--ocsp-server-url ] [-op ] [-opwd ] [-os ] [-pe ] [-pg ] [-pr ] [--proxy-host - ] [--proxy-port ] [--proxy-type ] [-q] [-r + ] [--proxy-port ] [-pl ] [--proxy-type ] [-q] [-r ] [--render-mode ] [-sn ] [-ta ] [-ts ] [--tsa-policy-oid ] [-tscf ] [-tscp ] [-tsct ] [-tsh ] [-tsp ] [-tsu ] [-upwd @@ -415,6 +415,9 @@ See <> for details. | `-cl, --certification-level ` | Certification level. Default: `NOT_CERTIFIED`. Values: `NOT_CERTIFIED`, `CERTIFIED_NO_CHANGES_ALLOWED`, `CERTIFIED_FORM_FILLING`, `CERTIFIED_FORM_FILLING_AND_ANNOTATIONS`. +| `-pl, --pades-level ` +| PAdES baseline level for the PAdES (`dss`) signing engine. Values (case-insensitive): `B`, `T`, `LT`, `LTA`. Honoured only by engines that support PAdES; the default OpenPDF engine rejects it (use `-eng dss`). `LT`/`LTA` require revocation data — see <>. See <>. + | `-sn, --signer-name ` | Signer name. Defaults to the common name (CN) of the chosen certificate. @@ -933,6 +936,65 @@ In the JavaFX UI, the active engine is chosen from the *Engine* selector in the Engines are discovered automatically: dropping a third-party engine jar (with its dependencies and a `META-INF/services/net.sf.jsignpdf.engine.SigningEngine` registration) into the `lib/` directory of an installed JSignPdf makes it appear in `--list-engines` and the toolbar selector. +=== PAdES & the DSS engine + +JSignPdf 3.1 bundles a second engine, *EU DSS (PAdES)* (id `dss`), built on the European Commission's https://github.com/esig/dss[Digital Signature Service] library. It produces *PAdES* signatures (`ETSI.CAdES.detached`) at the four ETSI baseline levels, which the OpenPDF engine cannot create: + +[cols="1,3"] +|=== +|Level |What it adds + +|`B` +|Basic signature (the default when `dss` is selected without a level). + +|`T` +|`B` + a signature timestamp from the configured <>. Setting a TSA while requesting `B` automatically upgrades to `T`. + +|`LT` +|`T` + embedded validation material (certificates and OCSP/CRL responses) for long-term validation. + +|`LTA` +|`LT` + an archive timestamp. +|=== + +Select it with `-eng dss` on the command line (or the *Engine* toolbar selector in the GUI) and pick the level with `-pl` / `--pades-level`: + +[source,shell] +---- +jsignpdf -eng dss -pl LT -ksf cert.p12 -ksp secret -ha SHA256 document.pdf +---- + +DSS requires a PAdES digest, so the hash algorithm must be `SHA256`, `SHA384` or `SHA512` (`SHA1`/`RIPEMD160` are rejected). Certificate-based PDF encryption and CloudFoxy external signing are not available with `dss`; use the OpenPDF engine for those. + +*Trust material for LT/LTA.* The `B` and `T` levels work fully offline (only `T` needs network access to the TSA). The `LT` and `LTA` levels embed revocation data, so they need reachable OCSP/CRL endpoints and a trust anchor set. These are configured with `engine.dss.*` keys in `advanced.properties` (also editable from the *DSS engine* tab of the Preferences dialog): + +[cols="2,3"] +|=== +|Key |Meaning + +|`engine.dss.online.enabled` +|Fetch revocation data (OCSP/CRL) and intermediate certificates (AIA) online. Must be `true` to produce `LT`/`LTA`. Default `false` (keeps `B`/`T` offline). + +|`engine.dss.trust.useDefaultLotl` +|Build trust anchors from the default EU List of Trusted Lists (LOTL). + +|`engine.dss.trust.lotlUrls` +|Comma-separated custom LOTL URLs. + +|`engine.dss.trust.certFiles` +|Comma-separated trusted X.509 certificate files. + +|`engine.dss.trust.certUrls` +|Comma-separated trusted X.509 certificate URLs. + +|`engine.dss.trust.truststoreFile` + +`engine.dss.trust.truststoreType` + +`engine.dss.trust.truststorePassword` +|A truststore holding trust anchors (type defaults to the JVM default). +|=== + +If `LT`/`LTA` is requested but revocation data cannot be reached (for example, with online fetching disabled), the signing fails with a logged error rather than silently emitting a weaker level. + == Advanced application configuration Application-wide tweaks beyond the per-document signing options live in `/advanced.properties` and -- for hardware tokens -- `/pkcs11.cfg`. Both are plain text files; the easiest way to edit them is the JavaFX <>, which writes them on _OK_. Power users can also hand-edit the files directly while JSignPdf is closed; the next launch picks up the changes. From 76cb781075f300230ef897016498fcdf86729c37 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 09:31:41 +0200 Subject: [PATCH 04/18] Remove stray XML tags from end of DSS engine design doc Co-Authored-By: Claude Opus 4.8 --- design-doc/3.1-engine-dss.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/design-doc/3.1-engine-dss.md b/design-doc/3.1-engine-dss.md index 31607d10..96cb7f1b 100644 --- a/design-doc/3.1-engine-dss.md +++ b/design-doc/3.1-engine-dss.md @@ -543,5 +543,3 @@ Per `AGENTS.md` ("not done until it lands in the docs"): - CloudFoxy / external signing through DSS. - A PDFBox *signing* engine (`jsignpdf-engine-pdfbox`). - LTA refresh / re-timestamping of already-signed documents. - - From 459a41710882fdd2c9684f85b9c56b7d256c3884 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 09:39:53 +0200 Subject: [PATCH 05/18] Address DSS engine review comments: proxy, LT/LTA TSA guard, TSA port - Wire JSignPdf's HTTP proxy into the DSS TSP/OCSP/CRL/AIA data loaders so PROXY_SUPPORT is honoured; SOCKS is reported as unsupported instead of silently bypassing the proxy. - Fail fast with a specific message when level LT/LTA is requested without a TSA (LT/LTA build on a signature timestamp), instead of failing deep in DSS. - Resolve the default port (443/80) from the URI scheme when the TSA URL omits an explicit port, so basic-auth credentials match the real connection. - Document that the encrypt-before-sign 128-bit key length matches the OpenPDF engine's password encryption (no strength downgrade). Co-Authored-By: Claude Opus 4.8 --- .../jsignpdf/translations/messages.properties | 2 + .../jsignpdf/engine/dss/DssSigningEngine.java | 74 +++++++++++++++++-- .../engine/dss/DssTrustConfigurer.java | 16 +++- .../engine/dss/DssSigningEngineTest.java | 17 ++++- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 3708c3f3..8d23a598 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -36,6 +36,8 @@ console.engineNotFound=Unknown signing engine ''{0}''. Use --list-engines to see console.engines=Available signing engines: console.dss.tsaUpgrade=A TSA is configured, upgrading the PAdES level B to T (so the signature timestamp is kept). console.dss.ltNoRevocation=The PAdES level LT/LTA needs revocation data, but online fetching is disabled (engine.dss.online.enabled=false) or no trust material is reachable. Signing aborted. +console.dss.ltNoTsa=The PAdES level LT/LTA builds on a signature timestamp, but no TSA is configured. Enable timestamping (-ts / TSA settings) or choose level B. Signing aborted. +console.dss.socksProxyUnsupported=The DSS engine does not support SOCKS proxies; OCSP/CRL/AIA/TSA requests will bypass the proxy. Configure an HTTP proxy to route DSS revocation/timestamp traffic. console.dss.unsupportedHash=The DSS engine does not support the hash algorithm ''{0}'' for PAdES. Use SHA-256, SHA-384 or SHA-512. console.dss.cannotEncryptSigned=Cannot encrypt a PDF that already contains signatures. console.getPrivateKey=Loading private key 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 552e616c..b4976938 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 @@ -11,6 +11,7 @@ import java.io.File; import java.io.FileOutputStream; +import java.net.Proxy; import java.net.URI; import java.security.PrivateKey; import java.security.cert.Certificate; @@ -63,6 +64,8 @@ import eu.europa.esig.dss.pades.SignatureImageTextParameters; import eu.europa.esig.dss.pades.signature.PAdESService; import eu.europa.esig.dss.service.http.commons.TimestampDataLoader; +import eu.europa.esig.dss.service.http.proxy.ProxyConfig; +import eu.europa.esig.dss.service.http.proxy.ProxyProperties; import eu.europa.esig.dss.service.tsp.OnlineTSPSource; import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier; @@ -215,16 +218,25 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC final DssTrustConfigurer trustConfigurer = new DssTrustConfigurer(engineConfig); final boolean ltOrLta = parameters.getSignatureLevel() == SignatureLevel.PAdES_BASELINE_LT || parameters.getSignatureLevel() == SignatureLevel.PAdES_BASELINE_LTA; - if (ltOrLta && !trustConfigurer.isOnlineEnabled()) { - LOGGER.severe(RES.get("console.dss.ltNoRevocation")); - return false; + if (ltOrLta) { + // LT/LTA build on a signature timestamp (level T); without a TSA, DSS would fail deep in + // signDocument(). Fail fast here with a clear message instead. + if (!useTsa) { + LOGGER.severe(RES.get("console.dss.ltNoTsa")); + return false; + } + if (!trustConfigurer.isOnlineEnabled()) { + LOGGER.severe(RES.get("console.dss.ltNoRevocation")); + return false; + } } - final CommonCertificateVerifier verifier = trustConfigurer.buildVerifier(); + final ProxyConfig proxyConfig = buildProxyConfig(options); + final CommonCertificateVerifier verifier = trustConfigurer.buildVerifier(proxyConfig); final PAdESService service = new PAdESService(verifier); if (useTsa) { LOGGER.info(RES.get("console.creatingTsaClient")); - service.setTspSource(buildTspSource(options, parameters, digestAlgorithm)); + service.setTspSource(buildTspSource(options, parameters, digestAlgorithm, proxyConfig)); } LOGGER.info(RES.get("console.processing")); @@ -253,12 +265,13 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC } private OnlineTSPSource buildTspSource(BasicSignerOptions options, PAdESSignatureParameters parameters, - DigestAlgorithm digestAlgorithm) { + DigestAlgorithm digestAlgorithm, ProxyConfig proxyConfig) { final String tsaUrl = options.getTsaUrl(); final TimestampDataLoader tsDataLoader = new TimestampDataLoader(); + tsDataLoader.setProxyConfig(proxyConfig); if (options.getTsaServerAuthn() == ServerAuthentication.PASSWORD) { final URI tsaUri = URI.create(tsaUrl); - tsDataLoader.addAuthentication(tsaUri.getHost(), tsaUri.getPort(), null, + tsDataLoader.addAuthentication(tsaUri.getHost(), resolvePort(tsaUri), null, StringUtils.defaultString(options.getTsaUser()), StringUtils.defaultString(options.getTsaPasswd()).toCharArray()); } @@ -276,6 +289,51 @@ private OnlineTSPSource buildTspSource(BasicSignerOptions options, PAdESSignatur return tspSource; } + /** Resolves the port for basic-auth registration, defaulting from the scheme when none is given. */ + private static int resolvePort(URI uri) { + final int port = uri.getPort(); + if (port >= 0) { + return port; + } + return "https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80; + } + + /** + * Translates JSignPdf's proxy settings into a DSS {@link ProxyConfig} so OCSP / CRL / AIA / TSA + * traffic honours the configured proxy (mirroring the OpenPDF engine's {@code createProxy()} use). + * DSS routes only HTTP-style proxies; SOCKS is unsupported, so it is reported rather than silently + * ignored. + * + * @return the proxy configuration, or {@code null} for a direct connection + */ + private static ProxyConfig buildProxyConfig(BasicSignerOptions options) { + if (!options.isAdvanced() || options.getProxyType() == Proxy.Type.DIRECT) { + return null; + } + if (options.getProxyType() == Proxy.Type.SOCKS) { + LOGGER.warning(RES.get("console.dss.socksProxyUnsupported")); + return null; + } + final String host = options.getProxyHost(); + if (StringUtils.isBlank(host)) { + return null; + } + final int port = options.getProxyPort(); + final ProxyConfig proxyConfig = new ProxyConfig(); + proxyConfig.setHttpProperties(proxyProperties("http", host, port)); + // An HTTP proxy is reached over http even for https targets (CONNECT tunnelling), hence "http". + proxyConfig.setHttpsProperties(proxyProperties("http", host, port)); + return proxyConfig; + } + + private static ProxyProperties proxyProperties(String scheme, String host, int port) { + final ProxyProperties props = new ProxyProperties(); + props.setScheme(scheme); + props.setHost(host); + props.setPort(port); + return props; + } + private File encryptPdf(File inFile, BasicSignerOptions options) throws Exception { try (PDDocument doc = Loader.loadPDF(inFile)) { if (!doc.getSignatureDictionaries().isEmpty()) { @@ -286,6 +344,8 @@ private File encryptPdf(File inFile, BasicSignerOptions options) throws Exceptio final String encOwnerPwd = StringUtils.defaultString(options.getPdfOwnerPwdStrX()); final String encUserPwd = StringUtils.defaultString(options.getPdfUserPwdStr()); final StandardProtectionPolicy policy = new StandardProtectionPolicy(encOwnerPwd, encUserPwd, ap); + // 128-bit matches the OpenPDF engine's password encryption (PdfStamper.setEncryption(true, ...) + // i.e. STANDARD_ENCRYPTION_128), so switching engines is not an encryption-strength downgrade. policy.setEncryptionKeyLength(128); doc.protect(policy); 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 067e65ea..68addd90 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 @@ -17,6 +17,8 @@ import eu.europa.esig.dss.service.crl.OnlineCRLSource; import eu.europa.esig.dss.service.http.commons.CommonsDataLoader; import eu.europa.esig.dss.service.http.commons.FileCacheDataLoader; +import eu.europa.esig.dss.service.http.commons.OCSPDataLoader; +import eu.europa.esig.dss.service.http.proxy.ProxyConfig; import eu.europa.esig.dss.service.ocsp.OnlineOCSPSource; import eu.europa.esig.dss.spi.DSSUtils; import eu.europa.esig.dss.spi.tsl.TrustedListsCertificateSource; @@ -74,9 +76,11 @@ boolean isOnlineEnabled() { * Builds a verifier configured with the trusted certificate sources and (when online is enabled) the * AIA / OCSP / CRL online sources required to embed validation material for LT/LTA. * + * @param proxyConfig the HTTP proxy configuration to route AIA / OCSP / CRL traffic through, or + * {@code null} for a direct connection * @return the configured certificate verifier */ - CommonCertificateVerifier buildVerifier() { + CommonCertificateVerifier buildVerifier(ProxyConfig proxyConfig) { CommonCertificateVerifier verifier = new CommonCertificateVerifier(); try { CertificateSource[] trustedSources = createTrustedCertSources(); @@ -87,9 +91,13 @@ CommonCertificateVerifier buildVerifier() { LOGGER.log(Level.WARNING, "Failed to configure DSS trusted certificate sources", e); } if (isOnlineEnabled()) { - verifier.setAIASource(new DefaultAIASource()); - verifier.setOcspSource(new OnlineOCSPSource()); - verifier.setCrlSource(new OnlineCRLSource()); + OCSPDataLoader ocspDataLoader = new OCSPDataLoader(); + ocspDataLoader.setProxyConfig(proxyConfig); + CommonsDataLoader dataLoader = new CommonsDataLoader(); + dataLoader.setProxyConfig(proxyConfig); + verifier.setAIASource(new DefaultAIASource(dataLoader)); + verifier.setOcspSource(new OnlineOCSPSource(ocspDataLoader)); + verifier.setCrlSource(new OnlineCRLSource(dataLoader)); } return verifier; } diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java index 33875845..d44476d8 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -117,10 +117,23 @@ public void visibleSignatureIsPlacedAndSigned() throws Exception { } @Test - public void ltWithoutOnlineFetchingFails() throws Exception { + public void ltWithoutTsaFails() throws Exception { BasicSignerOptions o = baseOptions(); o.setPadesLevel(PadesLevel.BASELINE_LT); - // online fetching disabled -> the engine must refuse rather than emit a weaker level + // LT/LTA build on a signature timestamp; with no TSA the engine must fail fast rather than + // dropping to a weaker level or blowing up deep inside DSS. + boolean ok = new DssSigningEngine().sign(o, EMPTY_CONFIG); + assertFalse("LT without a TSA must fail", ok); + } + + @Test + public void ltWithTsaButOfflineFails() throws Exception { + BasicSignerOptions o = baseOptions(); + o.setPadesLevel(PadesLevel.BASELINE_LT); + o.setTimestamp(true); + o.setTsaUrl("http://tsa.example.com/tsr"); + // A TSA is configured but online revocation fetching is disabled (empty config) -> the engine + // must refuse rather than emit a weaker level. The guard fires before any network access. boolean ok = new DssSigningEngine().sign(o, EMPTY_CONFIG); assertFalse("LT without revocation data must fail", ok); } From df88b4099b530f85d248c7b60e4f7bf148695ca7 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 09:49:06 +0200 Subject: [PATCH 06/18] Harden DSS font loading and verify achieved PAdES level in tests - DssFontUtils: materialise the font bytes into an InMemoryDocument while the stream is open, instead of handing DSS the stream that this method closes on return (DSSFileFont re-reads the font lazily in getJavaFont()/getInputStream()). - DssSigningEngineTest: assert the achieved baseline level via DSS's own validator (SimpleReport.getSignatureFormat) rather than only the subfilter, which is identical for B/T/LT/LTA. Assert visible-signature placement (page + rectangle). Add dss-validation / dss-policy-jaxb as test-scoped deps. Co-Authored-By: Claude Opus 4.8 --- engines/dss/pom.xml | 13 +++++ .../sf/jsignpdf/engine/dss/DssFontUtils.java | 7 ++- .../engine/dss/DssSigningEngineTest.java | 53 ++++++++++++++++--- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/engines/dss/pom.xml b/engines/dss/pom.xml index c3f29a12..428aab0f 100644 --- a/engines/dss/pom.xml +++ b/engines/dss/pom.xml @@ -59,5 +59,18 @@ junit test + + + + eu.europa.ec.joinup.sd-dss + dss-validation + test + + + eu.europa.ec.joinup.sd-dss + dss-policy-jaxb + test + diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java index d06b2c27..d22eca0e 100644 --- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils; +import eu.europa.esig.dss.model.InMemoryDocument; import eu.europa.esig.dss.pades.DSSFileFont; import eu.europa.esig.dss.pades.DSSFont; @@ -37,7 +38,11 @@ static DSSFont getVisibleSignatureFont() { try (InputStream is = fontPath != null ? new FileInputStream(fontPath) : DssFontUtils.class.getResourceAsStream(DEFAULT_EMBEDDED_FONT_PATH)) { if (is != null) { - return new DSSFileFont(is); + // Materialise the font bytes here, while the stream is open. DSSFileFont later re-reads + // the font lazily (getJavaFont()/getInputStream()), so it must own a self-contained, + // in-memory copy rather than the stream this method closes on return. (DSSFileFont(stream) + // happens to buffer eagerly today, but we don't want to depend on that internal detail.) + return new DSSFileFont(new InMemoryDocument(is.readAllBytes())); } } catch (Exception e) { Constants.LOGGER.log(Level.SEVERE, "Font loading failed" + (StringUtils.isNotEmpty(fontPath) ? ": " + fontPath : ""), diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java index d44476d8..539be6cd 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -7,6 +7,7 @@ import java.io.File; import java.security.Security; import java.util.HashMap; +import java.util.List; import java.util.Map; import net.sf.jsignpdf.BasicSignerOptions; @@ -18,9 +19,12 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Before; import org.junit.BeforeClass; @@ -28,6 +32,13 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; +import eu.europa.esig.dss.enumerations.SignatureLevel; +import eu.europa.esig.dss.model.FileDocument; +import eu.europa.esig.dss.simplereport.SimpleReport; +import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier; +import eu.europa.esig.dss.validation.SignedDocumentValidator; +import eu.europa.esig.dss.validation.reports.Reports; + /** * Signing tests for {@link DssSigningEngine}, adapted from the {@code jsignpdf-pades} signing suite to * drive the engine through the JSignPdf {@link BasicSignerOptions} model. Output is validated @@ -89,31 +100,47 @@ private BasicSignerOptions baseOptions() { } @Test - public void defaultLevelProducesPadesSignature() throws Exception { + public void defaultLevelProducesBaselineB() throws Exception { BasicSignerOptions o = baseOptions(); // padesLevel == null -> BASELINE_B boolean ok = new DssSigningEngine().sign(o, EMPTY_CONFIG); assertTrue("signing should succeed", ok); assertTrue("output should exist", outputFile.exists()); - assertPadesSignature(outputFile); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); } @Test - public void explicitBaselineBProducesPadesSignature() throws Exception { + public void explicitBaselineBProducesBaselineB() throws Exception { BasicSignerOptions o = baseOptions(); o.setPadesLevel(PadesLevel.BASELINE_B); assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); - assertPadesSignature(outputFile); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); } @Test - public void visibleSignatureIsPlacedAndSigned() throws Exception { + public void visibleSignatureIsPlacedOnRequestedPage() throws Exception { BasicSignerOptions o = baseOptions(); o.setVisible(true); o.setPage(1); + o.setPositionLLX(100f); + o.setPositionLLY(100f); + o.setPositionURX(300f); + o.setPositionURY(250f); o.setReason("testing"); o.setLocation("here"); assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); - assertPadesSignature(outputFile); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); + + try (PDDocument doc = Loader.loadPDF(outputFile)) { + List fields = doc.getSignatureFields(); + assertEquals("exactly one signature field expected", 1, fields.size()); + PDAnnotationWidget widget = fields.get(0).getWidgets().get(0); + assertTrue("signature widget must sit on the requested (first) page", + doc.getPage(0).getAnnotations().contains(widget)); + PDRectangle rect = widget.getRectangle(); + // The visible signature spans the requested box (URX-LLX x URY-LLY = 200 x 150). + assertEquals("visible signature width", 200f, rect.getWidth(), 1f); + assertEquals("visible signature height", 150f, rect.getHeight(), 1f); + } } @Test @@ -149,12 +176,24 @@ public void capabilitiesAreStaticAndDeclarePades() { assertFalse("PAdES disallows SHA-1", engine.capabilities().contains(Capability.HASH_SHA1)); } - private static void assertPadesSignature(File pdf) throws Exception { + /** + * Asserts both the PAdES subfilter (structural) and the achieved baseline level. The level is + * read back through DSS's own validator, since the subfilter alone ({@code ETSI.CAdES.detached}) is + * identical for B / T / LT / LTA and so cannot distinguish them. + */ + private static void assertSignatureLevel(File pdf, SignatureLevel expected) throws Exception { try (PDDocument doc = Loader.loadPDF(pdf)) { assertFalse("a signature must be present", doc.getSignatureDictionaries().isEmpty()); PDSignature sig = doc.getSignatureDictionaries().get(0); assertEquals("ETSI.CAdES.detached", sig.getSubFilter()); } + SignedDocumentValidator validator = SignedDocumentValidator.fromDocument(new FileDocument(pdf)); + validator.setCertificateVerifier(new CommonCertificateVerifier()); + Reports reports = validator.validateDocument(); + SimpleReport simpleReport = reports.getSimpleReport(); + assertEquals("exactly one signature expected", 1, simpleReport.getSignaturesCount()); + assertEquals("achieved PAdES baseline level", expected, + simpleReport.getSignatureFormat(simpleReport.getFirstSignatureId())); } /** Minimal in-memory {@link EngineConfig} backed by a map (keys already engine-relative). */ From c335d861a6a864464ca1a885c13cf1498a4a806e Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 09:57:12 +0200 Subject: [PATCH 07/18] Add offline level-T DSS signing tests via an embedded TSA server Port jsignpdf-pades' EmbeddedTsaServer (an in-JVM RFC 3161 TSA on a loopback port, backed by a self-signed timestamping cert and BouncyCastle TSP) into the engine-dss test suite, so level T can be produced and asserted without external network. Adds: - baselineBWithTsaUpgradesToT: B + TSA auto-upgrades to and validates as T - explicitBaselineTWithTsaProducesT: explicit T validates as T This closes the level-T verification gap; LT/LTA success still needs reachable revocation/AIA and stays out of the unit suite (the reference project covers LT/LTA only as no-trust failure cases too). Co-Authored-By: Claude Opus 4.8 --- .../engine/dss/DssSigningEngineTest.java | 40 ++++- .../engine/dss/EmbeddedTsaServer.java | 168 ++++++++++++++++++ 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedTsaServer.java diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java index 539be6cd..31878c4d 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -26,6 +26,7 @@ import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; @@ -53,6 +54,9 @@ public class DssSigningEngineTest { private static final EngineConfig EMPTY_CONFIG = new MapEngineConfig(new HashMap<>()); + /** In-JVM RFC 3161 TSA on a loopback port, so level-T signatures can be produced offline. */ + private static EmbeddedTsaServer tsaServer; + @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -60,8 +64,18 @@ public class DssSigningEngineTest { private File outputFile; @BeforeClass - public static void registerBc() { + public static void setUpClass() throws Exception { + // BC must be registered before the embedded TSA generates its signing material. Security.addProvider(new BouncyCastleProvider()); + tsaServer = new EmbeddedTsaServer(); + tsaServer.start(); + } + + @AfterClass + public static void stopTsa() { + if (tsaServer != null) { + tsaServer.stop(); + } } @Before @@ -99,6 +113,13 @@ private BasicSignerOptions baseOptions() { return o; } + /** Points the options at the embedded loopback TSA (no auth), so signing reaches baseline level T. */ + private void useEmbeddedTsa(BasicSignerOptions o) { + o.setTimestamp(true); + o.setTsaUrl(tsaServer.getUrl()); + o.setTsaHashAlg("SHA-256"); + } + @Test public void defaultLevelProducesBaselineB() throws Exception { BasicSignerOptions o = baseOptions(); // padesLevel == null -> BASELINE_B @@ -116,6 +137,23 @@ public void explicitBaselineBProducesBaselineB() throws Exception { assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); } + @Test + public void baselineBWithTsaUpgradesToT() throws Exception { + BasicSignerOptions o = baseOptions(); // padesLevel == null -> BASELINE_B, auto-upgraded to T by the TSA + useEmbeddedTsa(o); + assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_T); + } + + @Test + public void explicitBaselineTWithTsaProducesT() throws Exception { + BasicSignerOptions o = baseOptions(); + o.setPadesLevel(PadesLevel.BASELINE_T); + useEmbeddedTsa(o); + assertTrue(new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_T); + } + @Test public void visibleSignatureIsPlacedOnRequestedPage() throws Exception { BasicSignerOptions o = baseOptions(); diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedTsaServer.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedTsaServer.java new file mode 100644 index 00000000..6254cc33 --- /dev/null +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedTsaServer.java @@ -0,0 +1,168 @@ +package net.sf.jsignpdf.engine.dss; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.atomic.AtomicLong; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.tsp.TSPAlgorithms; +import org.bouncycastle.tsp.TimeStampRequest; +import org.bouncycastle.tsp.TimeStampResponse; +import org.bouncycastle.tsp.TimeStampResponseGenerator; +import org.bouncycastle.tsp.TimeStampTokenGenerator; + +import com.sun.net.httpserver.BasicAuthenticator; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +/** + * Embedded RFC 3161 TSA server for the DSS engine tests, ported from the {@code jsignpdf-pades} + * test suite. It runs in-JVM on a random loopback port (no external network), using a self-signed + * timestamping certificate generated on the fly and BouncyCastle TSP to produce timestamp responses, + * so level-T signatures can be produced and asserted deterministically. + */ +final class EmbeddedTsaServer { + + private static final String TSA_POLICY_OID = "1.2.3.4.5"; + + private HttpServer httpServer; + private PrivateKey tsaPrivateKey; + private X509Certificate tsaCertificate; + private final AtomicLong serialCounter = new AtomicLong(1); + private String requiredUsername; + private String requiredPassword; + + /** + * Configures the server to require HTTP Basic authentication. Must be called before {@link #start()}. + */ + void requireBasicAuth(String username, String password) { + this.requiredUsername = username; + this.requiredPassword = password; + } + + /** + * Generates a self-signed RSA key pair and certificate suitable for timestamping, and starts an HTTP + * server on a random available loopback port. + */ + void start() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + tsaPrivateKey = keyPair.getPrivate(); + + // Self-signed certificate with the id-kp-timeStamping extended key usage. + X500Name subject = new X500Name("CN=Test TSA, O=JSignPdf Test"); + BigInteger serial = BigInteger.valueOf(1); + Date notBefore = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000L); + Date notAfter = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000L); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, serial, notBefore, notAfter, subject, keyPair.getPublic()); + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); + certBuilder.addExtension(Extension.extendedKeyUsage, true, + new ExtendedKeyUsage(KeyPurposeId.id_kp_timeStamping)); + + X509CertificateHolder certHolder = certBuilder.build( + new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(tsaPrivateKey)); + tsaCertificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + + httpServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + HttpContext context = httpServer.createContext("/tsa", new TsaHandler()); + if (requiredUsername != null) { + context.setAuthenticator(new BasicAuthenticator("tsa") { + @Override + public boolean checkCredentials(String username, String password) { + return requiredUsername.equals(username) && requiredPassword.equals(password); + } + }); + } + httpServer.start(); + } + + /** Stops the HTTP server. */ + void stop() { + if (httpServer != null) { + httpServer.stop(0); + } + } + + /** @return the URL of the running TSA server. */ + String getUrl() { + int port = httpServer.getAddress().getPort(); + return "http://127.0.0.1:" + port + "/tsa"; + } + + /** HTTP handler that processes RFC 3161 timestamp requests. */ + private final class TsaHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(405, -1); + return; + } + + final byte[] requestBytes; + try (InputStream is = exchange.getRequestBody()) { + requestBytes = is.readAllBytes(); + } + + TimeStampRequest tsRequest = new TimeStampRequest(requestBytes); + + // SHA-1 digest calculator, used internally by the token generator for serial-number hashing. + DigestCalculator digestCalculator = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build() + .get(new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.3.14.3.2.26"))); + + TimeStampTokenGenerator tokenGenerator = new TimeStampTokenGenerator( + new JcaSimpleSignerInfoGeneratorBuilder() + .setProvider("BC") + .build("SHA256withRSA", tsaPrivateKey, tsaCertificate), + digestCalculator, + new ASN1ObjectIdentifier(TSA_POLICY_OID)); + + tokenGenerator.addCertificates(new JcaCertStore(Collections.singletonList(tsaCertificate))); + + TimeStampResponseGenerator responseGenerator = new TimeStampResponseGenerator( + tokenGenerator, TSPAlgorithms.ALLOWED); + + BigInteger serialNumber = BigInteger.valueOf(serialCounter.getAndIncrement()); + TimeStampResponse tsResponse = responseGenerator.generate(tsRequest, serialNumber, new Date()); + + byte[] responseBytes = tsResponse.getEncoded(); + exchange.getResponseHeaders().set("Content-Type", "application/timestamp-reply"); + exchange.sendResponseHeaders(200, responseBytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(responseBytes); + } + } catch (Exception e) { + exchange.sendResponseHeaders(500, -1); + } + } + } +} From 8df522bd48afd1db4c43c6607826f766bffe9ada Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 10:06:05 +0200 Subject: [PATCH 08/18] Address review: fail fast on bad DSS trust config, fix visible-image fallback, declare test deps - DssTrustConfigurer.buildVerifier now propagates trust-source load failures instead of swallowing them (WARNING + continue). DssSigningEngine catches and fails fast with the new console.dss.trustConfigFailed message, so a misconfigured truststore/cert/LOTL no longer signs without the intended trust anchors and surfaces an opaque DSS error later. - Visible signature: stop silently using the background image as the signature graphic in GRAPHIC_AND_DESCRIPTION mode when no graphic is configured. DSS renders a single image; graphic vs background are now cleanly mutually exclusive (graphic mode w/o graphic -> text only). - engines/dss now declares pdfbox (compile; used directly by the engine) and bouncycastle (test; BC provider + embedded TSA) explicitly instead of relying on transitive compile-scope deps. Co-Authored-By: Claude Opus 4.8 --- .../jsignpdf/translations/messages.properties | 1 + engines/dss/pom.xml | 19 +++++++++++++++++ .../jsignpdf/engine/dss/DssSigningEngine.java | 21 +++++++++++++------ .../engine/dss/DssTrustConfigurer.java | 17 ++++++--------- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 8d23a598..89742193 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -37,6 +37,7 @@ console.engines=Available signing engines: console.dss.tsaUpgrade=A TSA is configured, upgrading the PAdES level B to T (so the signature timestamp is kept). console.dss.ltNoRevocation=The PAdES level LT/LTA needs revocation data, but online fetching is disabled (engine.dss.online.enabled=false) or no trust material is reachable. Signing aborted. console.dss.ltNoTsa=The PAdES level LT/LTA builds on a signature timestamp, but no TSA is configured. Enable timestamping (-ts / TSA settings) or choose level B. Signing aborted. +console.dss.trustConfigFailed=Failed to load the configured DSS trust material (truststore / certificate file / certificate URL / LOTL). Check the engine.dss.trust.* settings. Signing aborted. console.dss.socksProxyUnsupported=The DSS engine does not support SOCKS proxies; OCSP/CRL/AIA/TSA requests will bypass the proxy. Configure an HTTP proxy to route DSS revocation/timestamp traffic. console.dss.unsupportedHash=The DSS engine does not support the hash algorithm ''{0}'' for PAdES. Use SHA-256, SHA-384 or SHA-512. console.dss.cannotEncryptSigned=Cannot encrypt a PDF that already contains signatures. diff --git a/engines/dss/pom.xml b/engines/dss/pom.xml index 428aab0f..38ea66a9 100644 --- a/engines/dss/pom.xml +++ b/engines/dss/pom.xml @@ -54,12 +54,31 @@ commons-lang3 + + + org.apache.pdfbox + pdfbox + + junit junit test + + + org.bouncycastle + bcprov-jdk18on + test + + + org.bouncycastle + bcpkix-jdk18on + 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 b4976938..52da8bd7 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 @@ -231,7 +231,16 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC } } final ProxyConfig proxyConfig = buildProxyConfig(options); - final CommonCertificateVerifier verifier = trustConfigurer.buildVerifier(proxyConfig); + final CommonCertificateVerifier verifier; + try { + verifier = trustConfigurer.buildVerifier(proxyConfig); + } catch (Exception e) { + // A configured trust source could not be loaded (bad truststore path/password, unreadable + // cert file/URL, ...). Fail fast with a clear message rather than signing without the + // intended trust anchors and surfacing an opaque DSS error later. + LOGGER.log(Level.SEVERE, RES.get("console.dss.trustConfigFailed"), e); + return false; + } final PAdESService service = new PAdESService(verifier); if (useTsa) { @@ -414,11 +423,11 @@ private void configureVisibleSignature(PAdESSignatureParameters parameters, Basi final RenderMode renderMode = options.getRenderMode(); final boolean withGraphic = renderMode == RenderMode.GRAPHIC_AND_DESCRIPTION; - // Image: the signature graphic (render mode GRAPHIC_AND_DESCRIPTION) takes priority; otherwise a - // background image, if configured. - final String graphicPath = options.getImgPath(); - final String bgImgPath = options.getBgImgPath(); - final String imagePath = (withGraphic && graphicPath != null) ? graphicPath : bgImgPath; + // DSS renders a single signature image. In GRAPHIC_AND_DESCRIPTION mode that image is the signature + // graphic (options.imgPath); in the other render modes it is the background image (options.bgImgPath). + // The two are mutually exclusive here (unlike OpenPDF, which layers them), and we never silently + // substitute one for the other: a graphic render mode with no graphic configured yields text only. + final String imagePath = withGraphic ? options.getImgPath() : options.getBgImgPath(); if (imagePath != null) { LOGGER.info(RES.get("console.createImage", imagePath)); imageParams.setImage(new FileDocument(imagePath)); 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 68addd90..21da5c38 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 @@ -1,14 +1,11 @@ package net.sf.jsignpdf.engine.dss; -import static net.sf.jsignpdf.Constants.LOGGER; - import java.io.File; import java.io.InputStream; import java.net.URL; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; -import java.util.logging.Level; import net.sf.jsignpdf.engine.EngineConfig; @@ -79,16 +76,14 @@ boolean isOnlineEnabled() { * @param proxyConfig the HTTP proxy configuration to route AIA / OCSP / CRL traffic through, or * {@code null} for a direct connection * @return the configured certificate verifier + * @throws Exception if a configured trust source (truststore / cert file / cert URL / LOTL) cannot be + * loaded; the caller fails fast rather than signing without the intended trust anchors */ - CommonCertificateVerifier buildVerifier(ProxyConfig proxyConfig) { + CommonCertificateVerifier buildVerifier(ProxyConfig proxyConfig) throws Exception { CommonCertificateVerifier verifier = new CommonCertificateVerifier(); - try { - CertificateSource[] trustedSources = createTrustedCertSources(); - if (trustedSources.length > 0) { - verifier.setTrustedCertSources(trustedSources); - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to configure DSS trusted certificate sources", e); + CertificateSource[] trustedSources = createTrustedCertSources(); + if (trustedSources.length > 0) { + verifier.setTrustedCertSources(trustedSources); } if (isOnlineEnabled()) { OCSPDataLoader ocspDataLoader = new OCSPDataLoader(); From f30eb3a6404fe51c5ed9b3864e2040ae52220cc7 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Thu, 18 Jun 2026 16:00:05 +0200 Subject: [PATCH 09/18] Cleanup the capability model --- design-doc/3.1-engine-dss.md | 2 +- design-doc/3.1-signing-engines.md | 4 +- .../net/sf/jsignpdf/engine/Capability.java | 2 +- .../jsignpdf/translations/messages.properties | 4 + .../jsignpdf/engine/dss/DssSigningEngine.java | 2 +- .../engine/openpdf/OpenPdfSigningEngine.java | 2 +- jsignpdf/pom.xml | 6 + .../engine/EngineMismatchValidator.java | 7 +- .../fx/preferences/PreferencesController.java | 47 +++++++- .../fx/preferences/PreferencesViewModel.java | 11 ++ .../fx/view/MainWindowController.java | 106 +++++++++++------- .../net/sf/jsignpdf/fx/view/MainWindow.fxml | 29 ++--- .../net/sf/jsignpdf/fx/view/Preferences.fxml | 12 ++ .../engine/EngineMismatchValidatorTest.java | 49 ++++++-- .../sf/jsignpdf/fx/FxTranslationsTest.java | 9 +- 15 files changed, 212 insertions(+), 80 deletions(-) diff --git a/design-doc/3.1-engine-dss.md b/design-doc/3.1-engine-dss.md index 96cb7f1b..e4ac339f 100644 --- a/design-doc/3.1-engine-dss.md +++ b/design-doc/3.1-engine-dss.md @@ -217,7 +217,7 @@ private static final Set CAPABILITIES = Set.copyOf(EnumSet.of( Capability.HASH_SHA256, Capability.HASH_SHA384, Capability.HASH_SHA512, - Capability.APPEND_MODE, // DSS signs incrementally by default + // No OVERWRITE_MODE: DSS always signs incrementally (PAdES); append is universal Capability.CERTIFICATION_LEVEL, Capability.ENCRYPTION_PASSWORD, Capability.PERMISSIONS_BITMASK, diff --git a/design-doc/3.1-signing-engines.md b/design-doc/3.1-signing-engines.md index 9e778438..afbb9468 100644 --- a/design-doc/3.1-signing-engines.md +++ b/design-doc/3.1-signing-engines.md @@ -234,7 +234,7 @@ public enum Capability { HASH_SHA1, HASH_SHA256, HASH_SHA384, HASH_SHA512, HASH_RIPEMD160, // document-level - APPEND_MODE, + OVERWRITE_MODE, // non-incremental rewrite; incremental append is universal CERTIFICATION_LEVEL, // DocMDP: all four levels ENCRYPTION_PASSWORD, ENCRYPTION_CERTIFICATE, @@ -417,7 +417,7 @@ table during phase 1 so each FX controller doesn't re-derive it. | Control | Capability gate(s) | |---|---| | Hash algorithm dropdown items | `HASH_SHA1`, `HASH_SHA256`, `HASH_SHA384`, `HASH_SHA512`, `HASH_RIPEMD160` (per-item filter) | -| "Append mode" checkbox | `APPEND_MODE` | +| "Append mode" checkbox (unchecking = overwrite) | `OVERWRITE_MODE` (append/incremental is universal; only the overwrite/unchecked state is gated) | | Certification level dropdown | `CERTIFICATION_LEVEL` | | Encryption section | `ENCRYPTION_PASSWORD ∪ ENCRYPTION_CERTIFICATE` (umbrella) | | Encryption type → PASSWORD radio | `ENCRYPTION_PASSWORD` | diff --git a/engines/api/src/main/java/net/sf/jsignpdf/engine/Capability.java b/engines/api/src/main/java/net/sf/jsignpdf/engine/Capability.java index 57171412..2f5f601f 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/engine/Capability.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/engine/Capability.java @@ -26,7 +26,7 @@ public enum Capability { HASH_RIPEMD160, // document-level - APPEND_MODE, + OVERWRITE_MODE, // rewrite the document non-incrementally; incremental append is universal and needs no capability CERTIFICATION_LEVEL, // DocMDP: all four levels ENCRYPTION_PASSWORD, ENCRYPTION_CERTIFICATE, diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 89742193..f1406a41 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -286,6 +286,7 @@ jfx.gui.engine.tooltip=Signing engine used to sign the document. jfx.gui.engine.unsupported=This option is not supported by the selected engine. jfx.gui.padesLevel.label=PAdES level: jfx.gui.padesLevel.tooltip=PAdES baseline level (only for PAdES engines such as DSS). LT/LTA embed revocation data and require the DSS trust configuration. +jfx.gui.padesLevel.help=PAdES baseline level for the produced signature. Only available for PAdES engines such as DSS, where it defaults to B (basic); LT/LTA embed revocation data and require a timestamp and the DSS trust configuration. jfx.gui.dss.section.label=DSS engine jfx.gui.dss.online.enabled=Fetch revocation data online (OCSP/CRL, AIA) jfx.gui.dss.online.enabled.tooltip=Required to produce LT/LTA. When off, the DSS engine works fully offline (B/T only). @@ -320,6 +321,7 @@ jfx.gui.toolbar.fit=Fit jfx.gui.toolbar.sign=Sign jfx.gui.toolbar.visibleSig.tooltip=Toggle visible signature jfx.gui.toolbar.tsa.tooltip=Toggle timestamp (TSA) +jfx.gui.panel.padesLevel=PAdES Level jfx.gui.panel.certificate=Certificate jfx.gui.panel.signatureProperties=Signature Properties jfx.gui.panel.signatureAppearance=Signature Appearance @@ -462,6 +464,8 @@ jfx.gui.preferences.button.cancel=Cancel jfx.gui.preferences.button.resetSection=Reset section to defaults jfx.gui.preferences.restartHint=Takes effect after restart jfx.gui.preferences.restartHint.tooltip=This setting is read once at startup. Restart JSignPdf for the change to take effect. +jfx.gui.preferences.section.engine=Signing engine +jfx.gui.preferences.engine.help=Signing engine used to sign documents. OpenPDF produces standard PDF signatures; DSS produces PAdES baseline signatures (B/T/LT/LTA). The selection applies immediately and is the default for both the GUI and the CLI. jfx.gui.preferences.section.font=Visible signature font jfx.gui.preferences.section.certificate=Certificate validation jfx.gui.preferences.section.network=Network and SSL 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 52da8bd7..89f9829d 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 @@ -91,7 +91,7 @@ public class DssSigningEngine implements SigningEngine { Capability.HASH_SHA256, Capability.HASH_SHA384, Capability.HASH_SHA512, - Capability.APPEND_MODE, // DSS signs incrementally by default + // No OVERWRITE_MODE: DSS always signs incrementally (PAdES requires it). Append is universal. Capability.CERTIFICATION_LEVEL, Capability.ENCRYPTION_PASSWORD, Capability.PERMISSIONS_BITMASK, diff --git a/engines/openpdf/src/main/java/net/sf/jsignpdf/engine/openpdf/OpenPdfSigningEngine.java b/engines/openpdf/src/main/java/net/sf/jsignpdf/engine/openpdf/OpenPdfSigningEngine.java index ed7e19e9..82c664f6 100644 --- a/engines/openpdf/src/main/java/net/sf/jsignpdf/engine/openpdf/OpenPdfSigningEngine.java +++ b/engines/openpdf/src/main/java/net/sf/jsignpdf/engine/openpdf/OpenPdfSigningEngine.java @@ -79,7 +79,7 @@ public class OpenPdfSigningEngine implements SigningEngine { Capability.SUBFILTER_ADBE_PKCS7_DETACHED, Capability.HASH_SHA1, Capability.HASH_SHA256, Capability.HASH_SHA384, Capability.HASH_SHA512, Capability.HASH_RIPEMD160, - Capability.APPEND_MODE, Capability.CERTIFICATION_LEVEL, + Capability.OVERWRITE_MODE, Capability.CERTIFICATION_LEVEL, Capability.ENCRYPTION_PASSWORD, Capability.ENCRYPTION_CERTIFICATE, Capability.PERMISSIONS_BITMASK, Capability.VISIBLE_SIGNATURE, Capability.VISIBLE_LAYER2_TEXT, Capability.VISIBLE_LAYER4_TEXT, Capability.VISIBLE_RENDER_MODE_DESCRIPTION_ONLY, Capability.VISIBLE_RENDER_MODE_GRAPHIC_AND_DESCRIPTION, diff --git a/jsignpdf/pom.xml b/jsignpdf/pom.xml index 8acd61a1..f162a52b 100644 --- a/jsignpdf/pom.xml +++ b/jsignpdf/pom.xml @@ -27,6 +27,12 @@ -Dglass.platform=Monocle -Dmonocle.platform=Headless -Dprism.order=sw + + + ${project.build.directory}/test-config + diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java index 1243783a..fca0b31b 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java @@ -97,9 +97,10 @@ public static List findMismatches(BasicSignerOptions o, SigningEngine } } - // append mode - if (o.isAppendX() && !caps.contains(Capability.APPEND_MODE)) { - out.add(new Mismatch("--append", Capability.APPEND_MODE)); + // overwrite (non-incremental) mode — incremental append is universal, so only a request to + // overwrite the document (append disabled) against an engine that can't do it is a mismatch. + if (!o.isAppendX() && !caps.contains(Capability.OVERWRITE_MODE)) { + out.add(new Mismatch("--append", Capability.OVERWRITE_MODE)); } // certification level 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 0195ade1..2ed6e3d2 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 @@ -27,6 +27,7 @@ import javafx.scene.control.ButtonBar.ButtonData; import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Dialog; import javafx.scene.control.DialogPane; @@ -40,7 +41,10 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; +import javafx.util.StringConverter; import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.engine.EngineRegistry; +import net.sf.jsignpdf.engine.SigningEngine; import net.sf.jsignpdf.fx.util.NativeFileChooser; import net.sf.jsignpdf.fx.util.NativeFileChooser.ExtensionFilter; import net.sf.jsignpdf.ssl.SSLInitializer; @@ -64,6 +68,7 @@ public class PreferencesController { private static final String DEFAULTS_RESOURCE = "/net/sf/jsignpdf/conf/advanced.default.properties"; @FXML private TabPane tabPane; + @FXML private Tab tabEngine; @FXML private Tab tabFont; @FXML private Tab tabCertificate; @FXML private Tab tabNetwork; @@ -72,6 +77,8 @@ public class PreferencesController { @FXML private Tab tabDss; @FXML private Tab tabPkcs11; + @FXML private ChoiceBox cmbEngine; + @FXML private TextField txtFontPath; @FXML private Button btnFontPathBrowse; @FXML private TextField txtFontName; @@ -108,6 +115,18 @@ public class PreferencesController { @FXML private void initialize() { + cmbEngine.getItems().setAll(EngineRegistry.getInstance().listAll()); + cmbEngine.setConverter(new StringConverter() { + @Override + public String toString(SigningEngine engine) { + return engine == null ? "" : engine.displayName(); + } + + @Override + public SigningEngine fromString(String s) { + return null; + } + }); cmbFontEncoding.getItems().addAll(ENCODINGS); cmbTsaHashAlgorithm.getItems().addAll(HASH_ALGORITHMS); // Empty-hint visibility is bound when bind() runs. @@ -183,6 +202,22 @@ public static boolean show(Stage owner) { void bind(PreferencesViewModel viewModel) { this.vm = viewModel; + + // The engine ChoiceBox holds SigningEngine objects; the VM stores the engine id string. Keep the two + // in sync manually (both directions, so "Reset section" reselecting the default reflects in the combo). + selectEngineById(vm.engineIdProperty().get()); + cmbEngine.getSelectionModel().selectedItemProperty().addListener((obs, oldEngine, sel) -> { + if (sel != null) { + vm.engineIdProperty().set(sel.id()); + } + }); + vm.engineIdProperty().addListener((obs, oldId, newId) -> { + SigningEngine sel = cmbEngine.getSelectionModel().getSelectedItem(); + if (sel == null || !sel.id().equals(newId)) { + selectEngineById(newId); + } + }); + txtFontPath.textProperty().bindBidirectional(vm.fontPathProperty()); txtFontName.textProperty().bindBidirectional(vm.fontNameProperty()); cmbFontEncoding.valueProperty().bindBidirectional(vm.fontEncodingProperty()); @@ -218,6 +253,14 @@ void bind(PreferencesViewModel viewModel) { vm.pdfLibOpenpdfOrderProperty().addListener((o, a, b) -> rerender.run()); } + private void selectEngineById(String id) { + EngineRegistry registry = EngineRegistry.getInstance(); + SigningEngine target = registry.findById(id).or(registry::getDefault).orElse(null); + if (target != null) { + cmbEngine.getSelectionModel().select(target); + } + } + private void rebuildPdfLibsPanel() { vboxPdfLibs.getChildren().clear(); // Iterate in current order (1, 2, 3). @@ -300,7 +343,9 @@ private void onPkcs11ResetSample() { private void resetActiveTabToDefaults() { Tab active = tabPane.getSelectionModel().getSelectedItem(); AdvancedConfig defaults = bundledDefaultsHolder(); - if (active == tabFont) { + if (active == tabEngine) { + vm.applyEngineDefaults(defaults); + } else if (active == tabFont) { vm.applyFontDefaults(defaults); } else if (active == tabCertificate) { vm.applyCertificateDefaults(defaults); 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 147a0f84..16bfbc5f 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 @@ -13,6 +13,7 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import net.sf.jsignpdf.utils.AdvancedConfig; +import net.sf.jsignpdf.utils.AppConfig; /** * Backing model for the Preferences dialog. Loaded from {@link AdvancedConfig} on dialog open and written back on OK. @@ -25,6 +26,8 @@ public class PreferencesViewModel { public static final String LIB_PDFBOX = "pdfbox"; public static final String LIB_OPENPDF = "openpdf"; + private final StringProperty engineId = new SimpleStringProperty(AppConfig.DEFAULT_ENGINE_ID); + private final StringProperty fontPath = new SimpleStringProperty(""); private final StringProperty fontName = new SimpleStringProperty(""); private final StringProperty fontEncoding = new SimpleStringProperty(""); @@ -58,6 +61,7 @@ public class PreferencesViewModel { /** Loads the VM from the given snapshot of {@link AdvancedConfig} and a pkcs11 file body. */ public void loadFrom(AdvancedConfig cfg, String pkcs11FileBody) { + engineId.set(cfg.getNotEmptyProperty("engine", AppConfig.DEFAULT_ENGINE_ID)); fontPath.set(orEmpty(cfg.getProperty("font.path"))); fontName.set(orEmpty(cfg.getProperty("font.name"))); fontEncoding.set(orEmpty(cfg.getProperty("font.encoding"))); @@ -86,6 +90,7 @@ public void loadFrom(AdvancedConfig cfg, String pkcs11FileBody) { /** Writes the VM back into the given {@link AdvancedConfig} (does not persist; caller should call {@code save()}). */ public void writeTo(AdvancedConfig cfg) { + cfg.setProperty("engine", orFallback(engineId.get(), AppConfig.DEFAULT_ENGINE_ID)); writeStringOrRemove(cfg, "font.path", fontPath.get()); writeStringOrRemove(cfg, "font.name", fontName.get()); writeStringOrRemove(cfg, "font.encoding", fontEncoding.get()); @@ -108,6 +113,7 @@ public void writeTo(AdvancedConfig cfg) { /** Resets every VM property to the bundled-default value (read from the given snapshot of bundled defaults). */ public void applyDefaults(AdvancedConfig defaults) { + applyEngineDefaults(defaults); applyFontDefaults(defaults); applyCertificateDefaults(defaults); applyNetworkDefaults(defaults); @@ -116,6 +122,10 @@ public void applyDefaults(AdvancedConfig defaults) { applyDssDefaults(defaults); } + public void applyEngineDefaults(AdvancedConfig defaults) { + engineId.set(orFallback(defaults.getBundledDefault("engine"), AppConfig.DEFAULT_ENGINE_ID)); + } + public void applyFontDefaults(AdvancedConfig defaults) { fontPath.set(orEmpty(defaults.getBundledDefault("font.path"))); fontName.set(orEmpty(defaults.getBundledDefault("font.name"))); @@ -232,6 +242,7 @@ private static void writeStringOrRemove(AdvancedConfig cfg, String key, String v } } + public StringProperty engineIdProperty() { return engineId; } public StringProperty fontPathProperty() { return fontPath; } public StringProperty fontNameProperty() { return fontName; } public StringProperty fontEncodingProperty() { return fontEncoding; } 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 dafd0a72..a0a540f1 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 @@ -55,7 +55,6 @@ 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; @@ -145,7 +144,6 @@ public class MainWindowController { @FXML private Button btnNextPage; @FXML private ToggleButton btnVisibleSig; @FXML private ToggleButton btnTsa; - @FXML private ChoiceBox cmbEngine; @FXML private Label lblPadesLevel; @FXML private ChoiceBox cmbPadesLevel; @FXML private ComboBox cmbPresets; @@ -154,6 +152,7 @@ public class MainWindowController { // Content area @FXML private SplitPane splitPane; @FXML private Accordion sidePanelAccordion; + @FXML private TitledPane padesLevelAccordionPane; @FXML private TitledPane signatureAppearanceAccordionPane; @FXML private TitledPane tsaAccordionPane; @FXML private TitledPane encryptionAccordionPane; @@ -411,59 +410,31 @@ private void initialize() { refreshRecentFilesMenu(); setupPresetCombo(); - setupEngineSelector(); + setupEngineCapabilityGating(); } /** - * Populates the toolbar engine selector from the {@link EngineRegistry}, binds it to the - * {@code engine=} key in {@code advanced.properties}, and drives the capability-based control gating - * off the selected engine. Switching the engine is immediate (no restart): the - * {@link EngineCapabilities} bindings re-evaluate so unsupported controls disable themselves. + * Drives the capability-based control gating off the active signing engine. The engine itself is now + * selected in File > Preferences (persisted to the {@code engine=} key in {@code advanced.properties}); + * this method seeds {@link EngineCapabilities} from that persisted value at startup and wires the + * capability-driven disabling of toolbar buttons and accordion sections. {@link #onPreferences()} + * refreshes the active engine after the dialog closes so switching it takes effect without a restart. */ - private void setupEngineSelector() { - if (cmbEngine == null) { - return; - } + private void setupEngineCapabilityGating() { final EngineRegistry registry = EngineRegistry.getInstance(); - cmbEngine.getItems().setAll(registry.listAll()); - cmbEngine.setConverter(new StringConverter() { - @Override - public String toString(SigningEngine engine) { - return engine == null ? "" : engine.displayName(); - } - - @Override - public SigningEngine fromString(String s) { - return null; - } - }); SigningEngine current = registry.findById(AppConfig.defaultEngineId()) .or(registry::getDefault).orElse(null); - if (current != null) { - cmbEngine.getSelectionModel().select(current); - } engineCapabilities.activeEngineProperty().set(current); - cmbEngine.getSelectionModel().selectedItemProperty().addListener((obs, oldEngine, sel) -> { - if (sel == null) { - return; - } - engineCapabilities.activeEngineProperty().set(sel); - try { - AdvancedConfig cfg = PropertyStoreFactory.getInstance().advancedConfig(); - cfg.setProperty("engine", sel.id()); - cfg.save(); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to persist engine selection", e); - } - }); - // Capability-driven section gating at the umbrella (accordion-pane) granularity. These panes // are not governed by the document-loaded disable logic, so binding their disableProperty here // is safe. (With OpenPDF — the only engine in phase 1 — every capability is present, so nothing // is disabled; the wiring exists for reduced-capability engines added in phase 2.) engineCapabilities.gate(btnTsa, Capability.TSA); setupPadesLevelSelector(); + if (padesLevelAccordionPane != null) { + engineCapabilities.gate(padesLevelAccordionPane, Capability.PADES_BASELINE_B); + } if (signatureAppearanceAccordionPane != null) { engineCapabilities.gate(signatureAppearanceAccordionPane, Capability.VISIBLE_SIGNATURE); } @@ -490,7 +461,9 @@ public SigningEngine fromString(String s) { * Populates and gates the PAdES-level dropdown. The control is bound to the signing ViewModel's * {@code padesLevel} property and gated as a unit on {@link Capability#PADES_BASELINE_B}: an engine * that cannot produce even B is not a PAdES engine, so the dropdown disables/greys for OpenPDF and - * enables for DSS. An empty selection means "engine default". + * enables for DSS. The level defaults to {@link PadesLevel#BASELINE_B} for PAdES engines (so the + * dropdown isn't empty) and is cleared for non-PAdES engines (a stale level would otherwise trip the + * {@link EngineMismatchValidator}). */ private void setupPadesLevelSelector() { if (cmbPadesLevel == null) { @@ -513,6 +486,35 @@ public PadesLevel fromString(String s) { if (lblPadesLevel != null) { engineCapabilities.gate(lblPadesLevel, Capability.PADES_BASELINE_B); } + + // Seed a sensible default for the current engine and keep it in step when the engine changes. + applyPadesLevelDefaultForEngine(engineCapabilities.activeEngineProperty().get()); + engineCapabilities.activeEngineProperty().addListener( + (obs, oldEngine, newEngine) -> applyPadesLevelDefaultForEngine(newEngine)); + // Re-apply the engine-appropriate value whenever the level is changed by an options/preset load or + // a reset (those call syncFromOptions/resetToDefaults). This keeps the dropdown from going empty + // while a PAdES engine is active, and clears a stale level loaded under a non-PAdES engine (which + // would otherwise trip the EngineMismatchValidator at sign time). + signingVM.padesLevelProperty().addListener((obs, oldLevel, newLevel) -> + applyPadesLevelDefaultForEngine(engineCapabilities.activeEngineProperty().get())); + } + + /** + * Picks the PAdES level appropriate for the given engine: {@link PadesLevel#BASELINE_B} as a non-empty + * default when the engine is PAdES-capable and no level is set yet, or {@code null} when the engine is + * not PAdES-capable (OpenPDF) so the level can't reach the engine mismatch validator. An explicit + * user choice (e.g. LT/LTA) is preserved while a PAdES engine stays active. + */ + private void applyPadesLevelDefaultForEngine(SigningEngine engine) { + boolean padesCapable = engine != null + && engine.capabilities().contains(Capability.PADES_BASELINE_B); + if (padesCapable) { + if (signingVM.padesLevelProperty().get() == null) { + signingVM.padesLevelProperty().set(PadesLevel.BASELINE_B); + } + } else if (signingVM.padesLevelProperty().get() != null) { + signingVM.padesLevelProperty().set(null); + } } private void setupPresetCombo() { @@ -645,7 +647,23 @@ private void onManagePresets() { @FXML private void onPreferences() { - PreferencesController.show(stage); + boolean saved = PreferencesController.show(stage); + if (saved) { + // The engine may have been changed in Preferences. Re-seed the capability bindings from the + // persisted value so toolbar buttons and accordion sections re-gate immediately (no restart). + refreshActiveEngineFromConfig(); + } + } + + /** + * Re-seeds {@link EngineCapabilities#activeEngineProperty()} from the engine persisted in + * {@code advanced.properties}, so capability gating (and the PAdES-level default) re-evaluates after + * the engine changes via Preferences or a settings reset. + */ + private void refreshActiveEngineFromConfig() { + EngineRegistry registry = EngineRegistry.getInstance(); + registry.findById(AppConfig.defaultEngineId()).or(registry::getDefault) + .ifPresent(e -> engineCapabilities.activeEngineProperty().set(e)); } private String validationMessage(PresetValidation.Result result) { @@ -949,6 +967,10 @@ private void onResetSettings() { // Reset the ViewModel (which updates all bound UI controls) signingVM.resetToDefaults(); + // The wiped config reverts the engine to the bundled default; re-seed capability gating and + // the PAdES-level default to match. + refreshActiveEngineFromConfig(); + // Clear placement and close any open document if (documentVM.isDocumentLoaded()) { closeDocument(); diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml index 6d61bf78..efd8e866 100644 --- a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/MainWindow.fxml @@ -127,19 +127,6 @@ -
+ + + + + + + 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 32e2cb76..6b0bf87c 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 @@ -11,6 +11,18 @@ tabClosingPolicy="UNAVAILABLE" prefWidth="720" prefHeight="520"> + + + + + + + diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java index 29556ee5..95e3f46b 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java @@ -25,14 +25,39 @@ private static Set caps(List mismatches) { } @Test - public void emptyCapabilityEngineFlagsDefaultHashAndAppend() { - // A fresh options object signs with the default SHA-1 hash and append mode on, so an engine - // that supports neither must report both. + public void emptyCapabilityEngineFlagsDefaultHashButNotAppend() { + // A fresh options object signs with the default SHA-1 hash and append (incremental) mode on. + // Append is universal, so only the hash is flagged against a capability-less engine. BasicSignerOptions opts = new BasicSignerOptions(); StubSigningEngine engine = new StubSigningEngine("empty"); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertTrue(reported.contains(Capability.HASH_SHA1)); - assertTrue(reported.contains(Capability.APPEND_MODE)); + assertFalse(reported.contains(Capability.OVERWRITE_MODE)); + } + + @Test + public void overwriteFlaggedWhenEngineLacksCapability() { + // append=false (in advanced mode) requests a non-incremental rewrite, which needs OVERWRITE_MODE. + BasicSignerOptions opts = new BasicSignerOptions(); + opts.setAdvanced(true); + opts.setAppend(false); + StubSigningEngine engine = new StubSigningEngine("noOverwrite", Capability.HASH_SHA1); + List mismatches = EngineMismatchValidator.findMismatches(opts, engine); + Set reported = caps(mismatches); + assertTrue(reported.contains(Capability.OVERWRITE_MODE)); + Mismatch m = mismatches.stream().filter(x -> x.capability() == Capability.OVERWRITE_MODE) + .findFirst().orElseThrow(); + assertEquals("--append", m.option()); + } + + @Test + public void overwriteNotFlaggedWhenEngineSupportsIt() { + BasicSignerOptions opts = new BasicSignerOptions(); + opts.setAdvanced(true); + opts.setAppend(false); + StubSigningEngine engine = new StubSigningEngine("overwrite", + Capability.HASH_SHA1, Capability.OVERWRITE_MODE); + assertFalse(caps(EngineMismatchValidator.findMismatches(opts, engine)).contains(Capability.OVERWRITE_MODE)); } @Test @@ -48,7 +73,7 @@ public void missingVisibleUmbrellaSuppressesSubFields() { BasicSignerOptions opts = new BasicSignerOptions(); opts.setVisible(true); opts.setL2Text("custom text"); - StubSigningEngine engine = new StubSigningEngine("noVisible", Capability.HASH_SHA1, Capability.APPEND_MODE); + StubSigningEngine engine = new StubSigningEngine("noVisible", Capability.HASH_SHA1); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertTrue("umbrella must be reported", reported.contains(Capability.VISIBLE_SIGNATURE)); assertFalse("sub-field must NOT be reported when umbrella is missing", @@ -61,7 +86,7 @@ public void supportedVisibleUmbrellaStillFlagsSubField() { opts.setVisible(true); opts.setL2Text("custom text"); StubSigningEngine engine = new StubSigningEngine("visibleNoL2", - Capability.HASH_SHA1, Capability.APPEND_MODE, Capability.VISIBLE_SIGNATURE); + Capability.HASH_SHA1, Capability.VISIBLE_SIGNATURE); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertFalse(reported.contains(Capability.VISIBLE_SIGNATURE)); assertTrue(reported.contains(Capability.VISIBLE_LAYER2_TEXT)); @@ -73,7 +98,7 @@ public void tsaUmbrellaFlaggedWhenEnabledAndUnsupported() { opts.setAdvanced(true); opts.setTimestamp(true); opts.setTsaUrl("http://tsa.example.com"); - StubSigningEngine engine = new StubSigningEngine("noTsa", Capability.HASH_SHA1, Capability.APPEND_MODE); + StubSigningEngine engine = new StubSigningEngine("noTsa", Capability.HASH_SHA1); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertTrue(reported.contains(Capability.TSA)); } @@ -84,7 +109,7 @@ public void noTimestampMeansNoTsaMismatch() { opts.setAdvanced(true); // timestamp disabled -> TSA not evaluated even though the engine lacks it StubSigningEngine engine = new StubSigningEngine("noTsa", - Capability.HASH_SHA1, Capability.APPEND_MODE); + Capability.HASH_SHA1); assertFalse(caps(EngineMismatchValidator.findMismatches(opts, engine)).contains(Capability.TSA)); } @@ -94,7 +119,7 @@ public void padesLevelFlaggedWhenEngineLacksCapability() { BasicSignerOptions opts = new BasicSignerOptions(); opts.setAdvanced(true); opts.setPadesLevel(net.sf.jsignpdf.types.PadesLevel.BASELINE_LTA); - StubSigningEngine engine = new StubSigningEngine("noPades", Capability.HASH_SHA1, Capability.APPEND_MODE); + StubSigningEngine engine = new StubSigningEngine("noPades", Capability.HASH_SHA1); List mismatches = EngineMismatchValidator.findMismatches(opts, engine); Set reported = caps(mismatches); assertTrue(reported.contains(Capability.PADES_BASELINE_LTA)); @@ -109,7 +134,7 @@ public void padesLevelNotFlaggedWhenEngineSupportsIt() { opts.setAdvanced(true); opts.setPadesLevel(net.sf.jsignpdf.types.PadesLevel.BASELINE_T); StubSigningEngine engine = new StubSigningEngine("pades", - Capability.HASH_SHA1, Capability.APPEND_MODE, Capability.PADES_BASELINE_T); + Capability.HASH_SHA1, Capability.PADES_BASELINE_T); assertFalse(caps(EngineMismatchValidator.findMismatches(opts, engine)).contains(Capability.PADES_BASELINE_T)); } @@ -118,7 +143,7 @@ public void noPadesLevelMeansNoPadesMismatch() { // Default (null) pades level must never produce a mismatch, even against an engine without PAdES. BasicSignerOptions opts = new BasicSignerOptions(); opts.setAdvanced(true); - StubSigningEngine engine = new StubSigningEngine("noPades", Capability.HASH_SHA1, Capability.APPEND_MODE); + StubSigningEngine engine = new StubSigningEngine("noPades", Capability.HASH_SHA1); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertFalse(reported.contains(Capability.PADES_BASELINE_B)); } @@ -138,7 +163,7 @@ public void mismatchCarriesOptionLabel() { BasicSignerOptions opts = new BasicSignerOptions(); opts.setVisible(true); StubSigningEngine engine = new StubSigningEngine("empty2", - Capability.HASH_SHA1, Capability.APPEND_MODE); + Capability.HASH_SHA1); List mismatches = EngineMismatchValidator.findMismatches(opts, engine); Mismatch visible = mismatches.stream() .filter(m -> m.capability() == Capability.VISIBLE_SIGNATURE).findFirst().orElseThrow(); diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java index a07810ec..20f292f5 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java @@ -238,15 +238,18 @@ public void testAccordionPanelTitlesMatchTranslations() throws Exception { assertEquals("Signature properties panel for " + locale, bundle.getString("jfx.gui.panel.signatureProperties"), accordion.getPanes().get(1).getText()); + assertEquals("PAdES level panel for " + locale, + bundle.getString("jfx.gui.panel.padesLevel"), + accordion.getPanes().get(2).getText()); assertEquals("Signature appearance panel for " + locale, bundle.getString("jfx.gui.panel.signatureAppearance"), - accordion.getPanes().get(2).getText()); + accordion.getPanes().get(3).getText()); assertEquals("TSA panel for " + locale, bundle.getString("jfx.gui.panel.timestampValidation"), - accordion.getPanes().get(3).getText()); + accordion.getPanes().get(4).getText()); assertEquals("Encryption panel for " + locale, bundle.getString("jfx.gui.panel.encryptionRights"), - accordion.getPanes().get(4).getText()); + accordion.getPanes().get(5).getText()); } } From 4913669ac00663a2a0637d41784486c60f7ccf80 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 12:37:27 +0200 Subject: [PATCH 10/18] Fixes --- distribution/doc/release-notes/3.1.0.md | 34 ++++++++----------- .../net/sf/jsignpdf/BasicSignerOptions.java | 17 +++++++--- .../main/java/net/sf/jsignpdf/Constants.java | 2 +- .../jsignpdf/translations/messages.properties | 1 + .../jsignpdf/engine/dss/DssSigningEngine.java | 13 +++++-- .../engine/dss/DssSigningEngineTest.java | 11 ++++++ .../engine/EngineMismatchValidator.java | 14 +++++--- .../engine/EngineMismatchValidatorTest.java | 20 +++++++++-- pom.xml | 11 ++++++ 9 files changed, 87 insertions(+), 36 deletions(-) diff --git a/distribution/doc/release-notes/3.1.0.md b/distribution/doc/release-notes/3.1.0.md index 9a5ff6df..e60146a3 100644 --- a/distribution/doc/release-notes/3.1.0.md +++ b/distribution/doc/release-notes/3.1.0.md @@ -13,15 +13,14 @@ runtime. and enterprise deployability). - **Linux x64 and aarch64**: DEB, RPM, and portable ZIPs. Flatpak bundles are also published per architecture. - - **macOS Intel and Apple Silicon**: DMGs (unsigned for now — expect - a Gatekeeper warning; signing / notarization is tracked for a - follow-up) plus portable `.app` ZIPs. + - **macOS Apple Silicon**: DMG (unsigned for now — expect + a Gatekeeper warning) plus portable `.app` ZIP. - **Two cross-platform ZIPs** for users who already have Java 21: - - `jsignpdf-3.1.0-full.zip` — every dependency plus JavaFX natives + - `jsignpdf--full.zip` — every dependency plus JavaFX natives for all supported OS/arch combinations. A small bootstrap launcher picks the matching JavaFX classifier at startup; if none is available, the GUI falls back to Swing. - - `jsignpdf-3.1.0-minimal.zip` — no JavaFX at all. Suitable for + - `jsignpdf--minimal.zip` — no JavaFX at all. Suitable for headless / CLI signing, Linux aarch64, and downstream packagers (Debian, Homebrew). - **Fat jar dropped.** The shaded `jsignpdf-jar-with-dependencies.jar` @@ -34,29 +33,25 @@ runtime. selectable. JSignPdf ships the **OpenPDF** engine as the default, and its output is byte-for-byte identical to previous releases, so nothing changes by default. New `--list-engines` and `-eng` / `--engine` CLI - options, plus an *Engine* selector in the JavaFX toolbar, let you pick + options, plus an *Engine* selector in the JavaFX Preferences, let you pick the engine; the choice is stored in `advanced.properties` (`engine=openpdf`). Engines are discovered via `ServiceLoader` from - `lib/`, so third-party engines can be dropped in. See - `design-doc/3.1-signing-engines.md`. + `lib/`, so third-party engines can be dropped in. - **EU DSS / PAdES signing engine.** A second bundled engine (id `dss`), built on the European Commission's Digital Signature Service library, produces **PAdES** signatures (`ETSI.CAdES.detached`) at the ETSI baseline levels **B / T / LT / LTA** — which the OpenPDF engine cannot - create. Select it with `-eng dss` / the *Engine* toolbar selector and - choose the level with the new `-pl` / `--pades-level` option (`B`, `T`, - `LT`, `LTA`); `LT`/`LTA` embed revocation data and are configured via - the `engine.dss.*` trust keys in `advanced.properties` (also on the new - *DSS engine* Preferences tab). The default engine stays `openpdf`, so - there is no change unless you opt in. Bundling DSS adds roughly 30 MB+ - of jars to the distribution and puts an **LGPL-2.1** dependency on the - active signing path when `dss` is selected (JSignPdf is dual - MPL-2.0 / LGPL-2.1, so this is license-compatible). See - `design-doc/3.1-engine-dss.md` and the *Signing engines* chapter of the - user guide. + create. Select it with `-eng dss` / the *Engine* Preferences selector. ## Notes +- **Default hash algorithm is now SHA-256** (was SHA-1). SHA-1 is + collision-broken and rejected by PAdES and modern validators. + This only affects signing that relies on the + *implicit* default: a saved or explicit `hash.algorithm` (including + `-ha`) is untouched, and SHA-1 stays selectable for the OpenPDF + engine. Unattended/scripted signing without `-ha` will now produce + SHA-256 digests. - The CLI `usage:` line and bundled examples now use `jsignpdf` as the command name (instead of `java -jar JSignPdf.jar`), matching the launcher you actually invoke. @@ -65,7 +60,6 @@ runtime. always falls back to the Swing UI. The Linux aarch64 DEB / RPM / Flatpak builds are unaffected — their bundled Zulu+FX runtime supplies JavaFX as JDK modules. -- Tracking issue: [#391](https://github.com/intoolswetrust/jsignpdf/issues/391). Thanks to everyone who contributed code, translations, bug reports, and feedback! diff --git a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java index dbddb98b..2137ff82 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java @@ -1149,10 +1149,19 @@ public void setCrlEnabled(final boolean crlEnabled) { } public HashAlgorithm getHashAlgorithm() { - if (hashAlgorithm == null) { - hashAlgorithm = Constants.DEFVAL_HASH_ALGORITHM; - } - return hashAlgorithm; + return hashAlgorithm == null ? Constants.DEFVAL_HASH_ALGORITHM : hashAlgorithm; + } + + /** + * Whether a hash algorithm was explicitly selected (CLI {@code --hash-algorithm}, stored config, or + * GUI selection), as opposed to falling back to {@link Constants#DEFVAL_HASH_ALGORITHM}. Used to tell + * a deliberate user choice from the legacy default, so capability validation only fails fast on the + * former and engines may transparently upgrade the latter to a supported digest. + * + * @return true when a hash algorithm has been set + */ + public boolean isHashAlgorithmSet() { + return hashAlgorithm != null; } public HashAlgorithm getHashAlgorithmX() { diff --git a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java index 9e79b309..41010f0e 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java @@ -201,7 +201,7 @@ public class Constants { public static final long DEFVAL_SIG_SIZE = 15000L; public static final String DEFVAL_CACERTS_PASSWD = "changeit"; - public static final HashAlgorithm DEFVAL_HASH_ALGORITHM = HashAlgorithm.SHA1; + public static final HashAlgorithm DEFVAL_HASH_ALGORITHM = HashAlgorithm.SHA256; public static final boolean DEFVAL_APPEND = toBoolean(true); diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index f1406a41..19bbf232 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -40,6 +40,7 @@ console.dss.ltNoTsa=The PAdES level LT/LTA builds on a signature timestamp, but console.dss.trustConfigFailed=Failed to load the configured DSS trust material (truststore / certificate file / certificate URL / LOTL). Check the engine.dss.trust.* settings. Signing aborted. console.dss.socksProxyUnsupported=The DSS engine does not support SOCKS proxies; OCSP/CRL/AIA/TSA requests will bypass the proxy. Configure an HTTP proxy to route DSS revocation/timestamp traffic. console.dss.unsupportedHash=The DSS engine does not support the hash algorithm ''{0}'' for PAdES. Use SHA-256, SHA-384 or SHA-512. +console.dss.hashUpgrade=The default hash algorithm ''{0}'' is not a PAdES digest, using SHA-256 instead. console.dss.cannotEncryptSigned=Cannot encrypt a PDF that already contains signatures. console.getPrivateKey=Loading private key console.inFileNotFound.error=Input PDF was not found or it's not readable. 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 89f9829d..b00c4713 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 @@ -141,10 +141,17 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC } final HashAlgorithm hashAlgorithm = options.getHashAlgorithmX(); - final DigestAlgorithm digestAlgorithm = DssMappings.toDigestAlgorithm(hashAlgorithm); + DigestAlgorithm digestAlgorithm = DssMappings.toDigestAlgorithm(hashAlgorithm); if (digestAlgorithm == null) { - LOGGER.severe(RES.get("console.dss.unsupportedHash", hashAlgorithm.getAlgorithmName())); - return false; + // A deliberate selection of a non-PAdES digest is rejected by EngineMismatchValidator + // before we get here; reaching this branch means the legacy default (SHA-1) leaked + // through, so upgrade it transparently rather than failing on bare defaults. + if (options.isAdvanced() && options.isHashAlgorithmSet()) { + LOGGER.severe(RES.get("console.dss.unsupportedHash", hashAlgorithm.getAlgorithmName())); + return false; + } + LOGGER.info(RES.get("console.dss.hashUpgrade", hashAlgorithm.getAlgorithmName())); + digestAlgorithm = DigestAlgorithm.SHA256; } try (PrivateKeySignatureToken token = new PrivateKeySignatureToken(key, chain)) { diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java index 31878c4d..6da50d08 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -129,6 +129,17 @@ public void defaultLevelProducesBaselineB() throws Exception { assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); } + @Test + public void defaultHashSignsSuccessfully() throws Exception { + BasicSignerOptions o = baseOptions(); + // no explicit hash -> the global default (SHA-256) is a valid PAdES digest, so signing just works. + // The engine still carries a defensive upgrade for any non-PAdES default that might leak through. + o.setHashAlgorithm((net.sf.jsignpdf.types.HashAlgorithm) null); + assertFalse("precondition: hash is the unset default", o.isHashAlgorithmSet()); + assertTrue("signing with the default hash must succeed", new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_B); + } + @Test public void explicitBaselineBProducesBaselineB() throws Exception { BasicSignerOptions o = baseOptions(); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java index fca0b31b..37a9f22c 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java @@ -82,11 +82,15 @@ public static List findMismatches(BasicSignerOptions o, SigningEngine final List out = new ArrayList<>(); final var caps = engine.capabilities(); - // hash algorithm - final HashAlgorithm hash = o.getHashAlgorithmX(); - final Capability hashCap = HASH_CAPS.get(hash); - if (hashCap != null && !caps.contains(hashCap)) { - out.add(new Mismatch("--hash-algorithm", hashCap)); + // hash algorithm: only a deliberate (advanced-mode) selection is a user choice the engine must + // honour. The global default (SHA-1) is not — an engine that lacks it transparently upgrades to a + // supported digest, so signing with bare defaults must not fail fast here. + if (o.isAdvanced() && o.isHashAlgorithmSet()) { + final HashAlgorithm hash = o.getHashAlgorithmX(); + final Capability hashCap = HASH_CAPS.get(hash); + if (hashCap != null && !caps.contains(hashCap)) { + out.add(new Mismatch("--hash-algorithm", hashCap)); + } } // PAdES baseline level diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java index 95e3f46b..4f010df5 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java @@ -25,10 +25,24 @@ private static Set caps(List mismatches) { } @Test - public void emptyCapabilityEngineFlagsDefaultHashButNotAppend() { - // A fresh options object signs with the default SHA-1 hash and append (incremental) mode on. - // Append is universal, so only the hash is flagged against a capability-less engine. + public void defaultsProduceNoHashOrOverwriteMismatch() { + // A fresh options object carries the global default hash (never explicitly chosen) and append + // (incremental) mode on. An unchosen default is the engine's problem to honour/upgrade, and + // incremental append is universal — so a capability-less engine flags neither. BasicSignerOptions opts = new BasicSignerOptions(); + opts.setAdvanced(true); + StubSigningEngine engine = new StubSigningEngine("empty"); + Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); + assertFalse("default hash must not be treated as a user choice", reported.contains(Capability.HASH_SHA1)); + assertFalse("incremental append is universal", reported.contains(Capability.OVERWRITE_MODE)); + } + + @Test + public void explicitlyChosenUnsupportedHashIsFlagged() { + // When the user deliberately selects SHA-1 in advanced mode, an engine that lacks it must fail fast. + BasicSignerOptions opts = new BasicSignerOptions(); + opts.setAdvanced(true); + opts.setHashAlgorithm(net.sf.jsignpdf.types.HashAlgorithm.SHA1); StubSigningEngine engine = new StubSigningEngine("empty"); Set reported = caps(EngineMismatchValidator.findMismatches(opts, engine)); assertTrue(reported.contains(Capability.HASH_SHA1)); diff --git a/pom.xml b/pom.xml index bef8dff7..c5baea59 100644 --- a/pom.xml +++ b/pom.xml @@ -204,6 +204,17 @@ org.apache.maven.plugins maven-surefire-plugin ${maven.surefire.plugin.version} + + + + ${project.build.directory}/test-config + + org.apache.maven.plugins From c7deb05ae1f2dc6fe3b5cddcd460d16c007cd9a3 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 13:49:21 +0200 Subject: [PATCH 11/18] Append as default and the new --overwrite flag --- distribution/doc/release-notes/3.1.0.md | 13 +++++---- .../main/java/net/sf/jsignpdf/Constants.java | 2 ++ .../jsignpdf/translations/messages.properties | 3 ++- .../sf/jsignpdf/SignerOptionsFromCmdLine.java | 6 ++++- .../engine/EngineMismatchValidator.java | 7 ++--- .../fx/view/MainWindowController.java | 21 ++++++++------- .../view/SignaturePropertiesController.java | 24 +++++++++++++++++ .../SignerOptionsFromCmdLineTest.java | 27 +++++++++++++++++++ .../engine/EngineMismatchValidatorTest.java | 2 +- 9 files changed, 85 insertions(+), 20 deletions(-) diff --git a/distribution/doc/release-notes/3.1.0.md b/distribution/doc/release-notes/3.1.0.md index e60146a3..c2ba154e 100644 --- a/distribution/doc/release-notes/3.1.0.md +++ b/distribution/doc/release-notes/3.1.0.md @@ -28,11 +28,10 @@ runtime. is unchanged. The cross-platform ZIPs use `bin/jsignpdf.sh` / `bin\jsignpdf.cmd` launchers (and `com.intoolswetrust.jsignpdf.Bootstrap` for runtime checks + JavaFX classifier selection) instead. -- **`jsignpdf-3.1.0-SHA256SUMS.txt`** covers every release artifact. -- **Pluggable signing engines (phase 1).** The signing backend is now - selectable. JSignPdf ships the **OpenPDF** engine as the default, and - its output is byte-for-byte identical to previous releases, so nothing - changes by default. New `--list-engines` and `-eng` / `--engine` CLI +- **`jsignpdf--SHA256SUMS.txt`** covers every release artifact. +- **Pluggable signing engines.** The signing backend is now + selectable. JSignPdf ships the **OpenPDF** engine as the default. + New `--list-engines` and `-eng` / `--engine` CLI options, plus an *Engine* selector in the JavaFX Preferences, let you pick the engine; the choice is stored in `advanced.properties` (`engine=openpdf`). Engines are discovered via `ServiceLoader` from @@ -52,6 +51,10 @@ runtime. `-ha`) is untouched, and SHA-1 stays selectable for the OpenPDF engine. Unattended/scripted signing without `-ha` will now produce SHA-256 digests. +- **CLI signatures now append by default**, matching the GUI; use the + new `--overwrite` flag to replace existing signatures (the legacy + `--append`/`-a` flag is kept as a no-op). Overwrite is rejected by the + PAdES `dss` engine, which always signs incrementally. - The CLI `usage:` line and bundled examples now use `jsignpdf` as the command name (instead of `java -jar JSignPdf.jar`), matching the launcher you actually invoke. diff --git a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java index 41010f0e..da3a0348 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/Constants.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/Constants.java @@ -298,6 +298,8 @@ public class Constants { public static final String ARG_APPEND = "a"; public static final String ARG_APPEND_LONG = "append"; + public static final String ARG_OVERWRITE_LONG = "overwrite"; + public static final String ARG_QUIET_LONG = "quiet"; public static final String ARG_QUIET = "q"; diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 19bbf232..35987211 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -187,7 +187,8 @@ gui.vs.urx.label=Upper right X gui.vs.urx.tooltip=Value on Axis X for position of an upper right corner of signature for placing it on a page. gui.vs.ury.label=Upper right Y gui.vs.ury.tooltip=Value on Axis Y for position of an upper right corner of signature for placing it on a page. -hlp.append=add signature to existing ones. By default are existing signatures replaced by the new one. +hlp.append=add the signature to existing ones (incremental update). This is the default and kept for backward compatibility. +hlp.overwrite=replace existing signatures by rewriting the document (non-incremental). Not supported by all engines (e.g. the DSS/PAdES engine always appends). hlp.bgPath=background image path for visible signatures hlp.bgScale=background image scale for visible signatures. Insert positive value to multiply image size with the value. Insert zero value to fill whole background with it (stretch). Insert negative value to best fit resize. hlp.certLevel=level of certification. Default value is NOT_CERTIFIED. Available values are {0} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java index ce93061c..398ea131 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/SignerOptionsFromCmdLine.java @@ -140,7 +140,10 @@ public void loadCmdLine() throws ParseException { setLocation(line.getOptionValue(ARG_LOCATION)); if (line.hasOption(ARG_CONTACT)) setContact(line.getOptionValue(ARG_CONTACT)); - setAppend(line.hasOption(ARG_APPEND)); + // Append (incremental) is the safe default and matches the GUI; --overwrite opts into a + // non-incremental rewrite (only honoured by engines with the OVERWRITE_MODE capability). The + // legacy --append flag is kept for backward compatibility and is now a no-op (append is implied). + setAppend(!line.hasOption(ARG_OVERWRITE_LONG)); if (line.hasOption(ARG_CERT_LEVEL)) setCertLevel(line.getOptionValue(ARG_CERT_LEVEL)); if (line.hasOption(ARG_HASH_ALGORITHM)) @@ -373,6 +376,7 @@ private float getFloat(Object aVal, float aDefVal) { OPTS.addOption(OptionBuilder.withLongOpt(ARG_CONTACT_LONG).withDescription(RES.get("hlp.contact")).hasArg() .withArgName("contact").create(ARG_CONTACT)); OPTS.addOption(OptionBuilder.withLongOpt(ARG_APPEND_LONG).withDescription(RES.get("hlp.append")).create(ARG_APPEND)); + OPTS.addOption(OptionBuilder.withLongOpt(ARG_OVERWRITE_LONG).withDescription(RES.get("hlp.overwrite")).create()); OPTS.addOption(OptionBuilder.withLongOpt(ARG_CERT_LEVEL_LONG) .withDescription(RES.get("hlp.certLevel", getEnumValues(CertificationLevel.values()))).hasArg() .withArgName("level").create(ARG_CERT_LEVEL)); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java index 37a9f22c..fd7720aa 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java @@ -101,10 +101,11 @@ public static List findMismatches(BasicSignerOptions o, SigningEngine } } - // overwrite (non-incremental) mode — incremental append is universal, so only a request to - // overwrite the document (append disabled) against an engine that can't do it is a mismatch. + // overwrite (non-incremental) mode — incremental append is the default and universal, so only an + // explicit --overwrite request against an engine that can't do it (e.g. DSS, which PAdES forces to + // be incremental) is a mismatch. The request is deliberate, so fail fast rather than silently append. if (!o.isAppendX() && !caps.contains(Capability.OVERWRITE_MODE)) { - out.add(new Mismatch("--append", Capability.OVERWRITE_MODE)); + out.add(new Mismatch("--overwrite", Capability.OVERWRITE_MODE)); } // certification level 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 a0a540f1..ae3a14d2 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 @@ -446,15 +446,18 @@ private void setupEngineCapabilityGating() { Capability.ENCRYPTION_CERTIFICATE); } - // TODO(phase-2): field-level capability gating is still missing for controls that live inside - // the side-panel sub-controllers and already carry their own disable logic — hash algorithm, - // certification level, append mode, render-mode items, permission checkboxes, proxy fields, and - // the PKCS#11/CloudFoxy keystore-type items (see the control->capability table in - // design-doc/3.1-signing-engines.md). The CLI path is already comprehensive via - // EngineMismatchValidator, which is the authoritative table to mirror here. This asymmetry is - // invisible while OpenPDF (all capabilities) is the only engine; it must be closed when the - // first reduced-capability engine (DSS, see design-doc/3.1-engine-dss.md) lands so the GUI does - // not leave enabled options the engine will reject at sign time. + // Field-level gating for controls that live inside side-panel sub-controllers. The append toggle + // is the first of these: an engine without OVERWRITE_MODE (DSS) always appends, so the checkbox is + // forced on and disabled there. + if (signaturePropertiesController != null) { + signaturePropertiesController.gateCapabilities(engineCapabilities); + } + + // TODO(phase-2): field-level capability gating is still missing for the remaining sub-controller + // controls that carry their own disable logic — hash algorithm, certification level, render-mode + // items, permission checkboxes, proxy fields, and the PKCS#11/CloudFoxy keystore-type items (see + // the control->capability table in design-doc/3.1-signing-engines.md). The CLI path is already + // comprehensive via EngineMismatchValidator, which is the authoritative table to mirror here. } /** diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignaturePropertiesController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignaturePropertiesController.java index 90d83e2d..2f676e43 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignaturePropertiesController.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/SignaturePropertiesController.java @@ -9,6 +9,9 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; +import net.sf.jsignpdf.engine.Capability; +import net.sf.jsignpdf.engine.SigningEngine; +import net.sf.jsignpdf.fx.EngineCapabilities; import net.sf.jsignpdf.fx.util.NativeFileChooser; import net.sf.jsignpdf.fx.util.NativeFileChooser.ExtensionFilter; import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; @@ -56,6 +59,27 @@ private void bindToViewModel() { txtOutFile.textProperty().bindBidirectional(viewModel.outFileProperty()); } + /** + * Gates the "append signature" checkbox against the active engine's capabilities. Unchecking the box + * requests an overwrite (non-incremental rewrite), which needs {@link Capability#OVERWRITE_MODE}. + * Engines that lack it (e.g. the DSS/PAdES engine) always append, so the box is forced on and disabled + * with the shared "not supported" tooltip while such an engine is active. This mirrors the CLI + * fail-soft handled by {@link net.sf.jsignpdf.engine.EngineMismatchValidator}. + * + * @param caps the capability source driving the gating; must be wired after {@link #setViewModel} + */ + public void gateCapabilities(EngineCapabilities caps) { + caps.gate(chkAppend, Capability.OVERWRITE_MODE); + enforceAppendForEngine(caps.activeEngineProperty().get()); + caps.activeEngineProperty().addListener((obs, oldEngine, newEngine) -> enforceAppendForEngine(newEngine)); + } + + private void enforceAppendForEngine(SigningEngine engine) { + if (engine != null && !engine.capabilities().contains(Capability.OVERWRITE_MODE)) { + viewModel.appendProperty().set(true); + } + } + @FXML private void onBrowseOutFile() { File file = new NativeFileChooser() diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/SignerOptionsFromCmdLineTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/SignerOptionsFromCmdLineTest.java index b3eaef64..fd6d1091 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/SignerOptionsFromCmdLineTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/SignerOptionsFromCmdLineTest.java @@ -305,6 +305,33 @@ public void padesLevelOption_absentLeavesNull() throws Exception { assertNull(f.opts.getPadesLevel()); } + @Test + public void append_isDefaultWhenNeitherFlagGiven() throws Exception { + // Incremental append is the safe default and matches the GUI; omitting both flags must not request + // an overwrite (which a PAdES engine like DSS cannot honour). + Fixture f = new Fixture(""); + f.opts.setCmdLine(new String[] { "-ksf", "/tmp/x.p12" }); + f.opts.loadCmdLine(); + assertTrue(f.opts.isAppend()); + } + + @Test + public void overwriteFlag_disablesAppend() throws Exception { + Fixture f = new Fixture(""); + f.opts.setCmdLine(new String[] { "-ksf", "/tmp/x.p12", "--overwrite" }); + f.opts.loadCmdLine(); + assertFalse(f.opts.isAppend()); + } + + @Test + public void legacyAppendFlag_stillKeepsAppendOn() throws Exception { + // --append is now redundant (append is the default) but must remain a harmless no-op for scripts. + Fixture f = new Fixture(""); + f.opts.setCmdLine(new String[] { "-ksf", "/tmp/x.p12", "--append" }); + f.opts.loadCmdLine(); + assertTrue(f.opts.isAppend()); + } + /** Convenience wiring: captures warnings and feeds a canned stdin reader with no Console. */ private static final class Fixture { final SignerOptionsFromCmdLine opts = new SignerOptionsFromCmdLine(); diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java index 4f010df5..7105e32c 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/engine/EngineMismatchValidatorTest.java @@ -61,7 +61,7 @@ public void overwriteFlaggedWhenEngineLacksCapability() { assertTrue(reported.contains(Capability.OVERWRITE_MODE)); Mismatch m = mismatches.stream().filter(x -> x.capability() == Capability.OVERWRITE_MODE) .findFirst().orElseThrow(); - assertEquals("--append", m.option()); + assertEquals("--overwrite", m.option()); } @Test From 07aba585bb706c5137db506937600e4b36090370 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 13:54:54 +0200 Subject: [PATCH 12/18] docs --- website/docs/JSignPdf.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 9752fac4..7ac50bb9 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -932,7 +932,7 @@ The default engine is taken from the `engine` key in `/advanced.prop jsignpdf --engine openpdf -ksf cert.p12 -ksp secret document.pdf ---- -In the JavaFX UI, the active engine is chosen from the *Engine* selector in the toolbar; the choice is saved to `advanced.properties` immediately. When an engine does not support a particular option, the corresponding control is disabled (with an explanatory tooltip); on the command line, signing fails fast with a message listing the unsupported options. +In the JavaFX UI, the active engine is chosen from the *Engine* selector in the Preferences; the choice is saved to `advanced.properties` immediately. When an engine does not support a particular option, the corresponding control is disabled (with an explanatory tooltip); on the command line, signing fails fast with a message listing the unsupported options. Engines are discovered automatically: dropping a third-party engine jar (with its dependencies and a `META-INF/services/net.sf.jsignpdf.engine.SigningEngine` registration) into the `lib/` directory of an installed JSignPdf makes it appear in `--list-engines` and the toolbar selector. @@ -957,7 +957,7 @@ JSignPdf 3.1 bundles a second engine, *EU DSS (PAdES)* (id `dss`), built on the |`LT` + an archive timestamp. |=== -Select it with `-eng dss` on the command line (or the *Engine* toolbar selector in the GUI) and pick the level with `-pl` / `--pades-level`: +Select it with `-eng dss` on the command line (or the *PAdES Level* sidebar section in the GUI) and pick the level with `-pl` / `--pades-level`: [source,shell] ---- From cb5519a8d4c9d4501993fbef63eeefe2cd8cc733 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 15:55:43 +0200 Subject: [PATCH 13/18] Doc update --- AGENTS.md | 52 ++++++++++++++++++++++++++++---------- README.md | 2 +- website/docs/JSignPdf.adoc | 13 ++++++---- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 46b13816..fe689cb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,13 @@ jsignpdf-root/ ├── jsignpdf-bootstrap/ # Java-8 launcher (Bootstrap.java): JRE-version check + JFX │ classifier picker + reflective Signer.main. Used by the │ cross-platform ZIPs (bin/jsignpdf.sh|.cmd → Bootstrap → Signer). -├── jsignpdf/ # Main application (signing logic + GUI + CLI) +├── engines/ # Signing-engine SPI + bundled engines (Maven parent module) +│ ├── api/ # Engine SPI (SigningEngine, Capability) + the shared core model +│ │ (BasicSignerOptions, Constants, types/) and the canonical +│ │ messages*.properties resource bundle. +│ ├── openpdf/ # Default OpenPDF engine (id `openpdf`) — unchanged signatures. +│ └── dss/ # EU DSS PAdES engine (id `dss`) — PAdES B/T/LT/LTA. +├── jsignpdf/ # Main application (GUI + CLI + engine discovery/gating) ├── installcert/ # Certificate installer utility ├── distribution/ # Per-platform packaging — assembles full + minimal ZIPs and │ drives jpackage for MSI / DEB / RPM / DMG (windows/, linux/, @@ -41,21 +47,43 @@ Keep these in sync with the code; user-facing features are not "done" until they | `website/docs/JSignPdf.adoc` | **Authoritative user guide.** Single source of truth consumed by both the Hugo site (`website/content/docs/guide/index.adoc` is regenerated from it by `website/prepare.sh`) and the Maven PDF build in `distribution/`. | Any new or changed user-visible feature: new CLI flags, new GUI panels, changed defaults, new keystore types, new exit codes, etc. Update the synopsis block, the relevant option table, and add a dedicated subsection if the feature has non-obvious usage. | | `distribution/doc/release-notes/.md` | Release notes bundled with the artifact and used as the GitHub Release body. | Every release-worthy change. | | `README.md` | Top-level project landing page. Concise feature overview + pointers to the guide. | Only for high-signal changes (new feature categories, platform support, install paths). | -| `jsignpdf/src/main/resources/net/sf/jsignpdf/translations/messages.properties` | Canonical English resource bundle (all others are Weblate-synced). CLI `--help` text comes from here. | Any new CLI option or GUI string. Do not hand-edit non-English `messages_*.properties` files. | +| `engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties` | Canonical English resource bundle (all others are Weblate-synced). CLI `--help` text comes from here. Lives in the `engines/api` module so both the engines and the app can read it. | Any new CLI option or GUI string. Do not hand-edit non-English `messages_*.properties` files. | | `design-doc/-.md` | Design notes for larger changes. | Before implementing a non-trivial feature. | When a PR touches a user-visible feature without updating `JSignPdf.adoc`, flag it as incomplete. ## Source Code Layout -All source is under `jsignpdf/src/main/java/net/sf/jsignpdf/`: +Java code lives in two places under the `net.sf.jsignpdf` package root: the shared core + +engine SPI in `engines/api/` (and the bundled engines in `engines/openpdf/` and `engines/dss/`), +and the application (GUI + CLI) in `jsignpdf/`. + +**`engines/api/src/main/java/net/sf/jsignpdf/`** — shared core model + engine SPI (no UI deps): ``` net.sf.jsignpdf -├── Signer.java # Entry point - launches CLI or GUI -├── SignerLogic.java # Core signing engine (no UI dependencies) ├── BasicSignerOptions.java # Central model for all signing configuration +├── Constants.java # CLI arg names, default values, config keys +├── JSignEncryptor.java # PDF encryption helper +├── engine/ # Engine SPI: SigningEngine, Capability, EngineConfig +├── extcsp/ # External crypto providers (CloudFoxy) +├── ssl/ # SSL/TLS initialization +├── types/ # Enums and value types (HashAlgorithm, ...) +└── utils/ # KeyStoreUtils, ResourceProvider, PropertyProvider, etc. +``` + +The bundled engines register via `META-INF/services/net.sf.jsignpdf.engine.SigningEngine`: +`engines/openpdf/` (id `openpdf`, default) and `engines/dss/` (id `dss`, PAdES). + +**`jsignpdf/src/main/java/net/sf/jsignpdf/`** — application (GUI + CLI + engine wiring): + +``` +net.sf.jsignpdf +├── Signer.java # Entry point - launches CLI or GUI +├── SignerLogic.java # Drives a SigningEngine (no UI dependencies) +├── SignerOptionsFromCmdLine.java # CLI parsing into BasicSignerOptions ├── SignPdfForm.java # Swing GUI (legacy, .form files are IDE-generated) +├── engine/ # EngineRegistry (discovery), EngineMismatchValidator (CLI gating) ├── fx/ # JavaFX GUI (default) │ ├── JSignPdfApp.java # Application entry point │ ├── FxLauncher.java # Static launcher called from Signer.main() @@ -64,24 +92,22 @@ net.sf.jsignpdf │ ├── service/ # PdfRenderService, SigningService, KeyStoreService │ ├── control/ # PdfPageView, SignatureOverlay │ └── util/ # FxResourceProvider, SwingFxImageConverter, RecentFilesManager -├── crl/ # Certificate Revocation List handling -├── extcsp/ # External crypto providers (CloudFoxy) ├── preview/ # PDF page rendering (Pdf2Image) -├── ssl/ # SSL/TLS initialization -├── types/ # Enums and value types -└── utils/ # KeyStoreUtils, ResourceProvider, PropertyProvider, etc. +├── types/ # App-only enums and value types +└── utils/ # App-only helpers ``` ## Architecture ``` CLI (SignerOptionsFromCmdLine) ──┐ - ├──> BasicSignerOptions ──> SignerLogic.signFile() -GUI (JavaFX / Swing) ──┘ (model) (signing engine) + ├──> BasicSignerOptions ──> SignerLogic.signFile() ──> SigningEngine +GUI (JavaFX / Swing) ──┘ (model) (orchestration) (openpdf | dss) ``` - **`BasicSignerOptions`** is the central model. Both CLI and GUI populate it, then pass it to `SignerLogic`. -- **`SignerLogic`** is the signing engine. It has no UI dependencies. +- **`SignerLogic`** has no UI dependencies. It resolves a `SigningEngine` (via `EngineRegistry`) and delegates the actual signing to it. +- **Signing engines** are discovered with `ServiceLoader`. Each declares a set of `Capability` values; the CLI fails fast on unsupported options via `EngineMismatchValidator`, and the GUI disables the matching controls. - **JavaFX GUI** uses MVVM: ViewModels with JavaFX properties, FXML views with `%key` i18n, background services wrapping `SignerLogic` and `Pdf2Image`. ### i18n diff --git a/README.md b/README.md index 9cb0cc86..109c90dc 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Project home page: [jsignpdf.eu](https://jsignpdf.eu/) - **Timestamping**: RFC 3161 TSA with optional user/password authentication. - **Revocation info**: CRL and OCSP embedding for LTV workflows. - **Visible signatures**: customizable layout with `${signer}`, `${timestamp}`, and other placeholders. -- **Signing engines (OpenPDF / PAdES via DSS)**: pluggable signing backend, selectable per run (`-eng` / toolbar). The default **OpenPDF** engine is unchanged; the bundled **EU DSS** engine produces PAdES signatures at baseline levels B / T / LT / LTA (`--pades-level`). +- **Signing engines (OpenPDF / PAdES via DSS)**: pluggable signing backend, selectable per run (`-eng` / Preferences). The default **OpenPDF** engine is unchanged; the bundled **EU DSS** engine produces PAdES signatures at baseline levels B / T / LT / LTA (`--pades-level`). - **Internationalization**: maintained via [Weblate](https://hosted.weblate.org/projects/jsignpdf/messages/) (15+ languages). ## Install diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 7ac50bb9..73123a27 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -323,7 +323,7 @@ usage: jsignpdf [file1.pdf [file2.pdf ...]] [-a] [--bg-path ] [-l ] [--l2-text ] [--l4-text ] [-le] [-lk] [-lkt] [-llx ] [-lly ] [-lp] [-lpf ] [--ocsp] [--ocsp-server-url ] [-op ] [-opwd ] [-os - ] [-pe ] [-pg ] [-pr ] [--proxy-host + ] [--overwrite] [-pe ] [-pg ] [-pr ] [--proxy-host ] [--proxy-port ] [-pl ] [--proxy-type ] [-q] [-r ] [--render-mode ] [-sn ] [-ta ] [-ts ] [--tsa-policy-oid ] [-tscf ] [-tscp ] [-tsct @@ -407,10 +407,13 @@ See <> for details. |Option |Description | `-a, --append` -| Append signature to existing ones instead of replacing them. +| Append the signature to existing ones (incremental update). This is the default since JSignPdf 3.1, so the flag is kept only for backward compatibility and is now a no-op. + +| `--overwrite` +| Replace existing signatures by rewriting the document (non-incremental). Opt-out of the default append behaviour. Rejected by the DSS (PAdES) engine, which always signs incrementally -- see <>. | `-ha, --hash-algorithm ` -| Hash algorithm for the signature. Default: `SHA1`. Values: `SHA1`, `SHA256`, `SHA384`, `SHA512`, `RIPEMD160`. +| Hash algorithm for the signature. Default: `SHA256`. Values: `SHA1`, `SHA256`, `SHA384`, `SHA512`, `RIPEMD160`. (SHA-1 remains selectable for the OpenPDF engine; the DSS engine accepts only `SHA256`/`SHA384`/`SHA512`.) | `-cl, --certification-level ` | Certification level. Default: `NOT_CERTIFIED`. Values: `NOT_CERTIFIED`, `CERTIFIED_NO_CHANGES_ALLOWED`, `CERTIFIED_FORM_FILLING`, `CERTIFIED_FORM_FILLING_AND_ANNOTATIONS`. @@ -767,7 +770,7 @@ The reason, location, and contact fields provide additional information about th ==== Append signature -JSignPdf can work in two signing modes. When _Append signature_ is enabled, the new signature is appended and any previously-existing signatures stay unchanged; when it is disabled, existing signatures are replaced. The GUI enables _Append signature_ by default; on the CLI the default is replace (pass `-a` to append). _*This option is disabled for encrypted documents.*_ +JSignPdf can work in two signing modes. When _Append signature_ is enabled, the new signature is appended and any previously-existing signatures stay unchanged; when it is disabled, the existing signatures are replaced by rewriting the document. Append is the default in both the GUI and -- since JSignPdf 3.1 -- the CLI; on the command line, pass `--overwrite` to replace instead (the legacy `-a` / `--append` flag is now a no-op, kept for backward compatibility). The DSS (PAdES) engine always signs incrementally and rejects `--overwrite`. _*This option is disabled for encrypted documents.*_ ==== Certification level @@ -934,7 +937,7 @@ jsignpdf --engine openpdf -ksf cert.p12 -ksp secret document.pdf In the JavaFX UI, the active engine is chosen from the *Engine* selector in the Preferences; the choice is saved to `advanced.properties` immediately. When an engine does not support a particular option, the corresponding control is disabled (with an explanatory tooltip); on the command line, signing fails fast with a message listing the unsupported options. -Engines are discovered automatically: dropping a third-party engine jar (with its dependencies and a `META-INF/services/net.sf.jsignpdf.engine.SigningEngine` registration) into the `lib/` directory of an installed JSignPdf makes it appear in `--list-engines` and the toolbar selector. +Engines are discovered automatically: dropping a third-party engine jar (with its dependencies and a `META-INF/services/net.sf.jsignpdf.engine.SigningEngine` registration) into the `lib/` directory of an installed JSignPdf makes it appear in `--list-engines` and the Preferences engine selector. === PAdES & the DSS engine From 7729f537172483c653e6c94770790988cde7e699 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 16:00:12 +0200 Subject: [PATCH 14/18] PAdES support on website --- website/content/_index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/content/_index.md b/website/content/_index.md index abf9ed24..307b7dbc 100644 --- a/website/content/_index.md +++ b/website/content/_index.md @@ -66,6 +66,10 @@ toc: false icon="key" title="Hardware tokens" subtitle="Sign with PKCS#11 smart cards, HSMs and USB tokens — not just soft keys." >}} + {{< hextra/feature-card + icon="cube" + title="Signing engines & PAdES" + subtitle="Pluggable backend: the default OpenPDF engine, plus an EU DSS engine for PAdES baseline signatures (B / T / LT / LTA)." >}} {{< hextra/feature-card icon="sparkles" title="Free and open source" From ab227da5c3b491f40fc99265a27dc74a350c9e54 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 19 Jun 2026 23:52:35 +0200 Subject: [PATCH 15/18] fixes --- .../net/sf/jsignpdf/types/PadesLevel.java | 4 +- .../jsignpdf/translations/messages.properties | 2 +- .../sf/jsignpdf/engine/dss/DssFontUtils.java | 4 +- .../jsignpdf/engine/dss/DssSigningEngine.java | 26 +++++++----- .../engine/dss/DssTrustConfigurer.java | 40 ++++++++++++++++++- .../engine/dss/DssSigningEngineTest.java | 33 ++++++++++++++- 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java b/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java index afebb131..c1e99386 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/types/PadesLevel.java @@ -1,5 +1,7 @@ package net.sf.jsignpdf.types; +import java.util.Locale; + /** * PAdES baseline signature levels (ETSI EN 319 142). This is the single new per-document field the * DSS engine needs; it is selected by the user through {@code --pades-level} / the FX dropdown and @@ -36,7 +38,7 @@ public static PadesLevel fromString(String value) { if (value == null) { return null; } - String v = value.trim().toUpperCase(java.util.Locale.ENGLISH); + String v = value.trim().toUpperCase(Locale.ENGLISH); switch (v) { case "B": return BASELINE_B; diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 35987211..82724bd1 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -40,8 +40,8 @@ console.dss.ltNoTsa=The PAdES level LT/LTA builds on a signature timestamp, but console.dss.trustConfigFailed=Failed to load the configured DSS trust material (truststore / certificate file / certificate URL / LOTL). Check the engine.dss.trust.* settings. Signing aborted. console.dss.socksProxyUnsupported=The DSS engine does not support SOCKS proxies; OCSP/CRL/AIA/TSA requests will bypass the proxy. Configure an HTTP proxy to route DSS revocation/timestamp traffic. console.dss.unsupportedHash=The DSS engine does not support the hash algorithm ''{0}'' for PAdES. Use SHA-256, SHA-384 or SHA-512. -console.dss.hashUpgrade=The default hash algorithm ''{0}'' is not a PAdES digest, using SHA-256 instead. console.dss.cannotEncryptSigned=Cannot encrypt a PDF that already contains signatures. +console.dss.fontLoadFailed=Loading the visible-signature font failed: {0} console.getPrivateKey=Loading private key console.inFileNotFound.error=Input PDF was not found or it's not readable. console.keys=Key aliases in the keystore: diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java index d22eca0e..3b02d482 100644 --- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssFontUtils.java @@ -45,8 +45,8 @@ static DSSFont getVisibleSignatureFont() { return new DSSFileFont(new InMemoryDocument(is.readAllBytes())); } } catch (Exception e) { - Constants.LOGGER.log(Level.SEVERE, "Font loading failed" + (StringUtils.isNotEmpty(fontPath) ? ": " + fontPath : ""), - e); + final String fontSource = StringUtils.isNotEmpty(fontPath) ? fontPath : DEFAULT_EMBEDDED_FONT_PATH; + Constants.LOGGER.log(Level.SEVERE, Constants.RES.get("console.dss.fontLoadFailed", fontSource), e); } return null; } 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 b00c4713..097fa373 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 @@ -141,17 +141,14 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC } final HashAlgorithm hashAlgorithm = options.getHashAlgorithmX(); - DigestAlgorithm digestAlgorithm = DssMappings.toDigestAlgorithm(hashAlgorithm); + final DigestAlgorithm digestAlgorithm = DssMappings.toDigestAlgorithm(hashAlgorithm); if (digestAlgorithm == null) { - // A deliberate selection of a non-PAdES digest is rejected by EngineMismatchValidator - // before we get here; reaching this branch means the legacy default (SHA-1) leaked - // through, so upgrade it transparently rather than failing on bare defaults. - if (options.isAdvanced() && options.isHashAlgorithmSet()) { - LOGGER.severe(RES.get("console.dss.unsupportedHash", hashAlgorithm.getAlgorithmName())); - return false; - } - LOGGER.info(RES.get("console.dss.hashUpgrade", hashAlgorithm.getAlgorithmName())); - digestAlgorithm = DigestAlgorithm.SHA256; + // SHA-1 / RIPEMD-160 are not valid PAdES digests. The default (SHA-256) maps fine, so + // reaching here means a non-PAdES digest was explicitly selected in advanced mode. + // EngineMismatchValidator normally rejects this earlier; fail fast here too rather than + // silently substituting a digest. + LOGGER.severe(RES.get("console.dss.unsupportedHash", hashAlgorithm.getAlgorithmName())); + return false; } try (PrivateKeySignatureToken token = new PrivateKeySignatureToken(key, chain)) { @@ -212,6 +209,15 @@ public boolean sign(final BasicSignerOptions options, final EngineConfig engineC return false; } effectiveInFile = encryptedTempFile; + // DSS must open the temp with the password encryptPdf just used (not necessarily the + // input-decrypt password set above). PDFBox treats an empty owner password as "owner = + // user password", so when the owner password is empty fall back to the user password; + // otherwise DSS cannot decrypt the temp at all (the owner-empty / user-password-only case). + final String encOwnerPwd = options.getPdfOwnerPwdStrX(); + final String openPwd = StringUtils.isNotEmpty(encOwnerPwd) ? encOwnerPwd : options.getPdfUserPwdStr(); + if (StringUtils.isNotEmpty(openPwd)) { + parameters.setPasswordProtection(openPwd.toCharArray()); + } } final DSSDocument document = new FileDocument(effectiveInFile); 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 21da5c38..d94aa573 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 @@ -3,11 +3,16 @@ import java.io.File; import java.io.InputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import net.sf.jsignpdf.Constants; import net.sf.jsignpdf.engine.EngineConfig; +import net.sf.jsignpdf.utils.ConfigLocationResolver; import org.apache.commons.lang3.StringUtils; @@ -56,6 +61,16 @@ final class DssTrustConfigurer { /** Separator for the list-valued keys (lotlUrls / certFiles / certUrls). */ private static final String LIST_SEPARATOR = "[,;]+"; + /** Subdirectory (under the JSignPdf config dir, else the system temp dir) holding the cached trusted lists. */ + private static final String TL_CACHE_DIR_NAME = "dss-tl-cache"; + + /** + * How long a cached trusted-list / LOTL download stays fresh before DSS re-fetches it (24h). Trusted lists + * change infrequently, so this lets repeat / batch LT/LTA signing reuse the on-disk cache instead of + * re-downloading the LOTL on every {@code sign()} call. + */ + private static final long TL_CACHE_EXPIRATION_MS = 24L * 60 * 60 * 1000; + private final EngineConfig config; DssTrustConfigurer(EngineConfig config) { @@ -103,7 +118,10 @@ private CertificateSource[] createTrustedCertSources() throws Exception { LOTLSource[] lotlSources = getLotlSources(); if (lotlSources.length > 0) { TLValidationJob tlValidationJob = new TLValidationJob(); - tlValidationJob.setOnlineDataLoader(new FileCacheDataLoader(new CommonsDataLoader())); + FileCacheDataLoader onlineDataLoader = new FileCacheDataLoader(new CommonsDataLoader()); + onlineDataLoader.setFileCacheDirectory(tlCacheDirectory()); + onlineDataLoader.setCacheExpirationTime(TL_CACHE_EXPIRATION_MS); + tlValidationJob.setOnlineDataLoader(onlineDataLoader); tlValidationJob.setListOfTrustedListSources(lotlSources); TrustedListsCertificateSource trustedListsCertificateSource = new TrustedListsCertificateSource(); tlValidationJob.setTrustedListCertificateSource(trustedListsCertificateSource); @@ -135,6 +153,26 @@ private CertificateSource[] createTrustedCertSources() throws Exception { return trustedSources.toArray(new CertificateSource[0]); } + /** + * Resolves the directory DSS caches the downloaded trusted lists in: {@code /dss-tl-cache} when a + * JSignPdf config directory is available, otherwise a stable folder under the system temp dir. A persistent + * location (rather than {@link FileCacheDataLoader}'s default temp behaviour) lets the cache survive across + * runs so batch LT/LTA signing reuses it. + */ + private static File tlCacheDirectory() { + Path base = ConfigLocationResolver.getInstance().getConfigDir(); + File cacheDir = base != null + ? base.resolve(TL_CACHE_DIR_NAME).toFile() + : new File(System.getProperty("java.io.tmpdir"), "jsignpdf-" + TL_CACHE_DIR_NAME); + try { + Files.createDirectories(cacheDir.toPath()); + } catch (Exception e) { + // Non-fatal: DSS recreates the directory on demand; log and let signing proceed. + Constants.LOGGER.log(Level.WARNING, "Could not create DSS trusted-list cache directory " + cacheDir, e); + } + return cacheDir; + } + private LOTLSource[] getLotlSources() { List lotlSources = new ArrayList<>(); if (config.getBoolean(KEY_USE_DEFAULT_LOTL, false)) { diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java index 6da50d08..33248fcc 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssSigningEngineTest.java @@ -13,6 +13,7 @@ import net.sf.jsignpdf.BasicSignerOptions; import net.sf.jsignpdf.engine.Capability; import net.sf.jsignpdf.engine.EngineConfig; +import net.sf.jsignpdf.types.PDFEncryption; import net.sf.jsignpdf.types.PadesLevel; import org.apache.pdfbox.Loader; @@ -133,7 +134,6 @@ public void defaultLevelProducesBaselineB() throws Exception { public void defaultHashSignsSuccessfully() throws Exception { BasicSignerOptions o = baseOptions(); // no explicit hash -> the global default (SHA-256) is a valid PAdES digest, so signing just works. - // The engine still carries a defensive upgrade for any non-PAdES default that might leak through. o.setHashAlgorithm((net.sf.jsignpdf.types.HashAlgorithm) null); assertFalse("precondition: hash is the unset default", o.isHashAlgorithmSet()); assertTrue("signing with the default hash must succeed", new DssSigningEngine().sign(o, EMPTY_CONFIG)); @@ -165,6 +165,37 @@ public void explicitBaselineTWithTsaProducesT() throws Exception { assertSignatureLevel(outputFile, SignatureLevel.PAdES_BASELINE_T); } + @Test + public void inPlaceSigningProducesValidFile() throws Exception { + BasicSignerOptions o = baseOptions(); + // -o == input: DSS reads the source lazily and writes to a FileOutputStream on the same path. + // Confirm signing in place still yields a valid, single-signature PAdES file. + o.setOutFile(inputFile.getAbsolutePath()); + assertTrue("in-place signing must succeed", new DssSigningEngine().sign(o, EMPTY_CONFIG)); + assertSignatureLevel(inputFile, SignatureLevel.PAdES_BASELINE_B); + } + + @Test + public void userOnlyEncryptionSignsAndStaysEncrypted() throws Exception { + BasicSignerOptions o = baseOptions(); + // Encrypt-before-sign with a user password only (owner left empty). The engine must open the + // encrypted temp with the exact owner password it used (here the empty owner password) rather + // than relying on PDFBox's implicit empty-password fallback, then still produce a signed, + // still-encrypted output. + o.setPdfEncryption(PDFEncryption.PASSWORD); + o.setPdfUserPwd("userpass"); + assertTrue("signing a user-only-encrypted PDF must succeed", new DssSigningEngine().sign(o, EMPTY_CONFIG)); + + // The output is still encrypted (PDFBox maps an empty owner password to "owner = user password"), + // so it opens with the user password and carries a PAdES signature. A password-less load / the DSS + // validator cannot open it, hence the structural check here rather than assertSignatureLevel(). + try (PDDocument doc = Loader.loadPDF(outputFile, "userpass")) { + assertTrue("output must be encrypted", doc.isEncrypted()); + assertFalse("a signature must be present", doc.getSignatureDictionaries().isEmpty()); + assertEquals("ETSI.CAdES.detached", doc.getSignatureDictionaries().get(0).getSubFilter()); + } + } + @Test public void visibleSignatureIsPlacedOnRequestedPage() throws Exception { BasicSignerOptions o = baseOptions(); From 64f19fcc3e281ef4afc2e4e9cf5b0159c5363647 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Sat, 20 Jun 2026 10:49:33 +0200 Subject: [PATCH 16/18] Small fixes --- .../net/sf/jsignpdf/BasicSignerOptions.java | 29 +++++++++++++++++-- .../sf/jsignpdf/BasicSignerOptionsTest.java | 28 ++++++++++++++++++ website/docs/JSignPdf.adoc | 16 ++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java index 2137ff82..3357dda7 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java @@ -1055,10 +1055,35 @@ public String getTsaHashAlg() { } /** - * @return + * Returns the effective TSA hash algorithm name, falling back to the configured default when none + * is set, normalised to its canonical form. The TSA hash is a free-text value on the CLI + * ({@code --tsa-hash-alg}), the Swing dialog and the properties file, so a lowercase entry such as + * {@code sha256} would otherwise reach iText's {@code setDigestName} (NPE) or DSS's + * {@code DigestAlgorithm.forJavaName} (exception). Canonicalising here gives both engines a value + * they accept. + * + * @return the canonical TSA hash algorithm name (e.g. {@code SHA-256}) */ public String getTsaHashAlgWithFallback() { - return StringUtils.defaultIfBlank(tsaHashAlg, AppConfig.defaultTsaHashAlg()); + return normalizeTsaHashAlg(StringUtils.defaultIfBlank(tsaHashAlg, AppConfig.defaultTsaHashAlg())); + } + + /** + * Normalises a free-text TSA hash algorithm name to its canonical form. Recognised algorithms are + * mapped to their canonical spelling (e.g. {@code sha256} / {@code sha-256} → {@code SHA-256}); + * an unrecognised value is uppercased so it still survives the digest-name lookup in the signing + * engines instead of triggering a {@link NullPointerException}. + * + * @param value the raw hash name (may be {@code null} or blank) + * @return the canonical name, or {@code null} when the input is blank + */ + static String normalizeTsaHashAlg(String value) { + if (StringUtils.isBlank(value)) { + return null; + } + final String trimmed = value.trim(); + final HashAlgorithm known = HashAlgorithm.fromAlgorithmName(trimmed); + return known != null ? known.getAlgorithmName() : trimmed.toUpperCase(Locale.ROOT); } /** diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/BasicSignerOptionsTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/BasicSignerOptionsTest.java index 49f32c9e..2733295c 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/BasicSignerOptionsTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/BasicSignerOptionsTest.java @@ -224,6 +224,34 @@ public void loadOptions_storedFalseValuesArePreserved() { } } + /** + * A free-text TSA hash algorithm (CLI {@code --tsa-hash-alg}, Swing dialog, properties file) must be + * canonicalised before it reaches the signing engines, otherwise a lowercase entry such as + * {@code sha256} triggers an NPE in iText ({@code setDigestName}) or an exception in DSS + * ({@code DigestAlgorithm.forJavaName}). Regression guard for issues #126 / #181. + */ + @Test + public void getTsaHashAlgWithFallback_canonicalisesFreeTextValue() { + BasicSignerOptions opts = new BasicSignerOptions(); + + opts.setTsaHashAlg("sha256"); + assertEquals("SHA-256", opts.getTsaHashAlgWithFallback()); + + opts.setTsaHashAlg(" sha-512 "); + assertEquals("SHA-512", opts.getTsaHashAlgWithFallback()); + + opts.setTsaHashAlg("ripemd160"); + assertEquals("RIPEMD160", opts.getTsaHashAlgWithFallback()); + + // Unrecognised value is at least uppercased so the digest-name lookup does not NPE. + opts.setTsaHashAlg("sha3-256"); + assertEquals("SHA3-256", opts.getTsaHashAlgWithFallback()); + + // Blank falls back to the configured default (canonicalised). + opts.setTsaHashAlg(" "); + assertEquals("SHA-256", opts.getTsaHashAlgWithFallback()); + } + @Test public void createCopyShouldDefensivelyCopyCharArrays() { BasicSignerOptions original = new BasicSignerOptions(); diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 73123a27..028ec9ac 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -841,6 +841,22 @@ The _Acrobat 6 layer mode_ option (enabled by default) allows you to control whi _Signature Text_, _Status Text_, _Image_, and _Background Image_ inputs define the content of fields in a visible signature. _Font Size_ is used for setting the size of _Signature Text_, it should contain a positive decimal number. +The _Signature Text_ supports placeholders that are expanded when the document is signed: + +[cols="1,3"] +|=== +|Placeholder |Expands to + +|`${signer}` |Common name (CN) of the signing certificate. +|`${timestamp}` |Signing date and time -- use this to **show the date/time on the visible signature**. +|`${certificate}` |Description of the signing certificate. +|`${location}` |The _Location_ field value. +|`${reason}` |The _Reason_ field value. +|`${contact}` |The _Contact_ field value. +|=== + +For example, a _Signature Text_ of `Signed by ${signer}` + `on ${timestamp}` renders the signer name and the signing date/time. When _Signature Text_ is left empty, JSignPdf uses a default layout that already includes the signer and the date. + _Background image scale_ defines the size of a background image. Any negative number means the best-fit algorithm will be used. Zero value means to stretch, which fills the whole field -- it doesn't keep the image ratio. A positive value means the multiplicator of the original size. Supported file formats for the _Image_ and _Background Image_ are GIF, JPEG, JPEG2000, PNG, WMF, BMP, and TIFF. From ec7a8bf5002d10b5f0b840c22b91776897793c0b Mon Sep 17 00:00:00 2001 From: Josef Cacek Date: Sun, 21 Jun 2026 13:44:54 +0200 Subject: [PATCH 17/18] Apply suggestion from @kwart --- .../java/net/sf/jsignpdf/engine/EngineMismatchValidator.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java index fd7720aa..6d70586d 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/engine/EngineMismatchValidator.java @@ -82,9 +82,6 @@ public static List findMismatches(BasicSignerOptions o, SigningEngine final List out = new ArrayList<>(); final var caps = engine.capabilities(); - // hash algorithm: only a deliberate (advanced-mode) selection is a user choice the engine must - // honour. The global default (SHA-1) is not — an engine that lacks it transparently upgrades to a - // supported digest, so signing with bare defaults must not fail fast here. if (o.isAdvanced() && o.isHashAlgorithmSet()) { final HashAlgorithm hash = o.getHashAlgorithmX(); final Capability hashCap = HASH_CAPS.get(hash); From 81aa8fec60f202e5d78c155a4ff7830e3ac44ac9 Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Sun, 21 Jun 2026 14:29:31 +0200 Subject: [PATCH 18/18] Update jsignpdf-issues-review.md --- jsignpdf-issues-review.md | 142 +++++++++++++++----------------------- 1 file changed, 55 insertions(+), 87 deletions(-) diff --git a/jsignpdf-issues-review.md b/jsignpdf-issues-review.md index 4789ada5..5672e925 100644 --- a/jsignpdf-issues-review.md +++ b/jsignpdf-issues-review.md @@ -1,9 +1,16 @@ # JSignPdf — Open Issues Review and Action Plan -**Date:** 2026-04-19 -**Scope:** all 47 open issues at https://github.com/intoolswetrust/jsignpdf/issues +**Date:** 2026-04-19 (revised 2026-06-21) +**Scope:** open issues at https://github.com/intoolswetrust/jsignpdf/issues **Baseline:** `master` after OpenPDF 3 / Java 21 migration (commits `97c8fbe`, `b806893`, `158d4a7`) +> **2026-06-21 revision.** Issues that have since been closed on GitHub, and issues +> fully resolved by the EU DSS (PAdES) signing engine PR (#422), have been removed +> from this document. Issues that the DSS engine PR covers only *partially* are kept +> and annotated inline. The original review covered 47 open issues; 19 have been +> removed (10 closed on GitHub, 9 resolved by the DSS engine). With #349 (opened after +> the original review) added, 29 issues are tracked below. + --- ## Review team @@ -35,12 +42,11 @@ All experts reviewed the same 47 issues against the code on disk, flagged duplic ## Executive summary -- **14 issues can be closed** as already fixed by the OpenPDF 3 / Java 21 / JavaFX refresh, as resolved in comments, as duplicates, or as out-of-project-scope user questions. -- **One compliance cluster (6 issues: #27, #46, #95, #141, #247, #254)** dominates the real bug surface. Signatures produced today are not reliably LTV / PAdES B-LT — they claim to be but fail DSS/VRI in Adobe and the EU DSS demo. A focused "LTV hardening" milestone would close the largest and oldest tickets in one sweep. -- **Three long-standing quick wins** (#7 stdin password, #126/#181 TSA hash-name case, #254 CRL HTTPS redirect, #179 dark-theme selection, #282 hardcoded fonts) are each S-effort and P1. None should outlive the next release. -- **PKCS#11 stability** is the largest *support-traffic* cluster (7 issues). Most are environment-specific; a dedicated PKCS#11 troubleshooting page plus better diagnostics would absorb the recurring tickets at low engineering cost. -- **Visible-signature rendering** (date/time, timezone, alignment, width/height, font size) is the second largest user-visible cluster. Bundling them into a single "Visible Signature v2" release would close seven tickets and materially raise perceived quality vs. Adobe's output. -- **Documentation debt is real**: at least 9 open tickets are wholly or partly "user did not find the existing docs." A FAQ / troubleshooting chapter plus four focused cookbook sections (TSA, PKCS#11, LTV, install channels) would retire those without touching code. +- **The LTV compliance cluster is resolved** by the EU DSS (PAdES) signing engine (PR #422). Selecting `-eng dss` produces genuine PAdES B / B-T / B-LT / B-LTA output with a real DSS dictionary, full-chain revocation, and TSA-chain revocation material — closing #27, #46, #95, #247, and #254, which the OpenPDF engine structurally cannot satisfy. #141 is partially covered (an archive timestamp is produced at LTA signing time; standalone DocTimeStamp / LTA refresh remains open). +- **The remaining headline gap is algorithm agility.** `SignerLogic.java:411` still hardcodes RSA PKCS#1 v1.5 on the OpenPDF path, so **#255 (RSASSA-PSS)** is unaddressed. The DSS path derives the algorithm from the key, which should unblock **#23 (EC keys)** — needs retest on an EC token. +- **PKCS#11 stability** is the largest *support-traffic* cluster. Most are environment-specific; a dedicated PKCS#11 troubleshooting page plus better diagnostics would absorb the recurring tickets at low engineering cost. +- **Visible-signature rendering** (timezone, alignment, width/height, font size, date format) is the largest remaining user-visible cluster. Bundling them into a single "Visible Signature v2" release would close several tickets and materially raise perceived quality vs. Adobe's output. +- **Documentation debt is real**: several open tickets are wholly or partly "user did not find the existing docs." A FAQ / troubleshooting chapter plus focused cookbook sections (TSA, PKCS#11, LTV, install channels) would retire those without touching code. --- @@ -50,62 +56,48 @@ Issues recommended for closing with a short comment pointing to the current stat | # | Title | Reason | |---|---|---| -| **#11** | Use jsignpdf server-side | Support question; no actionable request. Point to CLI batch-mode docs and close. | -| **#14** | "File is certified instead of signed" | User chose a `CERTIFIED_*` level. Not a bug; clarify `certificationLevel` defaults in FAQ and close. | -| **#124** | TSA user/pass in console mode | Resolved in comments — use `JSignPdfC.exe` wrapper. Add to FAQ and close. | -| **#149** | Doc PDF for 2.2.0 missing `-sn` | `-sn/--signer-name` is present in the current guide (line 326). Close as fixed. | -| **#151** | "Cryptographically invalid" in Okular | Old poppler bug; `pdfsig` and Adobe accept. External-tool issue. Close. | | **#172** | No window on Win11 | Packaging / JRE issue. The 3.x installer already ships a bundled JRE via jpackage. Add a troubleshooting note and close unless reproducible on the current installer. | -| **#177** | Sign not working from PHP on Windows | Apache/XAMPP `shell_exec` environment issue, not JSignPdf. Close with troubleshooting note. | -| **#226** | "Slot with tokens: (none)" on server | No token attached to the server host. User misconfiguration; close. | | **#307** | Flatpak support | Already implemented: `distribution/linux/flatpak/` exists and deps are regenerated (commit `158d4a7`). Close when Flathub submission is filed. | Close-eligible conditional on verification (PR already merged or behaviour changed by OpenPDF 3 / JavaFX migration): | # | Title | Verify | |---|---|---| -| **#23** | "Private keys must be RSAPrivate(Crt)Key" | Retest on Java 21 + BC 1.84; probably fixed via current `SunPKCS11`/`JSignPKCS11` path. | +| **#23** | "Private keys must be RSAPrivate(Crt)Key" | The DSS engine derives the algorithm from the key (`EncryptionAlgorithm.forKey`), so EC keys should sign via `-eng dss`. Retest on an EC PKCS#11 token. | | **#63** | `LoginException: Unable to perform password callback` | Retest — likely benign on 3.x. | -| **#114** | Show date/time on visible signature | `${timestamp}` placeholder exists and is documented; close after a docs callout. | | **#139** | Comodo AAA auto-added to PKCS7 | Reporter never followed up. Close after a short investigation note in the FAQ. | | **#253** | PKCS11 not displayed by `-lkt` | Partly diagnostics; retest on Java 21 and improve error logging before closing. | --- -## The LTV compliance cluster (strategic) +## The LTV compliance cluster — resolved by the DSS engine (PR #422) -Six issues describe the same underlying gap: JSignPdf can produce signatures that **claim** long-term validation but don't meet ETSI EN 319 142-1 baseline PAdES B-LT, let alone B-LTA. +Five of the six issues in this historically dominant cluster are closed by the EU DSS (PAdES) signing engine. With `-eng dss` (and `engine.dss.online.enabled=true` or local trust material for LT/LTA), JSignPdf now produces signatures that meet ETSI EN 319 142-1 baseline PAdES B-LT / B-LTA — something the OpenPDF engine structurally cannot do. -| # | Aspect | Current state | +| # | Aspect | Resolution | |---|---|---| -| **#27** | LTV not recognized by Adobe | DSS dictionary is not written; only SignerInfo-embedded CRL/OCSP. | -| **#46** | PAdES B-LTA level support | No document timestamps; no VRI; author keeps `jsignpdf-pades` as a separate experiment. | -| **#95** | Embed revocation info for TSA cert | TSA chain revocation is not collected or embedded. | -| **#141** | Append-only document timestamp | Not supported (no `ETSI.RFC3161` DocTimeStamp). | -| **#247** | OCSP missing for intermediate certs | Loop walks only `chain[0]/chain[1]`; one OCSP response. | -| **#254** | CRL fetch fails on HTTP→HTTPS redirect | `URL.openConnection().getInputStream()` doesn't follow cross-scheme redirects. | - -**Recommendation — "LTV Hardening" milestone (L–XL, 2–3 weeks):** -1. Fix #247 (full-chain OCSP) and #254 (redirect-tolerant CRL fetch) first — both are S-effort and make existing B-LT-ish output genuinely B-LT-compliant. -2. Add a DSS/VRI writer (files `SignerLogic.java` lines 208–218 / 366–437, `crl/CRLInfo.java:126`). This resolves #27, #95, and the "B-LT" half of #46. -3. Implement document timestamps (#141) on top of the DSS writer — this is the missing piece for #46 B-LTA. -4. Document the compliance level honestly in the manual (current claim in `JSignPdf.adoc` overstates what is produced). +| **#27** | LTV not recognized by Adobe | DSS writes a real DSS dictionary at LT/LTA. **Closed.** | +| **#46** | PAdES B-LTA level support | Delivered directly (B / T / LT / LTA). **Closed.** | +| **#95** | Embed revocation info for TSA cert | Embedded as part of LT/LTA validation material. **Closed.** | +| **#247** | OCSP missing for intermediate certs | DSS collects the full chain. **Closed.** | +| **#254** | CRL fetch fails on HTTP→HTTPS redirect | DSS's own fetchers follow redirects; the legacy `CRLInfo` path is not used by `dss`. **Closed.** | +| **#141** | Append-only document timestamp | *Partial* — an archive timestamp is produced at LTA signing time, but a standalone `ETSI.RFC3161` DocTimeStamp and LTA refresh on an already-signed PDF remain out of scope. | -All six tickets close together. The author's separate `jsignpdf-pades` repo can feed this work. +**Remaining work:** #141 (standalone DocTimeStamp / LTA refresh). The DSS engine produces the timestamp inline at signing time; refreshing the LTA material on an existing signature is a separate, smaller feature on top of the DSS path. --- ## Algorithm-agility cluster -Today `SignerLogic.java:411` hardcodes `sgn.setExternalDigest(..., "RSA")`. This forces PKCS#1 v1.5 output even when the certificate mandates PSS, and blocks pluggable EC / EdDSA signatures. +Today `SignerLogic.java:411` hardcodes `sgn.setExternalDigest(..., "RSA")` on the OpenPDF path. This forces PKCS#1 v1.5 output even when the certificate mandates PSS, and blocks pluggable EC / EdDSA signatures. The DSS engine sidesteps part of this by deriving the algorithm from the key, but PSS is still not produced by either path. -| # | Aspect | -|---|---| -| **#255** | RSASSA-PSS required by PSS-only certificates (increasingly common for eIDAS QSCDs) | -| **#23** | EC / non-RSA private keys fail in the RSA path | -| **#33** | RFC 3161 TSA nonce (same abstraction, small add) | +| # | Aspect | State | +|---|---|---| +| **#255** | RSASSA-PSS required by PSS-only certificates (increasingly common for eIDAS QSCDs) | **Not covered** — both the OpenPDF and DSS tokens still emit RSA PKCS#1 v1.5, not PSS. | +| **#23** | EC / non-RSA private keys fail in the RSA path | *Partial* — the DSS token uses `EncryptionAlgorithm.forKey`, so EC keys should sign via `-eng dss`. Retest needed. | +| **#33** | RFC 3161 TSA nonce | *Partial* — the TSA policy OID is now wired through the DSS `OnlineTSPSource`; the **nonce is still not implemented**. | -**Recommendation — "Algorithm pluggability" (M, ~1 week):** introduce a `SignatureAlgorithm` abstraction (RSA / RSA-PSS / ECDSA / EdDSA), wire it through `SignerLogic` and the TSA client, and expose a CLI / GUI selector. Covers #255, parts of #23, and opens the door to #33 nonce support without more refactoring. +**Recommendation — "Algorithm pluggability" (M, ~1 week):** introduce a `SignatureAlgorithm` abstraction (RSA / RSA-PSS / ECDSA / EdDSA), wire it through `SignerLogic` and the TSA client, and expose a CLI / GUI selector. Covers #255, the remaining part of #23 on the OpenPDF path, and the #33 nonce without more refactoring. --- @@ -113,12 +105,11 @@ Today `SignerLogic.java:411` hardcodes `sgn.setExternalDigest(..., "RSA")`. This | # | Request | |---|---| -| **#7** | Read keystore password from stdin (not argv) | | **#20** | Remote signatures via web API | | **#180** | Generic JCA provider (Azure Key Vault, AWS KMS, GCP HSM) | | **#187** | Multiple PKCS#11 providers | -**Recommendation:** start with **#7 (S, P1, security hygiene)** and **#180 (M, P1)** — `--provider-class`/`--provider-arg` mirrors `jarsigner`, requires no new code paths, and largely subsumes #20. #187 (multi-PKCS#11) is a small follow-up once the provider mechanism is generalized. +**Recommendation:** start with **#180 (M, P1)** — `--provider-class`/`--provider-arg` mirrors `jarsigner`, requires no new code paths, and largely subsumes #20. #187 (multi-PKCS#11) is a small follow-up once the provider mechanism is generalized. --- @@ -128,28 +119,26 @@ Today `SignerLogic.java:411` hardcodes `sgn.setExternalDigest(..., "RSA")`. This |---|---|---| | **#67** | Text alignment (left/center/right) | P2 | | **#99** | Font size honored when signer name is shown | P2 (OpenPDF 3 may have fixed; verify) | -| **#114** | Date/time on visible signature | P1 (docs — already supported via `${timestamp}`) | | **#165** | Width/height input (not four corners) | P2 | | **#179** | Selection box invisible on dark themes | P1 (1-line fix in `SelectionImage.java`) | | **#231** | Configurable date format | P2 | | **#55** | Configurable timezone | P2 | -| **#282** | I18N fonts (hardcoded "Tahoma"/"Courier New") | P1 | -**Recommendation — "Visible Signature v2" (M, 3–5 days):** bundle all of these into one release. Most are one- or two-line changes in `SignerLogic.java`, `VisibleSignatureDialog.java`, `SignPdfForm.java`, and `SelectionImage.java`. Combined, they close eight tickets and materially raise parity with Adobe's rendered signature. +**Recommendation — "Visible Signature v2" (M, 3–5 days):** bundle these into one release. Most are one- or two-line changes in `SignerLogic.java`, `VisibleSignatureDialog.java`, `SignPdfForm.java`, and `SelectionImage.java`. Combined, they close several tickets and materially raise parity with Adobe's rendered signature. --- ## PKCS#11 / hardware token cluster -Seven issues, largely environment-specific: +Largely environment-specific: | # | Nature | |---|---| -| **#23**, **#63** | Probably already fixed; retest and close | +| **#23**, **#63** | Probably already fixed; retest and close (see algorithm cluster for #23) | | **#184** | Windows batch-mode hang after unregister — real bug in `PKCS11Utils.unregisterProviders` (P1) | | **#186** | First-click "No private key" — driver warm-up race (P2, retry-on-empty) | | **#187** | Multi-provider support (P2, see pluggability cluster) | -| **#226**, **#253** | Environment / server deployment issues — diagnostics + docs | +| **#253** | Environment / server deployment issue — diagnostics + docs | **Recommendation:** fix **#184** in code (this is a reproducible Windows bug, not user env) and invest in a dedicated **PKCS#11 troubleshooting chapter** (`docs/pkcs11.md`) covering driver paths per OS, headless servers, login modes, and common errors. This one doc page will absorb the majority of PKCS#11 support issues at low cost. @@ -157,68 +146,50 @@ Seven issues, largely environment-specific: ## Per-issue consolidated table -Columns: **Status** — `close` (see quick-close list), `valid` (open, action needed), `docs` (resolvable by documentation), `cluster` (tracked in a cluster above); **E** effort S/M/L/XL; **Pri** priority. +Columns: **Status** — `close` (see quick-close list), `valid` (open, action needed), `docs` (resolvable by documentation), `cluster` (tracked in a cluster above), `partial` (partly covered by the DSS engine PR #422); **E** effort S/M/L/XL; **Pri** priority. | # | Title (short) | Status | E | Pri | Recommendation | |---|---|---|---|---|---| -| 7 | Read password from stdin | valid | S | P1 | Quick win. `System.console().readPassword()` in `SignerOptionsFromCmdLine.java`. | -| 11 | Server-side usage | close | — | — | Support question. Close with pointer to batch-mode docs. | -| 14 | "File certified instead of signed" | close | — | — | User config. Clarify in FAQ and close. | | 20 | Remote signatures via web API | cluster | L | P2 | Subsumed by #180 (JCA provider). | -| 23 | RSAPrivate(Crt)Key error | close? | S | P3 | Retest on Java 21; likely already fixed. | -| 27 | LTV Long-Term Validation | cluster | XL | P0 | Part of LTV Hardening milestone. | +| 23 | RSAPrivate(Crt)Key error | partial | S | P3 | DSS token derives algo from key (`EncryptionAlgorithm.forKey`); EC keys should sign via `-eng dss`. Retest on an EC PKCS#11 token. | | 30 | Sign multiple docs in GUI | valid | M | P2 | Multi-select in JavaFX file chooser. CLI already supports it. | -| 33 | TSA Nonce | valid | S | P2 | Wire BouncyCastle nonce; do alongside algorithm pluggability. | -| 46 | PAdES B-LTA level | cluster | XL | P0 | Strategic — LTV Hardening + DocTimeStamp (#141). | +| 33 | TSA Nonce | partial | S | P2 | TSA policy OID now wired through the DSS `OnlineTSPSource`; nonce still not implemented. Do alongside algorithm pluggability. | | 51 | Remove "Contact (optional)" | valid | S | P3 | Low priority — `/ContactInfo` is still a valid PAdES field; consider keeping but de-emphasizing. | | 55 | Timezone of signature date | cluster | S | P2 | Visible Signature v2. | | 63 | LoginException with PKCS11 | close? | S | P3 | Retest; add log suppression if cosmetic. | | 67 | Visible signature alignment | cluster | S | P2 | Visible Signature v2. | -| 95 | OCSP/CRL for TSA cert | cluster | M | P0 | LTV Hardening — required for PAdES B-LT conformance. | | 99 | Font size ignored with signer name | cluster | M | P2 | Visible Signature v2; verify vs. OpenPDF 3. | -| 114 | Show date/time on signature | docs | S | P1 | Already supported via `${timestamp}`; add a prominent example. | -| 124 | TSA user/pass in console mode | close | — | — | Resolved — use `.exe` wrapper. Add FAQ entry. | -| 126 | TSA NPE on lowercase hash | valid | S | P1 | Uppercase + validate in `TsaDialog`/`BasicSignerOptions`. Duplicate of #181. | | 139 | Comodo AAA auto-added | close? | S | P3 | Reporter silent; investigate once, add FAQ, close. | | 140 | Validate-only mode | valid | XL | P2 | Out of historical focus; if pursued, delegate to EU DSS or PDFBox rather than re-implement. | -| 141 | Append-only timestamp | cluster | L | P1 | LTV Hardening — enables B-LTA (#46). | +| 141 | Append-only timestamp | partial | L | P1 | DSS engine emits an archive timestamp at LTA signing; standalone DocTimeStamp / LTA refresh on an already-signed PDF still out of scope. | | 148 | Show equivalent CLI in GUI | valid | M | P2 | High-value learning aid; nice-to-have. | -| 149 | Docs PDF missing `-sn` | close | — | — | Already present in current guide. Close. | -| 151 | Okular reports invalid | close | — | — | External tool bug. Close. | | 165 | Width/height for visible sig | cluster | S | P2 | Visible Signature v2. | | 172 | Win11 window does not open | close? | S | P1 | Push users to bundled-JRE installer; add troubleshooting. | -| 177 | PHP shell_exec on Windows | close | — | — | Environment, not JSignPdf. | | 178 | Signing 1 GB PDFs | valid | XL | P2 | Constrained by OpenPDF architecture; document memory guidance meanwhile. | | 179 | Dark-theme selection invisible | valid | S | P1 | One-line fix in `SelectionImage.java:162` — theme-aware color or XOR. | | 180 | JCA provider support | cluster | M | P1 | Key-source pluggability — `--provider-class`/`--provider-arg`. | -| 181 | "One working TSA" | docs | S | P1 | Cookbook + GUI hash dropdown; fix #126 NPE. | | 184 | Batch-mode hangs after PKCS11 | valid | M | P1 | `AuthProvider.logout()`, remove the blind `Thread.sleep(1000)` in `PKCS11Utils.java:82-90`; force `System.exit` on CLI. | | 186 | "Private key not found" first click | valid | M | P2 | Retry-on-empty keystore load. | | 187 | Multiple PKCS11 providers | cluster | M | P2 | After #180 generalization. | | 223 | Sign existing sig form fields | valid | M | P1 | Expose `sap.setVisibleSignature(fieldName)`; remove the "clear existing fields" step when a named field is specified. | -| 226 | PKCS11 slot empty (server) | close | — | — | No token attached. Close. | | 231 | Date format | cluster | S | P2 | Visible Signature v2. Duplicate of #55 in spirit. | | 243 | `sun.misc.Unsafe` deprecation | valid | S | P2 | Track OpenPDF upstream; bump `openpdf.version` when fix lands. **Will be P0 on a future JDK.** | -| 247 | OCSP for intermediate certs | cluster | M | P0 | LTV Hardening — loop over `chain[0..n-1]`. | -| 251 | `--overwrite` / delete-source CLI | valid | S | P1 | Add flags in `SignerOptionsFromCmdLine.java`. Silent overwrite of signed output is a data-risk. | -| 252 | `$XDG_CONFIG_HOME` support | valid | S | P2 | `PropertyProvider.java:60` — honor XDG; matters for Flatpak (#307). | | 253 | PKCS11 not in `-lkt` | close? | M | P2 | Diagnostics + doc; possibly env-only. | -| 254 | CRL HTTP→HTTPS redirect | cluster | S | P1 | `crl/CRLInfo.java:126` — switch to `java.net.http.HttpClient` with `Redirect.NORMAL`. | -| 255 | RSASSA-PSS signing | cluster | M | P0 | Algorithm pluggability. P0 for certs that mandate PSS. | +| 255 | RSASSA-PSS signing | cluster | M | P0 | Algorithm pluggability. Not covered by the DSS engine — still RSA PKCS#1 v1.5. P0 for certs that mandate PSS. | | 259 | Configurable filename suffix | valid | S | P2 | CLI already supports `--out-suffix`; expose in GUI preferences. | -| 282 | I18N font glyphs (tofu) | valid | S | P1 | `SignPdfForm.java:531,993` — replace `"Tahoma"`/`"Courier New"` with `Font.DIALOG` or UIManager defaults. | | 307 | Flatpak support | close | S | — | Already implemented in `distribution/linux/flatpak/`. Close when Flathub submission is filed. | +| 349 | Translate website | valid | M | P3 | Website i18n (Docusaurus i18n). Community-PR-friendly; not planned. | --- ## Cross-cutting themes -1. **LTV is the single most valuable engineering investment.** Six tickets, from 2019 onward, converge on the same gap. One focused milestone closes them all. -2. **Error messages are the cheapest UX upgrade.** #126, #151, #226, #254, #63 all surface stack traces where a one-line "Hash name must be uppercase (SHA256)" or "CRL redirect to HTTPS not followed" would do. Adding a thin user-facing error layer pays off across dozens of tickets. -3. **CLI ↔ GUI feature parity** (#7, #30, #148, #251, #259) — the CLI has options the GUI lacks and vice versa. A small parity audit exposes most of them. +1. **LTV was the single most valuable engineering investment — now delivered.** Six tickets, from 2019 onward, converged on the same gap; the DSS engine (PR #422) closes five of them and partially covers #141. +2. **Error messages are the cheapest UX upgrade.** Several tickets surface stack traces where a one-line user-facing message would do (e.g. the residual #63 login noise). Adding a thin user-facing error layer pays off across dozens of tickets. +3. **CLI ↔ GUI feature parity** (#30, #148, #259) — the CLI has options the GUI lacks and vice versa. A small parity audit exposes most of them. 4. **Packaging has quietly matured**: Flatpak, Windows jpackage with bundled JRE, macOS DMG. Several "it doesn't run" issues (#172, #184) can be retired by steering users toward the bundled installer rather than `java -jar`. -5. **Swing/JavaFX duality**: JavaFX is now the default GUI. Several Swing-only tickets (#30, #51, #67, #114, #165, #179, #259, #282) should first be verified against the FX code path before being worked on — some may already be moot. -6. **Documentation discoverability** is a silent source of issues: #14, #30, #114, #124, #149, #181, #259 are partially or wholly "user didn't find the docs." A FAQ plus four cookbook pages (TSA, PKCS#11, LTV, install channels) would absorb most of them. +5. **Swing/JavaFX duality**: JavaFX is now the default GUI. Several Swing-only tickets (#30, #51, #67, #165, #179, #259) should first be verified against the FX code path before being worked on — some may already be moot. +6. **Documentation discoverability** is a silent source of issues: #30 and #259 are partially or wholly "user didn't find the docs." A FAQ plus cookbook pages (TSA, PKCS#11, LTV, install channels) would absorb most of them. --- @@ -226,20 +197,17 @@ Columns: **Status** — `close` (see quick-close list), `valid` (open, action ne | Milestone | Contents | Effort | Closes | |---|---|---|---| -| **3.1 — Quick wins** | #7, #126/#181, #179, #254, #282, #247; FAQ skeleton + TSA cookbook; housekeeping closes for #11, #14, #124, #149, #151, #177, #226, #307 | ~1 week | ≥12 issues | -| **3.2 — LTV Hardening** | DSS/VRI writer, full-chain OCSP done, TSA-chain revocation (#95), redirect-tolerant CRL. Honest compliance chapter in manual | 2–3 weeks | #27, #46 (B-LT), #95, #247, #254 | -| **3.3 — Algorithm pluggability + Key-source pluggability** | `SignatureAlgorithm` abstraction (#255, #23, #33), `--provider-class`/`--provider-arg` (#180), stdin password (#7 if not already), multi-PKCS#11 (#187), remote signing hook (#20) | ~2 weeks | #20, #23, #33, #180, #187, #255 | -| **3.4 — Visible Signature v2 + GUI parity** | #30, #51, #55, #67, #99, #114, #165, #231, #251, #259; JavaFX multi-select; verbose CLI preview (#148) | ~1 week | ≥10 issues | -| **3.5 — B-LTA** | Document timestamps (#141) on top of 3.2 | ~1 week | #46 (B-LTA), #141 | -| **Ongoing / low** | #140 (validation mode), #178 (large files), #186 (retry), #252 (XDG), #243 (track OpenPDF), #253 (diagnostics) | — | as PRs arrive | - -With this sequence, roughly **35 of the 47 open issues are actionable within ~2 months of focused work**, and the remainder are either environment-specific or low-priority cosmetic. +| **3.1 — DSS engine (PAdES)** | EU DSS signing engine: PAdES B / T / LT / LTA, DSS dictionary, full-chain + TSA-chain revocation, TSA hash hardening, `--pades-level`, `--overwrite` (PR #422) | delivered | #27, #46, #95, #247, #254 (+ #141 partial) | +| **3.2 — Algorithm pluggability + Key-source pluggability** | `SignatureAlgorithm` abstraction (#255, residual #23, #33 nonce), `--provider-class`/`--provider-arg` (#180), multi-PKCS#11 (#187), remote signing hook (#20) | ~2 weeks | #20, #33, #180, #187, #255 | +| **3.3 — Visible Signature v2 + GUI parity** | #30, #51, #55, #67, #99, #165, #231, #259; JavaFX multi-select; verbose CLI preview (#148) | ~1 week | several | +| **3.4 — LTA refresh** | Standalone DocTimeStamp / LTA refresh on already-signed PDFs (#141) on top of the DSS engine | ~1 week | #141 | +| **Ongoing / low** | #140 (validation mode), #178 (large files), #186 (retry), #243 (track OpenPDF), #253 (diagnostics), #63/#139/#172 (retest & close) | — | as PRs arrive | --- ## Notes on methodology and caveats - Each expert report is based on the issue text, comment threads, and a fresh read of the code. Where an expert said "covered in current code," it was grep-verified. Where they said "duplicate," the referenced ticket was cross-checked. -- Two recurring edge cases: (a) `jsignpdf-pades` — the separate LTV experimentation repo — means some LTV work may be further along than this master branch reflects; (b) the Swing → JavaFX migration means several "GUI bug" tickets should be retested on the FX code path before implementation effort is spent. -- Priorities reflect project-maintainer perspective, not end-user urgency for a specific workflow. A user dependent on PAdES B-LTA would see #46 as P0 today regardless of our P0/P1 label. +- The Swing → JavaFX migration means several "GUI bug" tickets should be retested on the FX code path before implementation effort is spent. +- Priorities reflect project-maintainer perspective, not end-user urgency for a specific workflow. - This plan omits estimates for administrative work (release notes, Flathub listing, website updates) — assume ~1 day per milestone for that.