diff --git a/.gitignore b/.gitignore index fe4cf46f..7824e500 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ target/ jsignpdf/conf/ dependency-reduced-pom.xml guide.md -distribution/demo/service-agreeement_signed.pdf +*_signed.pdf diff --git a/distribution/doc/release-notes/3.2.0.md b/distribution/doc/release-notes/3.2.0.md new file mode 100644 index 00000000..6c9cd588 --- /dev/null +++ b/distribution/doc/release-notes/3.2.0.md @@ -0,0 +1,8 @@ +# Version 3.2.0 + +A release about getting out of your way. The improvements below smooth over the rough edges you hit while signing: less guessing when something goes wrong, and fewer surprises when it goes right. + +- **New `debug` output for signing diagnostics** — enable it on the new _General_ tab of Preferences, or with `debug=true` in `advanced.properties` (or `-o debug=true` for a single CLI run), to log the signing certificate chain (subject, issuer, serial, validity, key usage, QC statements, and the AIA and CRL distribution-point URLs of each certificate) plus, for the DSS engine, the trust anchors it loaded and every AIA, CRL, and OCSP request with the target URL, the certificate it is for, the response size, the outcome, and the elapsed time. It is off by default so normal runs stay quiet; `-q` silences everything regardless. See issue 452. +- **Preferences dialog gains a _General_ tab** gathering the signing-engine selection and the new `debug` toggle; both apply immediately. +- **Visible signature images keep their aspect ratio with the DSS engine** — background and graphic images were previously stretched to fill the signature box and came out distorted. A `--bg-scale` of zero still stretches to fill; any other value fits the image and centers it, matching the OpenPDF engine. See issue 460. +- **Clear list in the Recent files menu** — the File menu's recent-files trail can now be emptied on its own, which previously required a factory reset that discarded every other setting as well. The item appears only when there is something to clear. See issue 453. diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java index 0ae8a7c2..ce6aa9a6 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java @@ -2,6 +2,7 @@ import java.util.Locale; import java.util.Map; +import java.util.logging.Level; import net.sf.jsignpdf.Constants; import net.sf.jsignpdf.engine.AdvancedEngineConfig; @@ -42,6 +43,28 @@ public static boolean relaxSslSecurity() { return cfg().getAsBool("relax.ssl.security", false); } + /** + * Whether verbose signing diagnostics are enabled ({@code debug} in {@code advanced.properties}). When on, + * the signing pipeline logs the certificate chain, the loaded trust anchors, and every TSA / AIA / CRL / + * OCSP call at {@link Level#FINE}. + */ + public static boolean debug() { + return cfg().getAsBool("debug", false); + } + + /** + * Aligns the {@code net.sf.jsignpdf} logger level with the {@link #debug()} setting: {@link Level#FINE} + * when debug is on, {@link Level#INFO} otherwise. A logger already muted to {@link Level#OFF} (the CLI + * {@code -q} / {@code --quiet} flag) is left untouched, so quiet always wins over debug. Safe to call at + * startup and again whenever the setting changes (e.g. the Preferences dialog toggles it live). + */ + public static void applyDebugLogLevel() { + if (Constants.LOGGER.getLevel() == Level.OFF) { + return; + } + Constants.LOGGER.setLevel(debug() ? Level.FINE : Level.INFO); + } + public static String pdf2imageLibraries() { return cfg().getNotEmptyProperty("pdf2image.libraries", Constants.PDF2IMAGE_LIBRARIES_DEFAULT); } diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/CertificateInfo.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/CertificateInfo.java new file mode 100644 index 00000000..b59cce34 --- /dev/null +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/CertificateInfo.java @@ -0,0 +1,335 @@ +package net.sf.jsignpdf.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HexFormat; +import java.util.List; +import java.util.logging.Level; + +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.x500.X500Principal; + +import net.sf.jsignpdf.Constants; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.CRLDistPoint; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; + +/** + * Read-only description of an X.509 certificate for the diagnostic log. Signing failures caused by + * certificates — a chain that is not anchored, revocation data that cannot be fetched, an expired + * intermediate — are reported by the underlying libraries with identifiers the user cannot act on (most + * notably DSS's {@code C-} token id). Logging the identity, validity, key usage, QC statements and + * the AIA / CRL distribution-point URLs of every certificate in the chain lets those reports be matched to a + * real certificate without reaching for {@code openssl} (issue #452). + * + *

+ * Everything here is best-effort: an extension that cannot be parsed is omitted rather than failing the + * signing operation. All output is diagnostic, hence English-only (like the rest of the FINE-level tracing). + *

