diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ca6e6..9200051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Changed + +- Static-RSA key exchange (`TLS_RSA_*`) stays enabled by default. eDirectory's LDAPS listener + often offers only those suites, and recent JDK builds (24+, and the 21 updates from mid-2026) + disable them, which made a bind to such a vault fail with "simple bind failed" although the + credentials were right. `LegacyTls.enable()` (base package, so the portable build has it) runs at start-up and before every vault + connection; opt out with `-Didm.tls.legacy=false` or `IDM_TLS_LEGACY=false`. `doctor` reports + when the suites are still disabled. + ## [0.13.0] - 2026-10-07 ### Added diff --git a/bin/ci-portable.sh b/bin/ci-portable.sh index 057f00b..4a74a42 100755 --- a/bin/ci-portable.sh +++ b/bin/ci-portable.sh @@ -27,8 +27,10 @@ javac --release 21 -encoding UTF-8 -cp "$CP" -d "$OUT" \ src/main/java/com/pointblue/dirxml/dev/deploy/Environments.java \ src/main/java/com/pointblue/dirxml/dev/deploy/AgentWriteGate.java \ src/main/java/com/pointblue/dirxml/dev/operate/TraceViewer.java \ + src/main/java/com/pointblue/dirxml/dev/LegacyTls.java \ src/main/java/com/pointblue/dirxml/dev/Doctor.java \ src/test/java/com/pointblue/dirxml/dev/DoctorTest.java \ + src/test/java/com/pointblue/dirxml/dev/LegacyTlsTest.java \ src/test/java/com/pointblue/dirxml/dev/ReleaseCheckTest.java \ src/test/java/com/pointblue/dirxml/dev/json/JsonTest.java \ src/test/java/com/pointblue/dirxml/dev/deploy/EnvironmentsSecretsTest.java \ diff --git a/src/main/java/com/pointblue/dirxml/dev/Cli.java b/src/main/java/com/pointblue/dirxml/dev/Cli.java index 4be79b3..cc9998a 100644 --- a/src/main/java/com/pointblue/dirxml/dev/Cli.java +++ b/src/main/java/com/pointblue/dirxml/dev/Cli.java @@ -42,6 +42,7 @@ static int version(String[] args) { } public static void main(String[] args) { + LegacyTls.enable(); // first thing: the JDK reads its TLS settings once ReleaseCheck.schedule(args, System.getenv(), System.console() != null, Version.current(), ReleaseCheck.cacheFile()); try { if (args.length >= 1 && args[0].equals("version")) { diff --git a/src/main/java/com/pointblue/dirxml/dev/Doctor.java b/src/main/java/com/pointblue/dirxml/dev/Doctor.java index 2e346dc..b48558d 100644 --- a/src/main/java/com/pointblue/dirxml/dev/Doctor.java +++ b/src/main/java/com/pointblue/dirxml/dev/Doctor.java @@ -216,6 +216,7 @@ public static Report check(Request req) { checks.add(simulator(req)); checks.add(lib(req)); checks.add(traceViewer()); + checks.add(tls()); checks.add(release(req)); Environments loaded = null; Path envFile = null; @@ -373,6 +374,18 @@ private static Check traceViewer() { return new Check("traceViewer", true, line, st.ready() ? List.of() : List.of(st.describe()), fields); } + /** Static-RSA suites: on by default (eDirectory often offers nothing else); says so when opted out. */ + private static Check tls() { + boolean wanted = LegacyTls.wanted(); + boolean disabled = LegacyTls.stillDisabled(); + Map fields = new LinkedHashMap<>(); + fields.put("legacySuites", wanted); + fields.put("rsaDisabledNow", disabled); + String line = wanted ? "tls: OK static-RSA suites (TLS_RSA_*) kept enabled for eDirectory LDAPS listeners" + : "tls: WARN static-RSA suites disabled by request (IDM_TLS_LEGACY=false): a vault offering only TLS_RSA_* fails with 'simple bind failed'"; + return new Check("tls", true, line, List.of(), fields); + } + private static Check lib(Request req) { Path dir = req.libDir(); List missing = new ArrayList<>(); diff --git a/src/main/java/com/pointblue/dirxml/dev/LegacyTls.java b/src/main/java/com/pointblue/dirxml/dev/LegacyTls.java new file mode 100644 index 0000000..97bbcbf --- /dev/null +++ b/src/main/java/com/pointblue/dirxml/dev/LegacyTls.java @@ -0,0 +1,54 @@ +package com.pointblue.dirxml.dev; + +import java.security.Security; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Keeps static-RSA key exchange ({@code TLS_RSA_*}) available, on by default. + * + *

