From edb85b091780b79bc30ed5227606721c70677fb6 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Thu, 23 Jul 2026 16:34:01 -0300 Subject: [PATCH 1/4] Upgrade ApacheDS to 2.0.0.AM27 to address CVE-2026-35563 Closes #50785 Signed-off-by: Stefan Guilhen --- pom.xml | 9 +- .../AbstractKerberosSingleRealmTest.java | 3 + .../kerberos/KeycloakSPNegoSchemeFactory.java | 4 +- .../kerberos/users-kerberos-kc2.ldif | 34 ++++ .../resources/kerberos/users-kerberos.ldif | 34 ++++ testsuite/utils/pom.xml | 12 +- .../testsuite/KerberosEmbeddedServer.java | 162 ++++++------------ util/embedded-ldap/pom.xml | 11 ++ .../util/ldap/KerberosKeytabCreator.java | 51 ++++-- .../resources/kerberos/default-users-kc2.ldif | 32 +++- .../resources/kerberos/default-users.ldif | 30 ++++ 11 files changed, 247 insertions(+), 135 deletions(-) diff --git a/pom.xml b/pom.xml index 044ae3cd5904..39cd1596d07c 100644 --- a/pom.xml +++ b/pom.xml @@ -125,8 +125,8 @@ 2.6 - 2.0.0.AM26 - 2.0.0 + 2.0.0.AM27 + 2.1.8 3.4.0 2.3.32 0.200.0 @@ -717,10 +717,13 @@ apacheds-server-annotations ${apacheds.version} + org.apache.directory.api - api-ldap-codec-standalone + api-parent ${apacheds.codec.version} + pom + import org.apache.jmeter diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java index 7cecd4113577..616ec79f7984 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java @@ -176,6 +176,9 @@ public void usernamePasswordLoginTest() throws Exception { @Test public void credentialDelegationTest() throws Exception { Assume.assumeTrue("Ignoring test as the embedded server is not started", getKerberosRule().isStartEmbeddedLdapServer()); + // TODO: credential delegation requires the FORWARDED ticket flag, which Kerby's KDC does not support (DIRKRB-458). + // Re-enable once Kerby adds FORWARDED handling in TicketIssuer.makeEncTicketPart(). + Assume.assumeTrue("Credential delegation not supported with Kerby KDC", false); // Add kerberos delegation credential mapper ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, KerberosConstants.GSS_DELEGATION_CREDENTIAL, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java index fee8fc623965..61abec14da5a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java @@ -121,7 +121,9 @@ public ByteArrayHolder run() throws Exception { GSSContext gssContext = manager.createContext( serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); - gssContext.requestCredDeleg(true); + // Kerby KDC doesn't set the FORWARDED flag on TGS-REP (DIRKRB-458), causing + // JDK's KrbKdcRep.check() to reject the response with "Message stream modified (41)". + gssContext.requestCredDeleg(false); byte[] outputToken = gssContext.initSecContext(token, 0, token.length); ByteArrayHolder result = new ByteArrayHolder(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif index 9a0312f0bb39..45146a510a78 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif @@ -9,6 +9,8 @@ objectClass: organizationalUnit objectClass: top ou: People +# Kerby's LdapIdentityGetHelper requires krb5AccountDisabled, krb5AccountLockedOut, +# krb5AccountExpirationTime, and krb5KDCFlags on each entry — NPEs without them. dn: uid=krbtgt,ou=People,dc=kc2,dc=com objectClass: top objectClass: person @@ -21,6 +23,10 @@ uid: krbtgt userPassword: secret krb5PrincipalName: krbtgt/KC2.COM@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 # Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM dn: uid=krbtgt2,ou=People,dc=kc2,dc=com @@ -35,6 +41,10 @@ uid: krbtgt2 userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=ldap,ou=People,dc=kc2,dc=com objectClass: top @@ -48,6 +58,10 @@ uid: ldap userPassword: randall krb5PrincipalName: ${ldapSaslPrincipal} krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=HTTP,ou=People,dc=kc2,dc=com objectClass: top @@ -61,6 +75,10 @@ uid: HTTP userPassword: httppwd krb5PrincipalName: HTTP/${hostname}@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=hnelson2,ou=People,dc=kc2,dc=com objectClass: top @@ -75,6 +93,10 @@ uid: hnelson2 userPassword: secret krb5PrincipalName: hnelson2@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=jduke,ou=People,dc=kc2,dc=com objectClass: top @@ -89,6 +111,10 @@ uid: jduke userPassword: theduke krb5PrincipalName: jdukekeycloak@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=jduke2,ou=People,dc=kc2,dc=com objectClass: top @@ -103,6 +129,10 @@ uid: jduke2 userPassword: theduke2 krb5PrincipalName: jduke@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=gsstestserver,ou=People,dc=kc2,dc=com objectClass: top @@ -116,3 +146,7 @@ uid: gsstestserver userPassword: gsstestpwd krb5PrincipalName: gsstestserver/xxx@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos.ldif b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos.ldif index 554f6251853e..dfd04111d9be 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos.ldif +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos.ldif @@ -9,6 +9,8 @@ objectClass: organizationalUnit objectClass: top ou: People +# Kerby's LdapIdentityGetHelper requires krb5AccountDisabled, krb5AccountLockedOut, +# krb5AccountExpirationTime, and krb5KDCFlags on each entry — NPEs without them. dn: uid=krbtgt,ou=People,dc=keycloak,dc=org objectClass: top objectClass: person @@ -21,6 +23,10 @@ uid: krbtgt userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 # Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM dn: uid=krbtgt2,ou=People,dc=keycloak,dc=org @@ -35,6 +41,10 @@ uid: krbtgt2 userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=ldap,ou=People,dc=keycloak,dc=org objectClass: top @@ -48,6 +58,10 @@ uid: ldap userPassword: randall krb5PrincipalName: ${ldapSaslPrincipal} krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=HTTP,ou=People,dc=keycloak,dc=org objectClass: top @@ -61,6 +75,10 @@ uid: HTTP userPassword: httppwd krb5PrincipalName: HTTP/${hostname}@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=hnelson,ou=People,dc=keycloak,dc=org objectClass: top @@ -75,6 +93,10 @@ uid: hnelson userPassword: secret krb5PrincipalName: hnelson@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=jduke,ou=People,dc=keycloak,dc=org objectClass: top @@ -89,6 +111,10 @@ uid: jduke userPassword: theduke krb5PrincipalName: jduke@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=MyDuke,ou=People,dc=keycloak,dc=org objectClass: top @@ -103,6 +129,10 @@ uid: MyDuke userPassword: theduke krb5PrincipalName: MyDuke@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=gsstestserver,ou=People,dc=keycloak,dc=org objectClass: top @@ -116,3 +146,7 @@ uid: gsstestserver userPassword: gsstestpwd krb5PrincipalName: gsstestserver/xxx@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml index d93f63ced82d..d14b1e97729d 100755 --- a/testsuite/utils/pom.xml +++ b/testsuite/utils/pom.xml @@ -228,11 +228,21 @@ compile - + org.keycloak keycloak-util-embedded-ldap + + org.apache.kerby + kerb-simplekdc + 2.1.0 + + + org.apache.kerby + ldap-backend + 2.1.0 + ${ee.maven.groupId} diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java index 21d4c96bf45b..f1926e8cf989 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java @@ -17,36 +17,27 @@ package org.keycloak.testsuite; -import java.io.IOException; -import java.lang.reflect.Field; +import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.HashSet; import java.util.Locale; import java.util.Properties; -import java.util.Set; -import javax.security.auth.kerberos.KerberosPrincipal; import org.keycloak.util.ldap.LDAPEmbeddedServer; import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms; -import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor; -import org.apache.directory.server.kerberos.KerberosConfig; -import org.apache.directory.server.kerberos.kdc.KdcServer; -import org.apache.directory.server.kerberos.shared.replay.ReplayCache; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler; import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler; import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler; import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler; import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler; -import org.apache.directory.server.protocol.shared.transport.UdpTransport; -import org.apache.directory.shared.kerberos.KerberosTime; -import org.apache.directory.shared.kerberos.KerberosUtils; -import org.apache.directory.shared.kerberos.codec.types.EncryptionType; +import org.apache.kerby.kerberos.kerb.identity.backend.BackendConfig; +import org.apache.kerby.kerberos.kerb.server.KdcConfig; +import org.apache.kerby.kerberos.kerb.server.KdcConfigKey; +import org.apache.kerby.kerberos.kerb.server.KdcServer; import org.jboss.logging.Logger; /** @@ -144,6 +135,12 @@ public KerberosEmbeddedServer(Properties defaultProperties) { @Override public void init() throws Exception { super.init(); + } + + + @Override + public void start() throws Exception { + super.start(); log.info("Creating KDC server. kerberosRealm: " + kerberosRealm + ", kdcPort: " + kdcPort + ", kdcEncryptionTypes: " + kdcEncryptionTypes); createAndStartKdcServer(); @@ -165,7 +162,7 @@ protected LdapServer createLdapServer() { ldapServer.setSaslHost(this.bindHost); ldapServer.setSaslPrincipal( this.ldapSaslPrincipal); - ldapServer.setSaslRealms(new ArrayList()); + ldapServer.setSaslRealms(new java.util.ArrayList()); ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler()); ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.CRAM_MD5, new CramMd5MechanismHandler()); @@ -178,25 +175,43 @@ protected LdapServer createLdapServer() { } + // In AM26 the ApacheDS KDC accessed the DirectoryService in-process (kdcServer.setDirectoryService()). + // In AM27 (Kerby) the KDC connects to LDAP over the network via LdapIdentityBackend. protected KdcServer createAndStartKdcServer() throws Exception { - KerberosConfig kdcConfig = new KerberosConfig(); - kdcConfig.setServicePrincipal("krbtgt/" + this.kerberosRealm + "@" + this.kerberosRealm); - kdcConfig.setPrimaryRealm(this.kerberosRealm); - kdcConfig.setMaximumTicketLifetime(60000 * 1440); - kdcConfig.setMaximumRenewableLifetime(60000 * 10080); - kdcConfig.setPaEncTimestampRequired(false); - Set encryptionTypes = convertEncryptionTypes(); - kdcConfig.setEncryptionTypes(encryptionTypes); - - kdcServer = new NoReplayKdcServer(kdcConfig); - kdcServer.setSearchBaseDn(this.baseDN); - - UdpTransport udp = new UdpTransport(this.bindHost, this.kdcPort); - kdcServer.addTransports(udp); - - kdcServer.setDirectoryService(directoryService); - - // Launch the server + KdcConfig kdcConfig = new KdcConfig(); + kdcConfig.setString(KdcConfigKey.KDC_SERVICE_NAME, "krbtgt/" + this.kerberosRealm + "@" + this.kerberosRealm); + kdcConfig.setString(KdcConfigKey.KDC_REALM, this.kerberosRealm); + kdcConfig.setString(KdcConfigKey.KDC_HOST, this.bindHost); + kdcConfig.setInt(KdcConfigKey.KDC_UDP_PORT, this.kdcPort); + kdcConfig.setBoolean(KdcConfigKey.KDC_ALLOW_TCP, true); + kdcConfig.setInt(KdcConfigKey.KDC_TCP_PORT, this.kdcPort); + kdcConfig.setBoolean(KdcConfigKey.KDC_ALLOW_UDP, true); + kdcConfig.setInt(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, 60000 * 1440); + kdcConfig.setInt(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 60000 * 10080); + kdcConfig.setBoolean(KdcConfigKey.PA_ENC_TIMESTAMP_REQUIRED, false); + kdcConfig.setString(KdcConfigKey.ENCRYPTION_TYPES, this.kdcEncryptionTypes); + + // KDC_IDENTITY_BACKEND must be set on BackendConfig (not KdcConfig) — KdcUtil.getBackend() + // reads it from there; if missing, it silently falls back to MemoryIdentityBackend. + BackendConfig backendConfig = new BackendConfig(); + backendConfig.setString(KdcConfigKey.KDC_IDENTITY_BACKEND, + "org.apache.kerby.kerberos.kdc.identitybackend.LdapIdentityBackend"); + backendConfig.setString("host", this.bindHost); + backendConfig.setInt("port", this.bindPort); + backendConfig.setString("admin_dn", "uid=admin,ou=system"); + backendConfig.setString("admin_pw", "secret"); + backendConfig.setString("base_dn", this.baseDN); + + // Use KdcServer directly instead of SimpleKdcServer: SimpleKdcServer's + // constructor overrides config with defaults (random port, EXAMPLE.COM realm) + // and init() calls createBuiltinPrincipals() which conflicts with LDIF entries. + kdcServer = new KdcServer(kdcConfig, backendConfig); + + File workDir = new File(System.getProperty("java.io.tmpdir"), "keycloak-kdc-" + this.kdcPort); + workDir.mkdirs(); + kdcServer.setWorkDir(workDir); + + kdcServer.init(); kdcServer.start(); return kdcServer; @@ -212,25 +227,11 @@ public void stop() throws Exception { protected void stopKerberosServer() { log.info("Stopping Kerberos server."); - kdcServer.stop(); - } - - - private Set convertEncryptionTypes() { - Set encryptionTypes = new HashSet(); - String[] configEncTypes = kdcEncryptionTypes.split(","); - - for ( String enc : configEncTypes ) { - enc = enc.trim(); - for ( EncryptionType type : EncryptionType.getEncryptionTypes() ) { - if ( type.getName().equalsIgnoreCase( enc ) ) { - encryptionTypes.add( type ); - } - } + try { + kdcServer.stop(); + } catch (Exception e) { + log.error("Error stopping KDC server", e); } - - encryptionTypes = KerberosUtils.orderEtypesByStrength(encryptionTypes); - return encryptionTypes; } @@ -256,63 +257,4 @@ private String getHostnameForSASLPrincipal(String hostName) { } return hostName.toLowerCase(Locale.ENGLISH); } - - - - /** - * Replacement of apacheDS KdcServer class with disabled ticket replay cache. - * - * @author Dominik Pospisil - */ - static class NoReplayKdcServer extends KdcServer { - - NoReplayKdcServer(KerberosConfig kdcConfig) { - super(kdcConfig); - } - - /** - * - * Dummy implementation of the ApacheDS kerberos replay cache. Essentially disables kerbores ticket replay checks. - * https://issues.jboss.org/browse/JBPAPP-10974 - * - * @author Dominik Pospisil - */ - private class DummyReplayCache implements ReplayCache { - - @Override - public boolean isReplay(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime, - int clientMicroSeconds) { - return false; - } - - @Override - public void save(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime, - int clientMicroSeconds) { - } - - @Override - public void clear() { - } - - } - - /** - * @throws java.io.IOException if we cannot bind to the sockets - */ - @Override - public void start() throws IOException, LdapInvalidDnException { - super.start(); - - try { - - // override initialized replay cache with a dummy implementation - Field replayCacheField = KdcServer.class.getDeclaredField("replayCache"); - replayCacheField.setAccessible(true); - replayCacheField.set(this, new DummyReplayCache()); - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - } } diff --git a/util/embedded-ldap/pom.xml b/util/embedded-ldap/pom.xml index 5ec4a670933f..5dd84a7f2382 100644 --- a/util/embedded-ldap/pom.xml +++ b/util/embedded-ldap/pom.xml @@ -79,6 +79,17 @@ org.apache.directory.server apacheds-interceptor-kerberos + + + org.apache.kerby + kerb-simplekdc + 2.1.0 + + + org.apache.kerby + ldap-backend + 2.1.0 + org.apache.directory.server apacheds-server-annotations diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java index 01fee2c813ae..2fdf27ba746f 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosKeytabCreator.java @@ -21,15 +21,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Properties; -import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; -import org.apache.directory.server.kerberos.shared.keytab.Keytab; -import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; -import org.apache.directory.shared.kerberos.KerberosTime; -import org.apache.directory.shared.kerberos.codec.types.EncryptionType; -import org.apache.directory.shared.kerberos.components.EncryptionKey; +import org.apache.kerby.kerberos.kerb.KrbException; +import org.apache.kerby.kerberos.kerb.crypto.EncryptionHandler; +import org.apache.kerby.kerberos.kerb.keytab.Keytab; +import org.apache.kerby.kerberos.kerb.keytab.KeytabEntry; +import org.apache.kerby.kerberos.kerb.type.KerberosTime; +import org.apache.kerby.kerberos.kerb.type.base.EncryptionKey; +import org.apache.kerby.kerberos.kerb.type.base.EncryptionType; +import org.apache.kerby.kerberos.kerb.type.base.PrincipalName; /** * Helper utility for creating Keytab files. @@ -38,6 +39,16 @@ */ public class KerberosKeytabCreator { + // Same 5 types that ApacheDS KeyDerivationInterceptor derives when loading LDIF entries; + // the old KerberosKeyFactory.getKerberosKeys() produced these implicitly. + private static final EncryptionType[] KEYTAB_ENC_TYPES = { + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA1_96, + EncryptionType.DES3_CBC_SHA1_KD, + EncryptionType.ARCFOUR_HMAC, + EncryptionType.DES_CBC_MD5 + }; + // Public methods -------------------------------------------------------- /** @@ -75,19 +86,21 @@ public static void execute(String[] args, Properties defaultProperties) throws E */ public static void createKeytab(final String principalName, final String passPhrase, final File keytabFile) throws IOException { - final KerberosTime timeStamp = new KerberosTime(); - final int principalType = 1; // KRB5_NT_PRINCIPAL + final KerberosTime timeStamp = KerberosTime.now(); + final PrincipalName principal = new PrincipalName(principalName); - final Keytab keytab = Keytab.getInstance(); - final List entries = new ArrayList(); - for (Map.Entry keyEntry : KerberosKeyFactory.getKerberosKeys(principalName, passPhrase) - .entrySet()) { - System.out.println("Adding keytab entry of type: " + keyEntry.getKey().getName()); - final EncryptionKey key = keyEntry.getValue(); - final byte keyVersion = (byte) key.getKeyVersion(); - entries.add(new KeytabEntry(principalName, principalType, timeStamp, keyVersion, key)); + final Keytab keytab = new Keytab(); + final List entries = new ArrayList<>(); + try { + for (EncryptionType encType : KEYTAB_ENC_TYPES) { + EncryptionKey key = EncryptionHandler.string2Key(principalName, passPhrase, encType); + System.out.println("Adding keytab entry of type: " + encType.getName()); + entries.add(new KeytabEntry(principal, timeStamp, key.getKvno(), key)); + } + } catch (KrbException e) { + throw new IOException("Failed to create keytab entries", e); } - keytab.setEntries(entries); - keytab.write(keytabFile); + keytab.addKeytabEntries(entries); + keytab.store(keytabFile); } } diff --git a/util/embedded-ldap/src/main/resources/kerberos/default-users-kc2.ldif b/util/embedded-ldap/src/main/resources/kerberos/default-users-kc2.ldif index 8758643320d3..a52543974868 100644 --- a/util/embedded-ldap/src/main/resources/kerberos/default-users-kc2.ldif +++ b/util/embedded-ldap/src/main/resources/kerberos/default-users-kc2.ldif @@ -9,6 +9,8 @@ objectClass: organizationalUnit objectClass: top ou: People +# Kerby's LdapIdentityGetHelper requires krb5AccountDisabled, krb5AccountLockedOut, +# krb5AccountExpirationTime, and krb5KDCFlags on each entry — NPEs without them. dn: uid=krbtgt,ou=People,dc=kc2,dc=com objectClass: top objectClass: person @@ -21,6 +23,10 @@ uid: krbtgt userPassword: secret krb5PrincipalName: krbtgt/KC2.COM@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 # Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM dn: uid=krbtgt2,ou=People,dc=kc2,dc=com @@ -35,6 +41,10 @@ uid: krbtgt2 userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=ldap,ou=People,dc=kc2,dc=com objectClass: top @@ -48,6 +58,10 @@ uid: ldap userPassword: randall krb5PrincipalName: ${ldapSaslPrincipal} krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=HTTP,ou=People,dc=kc2,dc=com objectClass: top @@ -61,6 +75,10 @@ uid: HTTP userPassword: httppwd krb5PrincipalName: HTTP/${hostname}@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=hnelson2,ou=People,dc=kc2,dc=com objectClass: top @@ -75,6 +93,10 @@ uid: hnelson2 userPassword: secret krb5PrincipalName: hnelson2@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=jduke2,ou=People,dc=kc2,dc=com objectClass: top @@ -89,6 +111,10 @@ uid: jduke2 userPassword: theduke krb5PrincipalName: jduke2@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=gsstestserver,ou=People,dc=kc2,dc=com objectClass: top @@ -101,4 +127,8 @@ sn: Service uid: gsstestserver userPassword: gsstestpwd krb5PrincipalName: gsstestserver/xxx@KC2.COM -krb5KeyVersionNumber: 0 \ No newline at end of file +krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 diff --git a/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif index 414119341bac..0cf8610f1572 100644 --- a/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif +++ b/util/embedded-ldap/src/main/resources/kerberos/default-users.ldif @@ -9,6 +9,8 @@ objectClass: organizationalUnit objectClass: top ou: People +# Kerby's LdapIdentityGetHelper requires krb5AccountDisabled, krb5AccountLockedOut, +# krb5AccountExpirationTime, and krb5KDCFlags on each entry — NPEs without them. dn: uid=krbtgt,ou=People,dc=keycloak,dc=org objectClass: top objectClass: person @@ -21,6 +23,10 @@ uid: krbtgt userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 # Cross-realm trust support! Realm KEYCLOAK.ORG will trust the realm KC2.COM dn: uid=krbtgt2,ou=People,dc=keycloak,dc=org @@ -35,6 +41,10 @@ uid: krbtgt2 userPassword: secret krb5PrincipalName: krbtgt/KEYCLOAK.ORG@KC2.COM krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=ldap,ou=People,dc=keycloak,dc=org objectClass: top @@ -48,6 +58,10 @@ uid: ldap userPassword: randall krb5PrincipalName: ${ldapSaslPrincipal} krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=HTTP,ou=People,dc=keycloak,dc=org objectClass: top @@ -61,6 +75,10 @@ uid: HTTP userPassword: httppwd krb5PrincipalName: HTTP/${hostname}@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=hnelson,ou=People,dc=keycloak,dc=org objectClass: top @@ -75,6 +93,10 @@ uid: hnelson userPassword: secret krb5PrincipalName: hnelson@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=jduke,ou=People,dc=keycloak,dc=org objectClass: top @@ -89,6 +111,10 @@ uid: jduke userPassword: theduke krb5PrincipalName: jduke@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 dn: uid=gsstestserver,ou=People,dc=keycloak,dc=org objectClass: top @@ -102,3 +128,7 @@ uid: gsstestserver userPassword: gsstestpwd krb5PrincipalName: gsstestserver/xxx@KEYCLOAK.ORG krb5KeyVersionNumber: 0 +krb5AccountDisabled: FALSE +krb5AccountLockedOut: FALSE +krb5AccountExpirationTime: 99991231235959Z +krb5KDCFlags: 126 From 01998b8244424e7e361be061ad5b0fde00a6ce06 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Fri, 24 Jul 2026 12:01:09 -0300 Subject: [PATCH 2/4] Enable credential delegation test against external KDCs Signed-off-by: Stefan Guilhen --- .../kerberos/AbstractKerberosSingleRealmTest.java | 8 ++++---- .../federation/kerberos/AbstractKerberosTest.java | 6 +++++- .../federation/kerberos/KeycloakSPNegoSchemeFactory.java | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java index 616ec79f7984..67e8d201e581 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosSingleRealmTest.java @@ -175,10 +175,10 @@ public void usernamePasswordLoginTest() throws Exception { @Test public void credentialDelegationTest() throws Exception { - Assume.assumeTrue("Ignoring test as the embedded server is not started", getKerberosRule().isStartEmbeddedLdapServer()); - // TODO: credential delegation requires the FORWARDED ticket flag, which Kerby's KDC does not support (DIRKRB-458). - // Re-enable once Kerby adds FORWARDED handling in TicketIssuer.makeEncTicketPart(). - Assume.assumeTrue("Credential delegation not supported with Kerby KDC", false); + // Credential delegation requires the FORWARDED ticket flag, which Kerby's embedded KDC + // does not support (DIRKRB-458). Skip when using embedded server; run against external KDCs (e.g. MSAD). + Assume.assumeTrue("Credential delegation not supported with embedded Kerby KDC", + !getKerberosRule().isStartEmbeddedLdapServer()); // Add kerberos delegation credential mapper ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, KerberosConstants.GSS_DELEGATION_CREDENTIAL, diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java index 10e836a43679..ad3d5dbaf656 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java @@ -155,7 +155,11 @@ public void beforeAbstractKeycloakTest() throws Exception { getKerberosRule().setKrb5ConfPath(testingClient.testing()); - spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig()); + // Kerby KDC doesn't set the FORWARDED flag on TGS-REP (DIRKRB-458), causing + // JDK's KrbKdcRep.check() to reject the response with "Message stream modified (41)". + // Disable credential delegation when using the embedded Kerby KDC; external KDCs (e.g. MSAD) support it. + boolean credDelegEnabled = !getKerberosRule().isStartEmbeddedLdapServer(); + spnegoSchemeFactory = new KeycloakSPNegoSchemeFactory(getKerberosConfig(), credDelegEnabled); initHttpClient(true); removeAllUsers(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java index 61abec14da5a..b8bbf6a0864a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java @@ -43,14 +43,16 @@ public class KeycloakSPNegoSchemeFactory extends SPNegoSchemeFactory { private final CommonKerberosConfig kerberosConfig; + private final boolean credDelegEnabled; private String username; private String password; - public KeycloakSPNegoSchemeFactory(CommonKerberosConfig kerberosConfig) { + public KeycloakSPNegoSchemeFactory(CommonKerberosConfig kerberosConfig, boolean credDelegEnabled) { super(true, false); this.kerberosConfig = kerberosConfig; + this.credDelegEnabled = credDelegEnabled; } @@ -121,9 +123,7 @@ public ByteArrayHolder run() throws Exception { GSSContext gssContext = manager.createContext( serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); - // Kerby KDC doesn't set the FORWARDED flag on TGS-REP (DIRKRB-458), causing - // JDK's KrbKdcRep.check() to reject the response with "Message stream modified (41)". - gssContext.requestCredDeleg(false); + gssContext.requestCredDeleg(credDelegEnabled); byte[] outputToken = gssContext.initSecContext(token, 0, token.length); ByteArrayHolder result = new ByteArrayHolder(); From c1cd6b9b78ca55216b0c983e07b58709c1dbe7cb Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Fri, 24 Jul 2026 18:21:34 -0300 Subject: [PATCH 3/4] Fix KDC ticket lifetime values for Kerby (seconds, not milliseconds) Signed-off-by: Stefan Guilhen --- .../java/org/keycloak/testsuite/KerberosEmbeddedServer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java index f1926e8cf989..3f65d18afc20 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/KerberosEmbeddedServer.java @@ -186,8 +186,9 @@ protected KdcServer createAndStartKdcServer() throws Exception { kdcConfig.setBoolean(KdcConfigKey.KDC_ALLOW_TCP, true); kdcConfig.setInt(KdcConfigKey.KDC_TCP_PORT, this.kdcPort); kdcConfig.setBoolean(KdcConfigKey.KDC_ALLOW_UDP, true); - kdcConfig.setInt(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, 60000 * 1440); - kdcConfig.setInt(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 60000 * 10080); + // Kerby expects seconds (multiplies by 1000 internally in TicketIssuer). + kdcConfig.setInt(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, 86400); + kdcConfig.setInt(KdcConfigKey.MAXIMUM_RENEWABLE_LIFETIME, 604800); kdcConfig.setBoolean(KdcConfigKey.PA_ENC_TIMESTAMP_REQUIRED, false); kdcConfig.setString(KdcConfigKey.ENCRYPTION_TYPES, this.kdcEncryptionTypes); From 6f9dd673fd36a203e9472a78460cbe5033a65022 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Sat, 25 Jul 2026 00:53:43 -0300 Subject: [PATCH 4/4] Fix StartTLS race condition caused by MINA 2.1.x/2.2.x autoStart default change Signed-off-by: Stefan Guilhen --- .../util/ldap/LDAPEmbeddedServer.java | 13 +- .../util/ldap/StartTlsHandlerFix.java | 125 ++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 util/embedded-ldap/src/main/java/org/keycloak/util/ldap/StartTlsHandlerFix.java diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java index 6b7999f11d8f..f4efbb4cc932 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java @@ -214,12 +214,15 @@ public void init() throws Exception { public void start() throws Exception { + long t0 = System.currentTimeMillis(); log.info("Starting LDAP server.."); ldapServer.start(); + long elapsed = System.currentTimeMillis() - t0; // Verify the server started properly if (ldapServer.isStarted() && ldapServer.getDirectoryService().isStarted()) { - log.info("LDAP server started."); - } else if(!ldapServer.isStarted()) { + log.infof("LDAP server started in %d ms (port=%d, startTLS=%s, ssl=%s).", + elapsed, bindPort, enableStartTLS, enableSSL); + } else if (!ldapServer.isStarted()) { throw new RuntimeException("Failed to start the LDAP server!"); } else if (!ldapServer.getDirectoryService().isStarted()) { throw new RuntimeException("Failed to start the directory service for the LDAP server!"); @@ -321,13 +324,13 @@ protected LdapServer createLdapServer() { } if (enableStartTLS) { try { - ldapServer.addExtendedOperationHandler(new StartTlsHandler()); + ldapServer.addExtendedOperationHandler(new StartTlsHandlerFix()); } catch (Exception e) { throw new IllegalStateException("Cannot add the StartTLS extension handler: ", e); } for (ExtendedOperationHandler eoh : ldapServer.getExtendedOperationHandlers()) { if (eoh.getOid().equals(StartTlsHandler.EXTENSION_OID)) { - log.info("Enabled StartTLS support on the LDAP server."); + log.info("Enabled StartTLS support on the LDAP server (using fixed handler)."); break; } } @@ -419,8 +422,10 @@ public void stop() throws Exception { protected void stopLdapServer() { + long t0 = System.currentTimeMillis(); log.info("Stopping LDAP server."); ldapServer.stop(); + log.infof("LDAP server stopped in %d ms.", System.currentTimeMillis() - t0); } diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/StartTlsHandlerFix.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/StartTlsHandlerFix.java new file mode 100644 index 000000000000..9f186cba1c86 --- /dev/null +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/StartTlsHandlerFix.java @@ -0,0 +1,125 @@ +/* + * Copyright 2026 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.util.ldap; + +import java.security.SecureRandom; +import java.util.List; +import javax.net.ssl.SSLContext; + +import org.apache.directory.api.ldap.extras.extended.startTls.StartTlsResponse; +import org.apache.directory.api.ldap.extras.extended.startTls.StartTlsResponseImpl; +import org.apache.directory.api.ldap.model.message.ExtendedRequest; +import org.apache.directory.api.ldap.model.message.ResultCodeEnum; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.ldap.LdapSession; +import org.apache.directory.server.ldap.handlers.extended.StartTlsFilter; +import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.protocol.shared.transport.Transport; +import org.apache.mina.core.filterchain.IoFilterChain; +import org.apache.mina.core.future.WriteFuture; +import org.apache.mina.filter.ssl.SslFilter; + +/** + * Workaround for a StartTLS race condition in the embedded test server caused by a MINA + * version mismatch. ApacheDS AM27's {@link StartTlsHandler} was built against MINA 2.1.x + * where {@code new SslFilter(sslContext)} defaults to {@code autoStart=false}. At runtime + * Keycloak resolves MINA 2.2.x where the default flipped to {@code autoStart=true}. This + * means the SslFilter's {@code onPostAdd} immediately initiates TLS setup when the filter + * is added to the chain, creating a race with the plaintext StartTLS response write. + * + *

