Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using null peer host for TLS and disabling host and port inference and SNI altogether. #1561

Merged
merged 7 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,29 @@ public SingleAddressHttpClientBuilder<U, R> appendClientFilter(Predicate<Streami
* @return {@code this}.
*/
public abstract SingleAddressHttpClientBuilder<U, R> sslConfig(ClientSslConfig sslConfig);

/**
* Toggle inference of value to use instead of {@link ClientSslConfig#peerHost()}
* from client's address when peer host is not specified. By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
public abstract SingleAddressHttpClientBuilder<U, R> inferPeerHost(boolean shouldInfer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We previously discussed in #1561 (comment), but doesn't look like we come to a conclusion. Does it make sense to join peer host and port together on ClientSslConfig API into a single method HostAndPort peerHostAndPort()? If yes, we can have a single method here: inferPeerHostAndPort(boolean) instead of two.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Scottmitch wdyt about single ClientSslConfig#peerHostAndPort() and inferPeerHostAndPort(boolean)?

Copy link
Contributor Author

@chemicL chemicL Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my understanding the peer port is not used when peer host is not inferred (and also is null). I think it's reasonable to provide an API where both are affected. And yes, the breaking API change would need to go to ClientSslConfig. Perhaps we could do it in a separate PR and for now add this feature with no breakage?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that if we merge host and port into one method, we can provide only one "infer" method and won't need to change client API later.
Let's make the final decision tomorrow morning. Everything else looks good in the PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @Scottmitch offline. He suggested to keep host and port independent because sometimes users don't know what is the port number (SRV resolutions or use of Unix Domain Sockets).


/**
* Toggle inference of value to use instead of {@link ClientSslConfig#peerPort()}
* from client's address when peer port is not specified (equals {@code -1}). By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
public abstract SingleAddressHttpClientBuilder<U, R> inferPeerPort(boolean shouldInfer);

/**
* Toggle <a href="https://datatracker.ietf.org/doc/html/rfc6066#section-3">SNI</a>
* hostname inference from client's address if not explicitly specified
* via {@link #sslConfig(ClientSslConfig)}. By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
public abstract SingleAddressHttpClientBuilder<U, R> inferSniHostname(boolean shouldInfer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,24 @@ public DefaultSingleAddressHttpClientBuilder<U, R> sslConfig(ClientSslConfig ssl
return this;
}

@Override
public SingleAddressHttpClientBuilder<U, R> inferPeerHost(boolean shouldInfer) {
config.inferPeerHost(shouldInfer);
return this;
}

@Override
public SingleAddressHttpClientBuilder<U, R> inferPeerPort(boolean shouldInfer) {
config.inferPeerPort(shouldInfer);
return this;
}

@Override
public SingleAddressHttpClientBuilder<U, R> inferSniHostname(boolean shouldInfer) {
config.inferSniHostname(shouldInfer);
return this;
}

void appendToStrategyInfluencer(MultiAddressHttpClientFilterFactory<U> multiAddressHttpClientFilterFactory) {
influencerChainBuilder.add(multiAddressHttpClientFilterFactory);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ final class HttpClientConfig {
@Nullable
private String fallbackPeerHost;
private int fallbackPeerPort = -1;
private boolean inferPeerHost = true;
private boolean inferPeerPort = true;
private boolean inferSniHostname = true;

HttpClientConfig() {
tcpConfig = new TcpClientConfig();
Expand All @@ -46,6 +49,9 @@ final class HttpClientConfig {
connectAddress = from.connectAddress;
fallbackPeerHost = from.fallbackPeerHost;
fallbackPeerPort = from.fallbackPeerPort;
inferPeerHost = from.inferPeerHost;
inferPeerPort = from.inferPeerPort;
inferSniHostname = from.inferSniHostname;
}

TcpClientConfig tcpConfig() {
Expand Down Expand Up @@ -73,6 +79,18 @@ void fallbackPeerPort(int fallbackPeerPort) {
this.fallbackPeerPort = fallbackPeerPort;
}

void inferPeerHost(boolean shouldInfer) {
this.inferPeerHost = shouldInfer;
}

void inferPeerPort(boolean shouldInfer) {
this.inferPeerPort = shouldInfer;
}

void inferSniHostname(boolean shouldInfer) {
this.inferSniHostname = shouldInfer;
}

ReadOnlyHttpClientConfig asReadOnly() {
applySslConfigOverrides();
final ReadOnlyHttpClientConfig roConfig = new ReadOnlyHttpClientConfig(this);
Expand All @@ -93,11 +111,17 @@ private void applySslConfigOverrides() {
tcpConfig.sslConfig(new DelegatingClientSslConfig(sslConfig) {
@Nullable
private final List<String> alpnProtocols = httpAlpnProtocols(configAlpn, httpAlpnProtocols);

@Nullable
private final String peerHost = configPeerHost == null ? fallbackPeerHost : configPeerHost;
private final int peerPort = configPeerPort < 0 ? fallbackPeerPort : configPeerPort;
private final String peerHost =
(configPeerHost == null && inferPeerHost) ? fallbackPeerHost : configPeerHost;

private final int peerPort =
(configPeerPort < 0 && inferPeerPort) ? fallbackPeerPort : configPeerPort;

@Nullable
private final String sniHostname = configSni == null ? filterSniName(fallbackPeerHost) : configSni;
private final String sniHostname =
(configSni == null && inferSniHostname) ? filterSniName(fallbackPeerHost) : configSni;

@Override
public List<String> alpnProtocols() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private static void hostToCharSequenceFunction(String hostNamePrefix, String hos
forResolvedAddress(hostNamePrefix + hostName + hostNameSuffix + (port == null ? "" : port),
serverCtx.listenAddress())
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.disableHostnameVerification().build())
.hostnameVerificationAlgorithm("").build())
.buildBlocking()) {
ReservedBlockingHttpConnection conn = client.reserveConnection(client.get("/"));
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import org.junit.jupiter.api.Test;

import java.util.function.Function;
import java.net.InetAddress;
import javax.net.ssl.SSLHandshakeException;

import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname;
Expand All @@ -37,8 +37,10 @@

class SniTest {
private static final String SNI_HOSTNAME = "servicetalk.io";

@Test
void sniSuccess() throws Exception {

try (ServerContext serverContext = HttpServers.forAddress(localAddress(0))
.sslConfig(untrustedServerConfig(), singletonMap(SNI_HOSTNAME, trustedServerConfig()))
.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok());
Expand All @@ -49,15 +51,10 @@ void sniSuccess() throws Exception {

@Test
void sniDefaultFallbackSuccess() throws Exception {
sniDefaultFallbackSuccess(SniTest::newClient);
}

private static void sniDefaultFallbackSuccess(Function<ServerContext, BlockingHttpClient> clientFunc)
throws Exception {
try (ServerContext serverContext = HttpServers.forAddress(localAddress(0))
.sslConfig(trustedServerConfig(), singletonMap("no_match" + SNI_HOSTNAME, untrustedServerConfig()))
.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok());
BlockingHttpClient client = clientFunc.apply(serverContext)) {
BlockingHttpClient client = newClient(serverContext)) {
assertEquals(HttpResponseStatus.OK, client.request(client.get("/")).status());
}
}
Expand Down Expand Up @@ -94,10 +91,37 @@ void sniClientDefaultServerSuccess() throws Exception {

@Test
void noSniClientDefaultServerFallbackSuccess() throws Exception {
sniDefaultFallbackSuccess(serverContext -> HttpClients.forSingleAddress(serverHostAndPort(serverContext))
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()).build())
.buildBlocking());
try (ServerContext serverContext = HttpServers.forAddress(localAddress(0))
.sslConfig(trustedServerConfig(), singletonMap("localhost", untrustedServerConfig()))
.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok());
BlockingHttpClient client = HttpClients.forSingleAddress(
InetAddress.getLoopbackAddress().getHostName(),
serverHostAndPort(serverContext).port())
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()).build())
.inferSniHostname(false)
.buildBlocking()) {
assertEquals(HttpResponseStatus.OK, client.request(client.get("/")).status());
}
}

@Test
void noSniClientDefaultServerFallbackFailExpected() throws Exception {
try (ServerContext serverContext = HttpServers.forAddress(localAddress(0))
.sslConfig(
untrustedServerConfig(),
singletonMap(InetAddress.getLoopbackAddress().getHostName(), trustedServerConfig())
)
.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok());
BlockingHttpClient client = HttpClients.forSingleAddress(
InetAddress.getLoopbackAddress().getHostName(),
serverHostAndPort(serverContext).port()
)
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build())
.inferSniHostname(false)
.buildBlocking()) {
assertThrows(SSLHandshakeException.class, () -> client.request(client.get("/")));
}
}

private static BlockingHttpClient newClient(ServerContext serverContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.servicetalk.http.api.StreamingHttpService;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
import io.servicetalk.transport.api.HostAndPort;
import io.servicetalk.transport.api.ServerContext;
import io.servicetalk.transport.api.ServerSslConfigBuilder;

Expand All @@ -34,6 +35,7 @@
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;

import java.net.InetAddress;
import java.nio.channels.ClosedChannelException;
import java.security.cert.CertificateException;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -174,6 +176,20 @@ void singleAddressClientWithSslToSecureServer() throws Exception {
}
}

@Test
void singleAddressClientWithSslToSecureServerWithoutPeerHost() throws Exception {
assert secureServerCtx != null;
try (BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(secureServerCtx))
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
// if verification is not disabled, identity check fails against the undefined address
.hostnameVerificationAlgorithm("")
.build())
.inferPeerHost(false)
.buildBlocking()) {
testRequestResponse(client, "/", true);
}
}

@Test
void hostNameVerificationIsEnabledByDefault() throws Exception {
assert secureServerCtx != null;
Expand All @@ -188,6 +204,21 @@ void hostNameVerificationIsEnabledByDefault() throws Exception {
}
}

@Test
void hostNameVerificationUsesInferredAddress() throws Exception {
assert secureServerCtx != null;

HostAndPort localAddress = HostAndPort.of(
InetAddress.getLoopbackAddress().getHostName(), serverHostAndPort(secureServerCtx).port()
);

try (BlockingHttpClient client = HttpClients.forSingleAddress(localAddress)
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build())
.buildBlocking()) {
testRequestResponse(client, "/", true);
}
}

@Test
void multiAddressClientWithSslToSecureServer() throws Exception {
try (BlockingHttpClient client = HttpClients.forMultiAddressUrl()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public interface ClientSslConfig extends SslConfig {

/**
* Get the non-authoritative name of the peer.
* @return the non-authoritative name of the peer, or {@code null} if unavailable (which may disable
* {@link #hostnameVerificationAlgorithm() hostname verification} and
* @return the non-authoritative name of the peer, or {@code null} if unavailable (which may require disabling
* {@link #hostnameVerificationAlgorithm() hostname verification}, and may disable
* <a href="https://tools.ietf.org/html/rfc5077">session resumption</a>).
* @see SSLEngine#getPeerHost()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public ClientSslConfigBuilder(Supplier<InputStream> trustCertChainSupplier) {
*
* @param algorithm The algorithm to use when verifying the host name.
* See <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#jssenames">
* Endpoint Identification Algorithm Name</a>
* Endpoint Identification Algorithm Name</a>.
* An empty {@code String} ({@code ""}) disables hostname verification.
* @return {@code this}.
* @see SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
Expand All @@ -89,6 +90,7 @@ public ClientSslConfigBuilder hostnameVerificationAlgorithm(String algorithm) {
* @deprecated Disabling hostname verification may leave you vulnerable to man-in-the-middle attacks. See
* <a href="https://tools.ietf.org/search/rfc2818#section-3.1">server identity</a> on the risks of disabling.
* If the expected value isn't automatically inferred use {@link #peerHost(String)} to set the expected value.
* When disabling is intended, use {@link #hostnameVerificationAlgorithm} with {@code ""} as argument.
* @return {@code this}.
* @see SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
Expand All @@ -104,10 +106,7 @@ public ClientSslConfigBuilder disableHostnameVerification() {
* @return {@code this}.
* @see SSLEngine#getPeerHost()
*/
public ClientSslConfigBuilder peerHost(String peerHost) {
if (peerHost.isEmpty()) {
throw new IllegalArgumentException("peerHost cannot be empty");
}
public ClientSslConfigBuilder peerHost(@Nullable String peerHost) {
this.peerHost = peerHost;
return this;
}
Expand Down Expand Up @@ -166,8 +165,10 @@ private static final class DefaultClientSslConfig extends AbstractSslConfig impl
@Nullable
private final String sniHostname;

DefaultClientSslConfig(@Nullable final String hostnameVerificationAlgorithm, @Nullable final String peerHost,
final int peerPort, @Nullable final String sniHostname,
DefaultClientSslConfig(@Nullable final String hostnameVerificationAlgorithm,
@Nullable final String peerHost,
final int peerPort,
@Nullable final String sniHostname,
@Nullable final TrustManagerFactory trustManagerFactory,
@Nullable final Supplier<InputStream> trustCertChainSupplier,
@Nullable final KeyManagerFactory keyManagerFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public ClientSslConfig asSslConfig() {
}

if (hostnameVerificationAlgorithm == null) {
builder.disableHostnameVerification();
builder.hostnameVerificationAlgorithm("");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add at the end of @deprecated description of ClientSslConfigBuilder#disableHostnameVerification() that if users are sure they need to disable it, they can use hostnameVerificationAlgorithm("").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} else {
builder.hostnameVerificationAlgorithm(hostnameVerificationAlgorithm);
}
Expand Down