Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public async Task Mutual_TLS_should_fail_when_client_has_no_certificate()
InitializeLogger(client, "[CLIENT] ");

// Should fail to connect because server requires client certificate
// Enhanced error message "no client certificate provided" will be logged to server logs
await Assert.ThrowsAsync<AskTimeoutException>(async () =>
{
await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(3));
Expand Down Expand Up @@ -259,6 +260,7 @@ public async Task Mutual_TLS_should_fail_when_client_has_different_valid_certifi
InitializeLogger(client, "[CLIENT] ");

// Connection should fail due to certificate mismatch
// Enhanced error message with certificate validation details will be logged to server logs
await Assert.ThrowsAsync<AskTimeoutException>(async () =>
{
await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(3));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,11 @@ public async Task If_EnableSsl_configuration_is_true_but_not_valid_certificate_p

var realException = GetInnerMostException<CryptographicException>(aggregateException);
Assert.NotNull(realException);
// TODO: this error message is not correct, but wanted to keep this assertion here in case someone else
// wants to fix it in the future.
//Assert.Equal("The specified network password is not correct.", realException.Message);
// NOTE: The error message for incorrect certificate password comes from the .NET Framework
// during X509Certificate2 construction, not from our code. The exact message is platform-dependent
// (e.g., "The specified network password is not correct" on Windows, different on Linux).
// We cannot improve this message as it's not generated by our TLS handshake code.
// Enhanced error messages are provided during TLS handshake failures (see DotNettyTlsHandshakeFailureSpec).
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public async Task Client_side_tls_handshake_failure_should_shutdown_client()
var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo";

// Trigger TLS handshake failure during association
// The enhanced error message will be logged, but we can't easily assert on it
// in a multi-system test without using the TestKit's Sys
client.ActorSelection(serverEchoPath).Tell("hello");

// Client should shutdown due to TLS failure
Expand Down
12 changes: 10 additions & 2 deletions src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,12 @@ private void SetServerPipeline(IChannel channel)
{
if (certificate == null)
{
Log.Warning("Mutual TLS: Client connection rejected - no client certificate provided");
Log.Error("Mutual TLS authentication failed: Client did not provide a certificate.\n" +
"Server requires mutual TLS (require-mutual-authentication = true).\n" +
"Suggestions:\n" +
" - Ensure client has mutual TLS enabled (require-mutual-authentication = true)\n" +
" - Verify client certificate is properly configured and accessible\n" +
" - Check client-side logs for certificate loading errors");
return false;
}

Expand All @@ -432,7 +437,10 @@ private void SetServerPipeline(IChannel channel)

if (errors != SslPolicyErrors.None)
{
Log.Warning("Mutual TLS: Client certificate validation failed with errors: {0}", errors);
// Build detailed error message with certificate details and suggestions
var cert = certificate as X509Certificate2;
var detailedError = TlsErrorMessageBuilder.BuildSslPolicyErrorMessage(errors, cert, chain);
Log.Error("Mutual TLS authentication failed: Client certificate validation error.\n{0}", detailedError);
return false;
}

Expand Down
227 changes: 227 additions & 0 deletions src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,231 @@ private SslSettings(string certificatePath, string certificatePassword, X509KeyS
RequireMutualAuthentication = requireMutualAuthentication;
}
}

/// <summary>
/// INTERNAL API
///
/// Helper class for building human-readable error messages for TLS/SSL certificate validation failures.
/// Provides detailed diagnostics and actionable suggestions for common certificate issues.
/// </summary>
internal static class TlsErrorMessageBuilder
{
/// <summary>
/// Builds a detailed error message for SSL policy errors encountered during TLS handshake.
/// </summary>
/// <param name="errors">The SSL policy errors from certificate validation callback</param>
/// <param name="certificate">The certificate that failed validation (may be null)</param>
/// <param name="chain">The X509 chain used for validation (may be null)</param>
/// <returns>A human-readable error message with diagnostics and suggestions</returns>
public static string BuildSslPolicyErrorMessage(
System.Net.Security.SslPolicyErrors errors,
X509Certificate2? certificate,
X509Chain? chain)
{
var message = new System.Text.StringBuilder();
message.AppendLine("TLS/SSL certificate validation failed:");

// Interpret SslPolicyErrors flags
if ((errors & System.Net.Security.SslPolicyErrors.None) != System.Net.Security.SslPolicyErrors.None)
{
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNotAvailable) != 0)
{
message.AppendLine(" - Remote certificate not available");
message.AppendLine(" Suggestion: Ensure the remote endpoint provides a valid TLS certificate");
}

if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
{
message.AppendLine(" - Remote certificate name mismatch");
message.AppendLine(" Suggestion: Verify certificate CN/SAN matches the target hostname");
if (certificate != null)
{
var cn = certificate.GetNameInfo(X509NameType.DnsName, false);
message.AppendLine($" Certificate CN: {cn}");
}
}

if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
{
message.AppendLine(" - Certificate chain validation errors");

if (chain != null && chain.ChainStatus.Length > 0)
{
var chainStatusMsg = BuildX509ChainStatusMessage(chain.ChainStatus);
message.Append(chainStatusMsg);
}
else
{
message.AppendLine(" Suggestion: Certificate chain cannot be validated. " +
"Install required intermediate CA certificates.");
}
}
}

// Add certificate details if available
if (certificate != null)
{
message.AppendLine($"\nCertificate Details:");
message.AppendLine($" Subject: {certificate.Subject}");
message.AppendLine($" Issuer: {certificate.Issuer}");
message.AppendLine($" Thumbprint: {certificate.Thumbprint}");
message.AppendLine($" Valid From: {certificate.NotBefore:yyyy-MM-dd HH:mm:ss}");
message.AppendLine($" Valid To: {certificate.NotAfter:yyyy-MM-dd HH:mm:ss}");
message.AppendLine($" Has Private Key: {certificate.HasPrivateKey}");
}

return message.ToString().TrimEnd();
}

/// <summary>
/// Builds a detailed message explaining X509 chain status errors.
/// </summary>
/// <param name="chainStatus">Array of chain status from X509Chain validation</param>
/// <returns>Human-readable explanation of chain errors with suggestions</returns>
public static string BuildX509ChainStatusMessage(X509ChainStatus[] chainStatus)
{
var message = new System.Text.StringBuilder();

foreach (var status in chainStatus)
{
// Skip "NoError" status
if (status.Status == X509ChainStatusFlags.NoError)
continue;

message.AppendLine($" - {status.Status}: {status.StatusInformation}");

// Add specific suggestions based on chain status
var suggestion = GetChainStatusSuggestion(status.Status);
if (!string.IsNullOrEmpty(suggestion))
{
message.AppendLine($" Suggestion: {suggestion}");
}
}

return message.ToString();
}

/// <summary>
/// Maps X509ChainStatusFlags to actionable suggestions for fixing the issue.
/// </summary>
private static string GetChainStatusSuggestion(X509ChainStatusFlags status)
{
return status switch
{
X509ChainStatusFlags.NotTimeValid =>
"Certificate has expired or is not yet valid. Check system clock and certificate validity period.",

X509ChainStatusFlags.NotTimeNested =>
"Certificate validity period does not nest correctly within the chain.",

X509ChainStatusFlags.Revoked =>
"Certificate has been revoked. Contact certificate issuer.",

X509ChainStatusFlags.NotSignatureValid =>
"Certificate signature is invalid. Certificate may be corrupted.",

X509ChainStatusFlags.NotValidForUsage =>
"Certificate is not valid for the intended usage. Check Extended Key Usage (EKU) extensions.",

X509ChainStatusFlags.UntrustedRoot =>
"Certificate chain terminates in an untrusted root. Install root CA certificate in Trusted Root Certification Authorities store.",

X509ChainStatusFlags.RevocationStatusUnknown =>
"Revocation status cannot be determined. Check network connectivity to CRL/OCSP endpoints.",

X509ChainStatusFlags.Cyclic =>
"Certificate chain contains a cycle. Certificate configuration is invalid.",

X509ChainStatusFlags.InvalidExtension =>
"Certificate contains an invalid extension.",

X509ChainStatusFlags.InvalidPolicyConstraints =>
"Certificate policy constraints are invalid.",

X509ChainStatusFlags.InvalidBasicConstraints =>
"Basic constraints are invalid. CA certificate may be missing CA:TRUE constraint.",

X509ChainStatusFlags.InvalidNameConstraints =>
"Name constraints in certificate are invalid.",

X509ChainStatusFlags.HasNotSupportedNameConstraint =>
"Certificate contains name constraints that are not supported.",

X509ChainStatusFlags.HasNotDefinedNameConstraint =>
"Certificate has undefined name constraints.",

X509ChainStatusFlags.HasNotPermittedNameConstraint =>
"Certificate name violates name constraints.",

X509ChainStatusFlags.HasExcludedNameConstraint =>
"Certificate name is explicitly excluded by name constraints.",

X509ChainStatusFlags.PartialChain =>
"Certificate chain is incomplete. Install all intermediate CA certificates from your certificate provider.",

X509ChainStatusFlags.CtlNotTimeValid =>
"Certificate Trust List (CTL) is not time-valid.",

X509ChainStatusFlags.CtlNotSignatureValid =>
"Certificate Trust List (CTL) signature is invalid.",

X509ChainStatusFlags.CtlNotValidForUsage =>
"Certificate Trust List (CTL) is not valid for this usage.",

X509ChainStatusFlags.OfflineRevocation =>
"Revocation checking is offline. Enable network access or disable revocation checking for testing.",

X509ChainStatusFlags.NoIssuanceChainPolicy =>
"Certificate does not have a valid issuance policy.",

X509ChainStatusFlags.ExplicitDistrust =>
"Certificate is explicitly distrusted. Remove from Distrusted Certificates store if this is incorrect.",

X509ChainStatusFlags.HasNotSupportedCriticalExtension =>
"Certificate has an unsupported critical extension.",

X509ChainStatusFlags.HasWeakSignature =>
"Certificate uses a weak signature algorithm (e.g., SHA1). Use SHA256 or stronger.",

_ => string.Empty
};
}

/// <summary>
/// Builds an error message for TLS handshake exceptions.
/// Attempts to extract meaningful information from CryptographicException and AuthenticationException.
/// </summary>
public static string BuildTlsHandshakeErrorMessage(Exception exception, bool isClient)
{
var role = isClient ? "Client" : "Server";
var message = new System.Text.StringBuilder();

message.AppendLine($"TLS handshake failed ({role} side):");
message.AppendLine($" Error: {exception.Message}");

// Provide role-specific suggestions
if (isClient)
{
message.AppendLine("\nClient-side TLS troubleshooting:");
message.AppendLine(" - Verify server certificate is trusted (install root CA if using self-signed)");
message.AppendLine(" - Check certificate hostname matches connection target");
message.AppendLine(" - For mutual TLS, ensure client certificate is configured, accessible, and trusted by server");
message.AppendLine(" - Server and client certificates must have compatible trust chains");
}
else
{
message.AppendLine("\nServer-side TLS troubleshooting:");
message.AppendLine(" - Verify server certificate has accessible private key");
message.AppendLine(" - For mutual TLS, check if client is providing a certificate");
message.AppendLine(" - Review certificate validation requirements (suppress-validation for testing)");
}

if (exception.InnerException != null)
{
message.AppendLine($"\nInner Exception: {exception.InnerException.Message}");
}

return message.ToString().TrimEnd();
}
}
}
23 changes: 21 additions & 2 deletions src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,14 @@ public override void UserEventTriggered(IChannelHandlerContext context, object e
if (evt is TlsHandshakeCompletionEvent { IsSuccessful: false } tlsEvent)
{
var ex = tlsEvent.Exception ?? new Exception("TLS handshake failed.");
Log.Error(ex, "TLS handshake failed. Channel [{0}->{1}](Id={2})",
context.Channel.LocalAddress, context.Channel.RemoteAddress, context.Channel.Id);

// Determine if this is client or server side based on handler type
var isClient = this is TcpClientHandler;
var detailedError = TlsErrorMessageBuilder.BuildTlsHandshakeErrorMessage(ex, isClient);

Log.Error(ex, "TLS handshake failed on channel [{0}->{1}](Id={2})\n{3}",
context.Channel.LocalAddress, context.Channel.RemoteAddress,
context.Channel.Id, detailedError);

// Shutdown the ActorSystem on TLS handshake failure
var cs = CoordinatedShutdown.Get(Transport.System);
Expand Down Expand Up @@ -120,6 +126,19 @@ public override void ExceptionCaught(IChannelHandlerContext context, Exception e

NotifyListener(new Disassociated(DisassociateInfo.Shutdown));
}
// Enhanced TLS exception handling
else if (exception is System.Security.Authentication.AuthenticationException
or System.Security.Cryptography.CryptographicException)
{
// Determine if this is client or server side based on handler type
var isClient = this is TcpClientHandler;
var detailedError = TlsErrorMessageBuilder.BuildTlsHandshakeErrorMessage(exception, isClient);

Log.Error(exception, "TLS exception on channel [{0}->{1}](Id={2})\n{3}",
context.Channel.LocalAddress, context.Channel.RemoteAddress, context.Channel.Id, detailedError);

NotifyListener(new Disassociated(DisassociateInfo.Unknown));
}
else
{
base.ExceptionCaught(context, exception);
Expand Down
Loading