This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 9f8198330 feat(csharp): add support for TLS TCP communication (#2395)
9f8198330 is described below
commit 9f81983301bee4094cfe3306b57ad89374def4b9
Author: Łukasz Zborek <[email protected]>
AuthorDate: Sun Nov 23 21:14:53 2025 +0100
feat(csharp): add support for TLS TCP communication (#2395)
- Add support for certificates in TCP TLS communication
- Add docker image building in E2E tests (similar to BDD tests)
---
.github/actions/csharp-dotnet/pre-merge/action.yml | 27 ++--
.../utils/docker-build-test-server/action.yml | 105 ++++++++++++++
.../Fixtures/IggyServerFixture.cs | 156 ++++++++++++---------
.../Fixtures/IggyTlsServerFixture.cs} | 33 +++--
.../Helpers/ResourceMapping.cs} | 23 +--
.../IggyTlsConnectionTests.cs | 101 +++++++++++++
.../Iggy_SDK.Tests.Integration.csproj | 19 +++
.../Iggy_SDK/Configuration/TlsConfiguration.cs | 6 +-
.../InvalidCertificatePathException.cs} | 23 ++-
.../IggyClient/Implementations/TcpMessageStream.cs | 90 +++++++++++-
foreign/csharp/README.md | 2 +-
11 files changed, 445 insertions(+), 140 deletions(-)
diff --git a/.github/actions/csharp-dotnet/pre-merge/action.yml
b/.github/actions/csharp-dotnet/pre-merge/action.yml
index 712dda261..ac4d404e9 100644
--- a/.github/actions/csharp-dotnet/pre-merge/action.yml
+++ b/.github/actions/csharp-dotnet/pre-merge/action.yml
@@ -15,8 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-# TODO(hubcio): Currently, C# tests don't need server. They use testcontainers
with 'edge' image.
-# We should change this to use server-start/stop action, so that
code from PR is tested.
name: csharp-dotnet-pre-merge
description: .NET pre-merge testing github iggy actions
@@ -40,11 +38,6 @@ runs:
with:
cache-targets: false # Only cache registry and git deps, not target
dir (sccache handles that)
- - name: Install netcat
- if: inputs.task == 'e2e'
- run: sudo apt-get update && sudo apt-get install -y netcat-openbsd
- shell: bash
-
- name: Restore dependencies
run: |
dotnet restore foreign/csharp/Iggy_SDK.sln
@@ -77,27 +70,23 @@ runs:
shell: bash
- - name: Start Iggy server
- id: iggy
+ - name: Build Iggy server Docker image
+ id: docker_build
if: inputs.task == 'e2e'
- uses: ./.github/actions/utils/server-start
- continue-on-error: true
+ uses: ./.github/actions/utils/docker-build-test-server
+ with:
+ image-tag: "iggy-server:test"
+ libc: "glibc"
+ profile: "debug"
- name: Run integration tests
if: inputs.task == 'e2e'
env:
- IGGY_SERVER_HOST: 127.0.0.1
+ IGGY_SERVER_DOCKER_IMAGE: ${{ steps.docker_build.outputs.docker_image
}}
run: |
dotnet test foreign/csharp/Iggy_SDK.Tests.Integration --no-build
--verbosity normal
shell: bash
- - name: Stop Iggy server
- if: inputs.task == 'e2e'
- uses: ./.github/actions/utils/server-stop
- with:
- pid-file: ${{ steps.iggy.outputs.pid_file }}
- log-file: ${{ steps.iggy.outputs.log_file }}
-
- name: Build Release
if: inputs.task == 'build'
run: |
diff --git a/.github/actions/utils/docker-build-test-server/action.yml
b/.github/actions/utils/docker-build-test-server/action.yml
new file mode 100644
index 000000000..a82ae762a
--- /dev/null
+++ b/.github/actions/utils/docker-build-test-server/action.yml
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+name: docker-build-test-server
+description: Build Iggy server binaries and Docker image with prebuilt binaries
+inputs:
+ dockerfile:
+ description: "Path to Dockerfile"
+ required: false
+ default: "core/server/Dockerfile"
+ dockerfile-target:
+ description: "Dockerfile target stage (e.g., runtime-prebuilt)"
+ required: false
+ default: "runtime-prebuilt"
+ image-tag:
+ description: "Docker image tag (without registry/name)"
+ required: false
+ default: "iggy-server:test"
+ docker-build-context:
+ description: "Docker build context"
+ required: false
+ default: "."
+ prebuilt-server-binary:
+ description: "Path to prebuilt iggy-server binary"
+ required: false
+ default: "target/debug/iggy-server"
+ prebuilt-cli-binary:
+ description: "Path to prebuilt iggy CLI binary"
+ required: false
+ default: "target/debug/iggy"
+ libc:
+ description: "Libc type (musl or glibc)"
+ required: false
+ default: "glibc"
+ profile:
+ description: "Build profile (debug or release)"
+ required: false
+ default: "debug"
+outputs:
+ docker_image:
+ description: "Full Docker image name built"
+ value: ${{ steps.build.outputs.image_tag }}
+runs:
+ using: "composite"
+ steps:
+ - name: Build Iggy server and CLI binaries
+ shell: bash
+ run: |
+ echo "Building Iggy server and CLI binaries with cargo..."
+ cargo build --locked --bin iggy-server --bin iggy
+
+ echo "✅ Binaries built successfully:"
+ ls -lh ${{ inputs.prebuilt-server-binary }} ${{
inputs.prebuilt-cli-binary }}
+
+ - name: Build Docker image with prebuilt binaries
+ id: build
+ shell: bash
+ run: |
+ set -euo pipefail
+ DOCKERFILE="${{ inputs.dockerfile }}"
+ DOCKERFILE_TARGET="${{ inputs.dockerfile-target }}"
+ IMAGE_TAG="${{ inputs.image-tag }}"
+ BUILD_CONTEXT="${{ inputs.docker-build-context }}"
+ PREBUILT_IGGY_SERVER="${{ inputs.prebuilt-server-binary }}"
+ PREBUILT_IGGY_CLI="${{ inputs.prebuilt-cli-binary }}"
+ LIBC="${{ inputs.libc }}"
+ PROFILE="${{ inputs.profile }}"
+
+ echo "Building Docker image with prebuilt binaries..."
+ echo " Dockerfile: $DOCKERFILE"
+ echo " Target: $DOCKERFILE_TARGET"
+ echo " Image Tag: $IMAGE_TAG"
+ echo " Context: $BUILD_CONTEXT"
+ echo " Server Binary: $PREBUILT_IGGY_SERVER"
+ echo " CLI Binary: $PREBUILT_IGGY_CLI"
+ echo " Libc: $LIBC"
+ echo " Profile: $PROFILE"
+
+ docker build \
+ -f "$DOCKERFILE" \
+ --target "$DOCKERFILE_TARGET" \
+ -t "$IMAGE_TAG" \
+ --build-arg PREBUILT_IGGY_SERVER="$PREBUILT_IGGY_SERVER" \
+ --build-arg PREBUILT_IGGY_CLI="$PREBUILT_IGGY_CLI" \
+ --build-arg LIBC="$LIBC" \
+ --build-arg PROFILE="$PROFILE" \
+ "$BUILD_CONTEXT"
+
+ echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
+ echo "✅ Docker image built successfully: $IMAGE_TAG"
diff --git
a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
index a67b66493..bc81d3087 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
@@ -1,74 +1,109 @@
-// // Licensed to the Apache Software Foundation (ASF) under one
-// // or more contributor license agreements. See the NOTICE file
-// // distributed with this work for additional information
-// // regarding copyright ownership. The ASF licenses this file
-// // to you 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
-// //
-// // http://www.apache.org/licenses/LICENSE-2.0
-// //
-// // Unless required by applicable law or agreed to in writing,
-// // software distributed under the License is distributed on an
-// // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// // KIND, either express or implied. See the License for the
-// // specific language governing permissions and limitations
-// // under the License.
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Apache.Iggy.IggyClient;
+using Apache.Iggy.Tests.Integrations.Helpers;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using TUnit.Core.Interfaces;
-using TUnit.Core.Logging;
namespace Apache.Iggy.Tests.Integrations.Fixtures;
public class IggyServerFixture : IAsyncInitializer, IAsyncDisposable
{
- private readonly IContainer _iggyContainer = new
ContainerBuilder().WithImage("apache/iggy:edge")
- .WithPortBinding(3000, true)
- .WithPortBinding(8090, true)
- .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
-
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(8090))
- .WithName($"{Guid.NewGuid()}")
- .WithEnvironment("IGGY_ROOT_USERNAME", "iggy")
- .WithEnvironment("IGGY_ROOT_PASSWORD", "iggy")
- .WithEnvironment("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
- .WithEnvironment("IGGY_HTTP_ADDRESS", "0.0.0.0:3000")
- //.WithEnvironment("IGGY_CLUSTER_ENABLED", "true")
- // .WithEnvironment("IGGY_CLUSTER_NODES_0_NAME", "iggy-node-1")
- // .WithEnvironment("IGGY_CLUSTER_NODES_0_ADDRESS", "127.0.0.1:8090")
- // .WithEnvironment("IGGY_CLUSTER_NODES_1_NAME", "iggy-node-2")
- // .WithEnvironment("IGGY_CLUSTER_NODES_1_ADDRESS", "127.0.0.1:8092")
- // .WithEnvironment("IGGY_CLUSTER_ENABLED", "true")
- // .WithEnvironment("IGGY_CLUSTER_ENABLED", "true")
- // .WithEnvironment("IGGY_CLUSTER_ENABLED", "true")
- //.WithEnvironment("IGGY_SYSTEM_LOGGING_LEVEL", "trace")
- //.WithEnvironment("RUST_LOG", "trace")
- .WithPrivileged(true)
- .WithCleanUp(true)
- .Build();
-
- private string? _iggyServerHost;
+ protected IContainer? IggyContainer;
+
+ /// <summary>
+ /// Docker image to use. Can be overridden via
IGGY_SERVER_DOCKER_IMAGE environment variable
+ /// or by subclasses. Defaults to apache/iggy:edge if not specified.
+ /// </summary>
+ private string DockerImage =>
+ Environment.GetEnvironmentVariable("IGGY_SERVER_DOCKER_IMAGE") ??
"apache/iggy:edge";
+
+ /// <summary>
+ /// Environment variables for the container. Override in subclasses to
customize.
+ /// </summary>
+ protected virtual Dictionary<string, string> EnvironmentVariables => new()
+ {
+ { "IGGY_ROOT_USERNAME", "iggy" },
+ { "IGGY_ROOT_PASSWORD", "iggy" },
+ { "IGGY_TCP_ADDRESS", "0.0.0.0:8090" },
+ { "IGGY_HTTP_ADDRESS", "0.0.0.0:3000" }
+ };
+
+ /// <summary>
+ /// Enables iggy server trace logs.
+ /// </summary>
+ protected bool EnabledServerTraceLogs => false;
+
+ /// <summary>
+ /// Resource mappings (volumes, etc.) for the container. Override in
subclasses to add custom mappings.
+ /// </summary>
+ protected virtual ResourceMapping[] ResourceMappings => [];
+
+ public IggyServerFixture()
+ {
+ var builder = new ContainerBuilder()
+ .WithImage(DockerImage)
+ .WithPortBinding(3000, true)
+ .WithPortBinding(8090, true)
+ .WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
+
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(8090))
+ .WithName($"{Guid.NewGuid()}")
+ .WithPrivileged(true)
+ .WithCleanUp(true);
+
+ foreach (var (key, value) in EnvironmentVariables)
+ {
+ builder = builder.WithEnvironment(key, value);
+ }
+
+ if (EnabledServerTraceLogs)
+ {
+ builder = builder
+ .WithEnvironment("IGGY_SYSTEM_LOGGING_LEVEL", "trace")
+ .WithEnvironment("RUST_LOG", "trace");
+
+ }
+
+ foreach (var mapping in ResourceMappings)
+ {
+ builder = builder.WithResourceMapping(mapping.Source,
mapping.Destination);
+ }
+
+ IggyContainer = builder.Build();
+ }
public async ValueTask DisposeAsync()
{
- await _iggyContainer.StopAsync();
+ if (IggyContainer == null)
+ {
+ return;
+ }
+
+ await IggyContainer.StopAsync();
}
public virtual async Task InitializeAsync()
{
- var logger = TestContext.Current!.GetDefaultLogger();
- _iggyServerHost =
Environment.GetEnvironmentVariable("IGGY_SERVER_HOST");
-
- await logger.LogInformationAsync($"Iggy server host:
{_iggyServerHost}");
- if (string.IsNullOrEmpty(_iggyServerHost))
- {
- await _iggyContainer.StartAsync();
- }
+ await IggyContainer!.StartAsync();
await CreateTcpClient();
await CreateHttpClient();
@@ -131,22 +166,15 @@ public class IggyServerFixture : IAsyncInitializer,
IAsyncDisposable
return client;
}
- public string GetIggyAddress(Protocol protocol)
+ public virtual string GetIggyAddress(Protocol protocol)
{
- if (string.IsNullOrEmpty(_iggyServerHost))
- {
- var port = protocol == Protocol.Tcp
- ? _iggyContainer.GetMappedPublicPort(8090)
- : _iggyContainer.GetMappedPublicPort(3000);
-
- return protocol == Protocol.Tcp
- ? $"127.0.0.1:{port}"
- : $"http://127.0.0.1:{port}";
- }
+ var port = protocol == Protocol.Tcp
+ ? IggyContainer!.GetMappedPublicPort(8090)
+ : IggyContainer!.GetMappedPublicPort(3000);
return protocol == Protocol.Tcp
- ? $"{_iggyServerHost}:8090"
- : $"http://{_iggyServerHost}:3000";
+ ? $"127.0.0.1:{port}"
+ : $"http://127.0.0.1:{port}";
}
public static IEnumerable<Func<Protocol>> ProtocolData()
diff --git a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyTlsServerFixture.cs
similarity index 50%
copy from foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
copy to
foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyTlsServerFixture.cs
index a0adf9ebb..0d7ff109d 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyTlsServerFixture.cs
@@ -15,25 +15,36 @@
// specific language governing permissions and limitations
// under the License.
-namespace Apache.Iggy.Configuration;
+using Apache.Iggy.Tests.Integrations.Helpers;
+
+namespace Apache.Iggy.Tests.Integrations.Fixtures;
/// <summary>
-/// TLS configuration
+/// Iggy server fixture configured with TLS enabled.
+/// Requires certificates to be mounted in the container.
/// </summary>
-public class TlsSettings
+public class IggyTlsServerFixture : IggyServerFixture
{
/// <summary>
- /// Whether TLS is enabled.
+ /// Environment variables with TLS configuration enabled.
/// </summary>
- public bool Enabled { get; set; }
+ protected override Dictionary<string, string> EnvironmentVariables =>
new(base.EnvironmentVariables)
+ {
+ { "IGGY_TCP_TLS_ENABLED", "true" },
+ { "IGGY_TCP_TLS_CERT_FILE", "/app/certs/iggy_cert.pem" },
+ { "IGGY_TCP_TLS_KEY_FILE", "/app/certs/iggy_key.pem" }
+ };
/// <summary>
- /// The name of the server that shares ssl stream.
+ /// Resource mappings for TLS certificates.
/// </summary>
- public string Hostname { get; set; } = string.Empty;
+ protected override ResourceMapping[] ResourceMappings =>
+ [
+ new("Certs", "/app/certs/")
+ ];
- /// <summary>
- /// Whether to authenticate the client.
- /// </summary>
- public bool Authenticate { get; set; }
+ public override async Task InitializeAsync()
+ {
+ await IggyContainer!.StartAsync();
+ }
}
diff --git a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
b/foreign/csharp/Iggy_SDK.Tests.Integration/Helpers/ResourceMapping.cs
similarity index 60%
copy from foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
copy to foreign/csharp/Iggy_SDK.Tests.Integration/Helpers/ResourceMapping.cs
index a0adf9ebb..7325d8423 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Helpers/ResourceMapping.cs
@@ -15,25 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-namespace Apache.Iggy.Configuration;
+namespace Apache.Iggy.Tests.Integrations.Helpers;
-/// <summary>
-/// TLS configuration
-/// </summary>
-public class TlsSettings
-{
- /// <summary>
- /// Whether TLS is enabled.
- /// </summary>
- public bool Enabled { get; set; }
-
- /// <summary>
- /// The name of the server that shares ssl stream.
- /// </summary>
- public string Hostname { get; set; } = string.Empty;
-
- /// <summary>
- /// Whether to authenticate the client.
- /// </summary>
- public bool Authenticate { get; set; }
-}
+public record ResourceMapping(string Source, string Destination);
diff --git
a/foreign/csharp/Iggy_SDK.Tests.Integration/IggyTlsConnectionTests.cs
b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyTlsConnectionTests.cs
new file mode 100644
index 000000000..ab54d354a
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyTlsConnectionTests.cs
@@ -0,0 +1,101 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.Factory;
+using Apache.Iggy.Tests.Integrations.Fixtures;
+using Shouldly;
+
+namespace Apache.Iggy.Tests.Integrations;
+
+public class IggyTlsConnectionTests
+{
+ [ClassDataSource<IggyTlsServerFixture>(Shared = SharedType.PerAssembly)]
+ public required IggyTlsServerFixture Fixture { get; init; }
+
+ [Test]
+ public async Task Connect_WithTls_Should_Connect_Successfully()
+ {
+ using var client = IggyClientFactory.CreateClient(new
IggyClientConfigurator
+ {
+ BaseAddress = Fixture.GetIggyAddress(Protocol.Tcp),
+ Protocol = Protocol.Tcp,
+ ReconnectionSettings = new ReconnectionSettings { Enabled = false
},
+ AutoLoginSettings = new AutoLoginSettings
+ {
+ Enabled = true,
+ Username = "iggy",
+ Password = "iggy"
+ },
+ TlsSettings = new TlsSettings
+ {
+ Enabled = true,
+ Hostname = "localhost",
+ CertificatePath = "Certs/iggy_cert.pem"
+ }
+ });
+
+ await client.ConnectAsync();
+ var loginResult = await client.LoginUser("iggy", "iggy");
+
+ loginResult.ShouldNotBeNull();
+ }
+
+ [Test]
+ public async Task Connect_WithoutTls_Should_Throw_WhenTlsIsRequired()
+ {
+ using var client = IggyClientFactory.CreateClient(new
IggyClientConfigurator
+ {
+ BaseAddress = Fixture.GetIggyAddress(Protocol.Tcp),
+ Protocol = Protocol.Tcp,
+ ReconnectionSettings = new ReconnectionSettings { Enabled = false }
+ });
+
+ await client.ConnectAsync();
+ await
Should.ThrowAsync<IggyZeroBytesException>(client.LoginUser("iggy", "iggy"));
+ }
+
+ [Test]
+ public async Task Connect_WithTls_CA_Should_Connect_Successfully()
+ {
+ using var client = IggyClientFactory.CreateClient(new
IggyClientConfigurator
+ {
+ BaseAddress = Fixture.GetIggyAddress(Protocol.Tcp),
+ Protocol = Protocol.Tcp,
+ ReconnectionSettings = new ReconnectionSettings { Enabled = false
},
+ AutoLoginSettings = new AutoLoginSettings
+ {
+ Enabled = true,
+ Username = "iggy",
+ Password = "iggy"
+ },
+ TlsSettings = new TlsSettings
+ {
+ Enabled = true,
+ Hostname = "localhost",
+ CertificatePath = "Certs/iggy_ca_cert.pem"
+ }
+ });
+
+ await client.ConnectAsync();
+ var loginResult = await client.LoginUser("iggy", "iggy");
+
+ loginResult.ShouldNotBeNull();
+ }
+}
diff --git
a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
index 2eb6524d2..24b2162d9 100644
---
a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
+++
b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
@@ -26,4 +26,23 @@
<ProjectReference Include="..\Iggy_SDK\Iggy_SDK.csproj" />
</ItemGroup>
+ <ItemGroup>
+ <Content Include="..\..\..\core\certs\iggy.pfx">
+ <Link>Certs\iggy.pfx</Link>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Include="..\..\..\core\certs\iggy_ca_cert.pem">
+ <Link>Certs\iggy_ca_cert.pem</Link>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Include="..\..\..\core\certs\iggy_cert.pem">
+ <Link>Certs\iggy_cert.pem</Link>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ <Content Include="..\..\..\core\certs\iggy_key.pem">
+ <Link>Certs\iggy_key.pem</Link>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
</Project>
diff --git a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
b/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
index a0adf9ebb..b48ee5b9a 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
+++ b/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
@@ -28,12 +28,12 @@ public class TlsSettings
public bool Enabled { get; set; }
/// <summary>
- /// The name of the server that shares ssl stream.
+ /// The name of the server for TLS handshake.
/// </summary>
public string Hostname { get; set; } = string.Empty;
/// <summary>
- /// Whether to authenticate the client.
+ /// Path to the certificate (ca/self-signed) file.
/// </summary>
- public bool Authenticate { get; set; }
+ public string CertificatePath { get; set; } = string.Empty;
}
diff --git a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
b/foreign/csharp/Iggy_SDK/Exceptions/InvalidCertificatePathException.cs
similarity index 64%
copy from foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
copy to foreign/csharp/Iggy_SDK/Exceptions/InvalidCertificatePathException.cs
index a0adf9ebb..b79e15fe1 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/InvalidCertificatePathException.cs
@@ -15,25 +15,20 @@
// specific language governing permissions and limitations
// under the License.
-namespace Apache.Iggy.Configuration;
+namespace Apache.Iggy.Exceptions;
/// <summary>
-/// TLS configuration
+/// Exception thrown when the provided TLS certificate path is invalid.
/// </summary>
-public class TlsSettings
+public class InvalidCertificatePathException : Exception
{
/// <summary>
- /// Whether TLS is enabled.
+ /// Initializes a new instance of the <see
cref="InvalidCertificatePathException" /> class.
/// </summary>
- public bool Enabled { get; set; }
+ /// <param name="tlsCertificatePath">Certificate path</param>
+ public InvalidCertificatePathException(string tlsCertificatePath) : base(
+ $"Invalid TLS certificate path: {tlsCertificatePath}")
- /// <summary>
- /// The name of the server that shares ssl stream.
- /// </summary>
- public string Hostname { get; set; } = string.Empty;
-
- /// <summary>
- /// Whether to authenticate the client.
- /// </summary>
- public bool Authenticate { get; set; }
+ {
+ }
}
diff --git
a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
index 2dc06daa7..4ca8c1351 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
@@ -19,6 +19,7 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Net.Security;
using System.Net.Sockets;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using Apache.Iggy.Configuration;
using Apache.Iggy.ConnectionStream;
@@ -47,6 +48,7 @@ public sealed class TcpMessageStream : IIggyClient
private readonly ILogger<TcpMessageStream> _logger;
private readonly SemaphoreSlim _sendingSemaphore;
private ClusterNode? _currentLeaderNode;
+ private X509Certificate2Collection _customCaStore = [];
private bool _isConnecting;
private DateTimeOffset _lastConnectionTime;
private ConnectionState _state = ConnectionState.Disconnected;
@@ -823,7 +825,7 @@ public sealed class TcpMessageStream : IIggyClient
_stream = _configuration.TlsSettings.Enabled switch
{
- true => CreateSslStreamAndAuthenticate(socket,
_configuration.TlsSettings),
+ true => await CreateSslStreamAndAuthenticate(socket,
_configuration.TlsSettings),
false => new TcpConnectionStream(new NetworkStream(socket,
true))
};
@@ -911,14 +913,16 @@ public sealed class TcpMessageStream : IIggyClient
}
}
- private static TcpConnectionStream CreateSslStreamAndAuthenticate(Socket
socket, TlsSettings tlsSettings)
+ private async Task<TcpConnectionStream>
CreateSslStreamAndAuthenticate(Socket socket, TlsSettings tlsSettings)
{
+ ValidateCertificatePath(tlsSettings.CertificatePath);
+
+ _customCaStore = new X509Certificate2Collection();
+ _customCaStore.ImportFromPemFile(tlsSettings.CertificatePath);
var stream = new NetworkStream(socket, true);
- var sslStream = new SslStream(stream);
- if (tlsSettings.Authenticate)
- {
- sslStream.AuthenticateAsClient(tlsSettings.Hostname);
- }
+ var sslStream = new SslStream(stream, false,
RemoteCertificateValidationCallback);
+
+ await sslStream.AuthenticateAsClientAsync(tlsSettings.Hostname);
return new TcpConnectionStream(sslStream);
}
@@ -1095,4 +1099,76 @@ public sealed class TcpMessageStream : IIggyClient
_logger.LogInformation("Connection state changed: {PreviousState} ->
{CurrentState}", previousState, newState);
_connectionEvents.Publish(new
ConnectionStateChangedEventArgs(previousState, newState));
}
+
+ private void ValidateCertificatePath(string tlsCertificatePath)
+ {
+ if (string.IsNullOrEmpty(tlsCertificatePath)
+ || !File.Exists(tlsCertificatePath))
+ {
+ throw new InvalidCertificatePathException(tlsCertificatePath);
+ }
+ }
+
+ private bool RemoteCertificateValidationCallback(object sender,
X509Certificate? certificate, X509Chain? chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ if (sslPolicyErrors == SslPolicyErrors.None)
+ {
+ return true;
+ }
+
+ if (certificate is null)
+ {
+ return false;
+ }
+
+ if (certificate is not X509Certificate2 serverCert)
+ {
+ serverCert = new X509Certificate2(certificate);
+ }
+
+ if (_customCaStore.Any(ca => ca.Thumbprint == serverCert.Thumbprint))
+ {
+ if (DateTime.UtcNow <= serverCert.NotAfter && DateTime.UtcNow >=
serverCert.NotBefore)
+ {
+ return true;
+ }
+
+ _logger.LogError(
+ "Server certificate matches trusted key but is expired. Valid
from {NotBefore} to {NotAfter}",
+ serverCert.NotBefore, serverCert.NotAfter);
+ return false;
+ }
+
+
+ using var customChain = new X509Chain();
+ customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
+ customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ foreach (var ca in _customCaStore)
+ {
+ customChain.ChainPolicy.CustomTrustStore.Add(ca);
+ customChain.ChainPolicy.ExtraStore.Add(ca);
+ }
+
+ customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+
+ if (customChain.Build(new X509Certificate2(certificate)))
+ {
+ if
(!sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
+ {
+ return true;
+ }
+
+ _logger.LogError("Custom CA chain is valid, but hostname does not
match");
+ return false;
+ }
+
+ foreach (var chainStatus in customChain.ChainStatus)
+ {
+ _logger.LogWarning("Certificate validation failed: {ChainStatus} -
{StatusInformation}", chainStatus.Status,
+ chainStatus.StatusInformation);
+ }
+
+ return false;
+ }
}
diff --git a/foreign/csharp/README.md b/foreign/csharp/README.md
index 6e4a76228..48fdb1205 100644
--- a/foreign/csharp/README.md
+++ b/foreign/csharp/README.md
@@ -71,7 +71,7 @@ var client = IggyClientFactory.CreateClient(new
IggyClientConfigurator
{
Enabled = true,
Hostname = "iggy",
- Authenticate = true
+ CertificatePath = "/path/to/cert"
},
// Automatic reconnection with exponential backoff