diff --git a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java index d59f301a1d6..0db54e882ec 100644 --- a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java @@ -38,6 +38,7 @@ import javax.annotation.Nullable; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import org.slf4j.Logger; @@ -62,6 +63,7 @@ import com.linecorp.armeria.server.annotation.ResponseConverterFunction; import com.linecorp.armeria.server.logging.AccessLogWriter; +import io.netty.buffer.ByteBufAllocator; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; @@ -828,6 +830,14 @@ VirtualHost build() { } } + if (sslContext != null) { + try { + validateSslContext(sslContext); + } catch (Exception e) { + throw new RuntimeException("failed to validate SSL/TLS configuration", e); + } + } + final Function accessLoggerMapper = this.accessLoggerMapper != null ? this.accessLoggerMapper : serverBuilder.accessLoggerMapper(); @@ -841,6 +851,19 @@ VirtualHost build() { return decorator != null ? virtualHost.decorate(decorator) : virtualHost; } + void validateSslContext(SslContext sslContext) throws SSLException { + SSLEngine engine = sslContext.newEngine(ByteBufAllocator.DEFAULT); + engine.setUseClientMode(false); + + if (engine.getEnabledProtocols().length == 0) { + throw new RuntimeException("failed to enable protocols"); + } + + if (engine.getEnabledCipherSuites().length == 0) { + throw new RuntimeException("failed to enable cipher suites"); + } + } + @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues() diff --git a/core/src/test/java/com/linecorp/armeria/server/ServerTlsTest.java b/core/src/test/java/com/linecorp/armeria/server/ServerTlsTest.java new file mode 100644 index 00000000000..224f571c3d2 --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/server/ServerTlsTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2019 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.File; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; + +import io.netty.handler.ssl.util.SelfSignedCertificate; + +class ServerTlsTest { + + static final SelfSignedCertificate ssc; + + static { + try { + ssc = new SelfSignedCertificate("a.com"); + } catch (Exception e) { + throw new Error(e); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SSLv2Hello", "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"}) + void testTlsCustomizerValidProtocols(String protocol) throws SSLException { + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of(HttpStatus.OK)) + .tls(ssc.certificate(), ssc.privateKey(), sslContextBuilder -> { + sslContextBuilder.protocols(protocol); + }) + .build(); + } + + @ParameterizedTest + @ValueSource(strings = {"SSLv2", "SSLv2", "SSLv3", "WrongProtocolName"}) + void testTlsCustomizerInvalidProtocols(String protocol) { + assertThrows(RuntimeException.class, () -> { + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of(HttpStatus.OK)) + .tls(ssc.certificate(), ssc.privateKey(), sslContextBuilder -> { + sslContextBuilder.protocols(protocol); + }) + .build(); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "WrongPassword"}) + void testTlsWithInvalidKeyPassword(String password) throws CertificateException, SSLException { + assertThrows(RuntimeException.class, () -> { + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of(HttpStatus.OK)) + .tls(ssc.certificate(), ssc.privateKey(), password) + .build(); + }); + } + + @Test + void testTlsWithNeverInitializedKeyMangerFactory() { + assertThrows(RuntimeException.class, () -> { + KeyStore keyStore = KeyStore.getInstance("JKS"); + File file = new File(getClass().getResource("keystore.jks").toURI()); + keyStore.load(file.toURI().toURL().openStream(), "4t[9Pxc".toCharArray()); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + TrustManagerFactory tmf + = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of(HttpStatus.OK)) + .tls(kmf, sslContextBuilder -> { + sslContextBuilder.keyManager(kmf); + sslContextBuilder.trustManager(tmf); + }) + .build(); + }); + } + + @Test + void testTlsWithNeverInitializedTrustMangerFactory() { + assertThrows(RuntimeException.class, () -> { + KeyStore keyStore = KeyStore.getInstance("JKS"); + File file = new File(getClass().getResource("keystore.jks").toURI()); + keyStore.load(file.toURI().toURL().openStream(), "4t[9Pxc".toCharArray()); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, "B3g9s%ds".toCharArray()); + + TrustManagerFactory tmf + = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of(HttpStatus.OK)) + .tls(kmf, sslContextBuilder -> { + sslContextBuilder.keyManager(kmf); + sslContextBuilder.trustManager(tmf); + }) + .build(); + }); + } + + @Test + void testTlsWrongCipherSuites() throws CertificateException, SSLException { + List ciphers = Arrays.asList( + "WrongCipherName1", + "WrongCipherName2" + ); + + assertThrows(RuntimeException.class, () -> { + Server server + = new ServerBuilder().service("/", (ctx, res) -> HttpResponse.of("Hello world")) + .tls(ssc.certificate(), ssc.privateKey(), sslContextBuilder -> { + sslContextBuilder.ciphers(ciphers); + }) + .build(); + }); + } +} diff --git a/core/src/test/resources/com/linecorp/armeria/server/keystore.jks b/core/src/test/resources/com/linecorp/armeria/server/keystore.jks new file mode 100644 index 00000000000..51781d1aa79 Binary files /dev/null and b/core/src/test/resources/com/linecorp/armeria/server/keystore.jks differ