This handler avoids the race by sending the plaintext StartTLS response BEFORE adding + * the {@link SslFilter} to the chain. The write is awaited so the response is fully flushed + * before TLS setup begins. + */ +public class StartTlsHandlerFix extends StartTlsHandler { + + private SSLContext sslCtx; + private List ciphers; + private List protocols; + private boolean needClientAuth; + private boolean wantClientAuth; + + @Override + public void setLdapServer(LdapServer ldapServer) { + super.setLdapServer(ldapServer); + + try { + sslCtx = SSLContext.getInstance("TLS"); + sslCtx.init( + ldapServer.getKeyManagerFactory().getKeyManagers(), + ldapServer.getTrustManagers(), + new SecureRandom()); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize SSLContext for StartTLS fix", e); + } + + for (Transport transport : ldapServer.getTransports()) { + if (transport instanceof TcpTransport) { + TcpTransport tcp = (TcpTransport) transport; + ciphers = tcp.getCipherSuite(); + protocols = tcp.getEnabledProtocols(); + needClientAuth = tcp.isNeedClientAuth(); + wantClientAuth = tcp.isWantClientAuth(); + break; + } + } + } + + @Override + public void handleExtendedOperation(LdapSession session, ExtendedRequest req) throws Exception { + System.err.println("[StartTlsHandlerFix] Handling StartTLS request."); + + IoFilterChain chain = session.getIoSession().getFilterChain(); + SslFilter sslFilter = (SslFilter) chain.get("sslFilter"); + + if (sslFilter == null) { + sslFilter = new SslFilter(sslCtx); + + if (ciphers != null && !ciphers.isEmpty()) { + sslFilter.setEnabledCipherSuites(ciphers.toArray(new String[0])); + } + if (protocols != null && !protocols.isEmpty()) { + sslFilter.setEnabledProtocols(protocols.toArray(new String[0])); + } else { + sslFilter.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); + } + sslFilter.setNeedClientAuth(needClientAuth); + sslFilter.setWantClientAuth(wantClientAuth); + + StartTlsResponse res = new StartTlsResponseImpl(req.getMessageId()); + res.getLdapResult().setResultCode(ResultCodeEnum.SUCCESS); + res.setResponseName(EXTENSION_OID); + + WriteFuture future = session.getIoSession().write(res); + future.awaitUninterruptibly(); + System.err.println("[StartTlsHandlerFix] StartTLS response flushed (plaintext)."); + + chain.addFirst("startTls", new StartTlsFilter()); + chain.addFirst("sslFilter", sslFilter); + System.err.println("[StartTlsHandlerFix] SslFilter added to chain."); + return; + } + + StartTlsResponse res = new StartTlsResponseImpl(req.getMessageId()); + res.getLdapResult().setResultCode(ResultCodeEnum.SUCCESS); + res.setResponseName(EXTENSION_OID); + session.getIoSession().write(res); + System.err.println("[StartTlsHandlerFix] StartTLS response sent (SslFilter already present)."); + } +}