Skip to content

Commit

Permalink
Remove aliases, improve tests and add scripts
Browse files Browse the repository at this point in the history
- Remove connectivity settings alias
- Change type of TlsCaFile
- Add custom exception
- Cleanup tests
- Add gencert script for Windows, MacOS and WSL
- Add handler in ChannelFactory
  • Loading branch information
w1am committed Feb 7, 2024
1 parent fc9eec5 commit a248e17
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 60 deletions.
21 changes: 21 additions & 0 deletions gencert.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Write-Host ">> Generating certificate..."

# Create directory if it doesn't exist
New-Item -ItemType Directory -Path .\certs -Force

# Set permissions for the directory
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"

# Pull the Docker image
docker pull eventstore/es-gencert-cli:1.0.2

# Create CA certificate
docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca

# Create node certificate
docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost

# Set permissions recursively for the directory
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"

Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root
23 changes: 22 additions & 1 deletion gencert.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
#!/usr/bin/env bash

unameOutput="$(uname -sr)"
case "${unameOutput}" in
Linux*Microsoft*) machine=WSL;;
Linux*) machine=Linux;;
Darwin*) machine=MacOS;;
*) machine="${unameOutput}"
esac

echo ">> Generating certificate..."
mkdir -p certs

chmod 0755 ./certs
Expand All @@ -12,6 +21,18 @@ docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-

chmod -R 0755 ./certs

echo ">> Copying certificate..."
cp certs/ca/ca.crt /usr/local/share/ca-certificates/eventstore_ca.crt

sudo update-ca-certificates
if [ "${machine}" == "MacOS" ]; then
echo ">> Installing certificate on ${machine}..."
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/share/ca-certificates/eventstore_ca.crt
elif [ "$(machine)" == "Linux" ]; then
echo ">> Installing certificate on ${machine}..."
sudo update-ca-certificates
elif [ "$(machine)" == "WSL" ]; then
echo ">> Installing certificate on ${machine}..."
sudo update-ca-certificates
else
echo ">> Unknown platform. Please install the certificate manually."
fi
16 changes: 14 additions & 2 deletions src/EventStore.Client/ChannelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint
DisposeHttpClient = true,
MaxReceiveMessageSize = MaxReceiveMessageLength
});

HttpMessageHandler CreateHandler() {
if (settings.CreateHttpMessageHandler != null) {
return settings.CreateHttpMessageHandler.Invoke();
Expand All @@ -42,9 +42,17 @@ HttpMessageHandler CreateHandler() {
EnableMultipleHttp2Connections = true,
};

var sslOptions = new SslClientAuthenticationOptions();

if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
sslOptions.ClientCertificates?.Add(settings.ConnectivitySettings.TlsCaFile);
}

if (!settings.ConnectivitySettings.TlsVerifyCert) {
handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
sslOptions.RemoteCertificateValidationCallback = delegate { return true; };
}

handler.SslOptions = sslOptions;
#else
var handler = new WinHttpHandler {
TcpKeepAliveEnabled = true,
Expand All @@ -53,6 +61,10 @@ HttpMessageHandler CreateHandler() {
EnableMultipleHttp2Connections = true
};

if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile);
}

if (!settings.ConnectivitySettings.TlsVerifyCert) {
handler.ServerCertificateValidationCallback = delegate { return true; };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;

namespace EventStore.Client {
/// <summary>
Expand Down Expand Up @@ -111,7 +112,7 @@ public bool Insecure {
/// Path to a certificate file for secure connection. Not required for enabling secure connection. Useful for self-signed certificate
/// that are not installed on the system trust store.
/// </summary>
public string? TlsCaFile { get; set; }
public X509Certificate2? TlsCaFile { get; set; }

/// <summary>
/// The default <see cref="EventStoreClientConnectivitySettings"/>.
Expand Down
33 changes: 13 additions & 20 deletions src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,17 @@ private static EventStoreClientSettings CreateSettings(
if (typedOptions.TryGetValue(ConnectionName, out object? connectionName))
settings.ConnectionName = (string)connectionName;

var connSettings = settings.ConnectivitySettings;

if (typedOptions.TryGetValue(MaxDiscoverAttempts, out object? maxDiscoverAttempts))
connSettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;
settings.ConnectivitySettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;

if (typedOptions.TryGetValue(DiscoveryInterval, out object? discoveryInterval))
connSettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval);
settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval);

if (typedOptions.TryGetValue(GossipTimeout, out object? gossipTimeout))
connSettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout);
settings.ConnectivitySettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout);

if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) {
connSettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch {
settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch {
"leader" => EventStore.Client.NodePreference.Leader,
"follower" => EventStore.Client.NodePreference.Follower,
"random" => EventStore.Client.NodePreference.Random,
Expand Down Expand Up @@ -203,17 +201,17 @@ private static EventStoreClientSettings CreateSettings(
};
}

connSettings.Insecure = !useTls;
settings.ConnectivitySettings.Insecure = !useTls;

if (hosts.Length == 1 && scheme != UriSchemeDiscover) {
connSettings.Address = hosts[0].ToUri(useTls);
settings.ConnectivitySettings.Address = hosts[0].ToUri(useTls);
}
else {
if (hosts.Any(x => x is DnsEndPoint))
connSettings.DnsGossipSeeds =
settings.ConnectivitySettings.DnsGossipSeeds =
Array.ConvertAll(hosts, x => new DnsEndPoint(x.GetHost(), x.GetPort()));
else
connSettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x);
settings.ConnectivitySettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x);
}

