From cbc914259bc7906d6882cb91842f5e38d581db2e Mon Sep 17 00:00:00 2001 From: JiaLin Date: Sun, 26 Jul 2020 21:23:10 +0800 Subject: [PATCH 1/4] add client side TLS support --- pom.xml | 14 + .../pom.xml | 100 +++++++ .../cloud/netflix/eureka/AppRunner.java | 125 +++++++++ .../cloud/netflix/eureka/BaseCertTest.java | 93 +++++++ .../netflix/eureka/EurekaClientRunner.java | 99 +++++++ .../netflix/eureka/EurekaClientTest.java | 158 +++++++++++ .../netflix/eureka/EurekaServerRunner.java | 56 ++++ .../cloud/netflix/eureka/KeyAndCert.java | 94 +++++++ .../cloud/netflix/eureka/KeyTool.java | 126 +++++++++ ...coveryClientOptionalArgsConfiguration.java | 38 ++- .../netflix/eureka/config/TlsProperties.java | 255 ++++++++++++++++++ 11 files changed, 1152 insertions(+), 6 deletions(-) create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/pom.xml create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java create mode 100644 spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java diff --git a/pom.xml b/pom.xml index 0c41d1a44..9983be32b 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,9 @@ java 1.19.1 1.4.11.1 + + 1.64 + 2.1 @@ -176,6 +179,16 @@ ${xstream.version} + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + javax.xml + jaxb-impl + ${jaxb.version} + @@ -204,6 +217,7 @@ spring-cloud-netflix-ribbon spring-cloud-starter-netflix spring-cloud-netflix-hystrix + spring-cloud-netflix-eureka-client-tls-tests docs diff --git a/spring-cloud-netflix-eureka-client-tls-tests/pom.xml b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml new file mode 100644 index 000000000..a1fe4c08e --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-netflix + 2.2.4.BUILD-SNAPSHOT + .. + + spring-cloud-netflix-eureka-client-tls-tests + jar + Spring Cloud Netflix Eureka Client TLS Tests + Spring Cloud Netflix Eureka Client TLS Tests + + + + org.springframework.cloud + spring-cloud-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-netflix-eureka-server + + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-logging + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + org.springframework + spring-web + + + com.fasterxml.jackson.core + jackson-annotations + + + org.springframework.retry + spring-retry + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-aop + true + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.vintage + junit-vintage-engine + test + + + org.bouncycastle + bcpkix-jdk15on + test + + + javax.xml + jaxb-impl + + + diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java new file mode 100644 index 000000000..3e48433f9 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/AppRunner.java @@ -0,0 +1,125 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.SocketUtils; + +public class AppRunner implements AutoCloseable { + + private Class appClass; + + private Map props; + + private ConfigurableApplicationContext app; + + public AppRunner(Class appClass) { + this.appClass = appClass; + props = new LinkedHashMap<>(); + } + + public void property(String key, String value) { + props.put(key, value); + } + + public void start() { + if (app == null) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(appClass); + builder.properties("spring.jmx.enabled=false"); + builder.properties(String.format("server.port=%d", availabeTcpPort())); + builder.properties(props()); + + app = builder.build().run(); + } + } + + private int availabeTcpPort() { + return SocketUtils.findAvailableTcpPort(); + } + + private String[] props() { + List result = new ArrayList<>(); + + for (String key : props.keySet()) { + String value = props.get(key); + result.add(String.format("%s=%s", key, value)); + } + + return result.toArray(new String[0]); + } + + public void stop() { + if (app != null) { + app.stop(); + app = null; + } + } + + public ConfigurableApplicationContext app() { + return app; + } + + public String getProperty(String key) { + return app.getEnvironment().getProperty(key); + } + + public T getBean(Class type) { + return app.getBean(type); + } + + public ApplicationContext parent() { + return app.getParent(); + } + + public Map getParentBeans(Class type) { + return parent().getBeansOfType(type); + } + + public int port() { + if (app == null) { + throw new RuntimeException("App is not running."); + } + return app.getEnvironment().getProperty("server.port", Integer.class, -1); + } + + public String root() { + if (app == null) { + throw new RuntimeException("App is not running."); + } + + String protocol = tlsEnabled() ? "https" : "http"; + return String.format("%s://localhost:%d/", protocol, port()); + } + + private boolean tlsEnabled() { + return app.getEnvironment().getProperty("server.ssl.enabled", Boolean.class, + false); + } + + @Override + public void close() { + stop(); + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java new file mode 100644 index 000000000..b224ecb0c --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.security.KeyStore; + +import org.junit.BeforeClass; + +public class BaseCertTest { + + protected static final String KEY_STORE_PASSWORD = "test-key-store-password"; + + protected static final String KEY_PASSWORD = "test-key-password"; + + protected static final String WRONG_PASSWORD = "test-wrong-password"; + + protected static File caCert; + + protected static File wrongCaCert; + + protected static File serverCert; + + protected static File clientCert; + + protected static File wrongClientCert; + + @BeforeClass + public static void createCertificates() throws Exception { + KeyTool tool = new KeyTool(); + + KeyAndCert ca = tool.createCA("MyCA"); + KeyAndCert server = ca.sign("server"); + KeyAndCert client = ca.sign("client"); + + caCert = saveCert(ca); + serverCert = saveKeyAndCert(server); + clientCert = saveKeyAndCert(client); + + KeyAndCert wrongCa = tool.createCA("WrongCA"); + KeyAndCert wrongClient = wrongCa.sign("client"); + + wrongCaCert = saveCert(wrongCa); + wrongClientCert = saveKeyAndCert(wrongClient); + + System.setProperty("javax.net.ssl.trustStore", caCert.getAbsolutePath()); + System.setProperty("javax.net.ssl.trustStorePassword", KEY_STORE_PASSWORD); + } + + private static File saveKeyAndCert(KeyAndCert keyCert) throws Exception { + return saveKeyStore(keyCert.subject(), + () -> keyCert.storeKeyAndCert(KEY_PASSWORD)); + } + + private static File saveCert(KeyAndCert keyCert) throws Exception { + return saveKeyStore(keyCert.subject(), () -> keyCert.storeCert()); + } + + private static File saveKeyStore(String prefix, KeyStoreSupplier func) + throws Exception { + File result = File.createTempFile(prefix, ".p12"); + result.deleteOnExit(); + + try (OutputStream output = new FileOutputStream(result)) { + KeyStore store = func.createKeyStore(); + store.store(output, KEY_STORE_PASSWORD.toCharArray()); + } + return result; + } + + interface KeyStoreSupplier { + + KeyStore createKeyStore() throws Exception; + + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java new file mode 100644 index 000000000..6923e8ac2 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientRunner.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.io.File; +import java.util.function.BooleanSupplier; + +import org.springframework.cloud.client.discovery.DiscoveryClient; + +public class EurekaClientRunner extends AppRunner { + + public EurekaClientRunner(Class appClass, AppRunner server) { + super(appClass); + + property("eureka.client.registerWithEureka", "false"); + property("eureka.client.fetchRegistry", "true"); + property("eureka.client.serviceUrl.defaultZone", server.root() + "eureka/"); + property("eureka.client.refresh.enable", "true"); + } + + public EurekaClientRunner(Class appClass, AppRunner server, String service) { + this(appClass, server); + property("eureka.client.registerWithEureka", "true"); + property("spring.application.name", service); + } + + public void enableTls() { + property("eureka.client.tls.enabled", "true"); + } + + public void disableTls() { + property("eureka.client.tls.enabled", "false"); + } + + public void setKeyStore(File keyStore, String keyStorePassword, String keyPassword) { + property("eureka.client.tls.key-store", pathOf(keyStore)); + property("eureka.client.tls.key-store-password", keyStorePassword); + property("eureka.client.tls.key-password", keyPassword); + } + + public void setKeyStore(File keyStore) { + property("eureka.client.tls.key-store", pathOf(keyStore)); + } + + public void setTrustStore(File trustStore, String password) { + property("eureka.client.tls.trust-store", pathOf(trustStore)); + property("eureka.client.tls.trust-store-password", password); + } + + public void setTrustStore(File trustStore) { + property("eureka.client.tls.trust-store", pathOf(trustStore)); + } + + private String pathOf(File file) { + return String.format("file:%s", file.getAbsolutePath()); + } + + public void waitServiceViaEureka(int seconds) { + assertInSeconds(() -> foundServiceViaEureka(), seconds); + } + + private void assertInSeconds(BooleanSupplier assertion, int seconds) { + long start = System.currentTimeMillis(); + long limit = 1000L * seconds; + long duration = 0; + + do { + if (assertion.getAsBoolean()) { + return; + } + duration = System.currentTimeMillis() - start; + Thread.yield(); + + } + while (duration < limit); + + throw new RuntimeException(); + } + + public boolean foundServiceViaEureka() { + DiscoveryClient discovery = getBean(DiscoveryClient.class); + return !discovery.getServices().isEmpty(); + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java new file mode 100644 index 000000000..e2d066bf9 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.io.File; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EurekaClientTest extends BaseCertTest { + + private static EurekaServerRunner server; + + private static EurekaClientRunner service; + + @BeforeClass + public static void setupAll() { + startEurekaServer(); + startService(); + waitForRegistration(); + } + + @AfterClass + public static void tearDownAll() { + stopService(); + stopEurekaServer(); + } + + private static void startEurekaServer() { + server = new EurekaServerRunner(TestEurekaServer.class); + server.enableTls(); + server.setKeyStore(serverCert, KEY_STORE_PASSWORD, "server", KEY_PASSWORD); + server.setTrustStore(caCert, KEY_STORE_PASSWORD); + + server.start(); + } + + private static void stopEurekaServer() { + server.stop(); + } + + private static void startService() { + service = new EurekaClientRunner(TestApp.class, server, "testservice"); + enableTlsClient(service); + service.start(); + } + + private static void stopService() { + service.stop(); + } + + private static void waitForRegistration() { + try (EurekaClientRunner client = createEurekaClient()) { + enableTlsClient(client); + client.start(); + client.waitServiceViaEureka(60); + } + } + + private static EurekaClientRunner createEurekaClient() { + return new EurekaClientRunner(TestApp.class, server); + } + + private static void enableTlsClient(EurekaClientRunner runner) { + runner.enableTls(); + runner.setKeyStore(clientCert, KEY_STORE_PASSWORD, KEY_PASSWORD); + runner.setTrustStore(caCert, KEY_STORE_PASSWORD); + } + + /** + * Already proved this in waitForRegistration(). Keep this Test to express test + * purpose explicitly. + */ + @Test + public void clientCertCanWork() { + } + + @Test + public void noCertCannotWork() { + try (EurekaClientRunner client = createEurekaClient()) { + client.disableTls(); + client.start(); + assertThat(client.foundServiceViaEureka()).isFalse(); + } + } + + @Test + public void wrongCertCannotWork() { + try (EurekaClientRunner client = createEurekaClient()) { + enableTlsClient(client); + client.setKeyStore(wrongClientCert); + client.start(); + assertThat(client.foundServiceViaEureka()).isFalse(); + } + } + + @Test(expected = BeanCreationException.class) + public void wrongPasswordCauseFailure() { + EurekaClientRunner client = createEurekaClient(); + enableTlsClient(client); + client.setKeyStore(clientCert, WRONG_PASSWORD, WRONG_PASSWORD); + client.start(); + } + + @Test(expected = BeanCreationException.class) + public void nonExistKeyStoreCauseFailure() { + EurekaClientRunner client = createEurekaClient(); + enableTlsClient(client); + client.setKeyStore(new File("nonExistFile")); + client.start(); + } + + @Test + public void wrongTrustStoreCannotWork() { + try (EurekaClientRunner client = createEurekaClient()) { + enableTlsClient(client); + client.setTrustStore(wrongCaCert); + client.start(); + assertThat(client.foundServiceViaEureka()).isFalse(); + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + public static class TestApp { + + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @EnableEurekaServer + public static class TestEurekaServer { + + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java new file mode 100644 index 000000000..a5fb15763 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/EurekaServerRunner.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.io.File; + +public class EurekaServerRunner extends AppRunner { + + public EurekaServerRunner(Class appClass) { + super(appClass); + + property("eureka.client.registerWithEureka", "false"); + property("eureka.client.fetchRegistry", "false"); + property("eureka.server.waitTimeInMsWhenSyncEmpty", "0"); + property("eureka.client.refresh.enable", "true"); + } + + public void enableTls() { + property("server.ssl.enabled", "true"); + property("server.ssl.client-auth", "need"); + } + + public void setKeyStore(File keyStore, String keyStorePassword, String key, + String keyPassword) { + property("server.ssl.key-store", pathOf(keyStore)); + property("server.ssl.key-store-type", "PKCS12"); + property("server.ssl.key-store-password", keyStorePassword); + property("server.ssl.key-alias", key); + property("server.ssl.key-password", keyPassword); + } + + public void setTrustStore(File trustStore, String password) { + property("server.ssl.trust-store", pathOf(trustStore)); + property("server.ssl.trust-store-type", "PKCS12"); + property("server.ssl.trust-store-password", password); + } + + private String pathOf(File file) { + return String.format("file:%s", file.getAbsolutePath()); + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java new file mode 100644 index 000000000..192febd07 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyAndCert.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +public class KeyAndCert { + + private KeyPair keyPair; + + private X509Certificate certificate; + + public KeyAndCert(KeyPair keyPair, X509Certificate certificate) { + this.keyPair = keyPair; + this.certificate = certificate; + } + + public KeyPair keyPair() { + return keyPair; + } + + public PublicKey publicKey() { + return keyPair.getPublic(); + } + + public PrivateKey privateKey() { + return keyPair.getPrivate(); + } + + public X509Certificate certificate() { + return certificate; + } + + public String subject() { + String dn = certificate.getSubjectDN().getName(); + int index = dn.indexOf('='); + return dn.substring(index + 1); + } + + public KeyAndCert sign(String subject) throws Exception { + KeyTool tool = new KeyTool(); + return tool.signCertificate(subject, this); + } + + public KeyAndCert sign(KeyPair keyPair, String subject) throws Exception { + KeyTool tool = new KeyTool(); + return tool.signCertificate(keyPair, subject, this); + } + + public KeyStore storeKeyAndCert(String keyPassword) throws Exception { + KeyStore result = KeyStore.getInstance("PKCS12"); + result.load(null); + + result.setKeyEntry(subject(), keyPair.getPrivate(), keyPassword.toCharArray(), + certChain()); + return result; + } + + private Certificate[] certChain() { + return new Certificate[] { certificate() }; + } + + public KeyStore storeCert() throws Exception { + return storeCert("PKCS12"); + } + + public KeyStore storeCert(String storeType) throws Exception { + KeyStore result = KeyStore.getInstance(storeType); + result.load(null); + + result.setCertificateEntry(subject(), certificate()); + return result; + } + +} diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java new file mode 100644 index 000000000..b1e22fea1 --- /dev/null +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/KeyTool.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +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.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; + +public class KeyTool { + + private static final long ONE_DAY = 1000L * 60L * 60L * 24L; + + private static final long TEN_YEARS = ONE_DAY * 365L * 10L; + + public KeyAndCert createCA(String ca) throws Exception { + KeyPair keyPair = createKeyPair(); + X509Certificate certificate = createCert(keyPair, ca); + return new KeyAndCert(keyPair, certificate); + } + + public KeyAndCert signCertificate(String subject, KeyAndCert signer) + throws Exception { + return signCertificate(createKeyPair(), subject, signer); + } + + public KeyAndCert signCertificate(KeyPair keyPair, String subject, KeyAndCert signer) + throws Exception { + X509Certificate certificate = createCert(keyPair.getPublic(), signer.privateKey(), + signer.subject(), subject); + KeyAndCert result = new KeyAndCert(keyPair, certificate); + + return result; + } + + public KeyPair createKeyPair() throws Exception { + return createKeyPair(1024); + } + + public KeyPair createKeyPair(int keySize) throws Exception { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(keySize, new SecureRandom()); + return gen.generateKeyPair(); + } + + public X509Certificate createCert(KeyPair keyPair, String ca) throws Exception { + JcaX509v3CertificateBuilder builder = certBuilder(keyPair.getPublic(), ca, ca); + builder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.keyCertSign)); + builder.addExtension(Extension.basicConstraints, false, + new BasicConstraints(true)); + + return signCert(builder, keyPair.getPrivate()); + } + + public X509Certificate createCert(PublicKey publicKey, PrivateKey privateKey, + String issuer, String subject) throws Exception { + JcaX509v3CertificateBuilder builder = certBuilder(publicKey, issuer, subject); + builder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature)); + builder.addExtension(Extension.basicConstraints, false, + new BasicConstraints(false)); + + GeneralName[] names = new GeneralName[] { + new GeneralName(GeneralName.dNSName, "localhost") }; + builder.addExtension(Extension.subjectAlternativeName, false, + GeneralNames.getInstance(new DERSequence(names))); + + return signCert(builder, privateKey); + } + + private JcaX509v3CertificateBuilder certBuilder(PublicKey publicKey, String issuer, + String subject) { + X500Name issuerName = new X500Name(String.format("dc=%s", issuer)); + X500Name subjectName = new X500Name(String.format("dc=%s", subject)); + + long now = System.currentTimeMillis(); + BigInteger serialNum = BigInteger.valueOf(now); + Date notBefore = new Date(now - ONE_DAY); + Date notAfter = new Date(now + TEN_YEARS); + + return new JcaX509v3CertificateBuilder(issuerName, serialNum, notBefore, notAfter, + subjectName, publicKey); + } + + private X509Certificate signCert(JcaX509v3CertificateBuilder builder, + PrivateKey privateKey) throws Exception { + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA") + .build(privateKey); + X509CertificateHolder holder = builder.build(signer); + + return new JcaX509CertificateConverter().getCertificate(holder); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java index be5d14cc7..88300bcc7 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java @@ -16,15 +16,20 @@ package org.springframework.cloud.netflix.eureka.config; +import java.io.IOException; +import java.security.GeneralSecurityException; + import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs; @@ -35,8 +40,12 @@ import org.springframework.context.annotation.Configuration; * @author Daniel Lavoie */ @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(TlsProperties.class) public class DiscoveryClientOptionalArgsConfiguration { + @Autowired + private TlsProperties tls; + protected final Log logger = LogFactory.getLog(getClass()); @Bean @@ -45,9 +54,12 @@ public class DiscoveryClientOptionalArgsConfiguration { search = SearchStrategy.CURRENT) @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, havingValue = "false") - public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() { + public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() + throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses RestTemplate."); - return new RestTemplateDiscoveryClientOptionalArgs(); + RestTemplateDiscoveryClientOptionalArgs result = new RestTemplateDiscoveryClientOptionalArgs(); + setupTLS(result); + return result; } @Bean @@ -60,17 +72,31 @@ public class DiscoveryClientOptionalArgsConfiguration { search = SearchStrategy.CURRENT) @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", havingValue = "true") - public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs() { + public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs() + throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses WebClient."); - return new WebClientDiscoveryClientOptionalArgs(); + WebClientDiscoveryClientOptionalArgs result = new WebClientDiscoveryClientOptionalArgs(); + setupTLS(result); + return result; } @Bean @ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter") @ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT) - public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() { - return new MutableDiscoveryClientOptionalArgs(); + public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() + throws GeneralSecurityException, IOException { + logger.info("Eureka HTTP Client uses Jersey"); + MutableDiscoveryClientOptionalArgs result = new MutableDiscoveryClientOptionalArgs(); + setupTLS(result); + return result; + } + + private void setupTLS(AbstractDiscoveryClientOptionalArgs args) + throws GeneralSecurityException, IOException { + if (tls.isEnabled()) { + args.setSSLContext(tls.createSSLContext()); + } } @Configuration diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java new file mode 100644 index 000000000..9f8958789 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java @@ -0,0 +1,255 @@ +/* + * Copyright 2017-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka.config; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.UnrecoverableKeyException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.net.ssl.SSLContext; + +import org.apache.http.ssl.SSLContextBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +/** + * Eureka client TLS properties. + */ +@ConfigurationProperties(TlsProperties.PREFIX) +public class TlsProperties { + + /** + * Prefix for Eureka client TLS properties. + */ + public static final String PREFIX = "eureka.client.tls"; + + private static final String DEFAULT_STORE_TYPE = "PKCS12"; + + private static final Map EXTENSION_STORE_TYPES = extTypes(); + + private boolean enabled; + + private Resource keyStore; + + private String keyStoreType; + + private String keyStorePassword = ""; + + private String keyPassword = ""; + + private Resource trustStore; + + private String trustStoreType; + + private String trustStorePassword = ""; + + private static Map extTypes() { + Map result = new HashMap<>(); + + result.put("p12", "PKCS12"); + result.put("pfx", "PKCS12"); + result.put("jks", "JKS"); + + return Collections.unmodifiableMap(result); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Resource getKeyStore() { + return keyStore; + } + + public void setKeyStore(Resource keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public char[] keyStorePassword() { + return keyStorePassword.toCharArray(); + } + + public String getKeyPassword() { + return keyPassword; + } + + public void setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + } + + public char[] keyPassword() { + return keyPassword.toCharArray(); + } + + public Resource getTrustStore() { + return trustStore; + } + + public void setTrustStore(Resource trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public char[] trustStorePassword() { + return trustStorePassword.toCharArray(); + } + + @PostConstruct + public void postConstruct() { + if (keyStore != null && keyStoreType == null) { + keyStoreType = storeTypeOf(keyStore); + } + if (trustStore != null && trustStoreType == null) { + trustStoreType = storeTypeOf(trustStore); + } + } + + private String storeTypeOf(Resource resource) { + String extension = fileExtensionOf(resource); + String type = EXTENSION_STORE_TYPES.get(extension); + + return (type == null) ? DEFAULT_STORE_TYPE : type; + } + + private String fileExtensionOf(Resource resource) { + String name = resource.getFilename(); + int index = name.lastIndexOf('.'); + + return index < 0 ? "" : name.substring(index + 1).toLowerCase(); + } + + public SSLContext createSSLContext() throws GeneralSecurityException, IOException { + SSLContextBuilder builder = new SSLContextBuilder(); + char[] keyPassword = keyPassword(); + KeyStore keyStore = createKeyStore(); + + try { + builder.loadKeyMaterial(keyStore, keyPassword); + } + catch (UnrecoverableKeyException e) { + if (keyPassword.length == 0) { + // Retry if empty password, see + // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest + builder.loadKeyMaterial(keyStore, new char[] { '\0' }); + } + else { + throw e; + } + } + + KeyStore trust = createTrustStore(); + if (trust != null) { + builder.loadTrustMaterial(trust, null); + } + + return builder.build(); + } + + private KeyStore createKeyStore() throws GeneralSecurityException, IOException { + if (keyStore == null) { + throw new KeyStoreException("Keystore not specified."); + } + if (!keyStore.exists()) { + throw new KeyStoreException("Keystore not exists: " + keyStore); + } + + KeyStore result = KeyStore.getInstance(keyStoreType); + char[] keyStorePassword = keyStorePassword(); + + try { + loadKeyStore(result, keyStore, keyStorePassword); + } + catch (IOException e) { + // Retry if empty password, see + // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest + if (keyStorePassword.length == 0) { + loadKeyStore(result, keyStore, new char[] { '\0' }); + } + else { + throw e; + } + } + + return result; + } + + private static void loadKeyStore(KeyStore keyStore, Resource keyStoreResource, + char[] keyStorePassword) throws IOException, GeneralSecurityException { + try (InputStream inputStream = keyStoreResource.getInputStream()) { + keyStore.load(inputStream, keyStorePassword); + } + } + + private KeyStore createTrustStore() throws GeneralSecurityException, IOException { + if (trustStore == null) { + return null; + } + if (!trustStore.exists()) { + throw new KeyStoreException("KeyStore not exists: " + trustStore); + } + + KeyStore result = KeyStore.getInstance(trustStoreType); + try (InputStream input = trustStore.getInputStream()) { + result.load(input, trustStorePassword()); + } + return result; + } + +} From 072418826ee01b050f19963d6f7470aafe1a52ff Mon Sep 17 00:00:00 2001 From: JiaLin Date: Sat, 1 Aug 2020 22:19:48 +0800 Subject: [PATCH 2/4] Update spring-cloud-netflix.adoc --- .../main/asciidoc/spring-cloud-netflix.adoc | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 789672194..3e8578819 100755 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -87,6 +87,26 @@ For more complex needs, you can create a `@Bean` of type `DiscoveryClientOptiona NOTE: Because of a limitation in Eureka, it is not possible to support per-server basic auth credentials, so only the first set that are found is used. +When Eureka server requires client side certificate for authentication, the client side certificate and trust store can be configured via properties, as shown in following example: + +.application.yml +[source,yaml] +---- +eureka: + client: + tls: + enabled: true + key-store: + key-store-type: PKCS12 + key-store-password: + key-password: + trust-store: + trust-store-type: PKCS12 + trust-store-password: +---- + +The `eureka.client.tls.enabled` needs to be true to enable Eureka client side TLS. When `eureka.client.tls.trust-store` is omitted, a JVM default trust store is used. The default value for `eureka.client.tls.key-store-type` and `eureka.client.tls.trust-store-type` is PKCS12. When password properties are omitted, empty password is assumed. + === Status Page and Health Indicator The status page and health indicators for a Eureka instance default to `/info` and `/health` respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. From a3436d08eb337211736af3e1f42ea72dcd8253a7 Mon Sep 17 00:00:00 2001 From: JiaLin Date: Sun, 23 Aug 2020 15:53:14 +0800 Subject: [PATCH 3/4] Use common TLS classes --- .../cloud/netflix/eureka/BaseCertTest.java | 3 - ...coveryClientOptionalArgsConfiguration.java | 34 ++- .../eureka/config/EurekaTlsProperties.java | 33 +++ .../netflix/eureka/config/TlsProperties.java | 255 ------------------ 4 files changed, 48 insertions(+), 277 deletions(-) create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java delete mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java diff --git a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java index f4bb62115..c910111af 100644 --- a/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java +++ b/spring-cloud-netflix-eureka-client-tls-tests/src/test/java/org/springframework/cloud/netflix/eureka/BaseCertTest.java @@ -61,9 +61,6 @@ public abstract class BaseCertTest { wrongCaCert = saveCert(wrongCa); wrongClientCert = saveKeyAndCert(wrongClient); - - System.setProperty("javax.net.ssl.trustStore", caCert.getAbsolutePath()); - System.setProperty("javax.net.ssl.trustStorePassword", KEY_STORE_PASSWORD); } private static File saveKeyAndCert(KeyAndCert keyCert) throws Exception { diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java index 5b5b32aa9..d6ecf832a 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java @@ -31,6 +31,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClas import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.configuration.SSLContextFactory; +import org.springframework.cloud.configuration.TlsProperties; import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs; @@ -42,15 +44,15 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Daniel Lavoie */ @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(TlsProperties.class) +@EnableConfigurationProperties(EurekaTlsProperties.class) public class DiscoveryClientOptionalArgsConfiguration { - @Autowired - private TlsProperties tls; - protected static final Log logger = LogFactory .getLog(DiscoveryClientOptionalArgsConfiguration.class); + @Autowired + private EurekaTlsProperties tlsProperties; + @Bean @ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter") @ConditionalOnMissingBean(value = { AbstractDiscoveryClientOptionalArgs.class }, @@ -61,7 +63,7 @@ public class DiscoveryClientOptionalArgsConfiguration { throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses RestTemplate."); RestTemplateDiscoveryClientOptionalArgs result = new RestTemplateDiscoveryClientOptionalArgs(); - setupTLS(result); + setupTLS(result, tlsProperties); return result; } @@ -73,14 +75,15 @@ public class DiscoveryClientOptionalArgsConfiguration { throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses Jersey"); MutableDiscoveryClientOptionalArgs result = new MutableDiscoveryClientOptionalArgs(); - setupTLS(result); + setupTLS(result, tlsProperties); return result; } - private void setupTLS(AbstractDiscoveryClientOptionalArgs args) - throws GeneralSecurityException, IOException { - if (tls.isEnabled()) { - args.setSSLContext(tls.createSSLContext()); + private static void setupTLS(AbstractDiscoveryClientOptionalArgs args, + TlsProperties properties) throws GeneralSecurityException, IOException { + if (properties.isEnabled()) { + SSLContextFactory factory = new SSLContextFactory(properties); + args.setSSLContext(factory.createSSLContext()); } } @@ -92,7 +95,7 @@ public class DiscoveryClientOptionalArgsConfiguration { protected static class WebClientConfiguration { @Autowired - private TlsProperties tls; + private EurekaTlsProperties tlsProperties; @Bean @ConditionalOnMissingBean( @@ -105,17 +108,10 @@ public class DiscoveryClientOptionalArgsConfiguration { logger.info("Eureka HTTP Client uses WebClient."); WebClientDiscoveryClientOptionalArgs result = new WebClientDiscoveryClientOptionalArgs( builder::getIfAvailable); - setupTLS(result); + setupTLS(result, tlsProperties); return result; } - private void setupTLS(AbstractDiscoveryClientOptionalArgs args) - throws GeneralSecurityException, IOException { - if (tls.isEnabled()) { - args.setSSLContext(tls.createSSLContext()); - } - } - } @Configuration diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java new file mode 100644 index 000000000..441f79a2f --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.cloud.netflix.eureka.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.configuration.TlsProperties; + +/** + * Eureka client TLS properties. + */ +@ConfigurationProperties(EurekaTlsProperties.PREFIX) +public class EurekaTlsProperties extends TlsProperties { + + /** + * Prefix for Eureka client TLS properties. + */ + public static final String PREFIX = "eureka.client.tls"; + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java deleted file mode 100644 index 9f8958789..000000000 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/TlsProperties.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2017-2020 the original author or authors. - * - * 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 - * - * https://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.springframework.cloud.netflix.eureka.config; - -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.UnrecoverableKeyException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.annotation.PostConstruct; -import javax.net.ssl.SSLContext; - -import org.apache.http.ssl.SSLContextBuilder; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.io.Resource; - -/** - * Eureka client TLS properties. - */ -@ConfigurationProperties(TlsProperties.PREFIX) -public class TlsProperties { - - /** - * Prefix for Eureka client TLS properties. - */ - public static final String PREFIX = "eureka.client.tls"; - - private static final String DEFAULT_STORE_TYPE = "PKCS12"; - - private static final Map EXTENSION_STORE_TYPES = extTypes(); - - private boolean enabled; - - private Resource keyStore; - - private String keyStoreType; - - private String keyStorePassword = ""; - - private String keyPassword = ""; - - private Resource trustStore; - - private String trustStoreType; - - private String trustStorePassword = ""; - - private static Map extTypes() { - Map result = new HashMap<>(); - - result.put("p12", "PKCS12"); - result.put("pfx", "PKCS12"); - result.put("jks", "JKS"); - - return Collections.unmodifiableMap(result); - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Resource getKeyStore() { - return keyStore; - } - - public void setKeyStore(Resource keyStore) { - this.keyStore = keyStore; - } - - public String getKeyStoreType() { - return keyStoreType; - } - - public void setKeyStoreType(String keyStoreType) { - this.keyStoreType = keyStoreType; - } - - public String getKeyStorePassword() { - return keyStorePassword; - } - - public void setKeyStorePassword(String keyStorePassword) { - this.keyStorePassword = keyStorePassword; - } - - public char[] keyStorePassword() { - return keyStorePassword.toCharArray(); - } - - public String getKeyPassword() { - return keyPassword; - } - - public void setKeyPassword(String keyPassword) { - this.keyPassword = keyPassword; - } - - public char[] keyPassword() { - return keyPassword.toCharArray(); - } - - public Resource getTrustStore() { - return trustStore; - } - - public void setTrustStore(Resource trustStore) { - this.trustStore = trustStore; - } - - public String getTrustStoreType() { - return trustStoreType; - } - - public void setTrustStoreType(String trustStoreType) { - this.trustStoreType = trustStoreType; - } - - public String getTrustStorePassword() { - return trustStorePassword; - } - - public void setTrustStorePassword(String trustStorePassword) { - this.trustStorePassword = trustStorePassword; - } - - public char[] trustStorePassword() { - return trustStorePassword.toCharArray(); - } - - @PostConstruct - public void postConstruct() { - if (keyStore != null && keyStoreType == null) { - keyStoreType = storeTypeOf(keyStore); - } - if (trustStore != null && trustStoreType == null) { - trustStoreType = storeTypeOf(trustStore); - } - } - - private String storeTypeOf(Resource resource) { - String extension = fileExtensionOf(resource); - String type = EXTENSION_STORE_TYPES.get(extension); - - return (type == null) ? DEFAULT_STORE_TYPE : type; - } - - private String fileExtensionOf(Resource resource) { - String name = resource.getFilename(); - int index = name.lastIndexOf('.'); - - return index < 0 ? "" : name.substring(index + 1).toLowerCase(); - } - - public SSLContext createSSLContext() throws GeneralSecurityException, IOException { - SSLContextBuilder builder = new SSLContextBuilder(); - char[] keyPassword = keyPassword(); - KeyStore keyStore = createKeyStore(); - - try { - builder.loadKeyMaterial(keyStore, keyPassword); - } - catch (UnrecoverableKeyException e) { - if (keyPassword.length == 0) { - // Retry if empty password, see - // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest - builder.loadKeyMaterial(keyStore, new char[] { '\0' }); - } - else { - throw e; - } - } - - KeyStore trust = createTrustStore(); - if (trust != null) { - builder.loadTrustMaterial(trust, null); - } - - return builder.build(); - } - - private KeyStore createKeyStore() throws GeneralSecurityException, IOException { - if (keyStore == null) { - throw new KeyStoreException("Keystore not specified."); - } - if (!keyStore.exists()) { - throw new KeyStoreException("Keystore not exists: " + keyStore); - } - - KeyStore result = KeyStore.getInstance(keyStoreType); - char[] keyStorePassword = keyStorePassword(); - - try { - loadKeyStore(result, keyStore, keyStorePassword); - } - catch (IOException e) { - // Retry if empty password, see - // https://rt.openssl.org/Ticket/Display.html?id=1497&user=guest&pass=guest - if (keyStorePassword.length == 0) { - loadKeyStore(result, keyStore, new char[] { '\0' }); - } - else { - throw e; - } - } - - return result; - } - - private static void loadKeyStore(KeyStore keyStore, Resource keyStoreResource, - char[] keyStorePassword) throws IOException, GeneralSecurityException { - try (InputStream inputStream = keyStoreResource.getInputStream()) { - keyStore.load(inputStream, keyStorePassword); - } - } - - private KeyStore createTrustStore() throws GeneralSecurityException, IOException { - if (trustStore == null) { - return null; - } - if (!trustStore.exists()) { - throw new KeyStoreException("KeyStore not exists: " + trustStore); - } - - KeyStore result = KeyStore.getInstance(trustStoreType); - try (InputStream input = trustStore.getInputStream()) { - result.load(input, trustStorePassword()); - } - return result; - } - -} From eeaeeb3e6f6553182474520abf5c99a9e0f58ab3 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Mon, 24 Aug 2020 16:11:59 -0400 Subject: [PATCH 4/4] Code review changes --- pom.xml | 13 ------------ .../pom.xml | 3 +++ ...coveryClientOptionalArgsConfiguration.java | 20 ++++++++++--------- .../eureka/config/EurekaTlsProperties.java | 3 +-- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/pom.xml b/pom.xml index 02a496fac..afe0c0a1c 100644 --- a/pom.xml +++ b/pom.xml @@ -34,9 +34,6 @@ java 1.19.1 1.4.11.1 - - 1.64 - 2.1 @@ -179,16 +176,6 @@ ${xstream.version} - - org.bouncycastle - bcpkix-jdk15on - ${bouncycastle.version} - - - javax.xml - jaxb-impl - ${jaxb.version} - diff --git a/spring-cloud-netflix-eureka-client-tls-tests/pom.xml b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml index 649b6885d..5dc260500 100644 --- a/spring-cloud-netflix-eureka-client-tls-tests/pom.xml +++ b/spring-cloud-netflix-eureka-client-tls-tests/pom.xml @@ -90,11 +90,14 @@ org.bouncycastle bcpkix-jdk15on + 1.64 test javax.xml jaxb-impl + 2.1 + test diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java index d6ecf832a..7c2d69247 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java @@ -30,7 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.cloud.configuration.TlsProperties; import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs; @@ -44,14 +44,16 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Daniel Lavoie */ @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(EurekaTlsProperties.class) public class DiscoveryClientOptionalArgsConfiguration { protected static final Log logger = LogFactory .getLog(DiscoveryClientOptionalArgsConfiguration.class); - @Autowired - private EurekaTlsProperties tlsProperties; + @Bean + @ConfigurationProperties("eureka.client.tls") + public TlsProperties tlsProperties() { + return new TlsProperties(); + } @Bean @ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter") @@ -59,8 +61,8 @@ public class DiscoveryClientOptionalArgsConfiguration { search = SearchStrategy.CURRENT) @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, havingValue = "false") - public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() - throws GeneralSecurityException, IOException { + public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs( + TlsProperties tlsProperties) throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses RestTemplate."); RestTemplateDiscoveryClientOptionalArgs result = new RestTemplateDiscoveryClientOptionalArgs(); setupTLS(result, tlsProperties); @@ -71,8 +73,8 @@ public class DiscoveryClientOptionalArgsConfiguration { @ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter") @ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT) - public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() - throws GeneralSecurityException, IOException { + public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs( + TlsProperties tlsProperties) throws GeneralSecurityException, IOException { logger.info("Eureka HTTP Client uses Jersey"); MutableDiscoveryClientOptionalArgs result = new MutableDiscoveryClientOptionalArgs(); setupTLS(result, tlsProperties); @@ -95,7 +97,7 @@ public class DiscoveryClientOptionalArgsConfiguration { protected static class WebClientConfiguration { @Autowired - private EurekaTlsProperties tlsProperties; + private TlsProperties tlsProperties; @Bean @ConditionalOnMissingBean( diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java index 441f79a2f..9e9d17a2b 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaTlsProperties.java @@ -16,13 +16,12 @@ package org.springframework.cloud.netflix.eureka.config; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.configuration.TlsProperties; /** * Eureka client TLS properties. */ -@ConfigurationProperties(EurekaTlsProperties.PREFIX) + public class EurekaTlsProperties extends TlsProperties { /**