Skip to content

Commit

Permalink
Client TLS tests (#204)
Browse files Browse the repository at this point in the history
* TLS client verify tests

Tests to make sure TLS client certificates works.

* Also allow TLS 1.3
* Tweak to how client certificates taken in for Windows
* Format fixes

* Security docs

* Debugging CI

* Test workaround

* Reverted CI debugging changes
  • Loading branch information
mtmk authored Nov 13, 2023
1 parent c2ef9e9 commit 0fbb4e7
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 11 deletions.
68 changes: 68 additions & 0 deletions docs/documentation/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Security

NATS has a lot of [security features](https://docs.nats.io/nats-concepts/security) and .NET V2 client supports them all.
All you need to do is to pass your credentials to the connection.

```csharp
var opts = NatsOpts.Default with
{
AuthOpts = NatsAuthOpts.Default with
{
Username = "bob",
Password = "s3cr3t",
},
};

await using var nats = new NatsConnection(opts);
```

See also [user authentication tests](https://github.com/nats-io/nats.net.v2/blob/main/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs) for more examples.

## Implicit TLS Connections

As of NATS server version 2.10.4 and later, the server supports implicit TLS connections.
This means that the client can connect to the server using the default port of 4222 and the server will automatically upgrade the connection to TLS.
This is useful for environments where TLS is required by default.

```csharp
var opts = NatsOpts.Default with
{
TlsOpts = new NatsTlsOpts
{
Mode = TlsMode.Implicit,
},
};

await using var nats = new NatsConnection(opts);
```

## Mutual TLS Connections

The [server can require TLS certificates from a client](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/tls_mutual_auth) to validate
the client certificate matches a known or trusted CA and to provide authentication.

You can set the TLS options to use your client certificates when connecting to a server which requires TLS Mutual authentication.

```csharp
var opts = NatsOpts.Default with
{
TlsOpts = new NatsTlsOpts
{
CertFile = "path/to/cert.pem",
KeyFile = "path/to/key.pem",
CaFile = "path/to/ca.pem",
},
};

await using var nats = new NatsConnection(opts);
```

### Intermediate CA Certificates

When connecting using intermediate CA certificates, it might noy be possible to validate the client certificate and the TLS handshake may fail.

Unfortunately, for .NET client applications it isn't possible to pass additional intermediate certificates and the only
solution is to add the certificates to the certificate store manually.

See also:
https://learn.microsoft.com/en-us/dotnet/core/extensions/sslstream-troubleshooting#intermediate-certificates-are-not-sent
3 changes: 3 additions & 0 deletions docs/documentation/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@
- name: Serialization
href: serialization.md

- name: Security
href: security.md

- name: Updating Documentation
href: update-docs.md
2 changes: 1 addition & 1 deletion src/NATS.Client.Core/Internal/SslStreamConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private SslClientAuthenticationOptions SslClientAuthenticationOptions(NatsUri ur
var options = new SslClientAuthenticationOptions
{
TargetHost = uri.Host,
EnabledSslProtocols = SslProtocols.Tls12,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
ClientCertificates = _tlsCerts?.ClientCerts,
LocalCertificateSelectionCallback = lcsCb,
RemoteCertificateValidationCallback = rcsCb,
Expand Down
15 changes: 14 additions & 1 deletion src/NATS.Client.Core/Internal/TlsCerts.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

namespace NATS.Client.Core.Internal;
Expand Down Expand Up @@ -25,7 +26,19 @@ public TlsCerts(NatsTlsOpts tlsOpts)

if (tlsOpts.CertFile != default && tlsOpts.KeyFile != default)
{
ClientCerts = new X509Certificate2Collection(X509Certificate2.CreateFromPemFile(tlsOpts.CertFile, tlsOpts.KeyFile));
var clientCert = X509Certificate2.CreateFromPemFile(tlsOpts.CertFile, tlsOpts.KeyFile);

// On Windows, ephemeral keys/certificates do not work with schannel. e.g. unless stored in certificate store.
// https://github.com/dotnet/runtime/issues/66283#issuecomment-1061014225
// https://github.com/dotnet/runtime/blob/380a4723ea98067c28d54f30e1a652483a6a257a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs#L192-L197
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var ephemeral = clientCert;
clientCert = new X509Certificate2(clientCert.Export(X509ContentType.Pfx));
ephemeral.Dispose();
}

ClientCerts = new X509Certificate2Collection(clientCert);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/NATS.Client.JetStream/INatsJSStream.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NATS.Client.Core;
using NATS.Client.Core;
using NATS.Client.JetStream.Models;

namespace NATS.Client.JetStream;
Expand Down
46 changes: 46 additions & 0 deletions tests/NATS.Client.Core.Tests/TlsClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace NATS.Client.Core.Tests;

public class TlsClientTest
{
private readonly ITestOutputHelper _output;

public TlsClientTest(ITestOutputHelper output) => _output = output;

[Fact]
public async Task Client_connect_using_certificate()
{
await using var server = NatsServer.Start(
new NullOutputHelper(),
new NatsServerOptsBuilder()
.UseTransport(TransportType.Tls, tlsVerify: true)
.Build());

var clientOpts = server.ClientOpts(NatsOpts.Default with { Name = "tls-test-client" });
await using var nats = new NatsConnection(clientOpts);
await nats.ConnectAsync();
var rtt = await nats.PingAsync();
Assert.True(rtt > TimeSpan.Zero);
}

[Fact]
public async Task Client_cannot_connect_without_certificate()
{
await using var server = NatsServer.Start(
new NullOutputHelper(),
new NatsServerOptsBuilder()
.UseTransport(TransportType.Tls, tlsVerify: true)
.Build());

var clientOpts = server.ClientOpts(NatsOpts.Default);
clientOpts = clientOpts with { TlsOpts = clientOpts.TlsOpts with { CertFile = null, KeyFile = null } };
await using var nats = new NatsConnection(clientOpts);

var exceptionTask = Assert.ThrowsAsync<NatsException>(async () => await nats.ConnectAsync());

// TODO: On Linux failed mTLS connection hangs.
// In this scenario _sslStream.AuthenticateAsClientAsync() is not throwing exception on Linux
// which is causing the connection to hang. So if the serer is configured to verify the client
// and the client does not provide a certificate, the connection will hang on Linux.
await Task.WhenAny(exceptionTask, Task.Delay(3000));
}
}
23 changes: 22 additions & 1 deletion tests/NATS.Client.TestUtilities/NatsServerOpts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ public sealed class NatsServerOptsBuilder
private bool _enableWebSocket;
private bool _enableTls;
private bool _tlsFirst;
private bool _tlsVerify;
private bool _enableJetStream;
private string? _serverName;
private string? _tlsServerCertFile;
private string? _tlsServerKeyFile;
private string? _tlsClientCertFile;
private string? _tlsClientKeyFile;
private string? _tlsCaFile;
private TransportType? _transportType;
private bool _serverDisposeReturnsPorts;
Expand All @@ -32,10 +35,13 @@ public sealed class NatsServerOptsBuilder
EnableWebSocket = _enableWebSocket,
EnableTls = _enableTls,
TlsFirst = _tlsFirst,
TlsVerify = _tlsVerify,
EnableJetStream = _enableJetStream,
ServerName = _serverName,
TlsServerCertFile = _tlsServerCertFile,
TlsServerKeyFile = _tlsServerKeyFile,
TlsClientCertFile = _tlsClientCertFile,
TlsClientKeyFile = _tlsClientKeyFile,
TlsCaFile = _tlsCaFile,
ExtraConfigs = _extraConfigs,
TransportType = _transportType ?? TransportType.Tcp,
Expand All @@ -62,7 +68,7 @@ public NatsServerOptsBuilder Trace()
return this;
}

public NatsServerOptsBuilder UseTransport(TransportType transportType, bool tlsFirst = false)
public NatsServerOptsBuilder UseTransport(TransportType transportType, bool tlsFirst = false, bool tlsVerify = false)
{
_transportType = transportType;

Expand All @@ -76,8 +82,16 @@ public NatsServerOptsBuilder UseTransport(TransportType transportType, bool tlsF
_enableTls = true;
_tlsServerCertFile = "resources/certs/server-cert.pem";
_tlsServerKeyFile = "resources/certs/server-key.pem";

if (tlsVerify)
{
_tlsClientCertFile = "resources/certs/client-cert.pem";
_tlsClientKeyFile = "resources/certs/client-key.pem";
}

_tlsCaFile = "resources/certs/ca-cert.pem";
_tlsFirst = tlsFirst;
_tlsVerify = tlsVerify;
}
else if (transportType == TransportType.WebSocket)
{
Expand Down Expand Up @@ -165,6 +179,8 @@ public NatsServerOpts()

public bool TlsFirst { get; init; } = false;

public bool TlsVerify { get; init; } = false;

public TransportType TransportType { get; init; }

public bool Trace { get; init; }
Expand Down Expand Up @@ -233,6 +249,11 @@ public string ConfigFileContents
sb.AppendLine($" handshake_first: true");
}

if (TlsVerify)
{
sb.AppendLine($" verify_and_map: true");
}

sb.AppendLine("}");
}

Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/CmdArgs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace NATS.Client.Testing.Failground;

Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/ConsumeTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.JetStream;
Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/ITest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NATS.Client.Testing.Failground;
namespace NATS.Client.Testing.Failground;

public interface ITest
{
Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/OrderedConsumeTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.JetStream;
Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using NATS.Client.Testing.Failground;

try
Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/PubSubTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;

namespace NATS.Client.Testing.Failground;
Expand Down
2 changes: 1 addition & 1 deletion tests/NATS.Client.Testing.Failground/StayConnectedTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.Testing.Failground;
Expand Down

0 comments on commit 0fbb4e7

Please sign in to comment.