Skip to content

Commit 8644c59

Browse files
committed
Improve TLS/SSL certificate error messages during handshake failures (#7891)
* Improve TLS/SSL certificate error messages during handshake failures (#7890) Added TlsErrorMessageBuilder helper class to provide human-readable error messages for TLS certificate validation failures. Enhanced error messages now include: - Detailed SSL policy error interpretations - X509 chain status diagnostics with actionable suggestions - Certificate details (subject, issuer, thumbprint, validity dates) - Role-specific troubleshooting guidance (client vs server) Updated certificate validation callback in mutual TLS to use enhanced error messages. Added TLS exception handling in TcpHandlers to detect and report AuthenticationException and CryptographicException with detailed diagnostics. All existing TLS tests continue to pass. * Enhance TLS error logging across all handshake scenarios Upgraded mutual TLS validation errors from Warning to Error level for better visibility. Enhanced error messages now cover all TLS failure scenarios: Server-side mutual TLS validation: - No client certificate provided: detailed error with troubleshooting steps - Client certificate validation failures: comprehensive chain validation diagnostics Client-side and general handshake failures: - Added enhanced error diagnostics to UserEventTriggered for TlsHandshakeCompletionEvent - Improved client-side troubleshooting guidance including certificate trust chain requirements - Both client and server TLS exceptions now include role-specific troubleshooting All error messages provide actionable suggestions and certificate details to aid in diagnosis.
1 parent b4fbd5f commit 8644c59

File tree

6 files changed

+267
-7
lines changed

6 files changed

+267
-7
lines changed

src/core/Akka.Remote.Tests/Transport/DotNettyMutualTlsSpec.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ public async Task Mutual_TLS_should_fail_when_client_has_no_certificate()
182182
InitializeLogger(client, "[CLIENT] ");
183183

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

261262
// Connection should fail due to certificate mismatch
263+
// Enhanced error message with certificate validation details will be logged to server logs
262264
await Assert.ThrowsAsync<AskTimeoutException>(async () =>
263265
{
264266
await client.ActorSelection(serverEchoPath).Ask<string>("hello", TimeSpan.FromSeconds(3));

src/core/Akka.Remote.Tests/Transport/DotNettySslSupportSpec.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,11 @@ public async Task If_EnableSsl_configuration_is_true_but_not_valid_certificate_p
248248

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

256258
[Theory]

src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public async Task Client_side_tls_handshake_failure_should_shutdown_client()
127127
var serverEchoPath = new RootActorPath(serverAddr) / "user" / "echo";
128128

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

132134
// Client should shutdown due to TLS failure

src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,12 @@ private void SetServerPipeline(IChannel channel)
420420
{
421421
if (certificate == null)
422422
{
423-
Log.Warning("Mutual TLS: Client connection rejected - no client certificate provided");
423+
Log.Error("Mutual TLS authentication failed: Client did not provide a certificate.\n" +
424+
"Server requires mutual TLS (require-mutual-authentication = true).\n" +
425+
"Suggestions:\n" +
426+
" - Ensure client has mutual TLS enabled (require-mutual-authentication = true)\n" +
427+
" - Verify client certificate is properly configured and accessible\n" +
428+
" - Check client-side logs for certificate loading errors");
424429
return false;
425430
}
426431

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

433438
if (errors != SslPolicyErrors.None)
434439
{
435-
Log.Warning("Mutual TLS: Client certificate validation failed with errors: {0}", errors);
440+
// Build detailed error message with certificate details and suggestions
441+
var cert = certificate as X509Certificate2;
442+
var detailedError = TlsErrorMessageBuilder.BuildSslPolicyErrorMessage(errors, cert, chain);
443+
Log.Error("Mutual TLS authentication failed: Client certificate validation error.\n{0}", detailedError);
436444
return false;
437445
}
438446

src/core/Akka.Remote/Transport/DotNetty/DotNettyTransportSettings.cs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,231 @@ private SslSettings(string certificatePath, string certificatePassword, X509KeyS
436436
RequireMutualAuthentication = requireMutualAuthentication;
437437
}
438438
}
439+
440+
/// <summary>
441+
/// INTERNAL API
442+
///
443+
/// Helper class for building human-readable error messages for TLS/SSL certificate validation failures.
444+
/// Provides detailed diagnostics and actionable suggestions for common certificate issues.
445+
/// </summary>
446+
internal static class TlsErrorMessageBuilder
447+
{
448+
/// <summary>
449+
/// Builds a detailed error message for SSL policy errors encountered during TLS handshake.
450+
/// </summary>
451+
/// <param name="errors">The SSL policy errors from certificate validation callback</param>
452+
/// <param name="certificate">The certificate that failed validation (may be null)</param>
453+
/// <param name="chain">The X509 chain used for validation (may be null)</param>
454+
/// <returns>A human-readable error message with diagnostics and suggestions</returns>
455+
public static string BuildSslPolicyErrorMessage(
456+
System.Net.Security.SslPolicyErrors errors,
457+
X509Certificate2? certificate,
458+
X509Chain? chain)
459+
{
460+
var message = new System.Text.StringBuilder();
461+
message.AppendLine("TLS/SSL certificate validation failed:");
462+
463+
// Interpret SslPolicyErrors flags
464+
if ((errors & System.Net.Security.SslPolicyErrors.None) != System.Net.Security.SslPolicyErrors.None)
465+
{
466+
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNotAvailable) != 0)
467+
{
468+
message.AppendLine(" - Remote certificate not available");
469+
message.AppendLine(" Suggestion: Ensure the remote endpoint provides a valid TLS certificate");
470+
}
471+
472+
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
473+
{
474+
message.AppendLine(" - Remote certificate name mismatch");
475+
message.AppendLine(" Suggestion: Verify certificate CN/SAN matches the target hostname");
476+
if (certificate != null)
477+
{
478+
var cn = certificate.GetNameInfo(X509NameType.DnsName, false);
479+
message.AppendLine($" Certificate CN: {cn}");
480+
}
481+
}
482+
483+
if ((errors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
484+
{
485+
message.AppendLine(" - Certificate chain validation errors");
486+
487+
if (chain != null && chain.ChainStatus.Length > 0)
488+
{
489+
var chainStatusMsg = BuildX509ChainStatusMessage(chain.ChainStatus);
490+
message.Append(chainStatusMsg);
491+
}
492+
else
493+
{
494+
message.AppendLine(" Suggestion: Certificate chain cannot be validated. " +
495+
"Install required intermediate CA certificates.");
496+
}
497+
}
498+
}
499+
500+
// Add certificate details if available
501+
if (certificate != null)
502+
{
503+
message.AppendLine($"\nCertificate Details:");
504+
message.AppendLine($" Subject: {certificate.Subject}");
505+
message.AppendLine($" Issuer: {certificate.Issuer}");
506+
message.AppendLine($" Thumbprint: {certificate.Thumbprint}");
507+
message.AppendLine($" Valid From: {certificate.NotBefore:yyyy-MM-dd HH:mm:ss}");
508+
message.AppendLine($" Valid To: {certificate.NotAfter:yyyy-MM-dd HH:mm:ss}");
509+
message.AppendLine($" Has Private Key: {certificate.HasPrivateKey}");
510+
}
511+
512+
return message.ToString().TrimEnd();
513+
}
514+
515+
/// <summary>
516+
/// Builds a detailed message explaining X509 chain status errors.
517+
/// </summary>
518+
/// <param name="chainStatus">Array of chain status from X509Chain validation</param>
519+
/// <returns>Human-readable explanation of chain errors with suggestions</returns>
520+
public static string BuildX509ChainStatusMessage(X509ChainStatus[] chainStatus)
521+
{
522+
var message = new System.Text.StringBuilder();
523+
524+
foreach (var status in chainStatus)
525+
{
526+
// Skip "NoError" status
527+
if (status.Status == X509ChainStatusFlags.NoError)
528+
continue;
529+
530+
message.AppendLine($" - {status.Status}: {status.StatusInformation}");
531+
532+
// Add specific suggestions based on chain status
533+
var suggestion = GetChainStatusSuggestion(status.Status);
534+
if (!string.IsNullOrEmpty(suggestion))
535+
{
536+
message.AppendLine($" Suggestion: {suggestion}");
537+
}
538+
}
539+
540+
return message.ToString();
541+
}
542+
543+
/// <summary>
544+
/// Maps X509ChainStatusFlags to actionable suggestions for fixing the issue.
545+
/// </summary>
546+
private static string GetChainStatusSuggestion(X509ChainStatusFlags status)
547+
{
548+
return status switch
549+
{
550+
X509ChainStatusFlags.NotTimeValid =>
551+
"Certificate has expired or is not yet valid. Check system clock and certificate validity period.",
552+
553+
X509ChainStatusFlags.NotTimeNested =>
554+
"Certificate validity period does not nest correctly within the chain.",
555+
556+
X509ChainStatusFlags.Revoked =>
557+
"Certificate has been revoked. Contact certificate issuer.",
558+
559+
X509ChainStatusFlags.NotSignatureValid =>
560+
"Certificate signature is invalid. Certificate may be corrupted.",
561+
562+
X509ChainStatusFlags.NotValidForUsage =>
563+
"Certificate is not valid for the intended usage. Check Extended Key Usage (EKU) extensions.",
564+
565+
X509ChainStatusFlags.UntrustedRoot =>
566+
"Certificate chain terminates in an untrusted root. Install root CA certificate in Trusted Root Certification Authorities store.",
567+
568+
X509ChainStatusFlags.RevocationStatusUnknown =>
569+
"Revocation status cannot be determined. Check network connectivity to CRL/OCSP endpoints.",
570+
571+
X509ChainStatusFlags.Cyclic =>
572+
"Certificate chain contains a cycle. Certificate configuration is invalid.",
573+
574+
X509ChainStatusFlags.InvalidExtension =>
575+
"Certificate contains an invalid extension.",
576+
577+
X509ChainStatusFlags.InvalidPolicyConstraints =>
578+
"Certificate policy constraints are invalid.",
579+
580+
X509ChainStatusFlags.InvalidBasicConstraints =>
581+
"Basic constraints are invalid. CA certificate may be missing CA:TRUE constraint.",
582+
583+
X509ChainStatusFlags.InvalidNameConstraints =>
584+
"Name constraints in certificate are invalid.",
585+
586+
X509ChainStatusFlags.HasNotSupportedNameConstraint =>
587+
"Certificate contains name constraints that are not supported.",
588+
589+
X509ChainStatusFlags.HasNotDefinedNameConstraint =>
590+
"Certificate has undefined name constraints.",
591+
592+
X509ChainStatusFlags.HasNotPermittedNameConstraint =>
593+
"Certificate name violates name constraints.",
594+
595+
X509ChainStatusFlags.HasExcludedNameConstraint =>
596+
"Certificate name is explicitly excluded by name constraints.",
597+
598+
X509ChainStatusFlags.PartialChain =>
599+
"Certificate chain is incomplete. Install all intermediate CA certificates from your certificate provider.",
600+
601+
X509ChainStatusFlags.CtlNotTimeValid =>
602+
"Certificate Trust List (CTL) is not time-valid.",
603+
604+
X509ChainStatusFlags.CtlNotSignatureValid =>
605+
"Certificate Trust List (CTL) signature is invalid.",
606+
607+
X509ChainStatusFlags.CtlNotValidForUsage =>
608+
"Certificate Trust List (CTL) is not valid for this usage.",
609+
610+
X509ChainStatusFlags.OfflineRevocation =>
611+
"Revocation checking is offline. Enable network access or disable revocation checking for testing.",
612+
613+
X509ChainStatusFlags.NoIssuanceChainPolicy =>
614+
"Certificate does not have a valid issuance policy.",
615+
616+
X509ChainStatusFlags.ExplicitDistrust =>
617+
"Certificate is explicitly distrusted. Remove from Distrusted Certificates store if this is incorrect.",
618+
619+
X509ChainStatusFlags.HasNotSupportedCriticalExtension =>
620+
"Certificate has an unsupported critical extension.",
621+
622+
X509ChainStatusFlags.HasWeakSignature =>
623+
"Certificate uses a weak signature algorithm (e.g., SHA1). Use SHA256 or stronger.",
624+
625+
_ => string.Empty
626+
};
627+
}
628+
629+
/// <summary>
630+
/// Builds an error message for TLS handshake exceptions.
631+
/// Attempts to extract meaningful information from CryptographicException and AuthenticationException.
632+
/// </summary>
633+
public static string BuildTlsHandshakeErrorMessage(Exception exception, bool isClient)
634+
{
635+
var role = isClient ? "Client" : "Server";
636+
var message = new System.Text.StringBuilder();
637+
638+
message.AppendLine($"TLS handshake failed ({role} side):");
639+
message.AppendLine($" Error: {exception.Message}");
640+
641+
// Provide role-specific suggestions
642+
if (isClient)
643+
{
644+
message.AppendLine("\nClient-side TLS troubleshooting:");
645+
message.AppendLine(" - Verify server certificate is trusted (install root CA if using self-signed)");
646+
message.AppendLine(" - Check certificate hostname matches connection target");
647+
message.AppendLine(" - For mutual TLS, ensure client certificate is configured, accessible, and trusted by server");
648+
message.AppendLine(" - Server and client certificates must have compatible trust chains");
649+
}
650+
else
651+
{
652+
message.AppendLine("\nServer-side TLS troubleshooting:");
653+
message.AppendLine(" - Verify server certificate has accessible private key");
654+
message.AppendLine(" - For mutual TLS, check if client is providing a certificate");
655+
message.AppendLine(" - Review certificate validation requirements (suppress-validation for testing)");
656+
}
657+
658+
if (exception.InnerException != null)
659+
{
660+
message.AppendLine($"\nInner Exception: {exception.InnerException.Message}");
661+
}
662+
663+
return message.ToString().TrimEnd();
664+
}
665+
}
439666
}

src/core/Akka.Remote/Transport/DotNetty/TcpTransport.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,14 @@ public override void UserEventTriggered(IChannelHandlerContext context, object e
8383
if (evt is TlsHandshakeCompletionEvent { IsSuccessful: false } tlsEvent)
8484
{
8585
var ex = tlsEvent.Exception ?? new Exception("TLS handshake failed.");
86-
Log.Error(ex, "TLS handshake failed. Channel [{0}->{1}](Id={2})",
87-
context.Channel.LocalAddress, context.Channel.RemoteAddress, context.Channel.Id);
86+
87+
// Determine if this is client or server side based on handler type
88+
var isClient = this is TcpClientHandler;
89+
var detailedError = TlsErrorMessageBuilder.BuildTlsHandshakeErrorMessage(ex, isClient);
90+
91+
Log.Error(ex, "TLS handshake failed on channel [{0}->{1}](Id={2})\n{3}",
92+
context.Channel.LocalAddress, context.Channel.RemoteAddress,
93+
context.Channel.Id, detailedError);
8894

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

121127
NotifyListener(new Disassociated(DisassociateInfo.Shutdown));
122128
}
129+
// Enhanced TLS exception handling
130+
else if (exception is System.Security.Authentication.AuthenticationException
131+
or System.Security.Cryptography.CryptographicException)
132+
{
133+
// Determine if this is client or server side based on handler type
134+
var isClient = this is TcpClientHandler;
135+
var detailedError = TlsErrorMessageBuilder.BuildTlsHandshakeErrorMessage(exception, isClient);
136+
137+
Log.Error(exception, "TLS exception on channel [{0}->{1}](Id={2})\n{3}",
138+
context.Channel.LocalAddress, context.Channel.RemoteAddress, context.Channel.Id, detailedError);
139+
140+
NotifyListener(new Disassociated(DisassociateInfo.Unknown));
141+
}
123142
else
124143
{
125144
base.ExceptionCaught(context, exception);

0 commit comments

Comments
 (0)