diff --git a/README.md b/README.md index a3bdeaf90..87d1cc2fd 100644 --- a/README.md +++ b/README.md @@ -617,6 +617,7 @@ assertEquals(8000, o.getMaxMessagesInOutgoingQueue()); | tls.algorithm | Property for the algorithm used to create an SSLContext | | credential.path | Property used to set the path to a credentials file to be used in a FileAuthHandler | | tls.first | Property used to set TLS Handshake First behavior | +| tls.verify.hostname | Property used to set tls hostname verification, on by default; false turns it off | | use.timeout.exception | Instruct the client to throw TimeoutException instead of CancellationException | | advanced.request.behavior | Surface a specific RequestFailureException (reason, status, last error) on no reply | | use.dispatcher.with.executor | Instruct dispatchers to dispatch all messages as a task | @@ -1116,6 +1117,8 @@ NATS supports TLS 1.2. The server can be configured to verify client certificate Connection nc = Nats.connect(options); ``` +In every case the client checks that the certificate the server presents is issued for the server name the connection was made with: the hostname in the url, or the ip address when the url has one, which the certificate must then carry as a subject alternative name. This is on by default, including with `opentls`. `tlsVerifyHostname(false)` on the builder, or the `io.nats.client.tls.verify.hostname=false` property, turns it off; see [TLS configuration](docs/TLS_CONFIGURATION.md). + If you want to try out these techniques, take a look at the [README.md](src/examples/java/io/nats/examples/README.md) for instructions. Also, here are some places in the code that may help diff --git a/docs/TLS_CONFIGURATION.md b/docs/TLS_CONFIGURATION.md index 9a62726f3..d81d14f39 100644 --- a/docs/TLS_CONFIGURATION.md +++ b/docs/TLS_CONFIGURATION.md @@ -1,6 +1,6 @@ # TLS configuration -How the client decides whether a connection uses TLS, which `SSLContext` it uses, what `opentls` does and does not do, and the rule for server lists that mix schemes. This is the detail behind the SSLContext and Connection Security sections of the [README](../README.md). +How the client decides whether a connection uses TLS, which `SSLContext` it uses, what `opentls` does and does not do, how the server's name is checked against its certificate, and the rule for server lists that mix schemes. This is the detail behind the SSLContext and Connection Security sections of the [README](../README.md). ## 1. One SSLContext per connection @@ -15,7 +15,7 @@ The context is accepted or created once, when `Options.Builder.build()` runs, in 5. `secure()` was called, the `io.nats.client.secure` property is true, or any bootstrap server has the `tls://` or `wss://` scheme: `SSLContext.getDefault()`, described in section 2. 6. Otherwise no context, and the connection is plain TCP. A server that requires TLS then fails the connect with "SSL required by server." -Rules 4 and 5 look at the bootstrap servers only when neither `opentls` nor `secure` was set explicitly. Asking for both is an error, section 4. +Rules 4 and 5 look at the bootstrap servers only when neither `opentls` nor `secure` was set explicitly. Asking for both is an error, section 5. ## 2. The default context: `tls://`, `wss://`, `secure()` @@ -43,11 +43,11 @@ With a trust-all context, anything on the network path that can answer the TCP c The client cannot notice. The handshake succeeded, the connection reports as secure, and no error, event or log line says that the certificate was not checked. This is the same for the equivalent switch in every other client, which is why Go's documentation says `InsecureSkipVerify` "should NOT be used in a production setting". -Separately from the chain check, the client does not check the server's hostname against the certificate in any configuration. With the default context or a truststore, the chain check still ties the certificate to an issuer you trust. A trust-all context removes that last check too, so no property of the certificate is tested at all. +Separately from the chain check, the client checks the server's name against the certificate, section 4, and does so under a trust-all context too. That check only proves that the certificate names the host, and with a trust-all context anyone can present a certificate that names the host, so it does not restore the proof the chain check gave. With `tlsVerifyHostname(false)` as well, no property of the certificate is tested at all. ### When it is acceptable, and what to use otherwise -`opentls` and a hand-supplied trust-all context are for development and tests, where the server is on the developer's own machine or the certificate is known to be throwaway. Outside that, do not use either. For a server with a private CA, put the CA in the JVM trust store, or in a truststore given to the client through the `truststore` properties, or build an `SSLContext` from it; sections 2 and 5. The effort is one `keytool` import, and the connection then proves who it is talking to. +`opentls` and a hand-supplied trust-all context are for development and tests, where the server is on the developer's own machine or the certificate is known to be throwaway. Outside that, do not use either. For a server with a private CA, put the CA in the JVM trust store, or in a truststore given to the client through the `truststore` properties, or build an `SSLContext` from it; sections 2 and 6. The effort is one `keytool` import, and the connection then proves who it is talking to. ### How it is selected @@ -55,7 +55,17 @@ Separately from the chain check, the client does not check the server's hostname Because the context is per connection, either form applies to every server the connection reaches, including discovered ones. -## 4. Asking for both contexts is rejected +## 4. Hostname verification + +After the chain check, the client checks that the certificate is issued for the server name the connection was made with. The check is the one the JDK performs for HTTPS: the name is matched against the certificate's subject alternative names, a DNS entry for a hostname, an ip address entry for an ip literal. It is on by default and applies under every context, including the trust-all context of section 3. + +The name checked is the hostname in the server url, in every `HostnameResolveMode`, including the modes that resolve the hostname to ip addresses before connecting. The same name is sent as the TLS server name (SNI), so a server or proxy that selects its certificate by name selects the right one. A server given by ip address in the url is checked as an ip address, so its certificate must carry that address as a subject alternative name. A server discovered from `connect_urls` as a bare ip address is checked against the hostname of the server that supplied it, when that server was configured by hostname, as nats.go does; otherwise it is checked as an ip address. + +The check is performed by the trust manager in use. The JDK performs it for its own trust managers, which the default context and a context built from a keystore and truststore have, and for any plain `X509TrustManager`, which it wraps; the trust-all manager of `opentls` is one of those. A custom `X509ExtendedTrustManager` is responsible for its own identity check, so a context built with one is only checked if the manager does it. + +A connection to a server whose certificate does not name it fails with a `CertificateException` that names the expected host. Fix it by reissuing the certificate for the name, or by using the name the certificate carries in the url. `tlsVerifyHostname(false)` on the builder, or the `io.nats.client.tls.verify.hostname=false` property, turns the check off; that removes the proof that the server is the one named in the url, section 3, so keep it for development against a certificate that is known not to name the server. + +## 5. Asking for both contexts is rejected Options that ask for the default context and the trust-all context at the same time are rejected when they are built, with `IllegalStateException`: @@ -69,16 +79,16 @@ One explicit choice is not a conflict: `opentls()` with `tls://` servers builds To fix rejected options, choose one of the two, or supply the context you want with `sslContext(ctx)`. -## 5. Client certificates +## 6. Client certificates A server configured with `verify: true` requires a client certificate. Supply it through the `keystore` and `keystorePassword` properties together with `truststore` and `truststorePassword`, through an `SSLContextFactory`, through an `SSLContext` built with key managers, or through the `javax.net.ssl.keyStore` system properties when using the default context. `opentls` presents no client certificate and cannot be used against such a server. The README's TLS Certs section shows how the test keystore and truststore are produced from the PEM files in `src/test/resources/certs`. -## 6. TLS handshake first +## 7. TLS handshake first `tlsFirst()` performs the TLS handshake before reading the server's INFO, for servers configured with `handshake_first`. It requires a context: building options with `tlsFirst()` and no context from the rules above fails with "SSL context required for tls handshake first". -## 7. What the server and client expect of each other +## 8. What the server and client expect of each other Whether the client attempts the TLS upgrade is decided by whether it has a context; whether the server requires or offers TLS comes from its INFO. The README's "TLS client versus server checks" table lists the six combinations and the two that fail with an `IOException`. diff --git a/src/main/java/io/nats/client/Options.java b/src/main/java/io/nats/client/Options.java index 5c52410e1..4e249a524 100644 --- a/src/main/java/io/nats/client/Options.java +++ b/src/main/java/io/nats/client/Options.java @@ -712,10 +712,10 @@ public static HostnameResolveMode get(String value) { */ public static final String PROP_TLS_FIRST = PFX + "tls.first"; /** - * Property used to configure tls hostname verification. + * Property used to configure tls hostname verification, which is on by default. * This property is a boolean flag, telling connections whether * to verify that the certificate the server presents is issued for the server name, - * see {@link Builder#tlsVerifyHostname() tlsVerifyHostname}. + * see {@link Builder#tlsVerifyHostname(boolean) tlsVerifyHostname}. */ public static final String PROP_TLS_VERIFY_HOSTNAME = PFX + "tls.verify.hostname"; /** @@ -1103,7 +1103,7 @@ public static class Builder { private boolean discardMessagesWhenOutgoingQueueFull = DEFAULT_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL; private boolean ignoreDiscoveredServers = false; private boolean tlsFirst = false; - private boolean tlsVerifyHostname = false; + private boolean tlsVerifyHostname = true; private boolean useTimeoutException = false; private boolean advancedRequestBehavior = false; private boolean useDispatcherWithExecutor = false; @@ -2314,22 +2314,31 @@ public Builder tlsFirst() { } /** - * Set TLS hostname verification on. Default is off. + * Set whether to verify that the certificate the server presents is issued for the server name. + * On by default; pass false to turn it off. + *
* When on, the certificate the server presents must be issued for the server name the * connection was made with: the configured hostname, in every {@link HostnameResolveMode}, - * or the ip address when the server was configured or discovered by ip address. + * or the ip address when the server was configured by ip address, in which case the + * certificate must carry that address as a subject alternative name. * The check is the one the JDK performs for HTTPS, against the certificate's * subject alternative names. A server discovered from connect_urls as a bare ip address - * is checked as an ip address, so its certificate must carry that address. + * is checked against the hostname of the server that supplied it, when that server was + * configured by hostname; otherwise it is checked as an ip address. * The check is performed by the trust manager in use. The JDK performs it for its own * trust managers, which an SSLContext built from a keystore and truststore or the default * SSLContext has, and for any plain X509TrustManager, which it wraps, including the * trust-all manager of {@link #opentls() opentls}. A custom X509ExtendedTrustManager * is responsible for its own identity check. + *
+ * Turning it off removes the proof that the server is the one named in the url. + * Do that only where the certificate is known not to name the server, such as development + * against a self-signed certificate, and prefer reissuing the certificate for the name. + * @param tlsVerifyHostname true to verify, false to skip the check * @return the Builder for chaining */ - public Builder tlsVerifyHostname() { - this.tlsVerifyHostname = true; + public Builder tlsVerifyHostname(boolean tlsVerifyHostname) { + this.tlsVerifyHostname = tlsVerifyHostname; return this; } @@ -3546,7 +3555,7 @@ public boolean isTlsFirst() { /** * Get whether to verify that the certificate the server presents is issued for the server name, - * see {@link Builder#tlsVerifyHostname() tlsVerifyHostname()} in the builder doc + * which is on by default, see {@link Builder#tlsVerifyHostname(boolean) tlsVerifyHostname(boolean)} in the builder doc * @return the flag */ public boolean isTlsVerifyHostname() { diff --git a/src/test/java/io/nats/client/OptionsTests.java b/src/test/java/io/nats/client/OptionsTests.java index 28301131f..7dde2b7fd 100644 --- a/src/test/java/io/nats/client/OptionsTests.java +++ b/src/test/java/io/nats/client/OptionsTests.java @@ -1635,10 +1635,11 @@ public void testHostnameResolveMode() { @Test public void testTlsVerifyHostname() { - assertFalse(new Options.Builder().build().isTlsVerifyHostname()); - Options options = new Options.Builder().tlsVerifyHostname().build(); - assertTrue(options.isTlsVerifyHostname()); - assertTrue(new Options.Builder(options).build().isTlsVerifyHostname()); + assertTrue(new Options.Builder().build().isTlsVerifyHostname()); + assertTrue(new Options.Builder().tlsVerifyHostname(true).build().isTlsVerifyHostname()); + Options options = new Options.Builder().tlsVerifyHostname(false).build(); + assertFalse(options.isTlsVerifyHostname()); + assertFalse(new Options.Builder(options).build().isTlsVerifyHostname()); Properties props = new Properties(); props.setProperty(Options.PROP_TLS_VERIFY_HOSTNAME, "true"); assertTrue(new Options.Builder(props).build().isTlsVerifyHostname()); diff --git a/src/test/java/io/nats/client/impl/ReconnectTests.java b/src/test/java/io/nats/client/impl/ReconnectTests.java index 312ff0a9b..7c58b5496 100644 --- a/src/test/java/io/nats/client/impl/ReconnectTests.java +++ b/src/test/java/io/nats/client/impl/ReconnectTests.java @@ -1,4 +1,4 @@ -// Copyright 2015-2018 The NATS Authors +// Copyright 2015-2026 The NATS 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: @@ -13,6 +13,7 @@ package io.nats.client.impl; +import io.nats.NatsRunnerUtils; import io.nats.NatsServerRunner; import io.nats.client.*; import io.nats.client.ConnectionListener.Events; @@ -553,13 +554,18 @@ public void testReconnectNoIPTLSConnection() throws Exception { "}" }; - // Regular tls for first connection, then no ip for second + // Both servers present the certificate that names localhost and carries no ip address. Connect by that + // name; the servers gossip each other by ip, and the reconnect to the gossiped ip is verified against + // the name of the server that gossiped it. try ( NatsTestServer ts = new NatsTestServer("src/test/resources/tls_noip.conf", tsInserts, tsPort, false); NatsTestServer ts2 = new NatsTestServer("src/test/resources/tls_noip.conf", ts2Inserts, ts2Port, false) ) { SslTestingHelper.setKeystoreSystemParameters(); + // the host must be the name localhost: the localhost uri helpers give 127.0.0.1 on Java 9 and later, + // which the certificate does not name + String tsUri = NatsRunnerUtils.getUri("nats", "localhost", tsPort); Options options = new Options.Builder() - .server(ts.getURI()) + .server(tsUri) .secure() .connectionListener(listener) .maxReconnects(20) // we get multiples for some, so need enough @@ -570,7 +576,7 @@ public void testReconnectNoIPTLSConnection() throws Exception { listener.prepForStatusChange(Events.DISCOVERED_SERVERS); nc = (NatsConnection) longConnectionWait(options); - assertEquals(nc.getConnectedUrl(), ts.getURI()); + assertEquals(nc.getConnectedUrl(), tsUri); flushAndWaitLong(nc, listener); // make sure we get the new server via info @@ -592,7 +598,9 @@ public void testURISchemeNoIPTLSConnection() throws Exception { SslTestingHelper.setKeystoreSystemParameters(); try (NatsTestServer ts = new NatsTestServer("src/test/resources/tls_noip.conf", false)) { Options options = new Options.Builder() - .server(ts.getLocalhostUri("tls")) + // the certificate names localhost only, so connect by that name; the default verification checks it. + // the host must be the name localhost: the localhost uri helpers give 127.0.0.1 on Java 9 and later + .server(NatsRunnerUtils.getUri("tls", "localhost", ts.getNatsPort())) .connectionTimeout(Duration.ofSeconds(5)) .maxReconnects(0) .build(); @@ -607,6 +615,9 @@ public void testURISchemeNoIPOpenTLSConnection() throws Exception { try (NatsTestServer ts = new NatsTestServer("src/test/resources/tls_noip.conf", false)) { Options options = new Options.Builder() .server(ts.getLocalhostUri("opentls")) + // the certificate names localhost only and the url is the ip. opentls trusts any chain, + // but the name is still checked by default, so the check is turned off here. + .tlsVerifyHostname(false) .maxReconnects(0) .build(); assertCanConnect(options); diff --git a/src/test/java/io/nats/client/impl/TlsHostnameTests.java b/src/test/java/io/nats/client/impl/TlsHostnameTests.java index 4fbb77a22..9b8aa5a26 100644 --- a/src/test/java/io/nats/client/impl/TlsHostnameTests.java +++ b/src/test/java/io/nats/client/impl/TlsHostnameTests.java @@ -73,8 +73,8 @@ * Each execution also checks whether the server pool was asked to resolve, which only the resolving modes do. *
* A server discovered from connect_urls as a bare ip address presents the hostname of the server that
- * supplied it, as nats.go does; and with {@link Options.Builder#tlsVerifyHostname()} the certificate is
- * checked against the server name.
+ * supplied it, as nats.go does. Hostname verification is on by default, so the certificate is checked
+ * against the server name; {@link Options.Builder#tlsVerifyHostname(boolean) tlsVerifyHostname(false)} turns it off.
*/
public class TlsHostnameTests {
private static final String SINGLE_LABEL_HOST = "nats";
@@ -384,7 +384,9 @@ private static Options.Builder reconnecting(SniTestServer first, DiagnosticSslCo
@ValueSource(booleans = {false, true})
public void discoveredIpRetainsHostnameOnReconnect(boolean tlsFirst) throws Exception {
// the first server gossips the second as a bare ip; after the first closes, the reconnect
- // to the bare ip must present the first server's hostname
+ // to the bare ip must present the first server's hostname. Verification is on by default and the
+ // certificate has a DNS name and no ip SAN, so the reconnect also only passes because it is
+ // verified against that hostname
DiagnosticSslContext context = dottedCertificates.clientContext();
CountDownLatch reconnected = new CountDownLatch(1);
try (SniTestServer second = new SniTestServer(dottedCertificates, tlsFirst, 0);
@@ -406,14 +408,13 @@ public void discoveredIpRetainsHostnameOnReconnect(boolean tlsFirst) throws Exce
}
@Test
- public void discoveredIpIsVerifiedByTheGossipingServersName() throws Exception {
- // with verification on, the certificate of the discovered bare ip has a DNS name and no ip SAN,
- // so the reconnect only passes because it is verified against the gossiping server's hostname
+ public void discoveredIpRetainsHostnameWithVerificationOff() throws Exception {
+ // with verification off, SNI alone carries the gossiping server's hostname to the discovered bare ip
DiagnosticSslContext context = dottedCertificates.clientContext();
CountDownLatch reconnected = new CountDownLatch(1);
try (SniTestServer second = new SniTestServer(dottedCertificates, false, 0);
SniTestServer first = new SniTestServer(dottedCertificates, false, second.port());
- Connection nc = Nats.connect(reconnecting(first, context, false, reconnected).tlsVerifyHostname().build())) {
+ Connection nc = Nats.connect(reconnecting(first, context, false, reconnected).tlsVerifyHostname(false).build())) {
nc.flush(Duration.ofSeconds(2));
first.close();
assertTrue(reconnected.await(10, TimeUnit.SECONDS), "Did not reconnect to the discovered ip");
@@ -518,7 +519,6 @@ public List