if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert)) {
Expand All @@ -223,14 +221,13 @@ private static EventStoreClientSettings CreateSettings(
if (typedOptions.TryGetValue(TlsCaFile, out var tlsCaFile)) {
var tlsCaFilePath = Path.GetFullPath((string)tlsCaFile);
if (!string.IsNullOrEmpty(tlsCaFilePath) && !File.Exists(tlsCaFilePath)) {
throw new FileNotFoundException($"Failed to load certificate. File was not found.");
throw new InvalidClientCertificateException($"Failed to load certificate. File was not found.");
}

try {
using var x509Certificate2 = new X509Certificate2(tlsCaFilePath);
settings.ConnectivitySettings.TlsCaFile = tlsCaFilePath;
settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath);
} catch (CryptographicException) {
throw new Exception("Failed to load certificate. Invalid file format.");
throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format.");
}
}

Expand All @@ -249,9 +246,7 @@ HttpMessageHandler CreateDefaultHandler() {
var sslOptions = new SslClientAuthenticationOptions();

if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
sslOptions.ClientCertificates = new X509Certificate2Collection {
new X509Certificate2(settings.ConnectivitySettings.TlsCaFile)
};
sslOptions.ClientCertificates?.Add(settings.ConnectivitySettings.TlsCaFile);
}

if (!settings.ConnectivitySettings.TlsVerifyCert) {
Expand All @@ -267,10 +262,8 @@ HttpMessageHandler CreateDefaultHandler() {
EnableMultipleHttp2Connections = true
};


if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
var clientCertificate = new X509Certificate2(settings.ConnectivitySettings.TlsCaFile);
handler.ClientCertificates.Add(clientCertificate);
handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile);
}

if (!settings.ConnectivitySettings.TlsVerifyCert) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace EventStore.Client {
/// <summary>
/// The exception that is thrown when a certificate is invalid or not found in the EventStoreDB connection string.
/// </summary>
public class InvalidClientCertificateException : ConnectionStringParseException {
/// <summary>
/// Constructs a new <see cref="InvalidClientCertificateException"/>.
/// </summary>
/// <param name="message"></param>
public InvalidClientCertificateException(string message)
: base(message) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ public class append_to_stream_with_tls_ca_file(ITestOutputHelper output, EventSt
: EventStoreTests<EventStoreFixture>(output, fixture) {
public static IEnumerable<object[]> CertPaths =>
new List<object[]> {
// relative
new object[] { Path.Combine("certs", "ca", "ca.crt") },

// absolute
new object[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt") },
};

Expand Down
48 changes: 15 additions & 33 deletions test/EventStore.Client.Tests/ConnectionStringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http;
using AutoFixture;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;

namespace EventStore.Client.Tests;

Expand All @@ -18,6 +19,8 @@ public class ConnectionStringTests {
)
);

fixture.Register<X509Certificate2>(() => null!);

return Enumerable.Range(0, 3).SelectMany(GetTestCases);

IEnumerable<object?[]> GetTestCases(int _) {
Expand Down Expand Up @@ -91,6 +94,11 @@ public class ConnectionStringTests {
static string MockingTone(string key) => new(key.Select((c, i) => i % 2 == 0 ? char.ToUpper(c) : char.ToLower(c)).ToArray());
}

public static IEnumerable<object?[]> InvalidClientCertificates() {
yield return new object?[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found") };
yield return new object?[] { Assembly.GetExecutingAssembly().Location };
}

[Theory]
[MemberData(nameof(ValidCases))]
public void valid_connection_string(string connectionString, EventStoreClientSettings expected) {
Expand Down Expand Up @@ -145,40 +153,14 @@ public void tls_verify_cert(bool tlsVerifyCert) {

#endif

[Fact]
public void should_throw_error_when_tls_ca_file_does_not_exists() {
var certificateFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found");

var connectionString = $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}";

var exception = Assert.ThrowsAsync<FileNotFoundException>(
() => {
EventStoreClientSettings.Create(connectionString);
return Task.CompletedTask;
}
);

Assert.NotNull(exception);
Assert.Equal("Failed to load certificate. File was not found.", exception.Result.Message);
}

[Fact]
public void should_throw_exception_when_wrong_format_is_used_for_certificate_file_in_connection_string() {
// We are using a file that is not a certificate.
string certificateFilePath = Assembly.GetExecutingAssembly().Location;

var connectionString =
$"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}";

var exception = Assert.ThrowsAsync<Exception>(
() => {
EventStoreClientSettings.Create(connectionString);
return Task.CompletedTask;
}
[Theory]
[MemberData(nameof(InvalidClientCertificates))]
public void connection_string_with_invalid_client_certificate_should_throw(string clientCertificatePath) {
Assert.Throws<InvalidClientCertificateException >(
() => EventStoreClientSettings.Create(
$"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}"
)
);

Assert.NotNull(exception);
Assert.Equal("Failed to load certificate. Invalid file format.", exception.Result.Message);
}

[Fact]
Expand Down

0 comments on commit a248e17

Please sign in to comment.