Skip to content

Commit a248e17

Browse files
committed
Remove aliases, improve tests and add scripts
- 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
1 parent fc9eec5 commit a248e17

File tree

8 files changed

+100
-60
lines changed

8 files changed

+100
-60
lines changed

gencert.ps1

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Write-Host ">> Generating certificate..."
2+
3+
# Create directory if it doesn't exist
4+
New-Item -ItemType Directory -Path .\certs -Force
5+
6+
# Set permissions for the directory
7+
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
8+
9+
# Pull the Docker image
10+
docker pull eventstore/es-gencert-cli:1.0.2
11+
12+
# Create CA certificate
13+
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
14+
15+
# Create node certificate
16+
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
17+
18+
# Set permissions recursively for the directory
19+
icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX"
20+
21+
Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root

gencert.sh

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
#!/usr/bin/env bash
22

3+
unameOutput="$(uname -sr)"
4+
case "${unameOutput}" in
5+
Linux*Microsoft*) machine=WSL;;
6+
Linux*) machine=Linux;;
7+
Darwin*) machine=MacOS;;
8+
*) machine="${unameOutput}"
9+
esac
10+
11+
echo ">> Generating certificate..."
312
mkdir -p certs
413

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

1322
chmod -R 0755 ./certs
1423

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

17-
sudo update-ca-certificates
27+
if [ "${machine}" == "MacOS" ]; then
28+
echo ">> Installing certificate on ${machine}..."
29+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/share/ca-certificates/eventstore_ca.crt
30+
elif [ "$(machine)" == "Linux" ]; then
31+
echo ">> Installing certificate on ${machine}..."
32+
sudo update-ca-certificates
33+
elif [ "$(machine)" == "WSL" ]; then
34+
echo ">> Installing certificate on ${machine}..."
35+
sudo update-ca-certificates
36+
else
37+
echo ">> Unknown platform. Please install the certificate manually."
38+
fi

src/EventStore.Client/ChannelFactory.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint
3030
DisposeHttpClient = true,
3131
MaxReceiveMessageSize = MaxReceiveMessageLength
3232
});
33-
33+
3434
HttpMessageHandler CreateHandler() {
3535
if (settings.CreateHttpMessageHandler != null) {
3636
return settings.CreateHttpMessageHandler.Invoke();
@@ -42,9 +42,17 @@ HttpMessageHandler CreateHandler() {
4242
EnableMultipleHttp2Connections = true,
4343
};
4444

45+
var sslOptions = new SslClientAuthenticationOptions();
46+
47+
if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
48+
sslOptions.ClientCertificates?.Add(settings.ConnectivitySettings.TlsCaFile);
49+
}
50+
4551
if (!settings.ConnectivitySettings.TlsVerifyCert) {
46-
handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
52+
sslOptions.RemoteCertificateValidationCallback = delegate { return true; };
4753
}
54+
55+
handler.SslOptions = sslOptions;
4856
#else
4957
var handler = new WinHttpHandler {
5058
TcpKeepAliveEnabled = true,
@@ -53,6 +61,10 @@ HttpMessageHandler CreateHandler() {
5361
EnableMultipleHttp2Connections = true
5462
};
5563

64+
if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
65+
handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile);
66+
}
67+
5668
if (!settings.ConnectivitySettings.TlsVerifyCert) {
5769
handler.ServerCertificateValidationCallback = delegate { return true; };
5870
}

src/EventStore.Client/EventStoreClientConnectivitySettings.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Net;
3+
using System.Security.Cryptography.X509Certificates;
34

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

116117
/// <summary>
117118
/// The default <see cref="EventStoreClientConnectivitySettings"/>.

src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs

+13-20
Original file line numberDiff line numberDiff line change
@@ -155,19 +155,17 @@ private static EventStoreClientSettings CreateSettings(
155155
if (typedOptions.TryGetValue(ConnectionName, out object? connectionName))
156156
settings.ConnectionName = (string)connectionName;
157157

158-
var connSettings = settings.ConnectivitySettings;
159-
160158
if (typedOptions.TryGetValue(MaxDiscoverAttempts, out object? maxDiscoverAttempts))
161-
connSettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;
159+
settings.ConnectivitySettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;
162160

163161
if (typedOptions.TryGetValue(DiscoveryInterval, out object? discoveryInterval))
164-
connSettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval);
162+
settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval);
165163

166164
if (typedOptions.TryGetValue(GossipTimeout, out object? gossipTimeout))
167-
connSettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout);
165+
settings.ConnectivitySettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout);
168166

