Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions docs/TLS_CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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()`

Expand Down Expand Up @@ -43,19 +43,29 @@ 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

`opentls` is never selected unless asked for, by one of three routes: the `opentls()` builder method, the `io.nats.client.opentls=true` property, or an `opentls://` scheme on a bootstrap server. The third route means a configuration value alone can select it; review server URLs that come from configuration with that in mind. A hand-supplied context is selected by `sslContext(ctx)` and is used whatever the flags and schemes say.

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`:

Expand All @@ -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`.
27 changes: 18 additions & 9 deletions src/main/java/io/nats/client/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
* <p>
* 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.
* <p>
* 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;
}

Expand Down Expand Up @@ -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() {
Expand Down
9 changes: 5 additions & 4 deletions src/test/java/io/nats/client/OptionsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
21 changes: 16 additions & 5 deletions src/test/java/io/nats/client/impl/ReconnectTests.java
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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();
Expand All @@ -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);
Expand Down
Loading
Loading