+ * + * @author Josef Cacek + */ +public class CertificateInfo { + + /** Timestamp layout for validity bounds — UTC, so logs from different machines compare directly. */ + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'") + .withZone(ZoneOffset.UTC); + + /** Key-usage bit names in the order of the {@code KeyUsage} BIT STRING (RFC 5280 §4.2.1.3). */ + private static final String[] KEY_USAGE_NAMES = { "digitalSignature", "nonRepudiation", "keyEncipherment", + "dataEncipherment", "keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly" }; + + /** Well-known ETSI QC statement OIDs, mapped to the names used in EN 319 412-5. */ + private static final String[][] QC_STATEMENT_NAMES = { + { "0.4.0.1862.1.1", "QcCompliance" }, + { "0.4.0.1862.1.2", "QcLimitValue" }, + { "0.4.0.1862.1.3", "QcRetentionPeriod" }, + { "0.4.0.1862.1.4", "QcSSCD" }, + { "0.4.0.1862.1.5", "QcPDS" }, + { "0.4.0.1862.1.6", "QcType" }, + { "0.4.0.1862.1.7", "QcCClegislation" }, + { "0.4.0.19495.2", "PSD2QcType" }, + { "1.3.6.1.5.5.7.11.2", "QcSemanticsId-eIDAS" } }; + + private CertificateInfo() { + } + + /** + * Logs the identity and revocation-relevant extensions of a whole certificate chain at {@link Level#FINE}. + * Nothing is computed when FINE is not loggable. + * + * @param label a short name for the chain, e.g. {@code "Signing certificate chain"} + * @param chain the chain, leaf first; {@code null} / empty is silently ignored + */ + public static void logChain(String label, Certificate[] chain) { + if (chain == null || chain.length == 0) { + return; + } + logChain(label, Arrays.asList(chain)); + } + + /** + * Logs the identity and revocation-relevant extensions of a certificate collection at {@link Level#FINE}. + * Non-X.509 entries are skipped. + * + * @param label a short name for the chain, e.g. {@code "Timestamp certificate chain"} + * @param chain the certificates, leaf first; {@code null} / empty is silently ignored + */ + public static void logChain(String label, Iterable chain) { + if (chain == null || !Constants.LOGGER.isLoggable(Level.FINE)) { + return; + } + final List certs = new ArrayList<>(); + for (Certificate cert : chain) { + if (cert instanceof X509Certificate) { + certs.add((X509Certificate) cert); + } + } + if (certs.isEmpty()) { + return; + } + final StringBuilder buf = new StringBuilder(label).append(" (").append(certs.size()) + .append(certs.size() == 1 ? " certificate)" : " certificates)"); + for (int i = 0; i < certs.size(); i++) { + buf.append(System.lineSeparator()).append(describe(certs.get(i), " [" + i + "] ")); + } + Constants.LOGGER.fine(buf.toString()); + } + + /** + * Builds the multi-line description of a single certificate: subject / issuer / serial / validity / + * fingerprint on the first lines, then only the extensions that are actually present. + * + * @param cert the certificate to describe + * @param prefix indentation for the first line; continuation lines are aligned under it + * @return the description, without a trailing line separator + */ + public static String describe(X509Certificate cert, String prefix) { + final String indent = " ".repeat(prefix.length()); + final StringBuilder buf = new StringBuilder(); + buf.append(prefix).append("subject=").append(cert.getSubjectX500Principal().getName()); + buf.append(System.lineSeparator()).append(indent).append("issuer=") + .append(cert.getIssuerX500Principal().getName()); + buf.append(System.lineSeparator()).append(indent).append("serial=") + .append(cert.getSerialNumber().toString(16)).append(", validity=") + .append(formatTimestamp(cert.getNotBefore())).append(" .. ") + .append(formatTimestamp(cert.getNotAfter())) + .append(expiryNote(cert)); + appendIfNotEmpty(buf, indent, "id=", StringUtils.defaultString(tokenId(cert))); + appendIfNotEmpty(buf, indent, "keyUsage=", String.join(",", keyUsages(cert))); + appendIfNotEmpty(buf, indent, "qcStatements=", String.join(",", qcStatements(cert))); + appendIfNotEmpty(buf, indent, "AIA caIssuers=", String.join(", ", caIssuersUrls(cert))); + appendIfNotEmpty(buf, indent, "AIA OCSP=", String.join(", ", ocspUrls(cert))); + appendIfNotEmpty(buf, indent, "CRL DP=", String.join(", ", crlDistributionPointUrls(cert))); + return buf.toString(); + } + + /** + * The DSS certificate token id ({@code C-} + uppercase SHA-256 hex of the DER encoding) — the + * identifier DSS uses when it reports a missing trust anchor or missing revocation data. + * + * @param cert the certificate + * @return the token id, or {@code null} when the certificate cannot be re-encoded + */ + public static String tokenId(X509Certificate cert) { + try { + final MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + return "C-" + HexFormat.of().withUpperCase().formatHex(sha256.digest(cert.getEncoded())); + } catch (NoSuchAlgorithmException | CertificateEncodingException e) { + return null; + } + } + + /** @return the principal's CN when present, otherwise its full RFC 2253 DN */ + public static String commonNameOrDn(X500Principal principal) { + try { + final LdapName ldapName = new LdapName(principal.getName()); + for (Rdn rdn : ldapName.getRdns()) { + if ("CN".equalsIgnoreCase(rdn.getType())) { + return rdn.getValue().toString(); + } + } + } catch (Exception e) { + Constants.LOGGER.log(Level.FINEST, "Unparseable distinguished name", e); + } + return principal.getName(); + } + + /** @return the certificate's subject CN, falling back to the full DN */ + public static String subjectOf(X509Certificate cert) { + return commonNameOrDn(cert.getSubjectX500Principal()); + } + + /** @return the certificate's issuer CN, falling back to the full DN */ + public static String issuerOf(X509Certificate cert) { + return commonNameOrDn(cert.getIssuerX500Principal()); + } + + /** @return the names of the asserted key-usage bits, empty when the extension is absent */ + public static List keyUsages(X509Certificate cert) { + final List usages = new ArrayList<>(); + final boolean[] bits = cert.getKeyUsage(); + if (bits != null) { + for (int i = 0; i < bits.length && i < KEY_USAGE_NAMES.length; i++) { + if (bits[i]) { + usages.add(KEY_USAGE_NAMES[i]); + } + } + } + return usages; + } + + /** + * @return the QC statements asserted by the certificate, by their EN 319 412-5 name where known and by raw + * OID otherwise; empty when the certificate carries no QCStatements extension + */ + public static List qcStatements(X509Certificate cert) { + final List statements = new ArrayList<>(); + final ASN1Primitive value = extensionValue(cert, Extension.qCStatements); + if (!(value instanceof ASN1Sequence)) { + return statements; + } + for (ASN1Encodable element : (ASN1Sequence) value) { + try { + final ASN1ObjectIdentifier oid = QCStatement.getInstance(element).getStatementId(); + statements.add(qcStatementName(oid.getId())); + } catch (Exception e) { + Constants.LOGGER.log(Level.FINEST, "Unparseable QCStatement", e); + } + } + return statements; + } + + /** @return the {@code caIssuers} access URLs from the Authority Information Access extension */ + public static List caIssuersUrls(X509Certificate cert) { + return accessUrls(cert, AccessDescription.id_ad_caIssuers); + } + + /** @return the {@code OCSP} access URLs from the Authority Information Access extension */ + public static List ocspUrls(X509Certificate cert) { + return accessUrls(cert, AccessDescription.id_ad_ocsp); + } + + /** @return the URLs listed in the CRL Distribution Points extension */ + public static List crlDistributionPointUrls(X509Certificate cert) { + final List urls = new ArrayList<>(); + final ASN1Primitive value = extensionValue(cert, Extension.cRLDistributionPoints); + if (value == null) { + return urls; + } + try { + for (DistributionPoint point : CRLDistPoint.getInstance(value).getDistributionPoints()) { + final DistributionPointName name = point.getDistributionPoint(); + if (name == null || name.getType() != DistributionPointName.FULL_NAME) { + continue; + } + for (GeneralName generalName : GeneralNames.getInstance(name.getName()).getNames()) { + addUriName(urls, generalName); + } + } + } catch (Exception e) { + Constants.LOGGER.log(Level.FINEST, "Unparseable CRL distribution points", e); + } + return urls; + } + + private static List accessUrls(X509Certificate cert, ASN1ObjectIdentifier method) { + final List urls = new ArrayList<>(); + final ASN1Primitive value = extensionValue(cert, Extension.authorityInfoAccess); + if (value == null) { + return urls; + } + try { + for (AccessDescription description : AuthorityInformationAccess.getInstance(value).getAccessDescriptions()) { + if (method.equals(description.getAccessMethod())) { + addUriName(urls, description.getAccessLocation()); + } + } + } catch (Exception e) { + Constants.LOGGER.log(Level.FINEST, "Unparseable authority information access", e); + } + return urls; + } + + private static void addUriName(List urls, GeneralName name) { + if (name != null && name.getTagNo() == GeneralName.uniformResourceIdentifier + && name.getName() instanceof ASN1String) { + urls.add(((ASN1String) name.getName()).getString()); + } + } + + private static ASN1Primitive extensionValue(X509Certificate cert, ASN1ObjectIdentifier oid) { + final byte[] encoded = cert.getExtensionValue(oid.getId()); + if (encoded == null) { + return null; + } + try { + return JcaX509ExtensionUtils.parseExtensionValue(encoded); + } catch (Exception e) { + Constants.LOGGER.log(Level.FINEST, "Unparseable certificate extension " + oid, e); + return null; + } + } + + private static String qcStatementName(String oid) { + for (String[] mapping : QC_STATEMENT_NAMES) { + if (mapping[0].equals(oid)) { + return mapping[1]; + } + } + return oid; + } + + /** + * Renders a point in time the way the signing diagnostics do: UTC, so log lines from different machines + * (and from the certificate, CRL and OCSP sides of the same run) compare directly. + * + * @param date the instant, may be {@code null} + * @return the formatted timestamp, or {@code "?"} for {@code null} + */ + public static String formatTimestamp(Date date) { + return date == null ? "?" : TIMESTAMP.format(date.toInstant()); + } + + private static void appendIfNotEmpty(StringBuilder buf, String indent, String caption, String value) { + if (!value.isEmpty()) { + buf.append(System.lineSeparator()).append(indent).append(caption).append(value); + } + } + + /** Flags the two states that make a chain fail validation for a reason unrelated to trust or revocation. */ + private static String expiryNote(X509Certificate cert) { + final Instant now = Instant.now(); + if (now.isAfter(cert.getNotAfter().toInstant())) { + return " (EXPIRED)"; + } + if (now.isBefore(cert.getNotBefore().toInstant())) { + return " (NOT YET VALID)"; + } + return ""; + } +} diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java index a7821d1a..f987202e 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/KeyStoreUtils.java @@ -387,6 +387,7 @@ public static PrivateKeyInfo getPkInfo(BasicSignerOptions options) final PrivateKey tmpPk = (PrivateKey) tmpKs.getKey(tmpAlias, options.getKeyPasswdX()); LOGGER.info(RES.get("console.getCertChain")); final Certificate[] tmpChain = tmpKs.getCertificateChain(tmpAlias); + CertificateInfo.logChain("Signing certificate chain", tmpChain); PrivateKeyInfo tmpResult = new PrivateKeyInfo(tmpPk, tmpChain); return tmpResult; } 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 54fa13ab..eee4942c 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 @@ -10,6 +10,13 @@ # override this, and the CLI --engine flag overrides it for a single invocation. engine=openpdf +# When true, JSignPdf logs verbose diagnostics during signing (FINE level): the +# signing certificate chain, the loaded trust anchors, and every TSA / AIA / CRL +# / OCSP network call the DSS engine makes. Off by default; enable it to +# investigate a signing failure. The CLI -q / --quiet flag silences all output +# regardless of this setting. +debug=false + # Engine-specific configuration. Each engine module reads its own keys under the # engine..* prefix (via EngineConfig). No keys are defined for the bundled # OpenPDF engine 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 f3bfb6ad..3c45c5cb 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 @@ -507,8 +507,10 @@ 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.section.general=General 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.general.debug=Debug output +jfx.gui.preferences.general.debug.help=Log verbose diagnostics during signing: the signing certificate chain, the loaded trust anchors, and every timestamp / AIA / CRL / OCSP network call. Enable this to investigate a signing failure. Applies immediately to 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/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties index 6b34eb70..e5f48bb6 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties @@ -504,8 +504,10 @@ jfx.gui.preferences.button.cancel=Zrušit jfx.gui.preferences.button.resetSection=Obnovit sekci na výchozí jfx.gui.preferences.restartHint=Projeví se po restartu jfx.gui.preferences.restartHint.tooltip=Toto nastavení se načítá pouze při spuštění. Restartujte JSignPdf, aby se změna projevila. -jfx.gui.preferences.section.engine=Podpisový engine +jfx.gui.preferences.section.general=Obecné jfx.gui.preferences.engine.help=Podpisový engine použitý k podepisování dokumentů. OpenPDF vytváří standardní PDF podpisy; DSS vytváří podpisy PAdES (B/T/LT/LTA). Výběr se projeví okamžitě a je výchozí pro GUI i CLI. +jfx.gui.preferences.general.debug=Ladicí výstup +jfx.gui.preferences.general.debug.help=Zaznamenávat podrobnou diagnostiku během podpisu: řetězec podpisového certifikátu, načtené kotvy důvěry a každé síťové volání časového razítka / AIA / CRL / OCSP. Zapněte pro řešení selhání podpisu. Projeví se okamžitě pro GUI i CLI. jfx.gui.preferences.section.font=Písmo viditelného podpisu jfx.gui.preferences.section.certificate=Ověření certifikátu jfx.gui.preferences.section.network=Síť a SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties index f5db3aae..b2495216 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties @@ -504,8 +504,10 @@ jfx.gui.preferences.button.cancel=Abbrechen jfx.gui.preferences.button.resetSection=Abschnitt auf Standardwerte zurücksetzen jfx.gui.preferences.restartHint=Wird nach Neustart wirksam jfx.gui.preferences.restartHint.tooltip=Diese Einstellung wird einmal beim Start gelesen. Starten Sie JSignPdf neu, damit die Änderung wirksam wird. -jfx.gui.preferences.section.engine=Signatur-Engine +jfx.gui.preferences.section.general=Allgemein jfx.gui.preferences.engine.help=Signatur-Engine, die zum Signieren von Dokumenten verwendet wird. OpenPDF erzeugt Standard-PDF-Signaturen; DSS erzeugt PAdES-Baseline-Signaturen (B/T/LT/LTA). Die Auswahl wird sofort wirksam und ist die Vorgabe für die Oberfläche und die Kommandozeile. +jfx.gui.preferences.general.debug=Debug-Ausgabe +jfx.gui.preferences.general.debug.help=Ausführliche Diagnose während des Signierens protokollieren: die Zertifikatskette des Unterzeichners, die geladenen Vertrauensanker und jeden Netzwerkaufruf für Zeitstempel / AIA / CRL / OCSP. Aktivieren Sie dies, um einen Signaturfehler zu untersuchen. Wird sofort für die Oberfläche und die Kommandozeile wirksam. jfx.gui.preferences.section.font=Schriftart der sichtbaren Signatur jfx.gui.preferences.section.certificate=Zertifikatsprüfung jfx.gui.preferences.section.network=Netzwerk und SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties index 0e532cd7..bf093e5a 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Ακύρωση jfx.gui.preferences.button.resetSection=Επαναφορά ενότητας στις προεπιλογές jfx.gui.preferences.restartHint=Ισχύει μετά την επανεκκίνηση jfx.gui.preferences.restartHint.tooltip=Αυτή η ρύθμιση διαβάζεται μία φορά κατά την εκκίνηση. Επανεκκινήστε το JSignPdf για να εφαρμοστεί η αλλαγή. -jfx.gui.preferences.section.engine=Μηχανή υπογραφής +jfx.gui.preferences.section.general=Γενικά jfx.gui.preferences.engine.help=Η μηχανή υπογραφής που χρησιμοποιείται για την υπογραφή εγγράφων. Το OpenPDF παράγει τυπικές υπογραφές PDF· το DSS παράγει βασικές υπογραφές PAdES (B/T/LT/LTA). Η επιλογή εφαρμόζεται άμεσα και αποτελεί την προεπιλογή τόσο για το γραφικό περιβάλλον όσο και για τη γραμμή εντολών. +jfx.gui.preferences.general.debug=Έξοδος αποσφαλμάτωσης +jfx.gui.preferences.general.debug.help=Καταγραφή αναλυτικών διαγνωστικών κατά την υπογραφή: την αλυσίδα πιστοποιητικών υπογραφής, τις φορτωμένες άγκυρες εμπιστοσύνης και κάθε δικτυακή κλήση χρονοσήμανσης / AIA / CRL / OCSP. Ενεργοποιήστε το για τη διερεύνηση μιας αποτυχίας υπογραφής. Εφαρμόζεται άμεσα τόσο στο γραφικό περιβάλλον όσο και στη γραμμή εντολών. jfx.gui.preferences.section.font=Γραμματοσειρά ορατής υπογραφής jfx.gui.preferences.section.certificate=Επικύρωση πιστοποιητικού jfx.gui.preferences.section.network=Δίκτυο και SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties index 586efe64..dcc52bd2 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties @@ -504,8 +504,10 @@ jfx.gui.preferences.button.cancel=Cancelar jfx.gui.preferences.button.resetSection=Restablecer sección a valores predeterminados jfx.gui.preferences.restartHint=Surte efecto tras reiniciar jfx.gui.preferences.restartHint.tooltip=Esta configuración se lee una vez al inicio. Reinicie JSignPdf para que el cambio surta efecto. -jfx.gui.preferences.section.engine=Motor de firma +jfx.gui.preferences.section.general=General jfx.gui.preferences.engine.help=Motor de firma usado para firmar documentos. OpenPDF produce firmas PDF estándar; DSS produce firmas PAdES base (B/T/LT/LTA). La selección se aplica de inmediato y es la predeterminada tanto para la interfaz gráfica como para la línea de comandos. +jfx.gui.preferences.general.debug=Salida de depuración +jfx.gui.preferences.general.debug.help=Registrar diagnósticos detallados durante la firma: la cadena de certificados de firma, los anclajes de confianza cargados y cada llamada de red de sello de tiempo / AIA / CRL / OCSP. Actívelo para investigar un fallo de firma. Se aplica de inmediato tanto a la interfaz gráfica como a la línea de comandos. jfx.gui.preferences.section.font=Fuente de la firma visible jfx.gui.preferences.section.certificate=Validación del certificado jfx.gui.preferences.section.network=Red y SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties index 7d14ebfc..eb96d334 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Annuler jfx.gui.preferences.button.resetSection=Réinitialiser la section aux valeurs par défaut jfx.gui.preferences.restartHint=Prend effet après le redémarrage jfx.gui.preferences.restartHint.tooltip=Ce paramètre est lu une seule fois au démarrage. Redémarrez JSignPdf pour que la modification prenne effet. -jfx.gui.preferences.section.engine=Moteur de signature +jfx.gui.preferences.section.general=Général jfx.gui.preferences.engine.help=Moteur de signature utilisé pour signer les documents. OpenPDF produit des signatures PDF standard ; DSS produit des signatures PAdES de base (B/T/LT/LTA). La sélection s'applique immédiatement et constitue la valeur par défaut pour l'interface graphique et la ligne de commande. +jfx.gui.preferences.general.debug=Sortie de débogage +jfx.gui.preferences.general.debug.help=Journaliser des diagnostics détaillés pendant la signature : la chaîne de certificats de signature, les ancres de confiance chargées et chaque appel réseau d'horodatage / AIA / CRL / OCSP. Activez cette option pour analyser un échec de signature. S'applique immédiatement à l'interface graphique et à la ligne de commande. jfx.gui.preferences.section.font=Police de la signature visible jfx.gui.preferences.section.certificate=Validation du certificat jfx.gui.preferences.section.network=Réseau et SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties index 70cef6fc..385d46c6 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties @@ -502,8 +502,10 @@ jfx.gui.preferences.button.cancel=Odustani jfx.gui.preferences.button.resetSection=Vrati odjeljak na zadane vrijednosti jfx.gui.preferences.restartHint=Stupa na snagu nakon ponovnog pokretanja jfx.gui.preferences.restartHint.tooltip=Ova se postavka čita jednom pri pokretanju. Ponovno pokrenite JSignPdf da bi promjena stupila na snagu. -jfx.gui.preferences.section.engine=Pogon za potpisivanje +jfx.gui.preferences.section.general=Općenito jfx.gui.preferences.engine.help=Pogon za potpisivanje koji se upotrebljava za potpisivanje dokumenata. OpenPDF stvara standardne PDF potpise; DSS stvara osnovne PAdES potpise (B/T/LT/LTA). Odabir se primjenjuje odmah i zadan je i za grafičko sučelje i za naredbeni redak. +jfx.gui.preferences.general.debug=Dijagnostički ispis +jfx.gui.preferences.general.debug.help=Bilježi detaljnu dijagnostiku tijekom potpisivanja: lanac certifikata za potpisivanje, učitana sidra povjerenja te svaki mrežni poziv vremenskog žiga / AIA / CRL / OCSP. Uključite za istraživanje neuspjeha potpisivanja. Primjenjuje se odmah i na grafičko sučelje i na naredbeni redak. jfx.gui.preferences.section.font=Font vidljivog potpisa jfx.gui.preferences.section.certificate=Provjera certifikata jfx.gui.preferences.section.network=Mreža i SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties index 94996085..0d598d30 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Mégse jfx.gui.preferences.button.resetSection=Szakasz visszaállítása alapértelmezésekre jfx.gui.preferences.restartHint=Újraindítás után lép életbe jfx.gui.preferences.restartHint.tooltip=Ez a beállítás csak indításkor kerül beolvasásra. Indítsa újra a JSignPdf-et, hogy a változás életbe lépjen. -jfx.gui.preferences.section.engine=Aláírómotor +jfx.gui.preferences.section.general=Általános jfx.gui.preferences.engine.help=A dokumentumok aláírásához használt aláírómotor. Az OpenPDF szabványos PDF-aláírásokat készít; a DSS PAdES-alapaláírásokat (B/T/LT/LTA) készít. A kiválasztás azonnal érvénybe lép, és ez az alapértelmezés a grafikus felülethez és a parancssorhoz is. +jfx.gui.preferences.general.debug=Hibakeresési kimenet +jfx.gui.preferences.general.debug.help=Részletes diagnosztika naplózása aláírás közben: az aláíró tanúsítványlánc, a betöltött megbízhatósági horgonyok, valamint minden időbélyeg / AIA / CRL / OCSP hálózati hívás. Kapcsolja be aláírási hiba kivizsgálásához. Azonnal érvénybe lép a grafikus felületen és a parancssorban is. jfx.gui.preferences.section.font=Látható aláírás betűtípusa jfx.gui.preferences.section.certificate=Tanúsítvány érvényesítése jfx.gui.preferences.section.network=Hálózat és SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties index e1b3e7df..69d457fa 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Չեղարկել jfx.gui.preferences.button.resetSection=Վերականգնել բաժինը՝ լռելյայն արժեքների jfx.gui.preferences.restartHint=Ուժի մեջ կմտնի վերագործարկումից հետո jfx.gui.preferences.restartHint.tooltip=Այս կարգավորումը կարդացվում է միայն գործարկման ժամանակ։ Փոփոխությունը ուժի մեջ մտցնելու համար վերագործարկեք JSignPdf-ը։ -jfx.gui.preferences.section.engine=Ստորագրման շարժիչ +jfx.gui.preferences.section.general=Ընդհանուր jfx.gui.preferences.engine.help=Փաստաթղթերը ստորագրելու համար օգտագործվող ստորագրման շարժիչը: OpenPDF-ը ստեղծում է ստանդարտ PDF ստորագրություններ. DSS-ը ստեղծում է PAdES բազային ստորագրություններ (B/T/LT/LTA): Ընտրությունը կիրառվում է անմիջապես և կանխադրված է ինչպես գրաֆիկական միջերեսի, այնպես էլ հրամանային տողի համար: +jfx.gui.preferences.general.debug=Վրիպազերծման ելք +jfx.gui.preferences.general.debug.help=Գրանցել մանրամասն ախտորոշում ստորագրման ընթացքում՝ ստորագրման վկայագրերի շղթան, բեռնված վստահության խարիսխները և ժամանակի դրոշմի / AIA / CRL / OCSP յուրաքանչյուր ցանցային կանչ: Միացրեք սա ստորագրման ձախողումը հետազոտելու համար: Կիրառվում է անմիջապես ինչպես գրաֆիկական միջերեսի, այնպես էլ հրամանային տողի համար: jfx.gui.preferences.section.font=Տեսանելի ստորագրության տառատեսակ jfx.gui.preferences.section.certificate=Վկայագրի վավերացում jfx.gui.preferences.section.network=Ցանց և SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties index 7e2aee30..5f7fce26 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties @@ -497,8 +497,10 @@ jfx.gui.preferences.button.cancel=Annulla jfx.gui.preferences.button.resetSection=Ripristina sezione ai valori predefiniti jfx.gui.preferences.restartHint=Ha effetto dopo il riavvio jfx.gui.preferences.restartHint.tooltip=Questa impostazione viene letta una sola volta all'avvio. Riavvia JSignPdf per applicare la modifica. -jfx.gui.preferences.section.engine=Motore di firma +jfx.gui.preferences.section.general=Generale jfx.gui.preferences.engine.help=Motore di firma usato per firmare i documenti. OpenPDF produce firme PDF standard; DSS produce firme PAdES base (B/T/LT/LTA). La selezione si applica immediatamente ed è quella predefinita sia per l'interfaccia grafica sia per la riga di comando. +jfx.gui.preferences.general.debug=Output di debug +jfx.gui.preferences.general.debug.help=Registra diagnostica dettagliata durante la firma: la catena di certificati di firma, le ancore di fiducia caricate e ogni chiamata di rete per marca temporale / AIA / CRL / OCSP. Attivalo per analizzare un errore di firma. Si applica immediatamente sia all'interfaccia grafica sia alla riga di comando. jfx.gui.preferences.section.font=Carattere della firma visibile jfx.gui.preferences.section.certificate=Convalida del certificato jfx.gui.preferences.section.network=Rete e SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties index 65f3daa8..370e5559 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=キャンセル jfx.gui.preferences.button.resetSection=セクションを既定値にリセット jfx.gui.preferences.restartHint=再起動後に有効 jfx.gui.preferences.restartHint.tooltip=この設定は起動時に一度だけ読み込まれます。変更を反映するには JSignPdf を再起動してください。 -jfx.gui.preferences.section.engine=署名エンジン +jfx.gui.preferences.section.general=全般 jfx.gui.preferences.engine.help=文書の署名に使用する署名エンジン。OpenPDF は標準の PDF 署名を生成し、DSS は PAdES ベースライン署名(B/T/LT/LTA)を生成します。選択は即座に適用され、GUI と CLI の両方の既定値になります。 +jfx.gui.preferences.general.debug=デバッグ出力 +jfx.gui.preferences.general.debug.help=署名中に詳細な診断情報を記録します: 署名証明書チェーン、読み込まれたトラストアンカー、タイムスタンプ / AIA / CRL / OCSP のすべてのネットワーク呼び出し。署名の失敗を調査するには有効にしてください。GUI と CLI の両方に即座に適用されます。 jfx.gui.preferences.section.font=可視署名のフォント jfx.gui.preferences.section.certificate=証明書の検証 jfx.gui.preferences.section.network=ネットワークと SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties index 885cb100..7802573a 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Avbryt jfx.gui.preferences.button.resetSection=Tilbakestill seksjon til standardverdier jfx.gui.preferences.restartHint=Trer i kraft etter omstart jfx.gui.preferences.restartHint.tooltip=Denne innstillingen leses én gang ved oppstart. Start JSignPdf på nytt for at endringen skal tre i kraft. -jfx.gui.preferences.section.engine=Signeringsmotor +jfx.gui.preferences.section.general=Generelt jfx.gui.preferences.engine.help=Signeringsmotor som brukes til å signere dokumenter. OpenPDF produserer standard PDF-signaturer; DSS produserer PAdES-baselinesignaturer (B/T/LT/LTA). Valget trer i kraft umiddelbart og er standard for både det grafiske grensesnittet og kommandolinjen. +jfx.gui.preferences.general.debug=Feilsøkingsutdata +jfx.gui.preferences.general.debug.help=Logg detaljert diagnostikk under signering: signeringssertifikatkjeden, de innlastede tillitsankrene og hvert nettverkskall for tidsstempel / AIA / CRL / OCSP. Aktiver dette for å undersøke en signeringsfeil. Trer i kraft umiddelbart for både det grafiske grensesnittet og kommandolinjen. jfx.gui.preferences.section.font=Skrift for synlig signatur jfx.gui.preferences.section.certificate=Sertifikatvalidering jfx.gui.preferences.section.network=Nettverk og SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties index 6016ccad..88970873 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Anuluj jfx.gui.preferences.button.resetSection=Przywróć sekcję do wartości domyślnych jfx.gui.preferences.restartHint=Działa po ponownym uruchomieniu jfx.gui.preferences.restartHint.tooltip=To ustawienie jest odczytywane tylko przy starcie. Uruchom ponownie JSignPdf, aby zmiana zaczęła obowiązywać. -jfx.gui.preferences.section.engine=Silnik podpisu +jfx.gui.preferences.section.general=Ogólne jfx.gui.preferences.engine.help=Silnik podpisu używany do podpisywania dokumentów. OpenPDF tworzy standardowe podpisy PDF; DSS tworzy podpisy PAdES (B/T/LT/LTA). Wybór jest stosowany natychmiast i stanowi wartość domyślną zarówno dla interfejsu graficznego, jak i wiersza poleceń. +jfx.gui.preferences.general.debug=Dane diagnostyczne +jfx.gui.preferences.general.debug.help=Rejestruj szczegółową diagnostykę podczas podpisywania: łańcuch certyfikatów podpisu, wczytane kotwice zaufania oraz każde wywołanie sieciowe znacznika czasu / AIA / CRL / OCSP. Włącz, aby zbadać niepowodzenie podpisywania. Stosowane natychmiast zarówno dla interfejsu graficznego, jak i wiersza poleceń. jfx.gui.preferences.section.font=Czcionka widocznego podpisu jfx.gui.preferences.section.certificate=Walidacja certyfikatu jfx.gui.preferences.section.network=Sieć i SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties index d010474b..a329bd21 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties @@ -504,8 +504,10 @@ jfx.gui.preferences.button.cancel=Cancelar jfx.gui.preferences.button.resetSection=Restaurar secção para valores predefinidos jfx.gui.preferences.restartHint=Tem efeito após reinício jfx.gui.preferences.restartHint.tooltip=Esta definição é lida uma vez no arranque. Reinicie o JSignPdf para que a alteração tenha efeito. -jfx.gui.preferences.section.engine=Motor de assinatura +jfx.gui.preferences.section.general=Geral jfx.gui.preferences.engine.help=Motor de assinatura usado para assinar documentos. O OpenPDF produz assinaturas PDF padrão; o DSS produz assinaturas PAdES base (B/T/LT/LTA). A seleção aplica-se imediatamente e é a predefinição tanto para a interface gráfica como para a linha de comandos. +jfx.gui.preferences.general.debug=Saída de depuração +jfx.gui.preferences.general.debug.help=Registar diagnósticos detalhados durante a assinatura: a cadeia de certificados de assinatura, as âncoras de confiança carregadas e cada chamada de rede de marca temporal / AIA / CRL / OCSP. Ative para investigar uma falha de assinatura. Aplica-se imediatamente tanto à interface gráfica como à linha de comandos. jfx.gui.preferences.section.font=Tipo de letra da assinatura visível jfx.gui.preferences.section.certificate=Validação do certificado jfx.gui.preferences.section.network=Rede e SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties index 5df16b1e..27c382ff 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=Отмена jfx.gui.preferences.button.resetSection=Сбросить раздел к значениям по умолчанию jfx.gui.preferences.restartHint=Вступает в силу после перезапуска jfx.gui.preferences.restartHint.tooltip=Эта настройка считывается один раз при запуске. Перезапустите JSignPdf, чтобы изменения вступили в силу. -jfx.gui.preferences.section.engine=Движок подписи +jfx.gui.preferences.section.general=Общие jfx.gui.preferences.engine.help=Движок подписи, используемый для подписания документов. OpenPDF создаёт стандартные подписи PDF; DSS создаёт базовые подписи PAdES (B/T/LT/LTA). Выбор применяется немедленно и является значением по умолчанию как для графического интерфейса, так и для командной строки. +jfx.gui.preferences.general.debug=Отладочный вывод +jfx.gui.preferences.general.debug.help=Записывать подробную диагностику во время подписания: цепочку сертификатов подписи, загруженные якоря доверия и каждый сетевой вызов метки времени / AIA / CRL / OCSP. Включите для расследования сбоя подписания. Применяется немедленно как к графическому интерфейсу, так и к командной строке. jfx.gui.preferences.section.font=Шрифт видимой подписи jfx.gui.preferences.section.certificate=Проверка сертификата jfx.gui.preferences.section.network=Сеть и SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties index 53b3becb..f424a74d 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties @@ -504,8 +504,10 @@ jfx.gui.preferences.button.cancel=Zrušiť jfx.gui.preferences.button.resetSection=Obnoviť sekciu na predvolené hodnoty jfx.gui.preferences.restartHint=Prejaví sa po reštarte jfx.gui.preferences.restartHint.tooltip=Toto nastavenie sa načítava len pri spustení. Reštartujte JSignPdf, aby sa zmena prejavila. -jfx.gui.preferences.section.engine=Podpisový engine +jfx.gui.preferences.section.general=Všeobecné jfx.gui.preferences.engine.help=Podpisový engine použitý na podpisovanie dokumentov. OpenPDF vytvára štandardné PDF podpisy; DSS vytvára podpisy PAdES (B/T/LT/LTA). Výber sa prejaví okamžite a je predvolený pre GUI aj CLI. +jfx.gui.preferences.general.debug=Ladiaci výstup +jfx.gui.preferences.general.debug.help=Zaznamenávať podrobnú diagnostiku počas podpisovania: reťazec podpisového certifikátu, načítané kotvy dôvery a každé sieťové volanie časovej pečiatky / AIA / CRL / OCSP. Zapnite na riešenie zlyhania podpisu. Prejaví sa okamžite pre GUI aj CLI. jfx.gui.preferences.section.font=Písmo viditeľného podpisu jfx.gui.preferences.section.certificate=Overenie certifikátu jfx.gui.preferences.section.network=Sieť a SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties index dbc67712..f4292557 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties @@ -503,8 +503,10 @@ jfx.gui.preferences.button.cancel=ரத்து செய் jfx.gui.preferences.button.resetSection=பகுதியை இயல்புநிலைகளுக்கு மீட்டமை jfx.gui.preferences.restartHint=மறுதொடக்கத்திற்குப் பிறகு செயல்படும் jfx.gui.preferences.restartHint.tooltip=இந்த அமைப்பு தொடக்கத்தில் ஒரே ஒருமுறை மட்டும் படிக்கப்படுகிறது. மாற்றம் செயல்பட JSignPdf-ஐ மறுதொடங்கவும். -jfx.gui.preferences.section.engine=கையொப்ப எஞ்சின் +jfx.gui.preferences.section.general=பொது jfx.gui.preferences.engine.help=ஆவணங்களைக் கையொப்பமிடப் பயன்படுத்தப்படும் கையொப்ப எஞ்சின். OpenPDF நிலையான PDF கையொப்பங்களை உருவாக்குகிறது; DSS PAdES அடிப்படை கையொப்பங்களை (B/T/LT/LTA) உருவாக்குகிறது. தேர்வு உடனடியாக நடைமுறைக்கு வருகிறது மற்றும் வரைகலை இடைமுகம் மற்றும் கட்டளை வரி ஆகிய இரண்டிற்கும் இயல்புநிலையாகும். +jfx.gui.preferences.general.debug=பிழைத்திருத்த வெளியீடு +jfx.gui.preferences.general.debug.help=கையொப்பமிடும்போது விரிவான கண்டறிதல் தகவலைப் பதிவு செய்யவும்: கையொப்பச் சான்றிதழ் சங்கிலி, ஏற்றப்பட்ட நம்பிக்கை நங்கூரங்கள் மற்றும் ஒவ்வொரு நேர முத்திரை / AIA / CRL / OCSP நெட்வொர்க் அழைப்பு. கையொப்பத் தோல்வியை ஆராய இதை இயக்கவும். வரைகலை இடைமுகம் மற்றும் கட்டளை வரி ஆகிய இரண்டிற்கும் உடனடியாகப் பொருந்தும். jfx.gui.preferences.section.font=புலப்படும் கையொப்பத்தின் எழுத்துரு jfx.gui.preferences.section.certificate=சான்றிதழ் சரிபார்ப்பு jfx.gui.preferences.section.network=வலையமைப்பு மற்றும் SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties index 7281019d..6f9c5149 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties @@ -448,8 +448,10 @@ jfx.gui.preferences.button.cancel=取消 jfx.gui.preferences.button.resetSection=将此部分重置为默认值 jfx.gui.preferences.restartHint=重启后生效 jfx.gui.preferences.restartHint.tooltip=此设置仅在启动时读取一次。请重启 JSignPdf 使更改生效。 -jfx.gui.preferences.section.engine=签名引擎 +jfx.gui.preferences.section.general=常规 jfx.gui.preferences.engine.help=用于对文档进行签名的签名引擎。OpenPDF 生成标准 PDF 签名;DSS 生成 PAdES 基线签名(B/T/LT/LTA)。所选项会立即生效,并作为图形界面和命令行的默认值。 +jfx.gui.preferences.general.debug=调试输出 +jfx.gui.preferences.general.debug.help=在签名期间记录详细诊断信息:签名证书链、已加载的信任锚,以及每次时间戳 / AIA / CRL / OCSP 网络调用。启用此项以排查签名失败。会立即应用于图形界面和命令行。 jfx.gui.preferences.section.font=可见签名字体 jfx.gui.preferences.section.certificate=证书验证 jfx.gui.preferences.section.network=网络和 SSL diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties index 09e7265a..4390a593 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties @@ -448,8 +448,10 @@ jfx.gui.preferences.button.cancel=取消 jfx.gui.preferences.button.resetSection=將此區段重設為預設值 jfx.gui.preferences.restartHint=重新啟動後生效 jfx.gui.preferences.restartHint.tooltip=此設定僅於啟動時讀取一次。請重新啟動 JSignPdf 以使變更生效。 -jfx.gui.preferences.section.engine=簽署引擎 +jfx.gui.preferences.section.general=一般 jfx.gui.preferences.engine.help=用於簽署文件的簽署引擎。OpenPDF 會產生標準 PDF 簽章;DSS 會產生 PAdES 基準簽章(B/T/LT/LTA)。所選項目會立即生效,並作為圖形介面與命令列的預設值。 +jfx.gui.preferences.general.debug=偵錯輸出 +jfx.gui.preferences.general.debug.help=在簽署期間記錄詳細診斷資訊:簽署憑證鏈、已載入的信任錨,以及每次時間戳記 / AIA / CRL / OCSP 網路呼叫。啟用此項以排查簽署失敗。會立即套用於圖形介面與命令列。 jfx.gui.preferences.section.font=可見簽章字型 jfx.gui.preferences.section.certificate=憑證驗證 jfx.gui.preferences.section.network=網路與 SSL diff --git a/engines/api/src/test/java/net/sf/jsignpdf/utils/CertificateInfoTest.java b/engines/api/src/test/java/net/sf/jsignpdf/utils/CertificateInfoTest.java new file mode 100644 index 00000000..434463ac --- /dev/null +++ b/engines/api/src/test/java/net/sf/jsignpdf/utils/CertificateInfoTest.java @@ -0,0 +1,213 @@ +package net.sf.jsignpdf.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HexFormat; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import net.sf.jsignpdf.Constants; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.CRLDistPoint; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests the certificate description used by the FINE-level signing diagnostics (issue #452). + */ +public class CertificateInfoTest { + + private static final String OCSP_URL = "http://ocsp.example.test/ocsp"; + private static final String CA_ISSUERS_URL = "http://aia.example.test/ca.crt"; + private static final String CRL_URL = "http://crl.example.test/root.crl"; + + @BeforeClass + public static void addProvider() { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + public void tokenIdIsDssCertificateTokenId() throws Exception { + final X509Certificate cert = selfSigned("CN=Token Id Test", true); + final String expected = "C-" + HexFormat.of().withUpperCase() + .formatHex(MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())); + assertEquals(expected, CertificateInfo.tokenId(cert)); + } + + @Test + public void describeContainsIdentityAndExtensions() throws Exception { + final X509Certificate cert = selfSigned("CN=Full Certificate,O=JSignPdf Test", true); + final String description = CertificateInfo.describe(cert, " [0] "); + + assertTrue(description, description.contains("subject=O=JSignPdf Test,CN=Full Certificate")); + assertTrue(description, description.contains("issuer=O=JSignPdf Test,CN=Full Certificate")); + assertTrue(description, description.contains("serial=" + cert.getSerialNumber().toString(16))); + assertTrue(description, description.contains("id=" + CertificateInfo.tokenId(cert))); + assertTrue(description, description.contains("digitalSignature")); + assertTrue(description, description.contains("nonRepudiation")); + assertTrue(description, description.contains("QcCompliance")); + assertTrue(description, description.contains(CA_ISSUERS_URL)); + assertTrue(description, description.contains(OCSP_URL)); + assertTrue(description, description.contains(CRL_URL)); + } + + @Test + public void describeOmitsAbsentExtensions() throws Exception { + final String description = CertificateInfo.describe(selfSigned("CN=Bare Certificate", false), " [0] "); + + assertTrue(description, description.contains("subject=CN=Bare Certificate")); + assertTrue(description, !description.contains("keyUsage=")); + assertTrue(description, !description.contains("AIA ")); + assertTrue(description, !description.contains("CRL DP=")); + assertTrue(description, !description.contains("qcStatements=")); + } + + @Test + public void extensionAccessorsReturnTheConfiguredUrls() throws Exception { + final X509Certificate cert = selfSigned("CN=Urls", true); + + assertEquals(List.of(CA_ISSUERS_URL), CertificateInfo.caIssuersUrls(cert)); + assertEquals(List.of(OCSP_URL), CertificateInfo.ocspUrls(cert)); + assertEquals(List.of(CRL_URL), CertificateInfo.crlDistributionPointUrls(cert)); + assertEquals(List.of("QcCompliance"), CertificateInfo.qcStatements(cert)); + } + + @Test + public void subjectAndIssuerPreferTheCommonName() throws Exception { + final X509Certificate cert = selfSigned("CN=Just The CN,O=Ignored", false); + + assertEquals("Just The CN", CertificateInfo.subjectOf(cert)); + assertEquals("Just The CN", CertificateInfo.issuerOf(cert)); + } + + @Test + public void expiredCertificateIsFlagged() throws Exception { + final Date longAgo = new Date(System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000); + final Date yesterday = new Date(System.currentTimeMillis() - 24L * 60 * 60 * 1000); + final X509Certificate cert = build("CN=Expired", false, longAgo, yesterday); + + assertTrue(CertificateInfo.describe(cert, "").contains("(EXPIRED)")); + } + + @Test + public void logChainEmitsOneFineRecordForTheWholeChain() throws Exception { + final Certificate[] chain = { selfSigned("CN=Leaf", true), selfSigned("CN=Issuer", false) }; + final List records = captureFine(() -> CertificateInfo.logChain("Signing certificate chain", chain)); + + assertEquals(1, records.size()); + final String message = records.get(0).getMessage(); + assertTrue(message, message.startsWith("Signing certificate chain (2 certificates)")); + assertTrue(message, message.contains("[0] subject=CN=Leaf")); + assertTrue(message, message.contains("[1] subject=CN=Issuer")); + } + + @Test + public void logChainIgnoresEmptyInput() throws Exception { + assertTrue(captureFine(() -> CertificateInfo.logChain("Chain", (Certificate[]) null)).isEmpty()); + assertTrue(captureFine(() -> CertificateInfo.logChain("Chain", new Certificate[0])).isEmpty()); + } + + /** Runs {@code action} with FINE enabled on the JSignPdf logger and returns the records it produced. */ + private static List captureFine(Runnable action) { + final List records = new ArrayList<>(); + final Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + handler.setLevel(Level.ALL); + final Level originalLevel = Constants.LOGGER.getLevel(); + Constants.LOGGER.setLevel(Level.FINE); + Constants.LOGGER.addHandler(handler); + try { + action.run(); + } finally { + Constants.LOGGER.removeHandler(handler); + Constants.LOGGER.setLevel(originalLevel); + } + return records; + } + + private static X509Certificate selfSigned(String dn, boolean withExtensions) throws Exception { + final Date notBefore = new Date(System.currentTimeMillis() - 24L * 60 * 60 * 1000); + final Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); + return build(dn, withExtensions, notBefore, notAfter); + } + + private static X509Certificate build(String dn, boolean withExtensions, Date notBefore, Date notAfter) + throws Exception { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + final KeyPair keyPair = kpg.generateKeyPair(); + final X500Name name = new X500Name(dn); + final JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(name, + BigInteger.valueOf(System.nanoTime()), notBefore, notAfter, name, keyPair.getPublic()); + if (withExtensions) { + builder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)); + builder.addExtension(Extension.authorityInfoAccess, false, new AuthorityInformationAccess( + new AccessDescription[] { + new AccessDescription(AccessDescription.id_ad_caIssuers, uri(CA_ISSUERS_URL)), + new AccessDescription(AccessDescription.id_ad_ocsp, uri(OCSP_URL)) })); + builder.addExtension(Extension.cRLDistributionPoints, false, new CRLDistPoint(new DistributionPoint[] { + new DistributionPoint(new DistributionPointName(new GeneralNames(uri(CRL_URL))), null, null) })); + final ASN1EncodableVector qcStatements = new ASN1EncodableVector(); + qcStatements.add(new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qcs_QcCompliance)); + builder.addExtension(Extension.qCStatements, false, new DERSequence(qcStatements)); + } + final ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); + final X509CertificateHolder holder = builder.build(signer); + final X509Certificate certificate = new JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(holder); + assertNotNull(certificate); + return certificate; + } + + private static GeneralName uri(String url) { + return new GeneralName(GeneralName.uniformResourceIdentifier, url); + } +} diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/CapturingTspSource.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/CapturingTspSource.java index f32ee58e..f51905fd 100644 --- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/CapturingTspSource.java +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/CapturingTspSource.java @@ -8,6 +8,7 @@ import java.util.logging.Level; import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.CertificateInfo; import eu.europa.esig.dss.enumerations.DigestAlgorithm; import eu.europa.esig.dss.enumerations.TimestampType; @@ -57,9 +58,13 @@ public TimestampBinary getTimeStampResponse(DigestAlgorithm digestAlgorithm, byt final long startNanos = System.nanoTime(); TimestampBinary token = delegate.getTimeStampResponse(digestAlgorithm, digest); final long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; + final int knownCerts = capturedCerts.size(); capture(token); if (debug) { logResponse(token, elapsedMs); + if (capturedCerts.size() > knownCerts) { + CertificateInfo.logChain("Timestamp certificate chain", capturedCerts.values()); + } } return token; } 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 6921971b..e0796d53 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 @@ -167,9 +167,14 @@ CommonCertificateVerifier buildVerifier(ProxyConfig proxyConfig) throws Exceptio 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)); + // The loaders and sources are wrapped for FINE-level tracing of every AIA / CRL / OCSP call + // (issue #452); the wrappers are pass-through and are skipped entirely when FINE is off. + verifier.setAIASource(LoggingAIASource.wrap( + new DefaultAIASource(LoggingDataLoader.wrap("AIA", dataLoader)))); + verifier.setOcspSource(LoggingRevocationSource.wrap("OCSP", + new OnlineOCSPSource(LoggingDataLoader.wrap("OCSP", ocspDataLoader)))); + verifier.setCrlSource(LoggingRevocationSource.wrap("CRL", + new OnlineCRLSource(LoggingDataLoader.wrap("CRL", dataLoader)))); } if (config.getBoolean(KEY_ALLOW_UNTRUSTED, false)) { relaxTrustAndRevocationAlerts(verifier); @@ -222,6 +227,7 @@ CertificateSource[] createTrustedCertSources() throws Exception { for (String certFile : splitList(config.getString(KEY_CERT_FILES))) { CommonTrustedCertificateSource source = new CommonTrustedCertificateSource(); source.addCertificate(DSSUtils.loadCertificate(new File(certFile))); + logSourceAnchors(KEY_CERT_FILES + "=" + certFile, source); trustedSources.add(source); } for (String certUrl : splitList(config.getString(KEY_CERT_URLS))) { @@ -229,6 +235,7 @@ CertificateSource[] createTrustedCertSources() throws Exception { try (InputStream is = new URL(certUrl).openStream()) { source.addCertificate(DSSUtils.loadCertificate(is)); } + logSourceAnchors(KEY_CERT_URLS + "=" + certUrl, source); trustedSources.add(source); } @@ -239,7 +246,9 @@ CertificateSource[] createTrustedCertSources() throws Exception { final String pwd = config.getString(KEY_TRUSTSTORE_PASSWORD, ""); KeyStoreCertificateSource source = new KeyStoreCertificateSource(new File(truststoreFile), type, pwd != null ? pwd.toCharArray() : null); - trustedSources.add(asTrusted(source)); + CertificateSource trusted = asTrusted(source); + logSourceAnchors(KEY_TRUSTSTORE_FILE + "=" + truststoreFile, trusted); + trustedSources.add(trusted); } if (config.getBoolean(KEY_SYSTEM_STORE, false)) { @@ -250,9 +259,37 @@ CertificateSource[] createTrustedCertSources() throws Exception { } } } + logTotalAnchors(trustedSources); return trustedSources.toArray(new CertificateSource[0]); } + /** Names one configured trust source and the number of anchors it contributed, at FINE (issue #452). */ + private static void logSourceAnchors(String origin, CertificateSource source) { + if (Constants.LOGGER.isLoggable(Level.FINE)) { + Constants.LOGGER.fine("Trust source " + origin + ": " + source.getCertificates().size() + + " anchor(s)"); + } + } + + /** + * Logs the total number of trust anchors across every configured source. This is the line that separates + * "nothing is trusted at all" (a mis-set / empty {@code engine.dss.trust.*} configuration) from "the + * anchors loaded but this particular CA is not among them" — both otherwise surface only as the same + * untrusted-chain alert (issue #452). No trust source at all is normal for the B / T levels, which need + * none; for LT/LTA {@code DssLtTrustPreflight} rejects that configuration before signing starts. + */ + private static void logTotalAnchors(List trustedSources) { + if (!Constants.LOGGER.isLoggable(Level.FINE)) { + return; + } + int total = 0; + for (CertificateSource source : trustedSources) { + total += source.getCertificates().size(); + } + Constants.LOGGER.fine("Trust anchors loaded: " + total + " from " + trustedSources.size() + + " configured source(s)"); + } + /** * The OS / JVM certificate stores to seed anchors from when {@link #KEY_SYSTEM_STORE} is on: the portable * {@code cacerts} on every platform, plus the machine root store on Windows (via SunMSCAPI) and the login diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssUntrustedChainReporter.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssUntrustedChainReporter.java index 9d119076..18100f57 100644 --- a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssUntrustedChainReporter.java +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/DssUntrustedChainReporter.java @@ -2,13 +2,10 @@ import static net.sf.jsignpdf.Constants.RES; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; -import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -17,9 +14,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import javax.security.auth.x500.X500Principal; +import net.sf.jsignpdf.utils.CertificateInfo; /** * Turns the opaque untrusted-chain {@code AlertException} DSS raises for the LT / LTA levels into an @@ -112,21 +107,14 @@ private static Map index(Collection toX509List(Certificate[] chain) { } private static String subjectOf(X509Certificate cert) { - return commonNameOrDn(cert.getSubjectX500Principal()); + return CertificateInfo.subjectOf(cert); } private static String issuerOf(X509Certificate cert) { - return commonNameOrDn(cert.getIssuerX500Principal()); - } - - /** Returns the principal's CN when present, otherwise its full RFC 2253 DN. */ - private static String commonNameOrDn(X500Principal principal) { - try { - LdapName ldapName = new LdapName(principal.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 principal.getName(); + return CertificateInfo.issuerOf(cert); } } diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingAIASource.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingAIASource.java new file mode 100644 index 00000000..9360edfa --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingAIASource.java @@ -0,0 +1,71 @@ +package net.sf.jsignpdf.engine.dss; + +import java.util.Set; +import java.util.logging.Level; + +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.CertificateInfo; + +import eu.europa.esig.dss.model.x509.CertificateToken; +import eu.europa.esig.dss.spi.x509.aia.AIASource; + +/** + * An {@link AIASource} decorator that reports the issuer-certificate downloads DSS performs to complete a + * chain. A chain the keystore delivers incomplete is silently completed over AIA, so a failure to reach the + * {@code caIssuers} URL surfaces much later as an unanchored chain; logging the lookup and how many + * certificates it yielded makes the two cases distinguishable (issue #452). + * + * @author Josef Cacek + */ +final class LoggingAIASource implements AIASource { + + private static final long serialVersionUID = 1L; + + private final AIASource delegate; + + LoggingAIASource(AIASource delegate) { + this.delegate = delegate; + } + + /** + * Wraps {@code delegate} only when FINE logging is active, so a normal run keeps DSS's original source. + * + * @param delegate the AIA source to trace + * @return the decorated source, or {@code delegate} itself when FINE is off + */ + static AIASource wrap(AIASource delegate) { + return Constants.LOGGER.isLoggable(Level.FINE) ? new LoggingAIASource(delegate) : delegate; + } + + @Override + public Set getCertificatesByAIA(CertificateToken certificateToken) { + final long startNanos = System.nanoTime(); + try { + final Set certificates = delegate.getCertificatesByAIA(certificateToken); + logResult(certificateToken, certificates != null ? certificates.size() : 0, startNanos, null); + return certificates; + } catch (RuntimeException e) { + logResult(certificateToken, 0, startNanos, e); + throw e; + } + } + + private void logResult(CertificateToken certificateToken, int count, long startNanos, RuntimeException failure) { + final long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; + final StringBuilder buf = new StringBuilder("AIA lookup for "); + if (certificateToken.getCertificate() != null) { + buf.append(CertificateInfo.subjectOf(certificateToken.getCertificate())); + } else { + buf.append(certificateToken.getSubject().getCanonical()); + } + buf.append(" (").append(certificateToken.getDSSIdAsString()).append("): "); + if (failure != null) { + buf.append("FAILED (").append(failure.getClass().getSimpleName()).append(": ") + .append(failure.getMessage()).append(')'); + } else { + buf.append(count).append(" issuer certificate(s)"); + } + buf.append(", elapsed=").append(elapsedMs).append("ms"); + Constants.LOGGER.fine(buf.toString()); + } +} diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingDataLoader.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingDataLoader.java new file mode 100644 index 00000000..0d9c0d36 --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingDataLoader.java @@ -0,0 +1,113 @@ +package net.sf.jsignpdf.engine.dss; + +import java.util.List; +import java.util.logging.Level; + +import net.sf.jsignpdf.Constants; + +import eu.europa.esig.dss.spi.client.http.DataLoader; + +/** + * A {@link DataLoader} decorator that traces every HTTP call DSS makes while collecting validation material. + * DSS's own logging goes through SLF4J, which JSignPdf does not wire to its {@code java.util.logging} console, + * so without this the AIA / CRL / OCSP traffic is invisible: a signature that fails for want of revocation data + * shows that it is missing but not which server was contacted, whether it answered, or how long it took + * (issue #452). + * + *