169167
if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) {
170-
connSettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch {
168+
settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch {
171169
"leader" => EventStore.Client.NodePreference.Leader,
172170
"follower" => EventStore.Client.NodePreference.Follower,
173171
"random" => EventStore.Client.NodePreference.Random,
@@ -203,17 +201,17 @@ private static EventStoreClientSettings CreateSettings(
203201
};
204202
}
205203

206-
connSettings.Insecure = !useTls;
204+
settings.ConnectivitySettings.Insecure = !useTls;
207205

208206
if (hosts.Length == 1 && scheme != UriSchemeDiscover) {
209-
connSettings.Address = hosts[0].ToUri(useTls);
207+
settings.ConnectivitySettings.Address = hosts[0].ToUri(useTls);
210208
}
211209
else {
212210
if (hosts.Any(x => x is DnsEndPoint))
213-
connSettings.DnsGossipSeeds =
211+
settings.ConnectivitySettings.DnsGossipSeeds =
214212
Array.ConvertAll(hosts, x => new DnsEndPoint(x.GetHost(), x.GetPort()));
215213
else
216-
connSettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x);
214+
settings.ConnectivitySettings.IpGossipSeeds = Array.ConvertAll(hosts, x => (IPEndPoint)x);
217215
}
218216

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

229227
try {
230-
using var x509Certificate2 = new X509Certificate2(tlsCaFilePath);
231-
settings.ConnectivitySettings.TlsCaFile = tlsCaFilePath;
228+
settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath);
232229
} catch (CryptographicException) {
233-
throw new Exception("Failed to load certificate. Invalid file format.");
230+
throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format.");
234231
}
235232
}
236233

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

251248
if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
252-
sslOptions.ClientCertificates = new X509Certificate2Collection {
253-
new X509Certificate2(settings.ConnectivitySettings.TlsCaFile)
254-
};
249+
sslOptions.ClientCertificates?.Add(settings.ConnectivitySettings.TlsCaFile);
255250
}
256251

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

270-
271265
if (settings.ConnectivitySettings is { TlsCaFile: not null, Insecure: false }) {
272-
var clientCertificate = new X509Certificate2(settings.ConnectivitySettings.TlsCaFile);
273-
handler.ClientCertificates.Add(clientCertificate);
266+
handler.ClientCertificates.Add(settings.ConnectivitySettings.TlsCaFile);
274267
}
275268

276269
if (!settings.ConnectivitySettings.TlsVerifyCert) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace EventStore.Client {
2+
/// <summary>
3+
/// The exception that is thrown when a certificate is invalid or not found in the EventStoreDB connection string.
4+
/// </summary>
5+
public class InvalidClientCertificateException : ConnectionStringParseException {
6+
/// <summary>
7+
/// Constructs a new <see cref="InvalidClientCertificateException"/>.
8+
/// </summary>
9+
/// <param name="message"></param>
10+
public InvalidClientCertificateException(string message)
11+
: base(message) { }
12+
}
13+
}

test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs

-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ public class append_to_stream_with_tls_ca_file(ITestOutputHelper output, EventSt
66
: EventStoreTests<EventStoreFixture>(output, fixture) {
77
public static IEnumerable<object[]> CertPaths =>
88
new List<object[]> {
9-
// relative
109
new object[] { Path.Combine("certs", "ca", "ca.crt") },
11-
12-
// absolute
1310
new object[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt") },
1411
};
1512

test/EventStore.Client.Tests/ConnectionStringTests.cs

+15-33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http;
33
using AutoFixture;
44
using System.Reflection;
5+
using System.Security.Cryptography.X509Certificates;
56

67
namespace EventStore.Client.Tests;
78

@@ -18,6 +19,8 @@ public class ConnectionStringTests {
1819
)
1920
);
2021

22+
fixture.Register<X509Certificate2>(() => null!);
23+
2124
return Enumerable.Range(0, 3).SelectMany(GetTestCases);
2225

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

97+
public static IEnumerable<object?[]> InvalidClientCertificates() {
98+
yield return new object?[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found") };
99+
yield return new object?[] { Assembly.GetExecutingAssembly().Location };
100+
}
101+
94102
[Theory]
95103
[MemberData(nameof(ValidCases))]
96104
public void valid_connection_string(string connectionString, EventStoreClientSettings expected) {
@@ -145,40 +153,14 @@ public void tls_verify_cert(bool tlsVerifyCert) {
145153

146154
#endif
147155

148-
[Fact]
149-
public void should_throw_error_when_tls_ca_file_does_not_exists() {
150-
var certificateFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found");
151-
152-
var connectionString = $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}";
153-
154-
var exception = Assert.ThrowsAsync<FileNotFoundException>(
155-
() => {
156-
EventStoreClientSettings.Create(connectionString);
157-
return Task.CompletedTask;
158-
}
159-
);
160-
161-
Assert.NotNull(exception);
162-
Assert.Equal("Failed to load certificate. File was not found.", exception.Result.Message);
163-
}
164-
165-
[Fact]
166-
public void should_throw_exception_when_wrong_format_is_used_for_certificate_file_in_connection_string() {
167-
// We are using a file that is not a certificate.
168-
string certificateFilePath = Assembly.GetExecutingAssembly().Location;
169-
170-
var connectionString =
171-
$"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}";
172-
173-
var exception = Assert.ThrowsAsync<Exception>(
174-
() => {
175-
EventStoreClientSettings.Create(connectionString);
176-
return Task.CompletedTask;
177-
}
156+
[Theory]
157+
[MemberData(nameof(InvalidClientCertificates))]
158+
public void connection_string_with_invalid_client_certificate_should_throw(string clientCertificatePath) {
159+
Assert.Throws<InvalidClientCertificateException >(
160+
() => EventStoreClientSettings.Create(
161+
$"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}"
162+
)
178163
);
179-
180-
Assert.NotNull(exception);
181-
Assert.Equal("Failed to load certificate. Invalid file format.", exception.Result.Message);
182164
}
183165

184166
[Fact]

0 commit comments

Comments
 (0)