add client side TLS support

This commit is contained in:
JiaLin
2020-07-26 21:23:10 +08:00
parent 3bd44d69ca
commit cbc914259b
11 changed files with 1152 additions and 6 deletions

14
pom.xml
View File

@@ -34,6 +34,9 @@
<sonar.language>java</sonar.language>
<eureka-jersey.version>1.19.1</eureka-jersey.version>
<xstream.version>1.4.11.1</xstream.version>
<bouncycastle.version>1.64</bouncycastle.version>
<jaxb.version>2.1</jaxb.version>
</properties>
<build>
<plugins>
@@ -176,6 +179,16 @@
<version>${xstream.version}</version>
</dependency>
<!-- end eureka deps -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxb-impl</artifactId>
<version>${jaxb.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<reporting>
@@ -204,6 +217,7 @@
<module>spring-cloud-netflix-ribbon</module>
<module>spring-cloud-starter-netflix</module>
<module>spring-cloud-netflix-hystrix</module>
<module>spring-cloud-netflix-eureka-client-tls-tests</module>
<module>docs</module>
</modules>
<profiles>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>2.2.4.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-netflix-eureka-client-tls-tests</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Netflix Eureka Client TLS Tests</name>
<description>Spring Cloud Netflix Eureka Client TLS Tests</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxb-impl</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<String, String> 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<String> 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> T getBean(Class<T> type) {
return app.getBean(type);
}
public ApplicationContext parent() {
return app.getParent();
}
public <T> Map<String, T> getParentBeans(Class<T> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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<String, String> 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<String, String> extTypes() {
Map<String, String> 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;
}
}