diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java index 823ab1dd3e59..982cce9726c3 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfiguration.java @@ -16,12 +16,28 @@ package org.springframework.boot.ldap.autoconfigure.embedded; +import java.io.IOException; import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; @@ -33,6 +49,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionMessage; @@ -47,6 +64,8 @@ import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; import org.springframework.boot.ldap.autoconfigure.LdapProperties; import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration.EmbeddedLdapAutoConfigurationRuntimeHints; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -60,9 +79,12 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -84,6 +106,8 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { private final EmbeddedLdapProperties embeddedProperties; + private final ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(); + private @Nullable InMemoryDirectoryServer server; EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) { @@ -91,7 +115,9 @@ public final class EmbeddedLdapAutoConfiguration implements DisposableBean { } @Bean - InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException { + InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext, + ObjectProvider sslBundles) throws LDAPException, KeyStoreException, IOException, + NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); String username = this.embeddedProperties.getCredential().getUsername(); @@ -100,9 +126,18 @@ InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) t config.addAdditionalBindCredentials(username, password); } setSchema(config); - InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP", - this.embeddedProperties.getPort()); - config.setListenerConfigs(listenerConfig); + if (this.embeddedProperties.getSsl().isEnabled()) { + EmbeddedLdapProperties.Ssl ssl = this.embeddedProperties.getSsl(); + SSLContext sslContext = getSslContext(ssl, sslBundles.getIfAvailable()); + SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory(); + SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); + config.setListenerConfigs(InMemoryListenerConfig.createLDAPSConfig("LDAPS", null, + this.embeddedProperties.getPort(), serverSocketFactory, clientSocketFactory)); + } + else { + config + .setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", this.embeddedProperties.getPort())); + } this.server = new InMemoryDirectoryServer(config); importLdif(this.server, applicationContext); this.server.startListening(); @@ -181,6 +216,70 @@ public void destroy() throws Exception { } } + private SSLContext getSslContext(EmbeddedLdapProperties.Ssl ssl, @Nullable SslBundles sslBundles) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + if (sslBundles != null && StringUtils.hasText(ssl.getBundle())) { + SslBundle sslBundle = sslBundles.getBundle(ssl.getBundle()); + Assert.notNull(sslBundle, "SSL bundle name has been set but no SSL bundles found in context"); + return sslBundle.createSslContext(); + + } + else { + SSLContext sslContext = SSLContext.getInstance(ssl.getAlgorithm()); + KeyManager[] keyManagers = configureKeyManagers(ssl); + TrustManager[] trustManagers = configureTrustManagers(ssl); + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + return sslContext; + } + } + + private KeyManager @Nullable [] configureKeyManagers(EmbeddedLdapProperties.Ssl ssl) throws KeyStoreException, + IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + String keyStoreName = ssl.getKeyStore(); + String keyStorePassword = ssl.getKeyStorePassword(); + String storeType = ssl.getKeyStoreType(); + char[] keyPassphrase = null; + if (keyStorePassword != null) { + keyPassphrase = keyStorePassword.toCharArray(); + } + KeyManager[] keyManagers = null; + if (StringUtils.hasText(keyStoreName)) { + Resource resource = this.resourceLoader.getResource(keyStoreName); + KeyStore ks = KeyStore.getInstance(storeType); + try (InputStream inputStream = resource.getInputStream()) { + ks.load(inputStream, keyPassphrase); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(ssl.getKeyStoreAlgorithm()); + kmf.init(ks, keyPassphrase); + keyManagers = kmf.getKeyManagers(); + } + return keyManagers; + } + + private TrustManager @Nullable [] configureTrustManagers(EmbeddedLdapProperties.Ssl ssl) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + String trustStoreName = ssl.getTrustStore(); + String trustStorePassword = ssl.getTrustStorePassword(); + String storeType = ssl.getTrustStoreType(); + char[] trustPassphrase = null; + if (trustStorePassword != null) { + trustPassphrase = trustStorePassword.toCharArray(); + } + TrustManager[] trustManagers = null; + if (StringUtils.hasText(trustStoreName)) { + Resource resource = this.resourceLoader.getResource(trustStoreName); + KeyStore tks = KeyStore.getInstance(storeType); + try (InputStream inputStream = resource.getInputStream()) { + tks.load(inputStream, trustPassphrase); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(ssl.getTrustStoreAlgorithm()); + tmf.init(tks); + trustManagers = tmf.getTrustManagers(); + } + return trustManagers; + } + /** * {@link SpringBootCondition} to determine when to apply embedded LDAP * auto-configuration. diff --git a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java index d348491af48a..3e85ec2ee13e 100644 --- a/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java +++ b/module/spring-boot-ldap/src/main/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapProperties.java @@ -16,9 +16,12 @@ package org.springframework.boot.ldap.autoconfigure.embedded; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; +import javax.net.ssl.SSLContext; + import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -62,6 +65,11 @@ public class EmbeddedLdapProperties { */ private final Validation validation = new Validation(); + /** + * SSL configuration. + */ + private final Ssl ssl = new Ssl(); + public int getPort() { return this.port; } @@ -98,6 +106,10 @@ public Validation getValidation() { return this.validation; } + public Ssl getSsl() { + return this.ssl; + } + public static class Credential { /** @@ -132,6 +144,174 @@ boolean isAvailable() { } + public static class Ssl { + + private static final String SUN_X509 = "SunX509"; + + private static final String DEFAULT_PROTOCOL; + + static { + String protocol = "TLSv1.1"; + try { + String[] protocols = SSLContext.getDefault().getSupportedSSLParameters().getProtocols(); + for (String prot : protocols) { + if ("TLSv1.2".equals(prot)) { + protocol = "TLSv1.2"; + break; + } + } + } + catch (NoSuchAlgorithmException ex) { + // nothing + } + DEFAULT_PROTOCOL = protocol; + } + + /** + * Whether to enable SSL support. + */ + private Boolean enabled = false; + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + /** + * Path to the key store that holds the SSL certificate. + */ + private @Nullable String keyStore; + + /** + * Key store type. + */ + private String keyStoreType = "PKCS12"; + + /** + * Password used to access the key store. + */ + private @Nullable String keyStorePassword; + + /** + * Key store algorithm. + */ + private String keyStoreAlgorithm = SUN_X509; + + /** + * Trust store that holds SSL certificates. + */ + private @Nullable String trustStore; + + /** + * Trust store type. + */ + private String trustStoreType = "JKS"; + + /** + * Password used to access the trust store. + */ + private @Nullable String trustStorePassword; + + /** + * Trust store algorithm. + */ + private String trustStoreAlgorithm = SUN_X509; + + /** + * SSL algorithm to use. + */ + private String algorithm = DEFAULT_PROTOCOL; + + public Boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + public @Nullable String getKeyStore() { + return this.keyStore; + } + + public void setKeyStore(@Nullable String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStoreType() { + return this.keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public @Nullable String getKeyStorePassword() { + return this.keyStorePassword; + } + + public void setKeyStorePassword(@Nullable String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyStoreAlgorithm() { + return this.keyStoreAlgorithm; + } + + public void setKeyStoreAlgorithm(String keyStoreAlgorithm) { + this.keyStoreAlgorithm = keyStoreAlgorithm; + } + + public @Nullable String getTrustStore() { + return this.trustStore; + } + + public void setTrustStore(@Nullable String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStoreType() { + return this.trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public @Nullable String getTrustStorePassword() { + return this.trustStorePassword; + } + + public void setTrustStorePassword(@Nullable String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStoreAlgorithm() { + return this.trustStoreAlgorithm; + } + + public void setTrustStoreAlgorithm(String trustStoreAlgorithm) { + this.trustStoreAlgorithm = trustStoreAlgorithm; + } + + public String getAlgorithm() { + return this.algorithm; + } + + public void setAlgorithm(String sslAlgorithm) { + this.algorithm = sslAlgorithm; + } + + } + public static class Validation { /** diff --git a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java index 14b4d020638c..c28fd29e4ed2 100644 --- a/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/module/spring-boot-ldap/src/test/java/org/springframework/boot/ldap/autoconfigure/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -20,8 +20,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.BindResult; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.LDAPConnection; @@ -32,6 +35,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.ldap.autoconfigure.LdapAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -54,7 +58,7 @@ class EmbeddedLdapAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class, SslAutoConfiguration.class)); @Test void testSetDefaultPort() { @@ -66,6 +70,98 @@ void testSetDefaultPort() { }); } + @Test + void testServerDefaultNoSsl() { + this.contextRunner + .withPropertyValues("spring.ldap.embedded.port:1234", "spring.ldap.embedded.base-dn:dc=spring,dc=org") + .run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAP"); + }); + } + + @Test + void testServerWithSslBundle() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.truststore.location=" + location + "test.jks"); + propertyValues.add("spring.ssl.bundle.jks.test.protocol=TLSv1.2"); + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:test"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAPS"); + assertThat(config.getListenPort()).isEqualTo(1234); + assertThat(server.getListenPort()).isEqualTo(1234); + assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); + }); + } + + @Test + void testServerWithInvalidSslBundleShouldFail() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ssl.bundle.jks.test.keystore.password=secret"); + propertyValues.add("spring.ssl.bundle.jks.test.keystore.location=" + location + "test.jks"); + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.bundle:foo"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("cannot be found"); + }); + } + + @Test + void testServerWithSsl() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "test.jks"); + propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "test.jks"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + InMemoryDirectoryServer server = context.getBean(InMemoryDirectoryServer.class); + assertThat(server.getConfig().getListenerConfigs().size()).isEqualTo(1); + InMemoryListenerConfig config = server.getConfig().getListenerConfigs().get(0); + assertThat(config.getListenerName()).isEqualTo("LDAPS"); + assertThat(config.getListenPort()).isEqualTo(1234); + assertThat(server.getListenPort()).isEqualTo(1234); + assertThat(server.getConnection("LDAPS").getSSLSession()).isNotNull(); + }); + } + + @Test + void testServerWithInvalidSslShouldFail() { + List propertyValues = new ArrayList<>(); + String location = "classpath:org/springframework/boot/ldap/autoconfigure/embedded/"; + propertyValues.add("spring.ldap.embedded.port:1234"); + propertyValues.add("spring.ldap.embedded.base-dn:dc=spring,dc=org"); + propertyValues.add("spring.ldap.embedded.ssl.enabled:true"); + propertyValues.add("spring.ldap.embedded.ssl.keyStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.keyStore=" + location + "foo"); + propertyValues.add("spring.ldap.embedded.ssl.trustStorePassword=secret"); + propertyValues.add("spring.ldap.embedded.ssl.trustStore=" + location + "foo"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("foo"); + assertThat(context).getFailure().hasMessageContaining("does not exist"); + }); + } + @Test void testRandomPortWithEnvironment() { this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> { diff --git a/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks new file mode 100644 index 000000000000..8413be810956 Binary files /dev/null and b/module/spring-boot-ldap/src/test/resources/org/springframework/boot/ldap/autoconfigure/embedded/test.jks differ