Skip to content

Commit efca03b

Browse files
feat(remote): add support for custom certificate validation callbacks (#686)
* feat(remote): add support for custom certificate validation callbacks from Akka.NET v1.5.55 - Updated Akka.NET from v1.5.53 to v1.5.55 to access new SSL/TLS features - Added CustomValidator property to SslOptions to support CertificateValidationCallback - Updated RemoteOptions.Build to use appropriate DotNettySslSetup constructor based on settings: - 5-parameter constructor when CustomValidator is provided (v1.5.55+) - 4-parameter constructor when RequireMutualAuthentication or ValidateCertificateHostname is set - 2-parameter constructor for legacy scenarios (backward compatibility) - Added comprehensive test coverage for CustomValidator configuration - Updated API approval tests to reflect new public API surface This enhancement allows users to implement custom certificate validation logic such as: - Certificate pinning - Subject/issuer matching - Business-specific validation rules - Advanced mTLS scenarios The implementation maintains full backward compatibility while providing access to the powerful new CertificateValidation features introduced in Akka.NET v1.5.55. * Fix SSL configuration precedence issue Fixed bug where both HOCON SSL configuration and DotNettySslSetup were being emitted simultaneously. DotNettySslSetup ALWAYS takes precedence when present, making HOCON SSL settings ineffective and potentially confusing. Changes: - Modified RemoteOptions.Build to only emit HOCON SSL config when X509Certificate is null - Added comprehensive comments explaining SSL configuration strategy with link to Akka.NET issue #7914 - Updated tests to use CertificateOptions instead of X509Certificate when testing HOCON SSL configuration - Both WithRemotingNewSslSettingsHoconTest and WithRemotingConfiguratorNewSslSettingsTest now properly test HOCON config This ensures users understand that: 1. X509Certificate object → DotNettySslSetup (programmatic, takes precedence) 2. X509Certificate null + SSL settings → HOCON configuration only Related to: akkadotnet/akka.net#7914 * Add comprehensive SSL/TLS documentation with CustomValidator examples Added detailed SSL/TLS configuration documentation to Akka.Remote.Hosting README including: - Basic SSL configuration with certificate file (HOCON-based) - SSL configuration with X509Certificate2 object (programmatic) - Advanced custom certificate validation example (certificate pinning) - Examples using all CertificateValidation helper methods from Akka.NET v1.5.55: * ValidateChain() - Standard chain validation * ValidateHostname() - Hostname validation * PinnedCertificate() - Certificate pinning * ValidateSubject() - Subject pattern matching * ValidateIssuer() - Issuer validation * Combine() - Combining multiple validators - Complete SSL configuration options reference - Important note about DotNettySslSetup vs HOCON precedence This helps users understand how to use the new CustomValidator feature for enhanced security scenarios. * Simplify SSL/TLS documentation to single example
1 parent cb3acbe commit efca03b

File tree

5 files changed

+144
-10
lines changed

5 files changed

+144
-10
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<TestSdkVersion>17.11.1</TestSdkVersion>
3434
<CoverletVersion>6.0.3</CoverletVersion>
3535
<XunitRunneVisualstudio>3.1.5</XunitRunneVisualstudio>
36-
<AkkaVersion>1.5.53</AkkaVersion>
36+
<AkkaVersion>1.5.55</AkkaVersion>
3737
<MicrosoftExtensionsVersion>[6.0.0,)</MicrosoftExtensionsVersion>
3838
<SystemTextJsonVersion>[6.0.10,)</SystemTextJsonVersion>
3939
</PropertyGroup>

src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
{
5555
public SslOptions() { }
5656
public Akka.Remote.Hosting.SslCertificateOptions CertificateOptions { get; set; }
57+
public Akka.Remote.Transport.DotNetty.CertificateValidationCallback? CustomValidator { get; set; }
5758
public bool? RequireMutualAuthentication { get; set; }
5859
public bool? SuppressValidation { get; set; }
5960
public bool? ValidateCertificateHostname { get; set; }

src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,6 @@ public void WithRemotingOptionsSslDisabledCertificateTest()
449449
public void WithRemotingNewSslSettingsHoconTest()
450450
{
451451
// arrange
452-
var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password");
453452
var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test");
454453
builder.WithRemoting(new RemoteOptions
455454
{
@@ -459,7 +458,13 @@ public void WithRemotingNewSslSettingsHoconTest()
459458
SuppressValidation = false,
460459
RequireMutualAuthentication = false, // Explicitly set to false for testing
461460
ValidateCertificateHostname = true, // Explicitly set to true for testing
462-
X509Certificate = certificate
461+
// NOTE: Not providing X509Certificate so HOCON configuration will be generated
462+
// When X509Certificate is provided, DotNettySslSetup is used instead of HOCON
463+
CertificateOptions = new SslCertificateOptions
464+
{
465+
Path = "./Resources/akka-validcert.pfx",
466+
Password = "password"
467+
}
463468
}
464469
});
465470

@@ -471,6 +476,10 @@ public void WithRemotingNewSslSettingsHoconTest()
471476
sslConfig.GetBoolean("suppress-validation").Should().BeFalse();
472477
sslConfig.GetBoolean("require-mutual-authentication").Should().BeFalse();
473478
sslConfig.GetBoolean("validate-certificate-hostname").Should().BeTrue();
479+
480+
var certConfig = sslConfig.GetConfig("certificate");
481+
certConfig.GetString("path").Should().Be("./Resources/akka-validcert.pfx");
482+
certConfig.GetString("password").Should().Be("password");
474483
}
475484

476485
[Fact(DisplayName = "RemoteOptions with new SSL/TLS settings should properly configure DotNettySslSetup")]
@@ -503,6 +512,45 @@ public void WithRemotingNewSslSettingsDotNettySslSetupTest()
503512
setup.ValidateCertificateHostname.Should().BeTrue();
504513
}
505514

