diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs index f6cab0b592d..04634a494ae 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs @@ -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(async () => { await client.ActorSelection(serverEchoPath).Ask("hello", TimeSpan.FromSeconds(3)); @@ -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(async () => { await client.ActorSelection(serverEchoPath).Ask("hello", TimeSpan.FromSeconds(3)); diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs index 5754f57db59..2f6c2e2597c 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs @@ -248,9 +248,11 @@ public async Task If_EnableSsl_configuration_is_true_but_not_valid_certificate_p var realException = GetInnerMostException(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] diff --git a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs index f88647f79d1..8f1616bed56 100644 --- a/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs +++ b/src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs @@ -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 diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs index f87eab23520..621f9e57fec 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs @@ -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; } @@ -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; } diff --git a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs index fa0307bd7b5..40ff883a58b 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs @@ -436,4 +436,231 @@ private SslSettings(string certificatePath, string certificatePassword, X509KeyS RequireMutualAuthentication = requireMutualAuthentication; } } + + /// + /// 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. + /// + internal static class TlsErrorMessageBuilder + { + /// + /// Builds a detailed error message for SSL policy errors encountered during TLS handshake. + /// + /// The SSL policy errors from certificate validation callback + /// The certificate that failed validation (may be null) + /// The X509 chain used for validation (may be null) + /// A human-readable error message with diagnostics and suggestions + 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(); + } + + /// + /// Builds a detailed message explaining X509 chain status errors. + /// + /// Array of chain status from X509Chain validation + /// Human-readable explanation of chain errors with suggestions + 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(); + } + + /// + /// Maps X509ChainStatusFlags to actionable suggestions for fixing the issue. + /// + 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 + }; + } + + /// + /// Builds an error message for TLS handshake exceptions. + /// Attempts to extract meaningful information from CryptographicException and AuthenticationException. + /// + 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(); + } + } } diff --git a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs index 219d8ef7bad..b3833e6a387 100644 --- a/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs +++ b/src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs @@ -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); @@ -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);