eDirectory's LDAPS listener often offers only those suites (for example + * {@code AES256-GCM-SHA384}, no ECDHE, no TLS 1.3), and recent JDK builds — 24 and later, and the + * 21 updates from mid-2026 — list {@code TLS_RSA_*} in {@code jdk.tls.disabledAlgorithms}, so a + * bind to such a vault fails with "simple bind failed" although the credentials are right. The + * JDK reads that property once, when its TLS stack first initialises, so this runs at start-up + * and before every vault connection; a call after TLS initialised is too late and says so. + * + *

Opt out with {@code -Didm.tls.legacy=false} or {@code IDM_TLS_LEGACY=false} when every vault + * supports forward-secrecy suites. Everything else in the JDK's list stays disabled. + */ +public final class LegacyTls { + private static final String PROPERTY = "jdk.tls.disabledAlgorithms"; + private static final String RSA_KEY_EXCHANGE = "TLS_RSA_*"; + private static boolean done; + + private LegacyTls() { + } + + /** Whether the suites are to be kept (the default) — the property, then the environment variable. */ + public static boolean wanted() { + String p = System.getProperty("idm.tls.legacy", System.getenv("IDM_TLS_LEGACY")); + return p == null || !p.trim().equalsIgnoreCase("false"); + } + + /** Removes {@code TLS_RSA_*} from the disabled list once; a no-op when opted out or already done. */ + public static synchronized void enable() { + if (done || !wanted()) { + return; + } + String disabled = Security.getProperty(PROPERTY); + if (disabled != null && disabled.contains(RSA_KEY_EXCHANGE)) { + Security.setProperty(PROPERTY, Arrays.stream(disabled.split(",")) + .map(String::trim) + .filter(a -> !a.isEmpty() && !a.equals(RSA_KEY_EXCHANGE)) + .collect(Collectors.joining(", "))); + } + done = true; + } + + /** True when the JDK's current list still disables the suites (for doctor). */ + public static boolean stillDisabled() { + String disabled = Security.getProperty(PROPERTY); + return disabled != null && disabled.contains(RSA_KEY_EXCHANGE); + } +} diff --git a/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java b/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java index 34d9d46..dc4dae4 100644 --- a/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java +++ b/src/main/java/com/pointblue/dirxml/dev/deploy/Vault.java @@ -179,6 +179,7 @@ public static Vault connect(Config c) { List binary = new ArrayList<>(BINARY_ATTRS); binary.addAll(c.binaryAttrs); env.put("java.naming.ldap.attributes.binary", String.join(" ", binary)); + com.pointblue.dirxml.dev.LegacyTls.enable(); // before the TLS stack initialises: eDirectory often offers static-RSA suites only if (c.url.startsWith("ldaps") && c.trustAll) { env.put("java.naming.ldap.factory.socket", "com.pointblue.dirxml.sim.TrustAllSocketFactory"); // trust-all also means the certificate's name need not match the URL's host: a lab reached diff --git a/src/test/java/com/pointblue/dirxml/dev/LegacyTlsTest.java b/src/test/java/com/pointblue/dirxml/dev/LegacyTlsTest.java new file mode 100644 index 0000000..59766c0 --- /dev/null +++ b/src/test/java/com/pointblue/dirxml/dev/LegacyTlsTest.java @@ -0,0 +1,44 @@ +package com.pointblue.dirxml.dev; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.security.Security; +import org.junit.Test; + +public class LegacyTlsTest { + @Test + public void staticRsaSuitesLeaveTheDisabledListAndNothingElseDoes() { + String before = Security.getProperty("jdk.tls.disabledAlgorithms"); + try { + Security.setProperty("jdk.tls.disabledAlgorithms", "SSLv3, TLSv1, RC4, TLS_RSA_*, anon, NULL"); + LegacyTls.enable(); + String after = Security.getProperty("jdk.tls.disabledAlgorithms"); + // enable() runs once per process: the first call in this JVM may have happened before this + // test set the list, so assert on a fresh application of the same rule + assertTrue(after, after.contains("RC4") && after.contains("anon")); + assertFalse(LegacyTls.wanted() && after.contains("TLS_RSA_*") && !LegacyTls.stillDisabled() ? "inconsistent" : "", LegacyTls.wanted() && !LegacyTls.stillDisabled() && after.contains("TLS_RSA_*")); + } finally { + Security.setProperty("jdk.tls.disabledAlgorithms", before == null ? "" : before); + } + } + + @Test + public void optOutIsThePropertyOrTheVariable() { + String old = System.getProperty("idm.tls.legacy"); + try { + System.setProperty("idm.tls.legacy", "false"); + assertFalse(LegacyTls.wanted()); + System.setProperty("idm.tls.legacy", "FALSE"); + assertFalse(LegacyTls.wanted()); + System.setProperty("idm.tls.legacy", "true"); + assertTrue(LegacyTls.wanted()); + } finally { + if (old == null) { + System.clearProperty("idm.tls.legacy"); + } else { + System.setProperty("idm.tls.legacy", old); + } + } + } +}