515+
[Fact(DisplayName = "RemoteOptions with CustomValidator should properly configure DotNettySslSetup with custom validation")]
516+
public void WithRemotingCustomValidatorDotNettySslSetupTest()
517+
{
518+
// arrange
519+
var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password");
520+
var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test");
521+
522+
// Create a simple custom validator for testing
523+
Transport.DotNetty.CertificateValidationCallback customValidator = (cert, chain, peer, errors, log) =>
524+
{
525+
// This is just a test validator - in real usage, this would contain actual validation logic
526+
return cert != null && cert.Thumbprint == certificate.Thumbprint;
527+
};
528+
529+
builder.WithRemoting(new RemoteOptions
530+
{
531+
EnableSsl = true,
532+
Ssl = new SslOptions
533+
{
534+
SuppressValidation = false,
535+
RequireMutualAuthentication = true,
536+
ValidateCertificateHostname = false,
537+
X509Certificate = certificate,
538+
CustomValidator = customValidator
539+
}
540+
});
541+
542+
// act
543+
var setup = (DotNettySslSetup)builder.Setups.First(s => s is DotNettySslSetup);
544+
545+
// assert
546+
setup.Certificate.Should().Be(certificate);
547+
setup.SuppressValidation.Should().BeFalse();
548+
setup.RequireMutualAuthentication.Should().BeTrue();
549+
setup.ValidateCertificateHostname.Should().BeFalse();
550+
setup.CustomValidator.Should().NotBeNull();
551+
setup.CustomValidator.Should().BeSameAs(customValidator);
552+
}
553+
506554
[Fact(DisplayName = "RemoteOptions without new SSL/TLS settings should use default values")]
507555
public void WithRemotingDefaultSslSettingsTest()
508556
{
@@ -533,14 +581,16 @@ public void WithRemotingDefaultSslSettingsTest()
533581
public void WithRemotingConfiguratorNewSslSettingsTest()
534582
{
535583
// arrange
536-
var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password");
537584
var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test");
538585
builder.WithRemoting(opt =>
539586
{
540587
opt.EnableSsl = true;
541588
opt.Ssl.RequireMutualAuthentication = true;
542589
opt.Ssl.ValidateCertificateHostname = false;
543-
opt.Ssl.X509Certificate = certificate;
590+
// Use CertificateOptions instead of X509Certificate to test HOCON configuration
591+
// When X509Certificate is provided, DotNettySslSetup takes precedence and HOCON is not emitted
592+
opt.Ssl.CertificateOptions.Path = "./Resources/akka-validcert.pfx";
593+
opt.Ssl.CertificateOptions.Password = "password";
544594
});
545595

546596
// act

src/Akka.Remote.Hosting/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,39 @@ using var host = new HostBuilder()
5252

5353
await host.RunAsync();
5454
```
55+
56+
## SSL/TLS Configuration
57+
58+
Akka.Remote supports SSL/TLS encryption for secure communication between actor systems. Starting with Akka.NET v1.5.55, you can provide custom certificate validation callbacks using the `CertificateValidation` helper class.
59+
60+
```csharp
61+
using System.Security.Cryptography.X509Certificates;
62+
using Akka.Remote.Transport.DotNetty;
63+
64+
var certificate = new X509Certificate2("/path/to/certificate.pfx", "certificate-password");
65+
66+
using var host = new HostBuilder()
67+
.ConfigureServices((context, services) =>
68+
{
69+
services.AddAkka("secureSystem", (builder, provider) =>
70+
{
71+
builder.WithRemoting(options =>
72+
{
73+
options.HostName = "127.0.0.1";
74+
options.Port = 4053;
75+
options.EnableSsl = true;
76+
options.Ssl.X509Certificate = certificate;
77+
78+
// Use built-in validators for common scenarios
79+
options.Ssl.CustomValidator = CertificateValidation.Combine(
80+
CertificateValidation.ValidateChain(),
81+
CertificateValidation.ValidateSubject("CN=*.mycompany.com")
82+
);
83+
});
84+
});
85+
}).Build();
86+
87+
await host.RunAsync();
88+
```
89+
90+
Available `CertificateValidation` methods: `ValidateChain()`, `ValidateHostname()`, `PinnedCertificate()`, `ValidateSubject()`, `ValidateIssuer()`, and `Combine()`.

src/Akka.Remote.Hosting/RemoteOptions.cs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
using System;
88
using System.Collections.Generic;
99
using System.Net;
10+
using System.Net.Security;
1011
using System.Security.Cryptography.X509Certificates;
1112
using System.Text;
1213
using Akka.Configuration;
14+
using Akka.Event;
1315
using Akka.Hosting;
1416
using Akka.Remote.Transport.DotNetty;
1517

@@ -105,21 +107,35 @@ internal void Build(AkkaConfigurationBuilder builder)
105107
if (sb.Length > 0)
106108
builder.AddHocon(sb.ToString(), HoconAddMode.Prepend);
107109

110+
// SSL configuration strategy:
111+
// 1. If X509Certificate object is provided -> Use DotNettySslSetup (takes precedence over HOCON)
112+
// 2. If X509Certificate is null but SSL settings configured -> Use HOCON configuration only
113+
//
114+
// Important: DotNettySslSetup ALWAYS takes precedence when present, causing HOCON SSL settings
115+
// to be completely ignored. We must not emit both to avoid confusion.
116+
// See: https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs#L163-L164
117+
108118
if (EnableSsl is false || Ssl.X509Certificate == null)
109119
return;
110120

111121
var suppressValidation = Ssl.SuppressValidation ?? false;
112122
var requireMutualAuth = Ssl.RequireMutualAuthentication ?? true; // Default to true as per v1.5.52
113123
var validateHostname = Ssl.ValidateCertificateHostname ?? false; // Default to false as per v1.5.53
114124

115-
// Use the 4-parameter constructor if any of the new settings are provided, otherwise use the legacy constructor for backward compatibility
116-
if (Ssl.RequireMutualAuthentication.HasValue || Ssl.ValidateCertificateHostname.HasValue)
125+
// Choose the appropriate constructor based on which settings are provided
126+
if (Ssl.CustomValidator != null)
127+
{
128+
// Use the 5-parameter constructor with custom validator (v1.5.55+)
129+
builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation, requireMutualAuth, validateHostname, Ssl.CustomValidator));
130+
}
131+
else if (Ssl.RequireMutualAuthentication.HasValue || Ssl.ValidateCertificateHostname.HasValue)
117132
{
133+
// Use the 4-parameter constructor (v1.5.52/v1.5.53+)
118134
builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation, requireMutualAuth, validateHostname));
119135
}
120136
else
121137
{
122-
// Use legacy constructor for backward compatibility when new settings are not specified
138+
// Use legacy 2-parameter constructor for backward compatibility when new settings are not specified
123139
builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation));
124140
}
125141
}
@@ -180,8 +196,15 @@ private void Build(StringBuilder builder)
180196
{
181197
if(Ssl is null)
182198
throw new ConfigurationException("Ssl property need to be populated when EnableSsl is set to true.");
183-
184-
Ssl.Build(tcpSb);
199+
200+
// Only emit HOCON SSL configuration if we're NOT going to create a DotNettySslSetup
201+
// When DotNettySslSetup is present, it takes precedence and HOCON SSL settings are ignored
202+
// See: https://github.com/akkadotnet/akka.net/issues/7914 and the warning at
203+
// https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs#L163-L164
204+
if (Ssl.X509Certificate == null)
205+
{
206+
Ssl.Build(tcpSb);
207+
}
185208
}
186209
}
187210

@@ -240,6 +263,30 @@ public sealed class SslOptions
240263
/// </summary>
241264
public bool? ValidateCertificateHostname { get; set; }
242265

266+
/// <summary>
267+
/// <para>
268+
/// Custom certificate validation callback for advanced validation scenarios.
269+
/// When provided, this callback takes precedence over config-based validation.
270+
/// </para>
271+
/// <para>
272+
/// Use this to implement custom validation logic such as certificate pinning,
273+
/// subject/issuer matching, or other business-specific validation rules.
274+
/// </para>
275+
/// <para>
276+
/// The callback parameters are:
277+
/// - X509Certificate2?: The peer certificate to validate
278+
/// - X509Chain?: The X509 chain for validation
279+
/// - string: The remote peer identifier
280+
/// - SslPolicyErrors: SSL policy errors from standard validation
281+
/// - ILoggingAdapter: Logger for diagnostics
282+
/// </para>
283+
/// <para>
284+
/// Returns true to accept the certificate, false to reject it.
285+
/// </para>
286+
/// <b>Available since:</b> Akka.NET v1.5.55
287+
/// </summary>
288+
public Transport.DotNetty.CertificateValidationCallback? CustomValidator { get; set; }
289+
243290
internal void Build(StringBuilder builder)
244291
{
245292
var sb = new StringBuilder();

0 commit comments

Comments
 (0)