+ * One line is logged per request at {@link Level#FINE}: the role ({@code AIA} / {@code CRL} / {@code OCSP}), the + * URL, the response size and the elapsed time — or the failure reason when the call did not return data. + * The delegate's result is passed through unchanged and exceptions are rethrown, so the decorator cannot alter + * the outcome of a fetch. + *

+ * + * @author Josef Cacek + */ +final class LoggingDataLoader implements DataLoader { + + private static final long serialVersionUID = 1L; + + /** Short name of what this loader fetches, used as the log prefix: {@code AIA}, {@code CRL} or {@code OCSP}. */ + private final String role; + + private final DataLoader delegate; + + LoggingDataLoader(String role, DataLoader delegate) { + this.role = role; + this.delegate = delegate; + } + + /** + * Wraps {@code delegate} only when FINE logging is active, so a normal run keeps DSS's original loader and + * pays nothing for the tracing. + * + * @param role the log prefix ({@code AIA} / {@code CRL} / {@code OCSP}) + * @param delegate the loader to trace + * @return the decorated loader, or {@code delegate} itself when FINE is off + */ + static DataLoader wrap(String role, DataLoader delegate) { + return Constants.LOGGER.isLoggable(Level.FINE) ? new LoggingDataLoader(role, delegate) : delegate; + } + + @Override + public byte[] get(String url) { + final long startNanos = System.nanoTime(); + try { + final byte[] data = delegate.get(url); + logResult("GET", url, data != null ? data.length : -1, startNanos, null); + return data; + } catch (RuntimeException e) { + logResult("GET", url, -1, startNanos, e); + throw e; + } + } + + @Override + public DataAndUrl get(List urlStrings) { + final long startNanos = System.nanoTime(); + try { + final DataAndUrl result = delegate.get(urlStrings); + final String url = result != null ? result.getUrlString() : String.valueOf(urlStrings); + logResult("GET", url, result != null && result.getData() != null ? result.getData().length : -1, + startNanos, null); + return result; + } catch (RuntimeException e) { + logResult("GET", String.valueOf(urlStrings), -1, startNanos, e); + throw e; + } + } + + @Override + public byte[] post(String url, byte[] content) { + final long startNanos = System.nanoTime(); + try { + final byte[] data = delegate.post(url, content); + logResult("POST", url, data != null ? data.length : -1, startNanos, null); + return data; + } catch (RuntimeException e) { + logResult("POST", url, -1, startNanos, e); + throw e; + } + } + + @Override + public void setContentType(String contentType) { + delegate.setContentType(contentType); + } + + private void logResult(String method, String url, int size, long startNanos, RuntimeException failure) { + final long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; + final StringBuilder buf = new StringBuilder(role).append(' ').append(method).append(' ').append(url) + .append(": "); + if (failure != null) { + buf.append("FAILED (").append(failure.getClass().getSimpleName()).append(": ") + .append(failure.getMessage()).append(')'); + } else if (size < 0) { + buf.append("no data"); + } else { + buf.append(size).append(" bytes"); + } + buf.append(", elapsed=").append(elapsedMs).append("ms"); + Constants.LOGGER.fine(buf.toString()); + } +} diff --git a/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingRevocationSource.java b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingRevocationSource.java new file mode 100644 index 00000000..6e847b14 --- /dev/null +++ b/engines/dss/src/main/java/net/sf/jsignpdf/engine/dss/LoggingRevocationSource.java @@ -0,0 +1,135 @@ +package net.sf.jsignpdf.engine.dss; + +import java.util.Date; +import java.util.List; +import java.util.logging.Level; + +import net.sf.jsignpdf.Constants; +import net.sf.jsignpdf.utils.CertificateInfo; + +import eu.europa.esig.dss.model.x509.CertificateToken; +import eu.europa.esig.dss.model.x509.revocation.Revocation; +import eu.europa.esig.dss.spi.x509.revocation.RevocationSource; +import eu.europa.esig.dss.spi.x509.revocation.RevocationSourceAlternateUrlsSupport; +import eu.europa.esig.dss.spi.x509.revocation.RevocationToken; + +/** + * A {@link RevocationSource} decorator that names the certificate behind every CRL / OCSP lookup DSS performs + * for the LT / LTA levels. {@link LoggingDataLoader} already traces the HTTP leg, but a URL alone does not say + * which certificate the fetch was for, nor what the answer said; this fills that in with the outcome: + * the responder URL DSS settled on, the certificate status and, for a CRL, the {@code thisUpdate} / + * {@code nextUpdate} window of what came back (issue #452). + * + *

+ * A revocation lookup has three outcomes and all three are reported: a token, {@code null} (no CRL / OCSP + * location on the certificate at all — the usual reason an LT signature cannot be built), or a + * {@code DSSExternalResourceException} when every access point failed. The delegate's behaviour is preserved in + * each case, exceptions included. + *

+ * + * @param the revocation type handled by the wrapped source (CRL or OCSP) + * + * @author Josef Cacek + */ +final class LoggingRevocationSource implements RevocationSourceAlternateUrlsSupport { + + private static final long serialVersionUID = 1L; + + /** Short name of the revocation mechanism, used as the log prefix: {@code CRL} or {@code OCSP}. */ + private final String role; + + private final RevocationSource delegate; + + LoggingRevocationSource(String role, RevocationSource delegate) { + this.role = role; + this.delegate = delegate; + } + + /** + * Wraps {@code delegate} only when FINE logging is active, so a normal run keeps DSS's original source. + * + * @param role the log prefix ({@code CRL} or {@code OCSP}) + * @param delegate the source to trace + * @return the decorated source, or {@code delegate} itself when FINE is off + */ + static RevocationSource wrap(String role, RevocationSource delegate) { + return Constants.LOGGER.isLoggable(Level.FINE) ? new LoggingRevocationSource<>(role, delegate) : delegate; + } + + @Override + public RevocationToken getRevocationToken(CertificateToken certificateToken, + CertificateToken issuerCertificateToken) { + final long startNanos = System.nanoTime(); + try { + final RevocationToken token = delegate.getRevocationToken(certificateToken, issuerCertificateToken); + logResult(certificateToken, token, startNanos, null); + return token; + } catch (RuntimeException e) { + logResult(certificateToken, null, startNanos, e); + throw e; + } + } + + @Override + public RevocationToken getRevocationToken(CertificateToken certificateToken, + CertificateToken issuerCertificateToken, List alternativeUrls) { + if (!(delegate instanceof RevocationSourceAlternateUrlsSupport)) { + return getRevocationToken(certificateToken, issuerCertificateToken); + } + @SuppressWarnings("unchecked") + final RevocationSourceAlternateUrlsSupport alternateUrlsDelegate = + (RevocationSourceAlternateUrlsSupport) delegate; + final long startNanos = System.nanoTime(); + try { + final RevocationToken token = alternateUrlsDelegate.getRevocationToken(certificateToken, + issuerCertificateToken, alternativeUrls); + logResult(certificateToken, token, startNanos, null); + return token; + } catch (RuntimeException e) { + logResult(certificateToken, null, startNanos, e); + throw e; + } + } + + private void logResult(CertificateToken certificateToken, RevocationToken token, long startNanos, + RuntimeException failure) { + final long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; + final StringBuilder buf = new StringBuilder(role).append(" lookup for ") + .append(subjectOf(certificateToken)).append(" (").append(certificateToken.getDSSIdAsString()) + .append("): "); + if (failure != null) { + buf.append("FAILED (").append(failure.getClass().getSimpleName()).append(": ") + .append(failure.getMessage()).append(')'); + } else if (token == null) { + buf.append("no ").append(role).append(" data (no access point on the certificate, or none usable)"); + } else { + buf.append("status=").append(token.getStatus()); + appendIfPresent(buf, ", source=", token.getSourceURL()); + appendDateIfPresent(buf, ", thisUpdate=", token.getThisUpdate()); + appendDateIfPresent(buf, ", nextUpdate=", token.getNextUpdate()); + appendDateIfPresent(buf, ", producedAt=", token.getProductionDate()); + appendDateIfPresent(buf, ", revokedAt=", token.getRevocationDate()); + appendIfPresent(buf, ", reason=", token.getReason()); + } + buf.append(", elapsed=").append(elapsedMs).append("ms"); + Constants.LOGGER.fine(buf.toString()); + } + + private static String subjectOf(CertificateToken certificateToken) { + return certificateToken.getCertificate() != null + ? CertificateInfo.subjectOf(certificateToken.getCertificate()) + : certificateToken.getSubject().getCanonical(); + } + + private static void appendIfPresent(StringBuilder buf, String caption, Object value) { + if (value != null) { + buf.append(caption).append(value); + } + } + + private static void appendDateIfPresent(StringBuilder buf, String caption, Date value) { + if (value != null) { + buf.append(caption).append(CertificateInfo.formatTimestamp(value)); + } + } +} diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssDebugTracingTest.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssDebugTracingTest.java new file mode 100644 index 00000000..647940eb --- /dev/null +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/DssDebugTracingTest.java @@ -0,0 +1,220 @@ +package net.sf.jsignpdf.engine.dss; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import net.sf.jsignpdf.Constants; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Test; + +import eu.europa.esig.dss.model.x509.CertificateToken; +import eu.europa.esig.dss.model.x509.revocation.crl.CRL; +import eu.europa.esig.dss.spi.client.http.DataLoader; +import eu.europa.esig.dss.spi.exception.DSSExternalResourceException; +import eu.europa.esig.dss.spi.x509.aia.AIASource; +import eu.europa.esig.dss.spi.x509.revocation.RevocationSource; + +/** + * Tests the FINE-level tracing decorators wired into the DSS certificate verifier (issue #452): every AIA / + * CRL / OCSP call must be reported with its URL, outcome and timing, and the decorators must pass the + * delegate's result (and failures) through untouched. + */ +public class DssDebugTracingTest { + + private static final String URL = "http://revocation.example.test/crl"; + + @Test + public void dataLoaderIsNotWrappedWhenFineIsOff() { + final DataLoader delegate = new StubDataLoader(new byte[10], null); + final Level originalLevel = Constants.LOGGER.getLevel(); + Constants.LOGGER.setLevel(Level.INFO); + try { + assertSame(delegate, LoggingDataLoader.wrap("CRL", delegate)); + } finally { + Constants.LOGGER.setLevel(originalLevel); + } + } + + @Test + public void dataLoaderLogsUrlAndResponseSize() { + final DataLoader delegate = new StubDataLoader(new byte[1234], null); + final List records = captureFine( + () -> assertEquals(1234, LoggingDataLoader.wrap("CRL", delegate).get(URL).length)); + + assertEquals(1, records.size()); + final String message = records.get(0).getMessage(); + assertTrue(message, message.startsWith("CRL GET " + URL + ": 1234 bytes")); + assertTrue(message, message.contains("elapsed=")); + } + + @Test + public void dataLoaderLogsFailureAndRethrows() { + final RuntimeException failure = new DSSExternalResourceException("HTTP status code : 503"); + final DataLoader delegate = new StubDataLoader(null, failure); + final List records = captureFine(() -> { + try { + LoggingDataLoader.wrap("OCSP", delegate).post(URL, new byte[1]); + fail("The delegate's exception must be propagated"); + } catch (DSSExternalResourceException e) { + assertSame(failure, e); + } + }); + + assertEquals(1, records.size()); + final String message = records.get(0).getMessage(); + assertTrue(message, message.contains("OCSP POST " + URL + ": FAILED")); + assertTrue(message, message.contains("HTTP status code : 503")); + } + + @Test + public void revocationSourceLogsTheCertificateWhenNoDataIsAvailable() throws Exception { + final CertificateToken certificate = certificateToken(); + final List records = captureFine(() -> { + final RevocationSource source = LoggingRevocationSource.wrap("CRL", (cert, issuer) -> null); + assertNull(source.getRevocationToken(certificate, certificate)); + }); + + assertEquals(1, records.size()); + final String message = records.get(0).getMessage(); + assertTrue(message, message.startsWith("CRL lookup for ")); + assertTrue(message, message.contains(certificate.getDSSIdAsString())); + assertTrue(message, message.contains("no CRL data")); + } + + @Test + public void revocationSourceLogsFailureAndRethrows() throws Exception { + final CertificateToken certificate = certificateToken(); + final RuntimeException failure = new DSSExternalResourceException("Unable to retrieve CRL"); + final List records = captureFine(() -> { + final RevocationSource source = LoggingRevocationSource.wrap("CRL", (cert, issuer) -> { + throw failure; + }); + try { + source.getRevocationToken(certificate, certificate); + fail("The delegate's exception must be propagated"); + } catch (DSSExternalResourceException e) { + assertSame(failure, e); + } + }); + + assertEquals(1, records.size()); + assertTrue(records.get(0).getMessage(), records.get(0).getMessage().contains("FAILED")); + } + + @Test + public void aiaSourceLogsTheNumberOfDownloadedIssuers() throws Exception { + final CertificateToken certificate = certificateToken(); + final List records = captureFine(() -> { + final AIASource source = LoggingAIASource.wrap(Collections::singleton); + assertEquals(1, source.getCertificatesByAIA(certificate).size()); + }); + + assertEquals(1, records.size()); + final String message = records.get(0).getMessage(); + assertTrue(message, message.startsWith("AIA lookup for ")); + assertTrue(message, message.contains("1 issuer certificate(s)")); + } + + /** Runs {@code action} with FINE enabled on the JSignPdf logger and returns the records it produced. */ + private static List captureFine(Runnable action) { + final List records = new ArrayList<>(); + final Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + handler.setLevel(Level.ALL); + final Level originalLevel = Constants.LOGGER.getLevel(); + Constants.LOGGER.setLevel(Level.FINE); + Constants.LOGGER.addHandler(handler); + try { + action.run(); + } finally { + Constants.LOGGER.removeHandler(handler); + Constants.LOGGER.setLevel(originalLevel); + } + return records; + } + + private static CertificateToken certificateToken() throws Exception { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + final KeyPair keyPair = kpg.generateKeyPair(); + final X500Name name = new X500Name("CN=Revocation Trace Test"); + final Date notBefore = new Date(System.currentTimeMillis() - 24L * 60 * 60 * 1000); + final Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); + final ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + final X509CertificateHolder holder = new JcaX509v3CertificateBuilder(name, BigInteger.ONE, notBefore, + notAfter, name, keyPair.getPublic()).build(signer); + return new CertificateToken(new JcaX509CertificateConverter().getCertificate(holder)); + } + + /** A {@link DataLoader} that answers every call with a fixed payload, or fails with a fixed exception. */ + private static final class StubDataLoader implements DataLoader { + + private static final long serialVersionUID = 1L; + + private final byte[] response; + private final RuntimeException failure; + + StubDataLoader(byte[] response, RuntimeException failure) { + this.response = response; + this.failure = failure; + } + + @Override + public byte[] get(String url) { + return answer(); + } + + @Override + public DataAndUrl get(List urlStrings) { + return new DataAndUrl(urlStrings.get(0), answer()); + } + + @Override + public byte[] post(String url, byte[] content) { + return answer(); + } + + @Override + public void setContentType(String contentType) { + } + + private byte[] answer() { + if (failure != null) { + throw failure; + } + return response; + } + } +} 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 0583f21c..a0838385 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 @@ -358,14 +358,50 @@ public void untrustedSignerChainReportsCertificateIdentity() throws Exception { assertTrue("must name the issuing CA to add", severe.contains("JSignPdf Test Root CA")); } - /** Collects the formatted messages of {@code SEVERE} log records, for asserting on engine diagnostics. */ + @Test + public void ltSigningTracesCertificatesAndRevocationFetchesAtFine() throws Exception { + // Issue #452: with FINE enabled the whole network-facing side of an LT signature must be visible -- + // the signer and timestamp chains, the trust anchors that were loaded, and every CRL / TSA call. + BasicSignerOptions o = caSignerOptions(PadesLevel.BASELINE_LT); + + CapturingLogHandler handler = new CapturingLogHandler(); + handler.setLevel(java.util.logging.Level.ALL); + java.util.logging.Logger logger = java.util.logging.Logger.getLogger("net.sf.jsignpdf"); + java.util.logging.Level originalLevel = logger.getLevel(); + logger.setLevel(java.util.logging.Level.FINE); + logger.addHandler(handler); + try { + assertTrue("LT must succeed with a trusted issuer and reachable CRL", + new DssSigningEngine().sign(o, caTrustConfig())); + } finally { + logger.removeHandler(handler); + logger.setLevel(originalLevel); + } + + String trace = handler.allMessages(); + assertTrue("must dump the signer chain", trace.contains("Signing certificate chain (2 certificates)")); + assertTrue("must name the signer certificate", trace.contains("JSignPdf Test Signer")); + assertTrue("must list the signer's CRL distribution point", + trace.contains("CRL DP=" + embeddedCa.getCrlUrl())); + assertTrue("must dump the timestamp chain", trace.contains("Timestamp certificate chain")); + assertTrue("must report how many trust anchors were loaded", trace.contains("Trust anchors loaded:")); + assertTrue("must trace the CRL HTTP fetch", trace.contains("CRL GET " + embeddedCa.getCrlUrl())); + assertTrue("must trace the CRL lookup outcome", trace.contains("CRL lookup for JSignPdf Test Signer")); + assertTrue("must report the CRL validity window", trace.contains("thisUpdate=")); + assertTrue("must trace the TSA request", trace.contains("TSA request -> ")); + } + + /** Collects the formatted messages of log records, for asserting on engine diagnostics. */ private static final class CapturingLogHandler extends java.util.logging.Handler { private final StringBuilder severe = new StringBuilder(); + private final StringBuilder all = new StringBuilder(); @Override public void publish(java.util.logging.LogRecord record) { + final String message = new java.util.logging.SimpleFormatter().formatMessage(record); + all.append(message).append('\n'); if (record.getLevel().intValue() >= java.util.logging.Level.SEVERE.intValue()) { - severe.append(new java.util.logging.SimpleFormatter().formatMessage(record)).append('\n'); + severe.append(message).append('\n'); } } @@ -380,6 +416,10 @@ public void close() { String severeMessages() { return severe.toString(); } + + String allMessages() { + return all.toString(); + } } /** diff --git a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedCa.java b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedCa.java index 1616535a..a52d6341 100644 --- a/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedCa.java +++ b/engines/dss/src/test/java/net/sf/jsignpdf/engine/dss/EmbeddedCa.java @@ -105,6 +105,11 @@ X509Certificate getCaCertificate() { return caCertificate; } + /** @return the loopback CRL endpoint issued certificates point their distribution point at. */ + String getCrlUrl() { + return crlUrl; + } + /** * Issues a fresh RSA-2048 leaf signing certificate (with a CRL distribution point pointing at this CA's * loopback CRL endpoint) and packages the private key plus the {@code [leaf, CA]} chain into an in-memory diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java index e30b2025..b6f5b831 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java @@ -25,6 +25,7 @@ import net.sf.jsignpdf.engine.EngineRegistry; import net.sf.jsignpdf.engine.SigningEngine; import net.sf.jsignpdf.ssl.SSLInitializer; +import net.sf.jsignpdf.utils.AppConfig; import net.sf.jsignpdf.utils.GuiUtils; import net.sf.jsignpdf.utils.KeyStoreUtils; import net.sf.jsignpdf.utils.PKCS11Utils; @@ -114,6 +115,10 @@ public static void main(String[] args) { parseCommandLine(args, tmpOpts); } + // Raise the log level to FINE when advanced.properties has debug=true (a no-op under -q, which has + // already muted the logger during parsing). Applies to both the CLI and the GUI launched below. + AppConfig.applyDebugLogLevel(); + try { SSLInitializer.init(); } catch (Exception e) { 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 2ceb3ce3..049c5327 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 @@ -49,6 +49,7 @@ import net.sf.jsignpdf.fx.util.NativeFileChooser.ExtensionFilter; import net.sf.jsignpdf.ssl.SSLInitializer; import net.sf.jsignpdf.utils.AdvancedConfig; +import net.sf.jsignpdf.utils.AppConfig; import net.sf.jsignpdf.utils.ConfigLocationResolver; import net.sf.jsignpdf.utils.FontUtils; import net.sf.jsignpdf.utils.PKCS11Utils; @@ -68,7 +69,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 tabGeneral; @FXML private Tab tabFont; @FXML private Tab tabCertificate; @FXML private Tab tabNetwork; @@ -78,6 +79,7 @@ public class PreferencesController { @FXML private Tab tabPkcs11; @FXML private ChoiceBox cmbEngine; + @FXML private CheckBox chkDebug; @FXML private TextField txtFontPath; @FXML private Button btnFontPathBrowse; @@ -221,6 +223,8 @@ void bind(PreferencesViewModel viewModel) { } }); + chkDebug.selectedProperty().bindBidirectional(vm.debugProperty()); + txtFontPath.textProperty().bindBidirectional(vm.fontPathProperty()); txtFontName.textProperty().bindBidirectional(vm.fontNameProperty()); cmbFontEncoding.valueProperty().bindBidirectional(vm.fontEncodingProperty()); @@ -349,8 +353,8 @@ private void onPkcs11ResetSample() { private void resetActiveTabToDefaults() { Tab active = tabPane.getSelectionModel().getSelectedItem(); AdvancedConfig defaults = bundledDefaultsHolder(); - if (active == tabEngine) { - vm.applyEngineDefaults(defaults); + if (active == tabGeneral) { + vm.applyGeneralDefaults(defaults); } else if (active == tabFont) { vm.applyFontDefaults(defaults); } else if (active == tabCertificate) { @@ -405,6 +409,11 @@ private boolean persist(AdvancedConfig cfg, Path pkcs11Path) { if (changed.stream().anyMatch(k -> k.startsWith("font."))) { FontUtils.reset(); } + if (changed.contains("debug")) { + // The logger is a process-global singleton, so a debug toggle takes effect for the next + // signing run without a restart. + AppConfig.applyDebugLogLevel(); + } if (changed.contains("relax.ssl.security")) { // Re-apply the trust-manager / hostname-verifier so a false→true toggle takes effect for the next request. // The JVM-wide system properties (jsse.enableSNIExtension, etc.) still need a restart — covered by the hint label. 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 663c28d7..5d2a6f2a 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 @@ -27,6 +27,7 @@ public class PreferencesViewModel { public static final String LIB_OPENPDF = "openpdf"; private final StringProperty engineId = new SimpleStringProperty(AppConfig.DEFAULT_ENGINE_ID); + private final BooleanProperty debug = new SimpleBooleanProperty(false); private final StringProperty fontPath = new SimpleStringProperty(""); private final StringProperty fontName = new SimpleStringProperty(""); @@ -65,6 +66,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)); + debug.set(cfg.getAsBool("debug", false)); fontPath.set(orEmpty(cfg.getProperty("font.path"))); fontName.set(orEmpty(cfg.getProperty("font.name"))); fontEncoding.set(orEmpty(cfg.getProperty("font.encoding"))); @@ -97,6 +99,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)); + cfg.setProperty("debug", debug.get()); writeStringOrRemove(cfg, "font.path", fontPath.get()); writeStringOrRemove(cfg, "font.name", fontName.get()); writeStringOrRemove(cfg, "font.encoding", fontEncoding.get()); @@ -122,7 +125,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); + applyGeneralDefaults(defaults); applyFontDefaults(defaults); applyCertificateDefaults(defaults); applyNetworkDefaults(defaults); @@ -131,8 +134,9 @@ public void applyDefaults(AdvancedConfig defaults) { applyDssDefaults(defaults); } - public void applyEngineDefaults(AdvancedConfig defaults) { + public void applyGeneralDefaults(AdvancedConfig defaults) { engineId.set(orFallback(defaults.getBundledDefault("engine"), AppConfig.DEFAULT_ENGINE_ID)); + debug.set(parseBool(defaults.getBundledDefault("debug"), false)); } public void applyFontDefaults(AdvancedConfig defaults) { @@ -255,6 +259,7 @@ private static void writeStringOrRemove(AdvancedConfig cfg, String key, String v } public StringProperty engineIdProperty() { return engineId; } + public BooleanProperty debugProperty() { return debug; } public StringProperty fontPathProperty() { return fontPath; } public StringProperty fontNameProperty() { return fontName; } public StringProperty fontEncodingProperty() { return fontEncoding; } diff --git a/jsignpdf/src/main/resources/logging.properties b/jsignpdf/src/main/resources/logging.properties index 3bf4dcb3..8585385c 100644 --- a/jsignpdf/src/main/resources/logging.properties +++ b/jsignpdf/src/main/resources/logging.properties @@ -3,4 +3,8 @@ handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=ALL java.util.logging.SimpleFormatter.format=%4$s %5$s%6$s%n # Levels: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL -net.sf.jsignpdf.level=FINE +# Baseline level for normal runs (progress messages). The advanced-config +# 'debug=true' key (Preferences > General, or advanced.properties) raises this +# to FINE at startup to surface the signing diagnostics; -q / --quiet lowers it +# to OFF. See net.sf.jsignpdf.utils.AppConfig#applyDebugLogLevel. +net.sf.jsignpdf.level=INFO 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 76a8f0f9..e4124840 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,8 +11,8 @@ tabClosingPolicy="UNAVAILABLE" prefWidth="720" prefHeight="520"> - - + + 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 20f292f5..d100f3cd 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/FxTranslationsTest.java @@ -57,6 +57,7 @@ public class FxTranslationsTest { @BeforeClass public static void initFx() throws Exception { + MonocleAssumption.assumeUsable(); CountDownLatch latch = new CountDownLatch(1); try { Platform.startup(latch::countDown); diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/MonocleAssumption.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/MonocleAssumption.java new file mode 100644 index 00000000..e74b83ce --- /dev/null +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/MonocleAssumption.java @@ -0,0 +1,34 @@ +package net.sf.jsignpdf.fx; + +import javafx.application.Platform; + +import org.junit.Assume; + +/** + * Guard for the tests that need a running JavaFX toolkit. Those rely on the headless Monocle platform the + * surefire configuration selects with {@code -Dglass.platform=Monocle}, which only works when JavaFX itself + * comes from the class path — as it does in CI, where the build runs on a Temurin JDK and JavaFX is + * pulled in as ordinary Maven jars. + * + *

+ * On a JDK that bundles JavaFX (e.g. Zulu {@code ca-fx}), {@code javafx.graphics} is instead resolved + * as a named platform module and cannot see the {@code openjfx-monocle} test jar on the class path. Toolkit + * startup then dies on the launcher thread with {@code PlatformFactory.getPlatformFactory()} returning + * {@code null}. That is worse than a failing test: {@code PlatformImpl}'s internal startup latch is never + * counted down while the toolkit still counts as initialised, so the next {@link Platform#runLater} parks + * forever in {@code waitForStart()} and the build hangs instead of failing. Skipping up front keeps such a + * JDK usable for everything else. + *

+ */ +public final class MonocleAssumption { + + private MonocleAssumption() { + } + + /** Skips the calling test (or class, from a {@code @BeforeClass}) unless Monocle can be applied. */ + public static void assumeUsable() { + Assume.assumeFalse("JavaFX is bundled in this JDK (javafx.graphics is a named module), so the Monocle" + + " headless platform cannot be applied - build with a JDK without JavaFX modules to run" + + " these tests", Platform.class.getModule().isNamed()); + } +} diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModelTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModelTest.java index 84c0ecde..5975faff 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModelTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModelTest.java @@ -125,6 +125,31 @@ public void applyDefaults_loadsBundled() throws Exception { assertEquals("SHA-256", vm.tsaHashAlgorithmProperty().get()); } + @Test + public void debug_roundTripsAndResetsToDefault() throws Exception { + Path file = tmp.newFolder().toPath().resolve("advanced.properties"); + AdvancedConfig cfg = new AdvancedConfig(file, bundledDefaults); + PreferencesViewModel vm = new PreferencesViewModel(); + + // Bundled default is off. + vm.loadFrom(cfg, ""); + assertFalse(vm.debugProperty().get()); + + // Turn it on and persist; it must be written under the 'debug' key. + vm.debugProperty().set(true); + vm.writeTo(cfg); + assertTrue(cfg.getAsBool("debug", false)); + + // Reload picks the persisted value back up. + PreferencesViewModel reloaded = new PreferencesViewModel(); + reloaded.loadFrom(cfg, ""); + assertTrue(reloaded.debugProperty().get()); + + // Reset-to-defaults for the General tab clears it. + reloaded.applyGeneralDefaults(new AdvancedConfig(null, bundledDefaults)); + assertFalse(reloaded.debugProperty().get()); + } + @Test public void dssSystemStoreAndMra_roundTripAndReset() throws Exception { Path file = tmp.newFolder().toPath().resolve("advanced.properties"); diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/view/SignaturePropertiesControllerTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/view/SignaturePropertiesControllerTest.java index 58d5a4be..c30108dc 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/view/SignaturePropertiesControllerTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/view/SignaturePropertiesControllerTest.java @@ -23,12 +23,14 @@ import net.sf.jsignpdf.engine.EngineConfig; import net.sf.jsignpdf.engine.SigningEngine; import net.sf.jsignpdf.fx.EngineCapabilities; +import net.sf.jsignpdf.fx.MonocleAssumption; import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel; public class SignaturePropertiesControllerTest { @BeforeClass public static void initFx() throws Exception { + MonocleAssumption.assumeUsable(); CountDownLatch latch = new CountDownLatch(1); try { Platform.startup(latch::countDown); diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/utils/AppConfigTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/utils/AppConfigTest.java index 9b53f525..346e096c 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/utils/AppConfigTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/utils/AppConfigTest.java @@ -2,6 +2,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.util.logging.Level; + +import net.sf.jsignpdf.Constants; import org.junit.Test; @@ -19,6 +24,7 @@ public void accessorsReturnSomething() { AppConfig.checkValidity(); AppConfig.checkKeyUsage(); AppConfig.checkCriticalExtensions(); + AppConfig.debug(); // String accessors backed by getNotEmptyProperty + literal default never return null/empty. assertNotNull(AppConfig.pdf2imageLibraries()); @@ -60,4 +66,68 @@ public void pdf2imageLibrariesFallsBackToConstants() { // bundled default matches the Constants literal. libs.isEmpty() ? net.sf.jsignpdf.Constants.PDF2IMAGE_LIBRARIES_DEFAULT : libs); } + + @Test + public void debugReadsTheDebugKey() { + AdvancedConfig cfg = PropertyStoreFactory.getInstance().advancedConfig(); + String original = cfg.hasUserOverride("debug") ? cfg.getProperty("debug") : null; + try { + cfg.setProperty("debug", "true"); + org.junit.Assert.assertTrue("debug() must read the debug key", AppConfig.debug()); + cfg.setProperty("debug", "false"); + org.junit.Assert.assertFalse(AppConfig.debug()); + } finally { + if (original != null) { + cfg.setProperty("debug", original); + } else { + cfg.removeProperty("debug"); + } + } + } + + @Test + public void applyDebugLogLevelMapsDebugToFineElseInfo() { + AdvancedConfig cfg = PropertyStoreFactory.getInstance().advancedConfig(); + String original = cfg.hasUserOverride("debug") ? cfg.getProperty("debug") : null; + Level originalLevel = Constants.LOGGER.getLevel(); + try { + Constants.LOGGER.setLevel(Level.INFO); + + cfg.setProperty("debug", "true"); + AppConfig.applyDebugLogLevel(); + assertSame("debug=true raises the logger to FINE", Level.FINE, Constants.LOGGER.getLevel()); + + cfg.setProperty("debug", "false"); + AppConfig.applyDebugLogLevel(); + assertSame("debug=false lowers the logger to INFO", Level.INFO, Constants.LOGGER.getLevel()); + } finally { + Constants.LOGGER.setLevel(originalLevel); + if (original != null) { + cfg.setProperty("debug", original); + } else { + cfg.removeProperty("debug"); + } + } + } + + @Test + public void applyDebugLogLevelLeavesQuietLoggerOff() { + // -q mutes the logger to OFF; debug must never override that. + AdvancedConfig cfg = PropertyStoreFactory.getInstance().advancedConfig(); + String original = cfg.hasUserOverride("debug") ? cfg.getProperty("debug") : null; + Level originalLevel = Constants.LOGGER.getLevel(); + try { + Constants.LOGGER.setLevel(Level.OFF); + cfg.setProperty("debug", "true"); + AppConfig.applyDebugLogLevel(); + assertSame("quiet (OFF) must win over debug", Level.OFF, Constants.LOGGER.getLevel()); + } finally { + Constants.LOGGER.setLevel(originalLevel); + if (original != null) { + cfg.setProperty("debug", original); + } else { + cfg.removeProperty("debug"); + } + } + } } diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index f4a74616..c1ba06c0 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -277,6 +277,7 @@ Presets are plain Java properties files under the < Preferences..._ (shortcut `Ctrl+,` / `⌘,`) for application-wide tweaks that fall outside the per-document signing options. The dialog has the following tabs: +* **General** -- the default signing engine, and the *Debug output* toggle that enables the verbose signing diagnostics (see <>) * **Visible Signature** -- font file, font name and encoding for the visible-signature L2 text * **Certificates** -- which certificate checks (validity, key usage, critical extensions) are applied when listing aliases * **Network** -- relax SSL security for TSA / OCSP / CRL traffic @@ -1041,7 +1042,7 @@ DSS requires a PAdES digest, so the hash algorithm must be `SHA256`, `SHA384` or |Re-sign with a larger reservation if the reserved `/Contents` turns out too small. Default `true`; each retry repeats signing (and refetches the TSA timestamp for level `T` and above). |=== -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. +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. The log also records which trust anchors were loaded and every AIA / CRL / OCSP call the engine made, which is usually enough to tell a network problem from a missing anchor -- see <>. On the command line these keys can be set for a single run with `-o` (see <>), without editing `advanced.properties`: @@ -1083,7 +1084,9 @@ Password-based PDF encryption combined with signing is likewise not honoured uni 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. -`advanced.properties` covers the same topics as the Preferences dialog (visible-signature font, certificate-validation toggles, relaxed SSL, PDF preview backend order, default TSA hash algorithm) plus a few file-editable knobs such as `output.suffix` -- the suffix appended to the input file name to derive the default output file name (bundled default `_signed`; e.g. set `output.suffix=_firmado` to produce `mydocument_firmado.pdf`). When a key is missing from `advanced.properties`, JSignPdf falls back to defaults bundled inside the application jar, so a fresh install needs no config file at all. +`advanced.properties` covers the same topics as the Preferences dialog (the signing engine, verbose `debug` output, visible-signature font, certificate-validation toggles, relaxed SSL, PDF preview backend order, default TSA hash algorithm) plus a few file-editable knobs such as `output.suffix` -- the suffix appended to the input file name to derive the default output file name (bundled default `_signed`; e.g. set `output.suffix=_firmado` to produce `mydocument_firmado.pdf`). When a key is missing from `advanced.properties`, JSignPdf falls back to defaults bundled inside the application jar, so a fresh install needs no config file at all. + +Set `debug=true` (bundled default `false`, the *Debug output* checkbox on the _General_ tab of the Preferences dialog) to log the verbose signing diagnostics -- the certificate chain, the loaded trust anchors, and every timestamp / OCSP / CRL / AIA network call. See <>. In batch mode, any of these keys can be overridden for a single run with the `-o key=value` command-line option (see <>), without changing the file. CLI overrides take precedence over `advanced.properties` and the bundled defaults, and are not persisted. @@ -1213,6 +1216,52 @@ If timestamping fails: * Check if the TSA server requires authentication and configure it accordingly. * If you get an `SSLHandshakeException`, see <>. +=== Reading the signing diagnostics + +With *debug output* enabled, JSignPdf logs the certificate material it uses and, for the DSS engine, every network call it makes while collecting validation data. This is what to look at when a signature fails for a reason that is not obvious from the error message itself. + +Turn it on by ticking *Debug output* on the _General_ tab of the Preferences dialog, or by setting `debug=true` in `advanced.properties` (it can also be set for a single CLI run with `-o debug=true`). It is off by default so normal runs stay quiet. Everything below is emitted only while it is on. + +*Certificate chain.* Right after the key is loaded, the whole signing chain is dumped -- one block per certificate, leaf first: + +[source] +---- +Signing certificate chain (2 certificates) + [0] subject=O=Acme,CN=Jane Doe + issuer=O=Acme,CN=Acme Issuing CA + serial=2a1f, validity=2026-01-01 00:00:00 UTC .. 2027-01-01 00:00:00 UTC + id=C-958E99061FA999181718D8FE3E6D70ECC4EF46DD78FC76E76AD348214E7AD6EE + keyUsage=digitalSignature,nonRepudiation + qcStatements=QcCompliance,QcSSCD + AIA caIssuers=http://pki.acme.example/ca.crt + AIA OCSP=http://ocsp.acme.example + CRL DP=http://crl.acme.example/issuing.crl +---- + +The `id=C-...` value is the identifier the DSS engine uses in its own error messages, so a `No revocation data found for C-...` failure can be matched to a real certificate without inspecting it with `openssl`. A certificate outside its validity window is flagged with `(EXPIRED)` / `(NOT YET VALID)` next to the dates. The timestamp token's own chain is dumped the same way once the TSA has answered. + +*Trust anchors (DSS engine).* Each configured `engine.dss.trust.*` source is logged with the number of anchors it contributed, followed by the total. A total of `0` means nothing is trusted at all -- a configuration problem -- as opposed to a specific CA simply not being covered: + +[source] +---- +Trust source trust.certFiles=/etc/pki/acme-root.pem: 1 anchor(s) +Trust anchors loaded: 1 from 1 configured source(s) +---- + +*Revocation and issuer downloads (DSS engine).* For `LT`/`LTA`, every AIA, CRL and OCSP call is reported twice: once for the HTTP request (URL, response size, elapsed time, or the failure) and once for what the answer meant, naming the certificate it was for: + +[source] +---- +CRL GET http://crl.acme.example/issuing.crl: 503 bytes, elapsed=18ms +CRL lookup for Jane Doe (C-958E...): status=GOOD, source=http://crl.acme.example/issuing.crl, + thisUpdate=2026-07-24 06:24:29 UTC, nextUpdate=2027-07-24 07:24:29 UTC, elapsed=27ms +OCSP lookup for Jane Doe (C-958E...): no OCSP data (no access point on the certificate, or none usable), elapsed=0ms +---- + +A `FAILED` line names the responder that could not be reached and why -- the usual cause of an `LT`/`LTA` failure behind a proxy or a firewall (see <>). A `no CRL data` / `no OCSP data` line means the certificate itself carries no matching distribution point, which no amount of trust configuration can fix. + +These messages are logged at the `FINE` level, which `debug=true` enables. The `-q` / `--quiet` flag silences all output (including the normal progress messages) regardless of the debug setting; for full control over what is logged, point Java at your own configuration with `JAVA_OPTS=-Djava.util.logging.config.file=`. + == Other command line tools === InstallCert Tool