diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs index 3ee769a26c4fae..d63a79c660fe3a 100644 --- a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs @@ -135,6 +135,12 @@ private static partial int AppleCryptoNative_SslSetTargetName( [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_SslHandshake")] internal static partial PAL_TlsHandshakeState SslHandshake(SafeSslHandle sslHandle); + [LibraryImport(Interop.Libraries.AppleCryptoNative)] + private static partial int AppleCryptoNative_SslSetError( + SafeSslHandle sslHandle, + TlsAlertMessage alertMessage, + out int pOSStatus); + [LibraryImport(Interop.Libraries.AppleCryptoNative)] private static partial int AppleCryptoNative_SslSetAcceptClientCert(SafeSslHandle sslHandle); @@ -328,6 +334,25 @@ internal static void SslBreakOnCertRequested(SafeSslHandle sslHandle, bool setBr throw new SslException(); } + internal static void SslSetError(SafeSslHandle sslHandle, TlsAlertMessage alertMessage) + { + int osStatus; + int result = AppleCryptoNative_SslSetError(sslHandle, alertMessage, out osStatus); + + if (result == 1) + { + return; + } + + if (result == 0) + { + throw CreateExceptionForOSStatus(osStatus); + } + + Debug.Fail($"AppleCryptoNative_SslSetError returned {result}"); + throw new SslException(); + } + internal static void SslSetCertificate(SafeSslHandle sslHandle, ReadOnlySpan certChainPtrs) { using (SafeCreateHandle cfCertRefs = CoreFoundation.CFArrayCreate(certChainPtrs)) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index 9ee1c4519b234f..5b1e3476f42779 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -374,6 +374,14 @@ private async Task ForceAuthenticationAsync(bool receiveFirst, byte[ throw new AuthenticationException(SR.Format(SR.net_auth_tls_alert, _lastFrame.AlertDescription.ToString()), token.GetException()); } + if (token.Status.ErrorCode == SecurityStatusPalErrorCode.CertValidationFailed && token.GetException() is Exception certException) + { + // The cert-validation path carries the user-visible exception directly; + // throw it without wrapping to preserve parity with the SendAuthResetSignal + // path used after handshake completion on all platforms. + ExceptionDispatchInfo.Throw(certException); + } + throw new AuthenticationException(SR.net_auth_SSPI, token.GetException()); } else if (token.Status.ErrorCode == SecurityStatusPalErrorCode.OK) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 4fc8b6732aa086..da230c89bf986e 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -898,6 +898,13 @@ private ProtocolToken GenerateToken(ReadOnlySpan inputBuffer, out int cons _sslAuthenticationOptions); } } + +#if TARGET_APPLE + if (token.Status.ErrorCode == SecurityStatusPalErrorCode.CertValidationNeeded) + { + token = VerifyRemoteCertificateAndGenerateNextToken(token); + } +#endif } while (cachedCreds && _credentialsHandle == null); } finally @@ -934,6 +941,28 @@ private ProtocolToken GenerateToken(ReadOnlySpan inputBuffer, out int cons return token; } +#if TARGET_APPLE + private ProtocolToken VerifyRemoteCertificateAndGenerateNextToken(ProtocolToken token) + { + // SecureTransport pauses the handshake (errSSL{Server,Client}AuthCompleted) before + // any bytes are produced for the next handshake flight, so the pending-writes buffer + // drained into token should be empty here. Assert to catch any future regression + // that would silently drop handshake bytes. + Debug.Assert(token.Size == 0, "Expected empty payload at CertValidationNeeded pause; dropping non-empty payload would lose handshake bytes."); + token.ReleasePayload(); + + ProtocolToken alertToken = default; + + if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)) + { + alertToken.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.CertValidationFailed, CreateCertificateValidationException(_sslAuthenticationOptions, sslPolicyErrors, chainStatus)); + return alertToken; + } + + return GenerateToken(ReadOnlySpan.Empty, out _); + } +#endif + internal ProtocolToken Renegotiate() { Debug.Assert(_securityContext != null); @@ -1244,7 +1273,7 @@ internal bool VerifyRemoteCertificate( if (!success) { #pragma warning disable CS0162 // unreachable code detected (compile time const) - if (SslStreamPal.CanGenerateCustomAlerts && !SslStreamPal.CertValidationInCallback) + if (SslStreamPal.CanGenerateCustomAlertsForContext(_securityContext) && !SslStreamPal.CertValidationInCallback) { CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); } @@ -1298,6 +1327,17 @@ private void CreateFatalHandshakeAlertToken(SslPolicyErrors sslPolicyErrors, X50 } } +#if TARGET_APPLE + if (_securityContext is not null && !SslStreamPal.IsAsyncSecurityContext(_securityContext)) + { + byte[] alertFrame = TlsFrameHelper.CreateAlertFrame(_lastFrame.Header.Version, (TlsAlertDescription)alertMessage); + if (alertFrame.Length != 0) + { + alertToken.SetPayload(alertFrame); + return; + } + } +#endif alertToken = GenerateAlertToken(); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs index 7b33f5a48dca9f..223e13a1d4a674 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs @@ -28,6 +28,11 @@ public static Exception GetException(SecurityStatusPal status) // due to handshake failures. internal const bool CanGenerateCustomAlerts = true; + internal static bool CanGenerateCustomAlertsForContext(SafeDeleteContext? _) + { + return CanGenerateCustomAlerts; + } + public static void VerifyPackageInfo() { } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs index 19cfd42baf353e..9386e8dcc10c68 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.OSX.cs @@ -32,7 +32,12 @@ public static Exception GetException(SecurityStatusPal status) // Since ST is not producing the framed empty message just call this false and avoid the // special case of an empty array being passed to the `fixed` statement. internal const bool CanEncryptEmptyMessage = false; - internal const bool CanGenerateCustomAlerts = false; + internal const bool CanGenerateCustomAlerts = true; + + internal static bool CanGenerateCustomAlertsForContext(SafeDeleteContext? securityContext) + { + return securityContext is SafeDeleteSslContext; + } public static void VerifyPackageInfo() { @@ -409,13 +414,7 @@ private static SecurityStatusPal PerformHandshake(SafeSslHandle sslHandle) return new SecurityStatusPal(SecurityStatusPalErrorCode.ContinueNeeded); case PAL_TlsHandshakeState.ServerAuthCompleted: case PAL_TlsHandshakeState.ClientAuthCompleted: - // The standard flow would be to call the verification callback now, and - // possibly abort. But the library is set up to call this "success" and - // do verification between "handshake complete" and "first send/receive". - // - // So, call SslHandshake again to indicate to Secure Transport that we've - // accepted this handshake and it should go into the ready state. - break; + return new SecurityStatusPal(SecurityStatusPalErrorCode.CertValidationNeeded); case PAL_TlsHandshakeState.ClientCertRequested: return new SecurityStatusPal(SecurityStatusPalErrorCode.CredentialsNeeded); case PAL_TlsHandshakeState.ClientHelloReceived: @@ -434,11 +433,23 @@ public static SecurityStatusPal ApplyAlertToken( TlsAlertType alertType, TlsAlertMessage alertMessage) { - // There doesn't seem to be an exposed API for writing an alert, - // the API seems to assume that all alerts are generated internally by - // SSLHandshake. Debug.Assert(CanGenerateCustomAlerts); - return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); + Debug.Assert(alertType == TlsAlertType.Fatal, $"SecureTransport derives the alert level from the OSStatus and emits only fatal alerts; unexpected alertType: {alertType}"); + + if (securityContext is not SafeDeleteSslContext context) + { + return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError); + } + + try + { + Interop.AppleCrypto.SslSetError(context.SslContext, alertMessage); + return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); + } + catch (Exception ex) + { + return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, ex); + } } #pragma warning restore IDE0060 diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs index cf4c5492dec78b..6e06a8e9d6f4e6 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs @@ -22,6 +22,11 @@ public static Exception GetException(SecurityStatusPal status) internal const bool CanEncryptEmptyMessage = false; internal const bool CanGenerateCustomAlerts = false; + internal static bool CanGenerateCustomAlertsForContext(SafeDeleteContext? _) + { + return CanGenerateCustomAlerts; + } + public static void VerifyPackageInfo() { } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index e83273147248d4..37040be88ebb40 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -57,6 +57,11 @@ public static Exception GetException(SecurityStatusPal status) internal const bool CanEncryptEmptyMessage = true; internal const bool CanGenerateCustomAlerts = true; + internal static bool CanGenerateCustomAlertsForContext(SafeDeleteContext? _) + { + return CanGenerateCustomAlerts; + } + private static readonly byte[] s_sessionTokenBuffer = InitSessionTokenBuffer(); private static byte[] InitSessionTokenBuffer() diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs index 78fee9964891bf..ca9848df93785c 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/TlsFrameHelper.cs @@ -335,7 +335,7 @@ public static byte[] CreateAlertFrame(SslProtocols version, TlsAlertDescription return CreateProtocolVersionAlert(version); } #pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete - else if ((int)version > (int)SslProtocols.Tls) + else if ((int)version >= (int)SslProtocols.Tls) #pragma warning restore SYSLIB0039 { // Create TLS1.2 alert diff --git a/src/libraries/System.Net.Security/src/System/Net/SecurityStatusPal.cs b/src/libraries/System.Net.Security/src/System/Net/SecurityStatusPal.cs index 7a28d1448a7ec0..94a22c717f02a1 100644 --- a/src/libraries/System.Net.Security/src/System/Net/SecurityStatusPal.cs +++ b/src/libraries/System.Net.Security/src/System/Net/SecurityStatusPal.cs @@ -34,6 +34,7 @@ internal enum SecurityStatusPalErrorCode Renegotiate, TryAgain, HandshakeStarted, + CertValidationNeeded, // Errors OutOfMemory, @@ -74,6 +75,7 @@ internal enum SecurityStatusPalErrorCode NoRenegotiation, KeySetDoesNotExist, ContextExpiredError, + CertValidationFailed, MutualAuthFailed } } diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs index 57f1794af59af9..89519d77118259 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net.Test.Common; @@ -210,8 +211,8 @@ await Task.WhenAll( } [Theory] - [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] + [MemberData(nameof(SupportedSslProtocolsExcludingMacOSSsl3))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux | TestPlatforms.OSX))] public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols protocol) { // When no RemoteCertificateValidationCallback is set and the server's cert @@ -221,9 +222,10 @@ public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols pro X509Certificate2 cert = Configuration.Certificates.GetSelfSignedServerCertificate(); (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); using (clientStream) - using (serverStream) + using (RecordingReadStream recordingServerStream = new RecordingReadStream(serverStream)) using (SslStream client = new SslStream(clientStream)) - using (SslStream server = new SslStream(serverStream)) + using (SslStream server = new SslStream(recordingServerStream)) + using (cert) { var serverOptions = new SslServerAuthenticationOptions { @@ -233,7 +235,7 @@ public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols pro var clientOptions = new SslClientAuthenticationOptions { - TargetHost = "localhost", + TargetHost = cert.GetNameInfo(X509NameType.DnsName, false), CertificateRevocationCheckMode = X509RevocationMode.NoCheck, EnabledSslProtocols = protocol, }; @@ -242,11 +244,11 @@ public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols pro Task clientTask = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); // Client should fail because the validation failed locally, and it should send an alert. - await Assert.ThrowsAsync(() => clientTask).WaitAsync(TestConfiguration.PassingTestTimeout); + AuthenticationException clientException = await Assert.ThrowsAsync(() => clientTask).WaitAsync(TestConfiguration.PassingTestTimeout); // Server side should receive the alert and fail the handshake, the exact timing depends on the platform // Windows: after the handshake, during data exchange - // Linux: during the handshake + // Linux/macOS: during the handshake Exception exception = await Assert.ThrowsAnyAsync(async () => { await serverTask; @@ -255,23 +257,32 @@ public async Task SslStream_NoCallback_UntrustedCert_SendsAlert(SslProtocols pro await server.ReadAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); }).WaitAsync(TestConfiguration.PassingTestTimeout); - Assert.NotNull(exception.InnerException); if (PlatformDetection.IsWindows) { Assert.IsType(exception); Assert.IsType(exception.InnerException); } - - if (PlatformDetection.IsLinux) + else if (PlatformDetection.IsLinux) { Assert.IsType(exception); + Assert.NotNull(exception.InnerException); + } + else if (PlatformDetection.IsOSX) + { + // Cross-platform parity: the cert validation exception should surface directly + // to the caller (matching Windows/Linux/Android behavior via SendAuthResetSignal), + // not wrapped in a generic AuthenticationException(SR.net_auth_SSPI, ...). + Assert.Null(clientException.InnerException); + + // Verify the actual UnknownCA alert bytes reached the peer over the wire. + Assert.True(recordingServerStream.ContainsAlert(TlsAlertDescription.UnknownCA), recordingServerStream.GetRecordedAlerts()); } } } [Theory] - [ClassData(typeof(SslProtocolSupport.SupportedSslProtocolsTestData))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux))] + [MemberData(nameof(SupportedSslProtocolsExcludingMacOSSsl3))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/18837", ~(TestPlatforms.Windows | TestPlatforms.Linux | TestPlatforms.OSX))] public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslProtocols protocol) { // When the server requires a client certificate and no @@ -280,10 +291,11 @@ public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslP X509Certificate2 cert = Configuration.Certificates.GetSelfSignedServerCertificate(); (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); - using (clientStream) + using (RecordingReadStream recordingClientStream = new RecordingReadStream(clientStream)) using (serverStream) - using (SslStream client = new SslStream(clientStream)) + using (SslStream client = new SslStream(recordingClientStream)) using (SslStream server = new SslStream(serverStream)) + using (cert) { var serverOptions = new SslServerAuthenticationOptions { @@ -294,7 +306,7 @@ public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslP var clientOptions = new SslClientAuthenticationOptions { - TargetHost = "localhost", + TargetHost = cert.GetNameInfo(X509NameType.DnsName, false), CertificateRevocationCheckMode = X509RevocationMode.NoCheck, RemoteCertificateValidationCallback = delegate { return true; }, ClientCertificates = new X509CertificateCollection { cert }, @@ -305,11 +317,12 @@ public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslP Task clientTask = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None); // Server should fail because the validation failed locally, and it should send an alert. - await Assert.ThrowsAsync(() => serverTask).WaitAsync(TestConfiguration.PassingTestTimeout); + AuthenticationException serverException = await Assert.ThrowsAsync(() => serverTask).WaitAsync(TestConfiguration.PassingTestTimeout); // Client side should receive the alert and fail the handshake, the exact timing depends on the platform // Windows: after the handshake, during data exchange // Linux: during the handshake, TLS 1.3 sends the alert after the handshake + // macOS: during the handshake Exception exception = await Assert.ThrowsAnyAsync(async () => { await clientTask; @@ -318,15 +331,15 @@ public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslP await client.ReadAsync(buffer).AsTask().WaitAsync(TestConfiguration.PassingTestTimeout); }).WaitAsync(TestConfiguration.PassingTestTimeout); - Assert.NotNull(exception.InnerException); if (PlatformDetection.IsWindows) { + Assert.NotNull(exception.InnerException); Assert.IsType(exception); Assert.IsType(exception.InnerException); } - - if (PlatformDetection.IsLinux) + else if (PlatformDetection.IsLinux) { + Assert.NotNull(exception.InnerException); if (protocol == SslProtocols.Tls13) { // failure during app data (read) @@ -342,6 +355,21 @@ public async Task SslStream_NoCallback_UntrustedClientCert_ServerSendsAlert(SslP Assert.NotNull(exception.InnerException.InnerException); Assert.Contains("alert", exception.InnerException.InnerException.Message); } + else if (PlatformDetection.IsOSX) + { + // Cross-platform parity: the cert validation exception should surface directly + // to the caller (matching Windows/Linux/Android behavior via SendAuthResetSignal), + // not wrapped in a generic AuthenticationException(SR.net_auth_SSPI, ...). + Assert.Null(serverException.InnerException); + + // Verify a fatal TLS alert reached the peer. SecureTransport may surface + // an untrusted-chain failure as either UnknownCA or BadCertificate depending + // on the negotiated protocol; both are valid "cert rejected" signals. + Assert.True( + recordingClientStream.ContainsAlert(TlsAlertDescription.UnknownCA) || + recordingClientStream.ContainsAlert(TlsAlertDescription.BadCertificate), + recordingClientStream.GetRecordedAlerts()); + } } } @@ -350,6 +378,20 @@ private bool FailClientCertificate(object sender, X509Certificate certificate, X return false; } + public static IEnumerable SupportedSslProtocolsExcludingMacOSSsl3() + { +#pragma warning disable 0618 // SSL2/3 are deprecated + // SecureTransport on modern macOS won't negotiate SSL 3.0, so handshakes hang + // until they time out. Mask Ssl3 out on macOS so xunit reports the data point + // as filtered rather than silently passing. + SslProtocols mask = PlatformDetection.IsOSX ? ~SslProtocols.Ssl3 : (SslProtocols)~0; +#pragma warning restore 0618 + foreach (SslProtocols protocol in SslProtocolSupport.EnumerateSupportedProtocols(mask)) + { + yield return new object[] { protocol }; + } + } + private bool AllowAnyServerCertificate( object sender, X509Certificate certificate, @@ -358,5 +400,125 @@ private bool AllowAnyServerCertificate( { return true; } + + private sealed class RecordingReadStream : DelegatingStream + { + private readonly List _readBytes = new List(); + + public RecordingReadStream(Stream innerStream) + : base(innerStream) + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesRead = base.Read(buffer, offset, count); + Record(new ReadOnlySpan(buffer, offset, bytesRead)); + return bytesRead; + } + + public override int Read(Span buffer) + { + int bytesRead = base.Read(buffer); + Record(buffer.Slice(0, bytesRead)); + return bytesRead; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int bytesRead = await base.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + Record(new ReadOnlySpan(buffer, offset, bytesRead)); + return bytesRead; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int bytesRead = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + Record(buffer.Span.Slice(0, bytesRead)); + return bytesRead; + } + + public bool ContainsAlert(TlsAlertDescription expectedDescription) + { + ReadOnlySpan remaining = GetRecordedBytes(); + while (remaining.Length >= TlsFrameHelper.HeaderSize) + { + TlsFrameHeader header = default; + if (!TlsFrameHelper.TryGetFrameHeader(remaining, ref header) || + header.Length <= 0 || + header.Length > remaining.Length) + { + return false; + } + + ReadOnlySpan frame = remaining.Slice(0, header.Length); + if (header.Type == TlsContentType.Alert) + { + TlsAlertLevel level = default; + TlsAlertDescription description = default; + if (TlsFrameHelper.TryGetAlertInfo(frame, ref level, ref description) && + level == TlsAlertLevel.Fatal && + description == expectedDescription) + { + return true; + } + } + + remaining = remaining.Slice(header.Length); + } + + return false; + } + + public string GetRecordedAlerts() + { + List alerts = new List(); + ReadOnlySpan remaining = GetRecordedBytes(); + while (remaining.Length >= TlsFrameHelper.HeaderSize) + { + TlsFrameHeader header = default; + if (!TlsFrameHelper.TryGetFrameHeader(remaining, ref header) || + header.Length <= 0 || + header.Length > remaining.Length) + { + break; + } + + ReadOnlySpan frame = remaining.Slice(0, header.Length); + if (header.Type == TlsContentType.Alert) + { + TlsAlertLevel level = default; + TlsAlertDescription description = default; + if (TlsFrameHelper.TryGetAlertInfo(frame, ref level, ref description)) + { + alerts.Add($"{level}:{description}"); + } + } + + remaining = remaining.Slice(header.Length); + } + + return alerts.Count == 0 ? "No TLS alerts recorded." : "Recorded TLS alerts: " + string.Join(", ", alerts); + } + + private void Record(ReadOnlySpan bytes) + { + lock (_readBytes) + { + foreach (byte b in bytes) + { + _readBytes.Add(b); + } + } + } + + private byte[] GetRecordedBytes() + { + lock (_readBytes) + { + return _readBytes.ToArray(); + } + } + } } } diff --git a/src/libraries/System.Net.Security/tests/UnitTests/TlsAlertsMatchWindowsInterop.cs b/src/libraries/System.Net.Security/tests/UnitTests/TlsAlertsMatchWindowsInterop.cs index 3627b6aa9a6ee6..43ef4ab7999687 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/TlsAlertsMatchWindowsInterop.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/TlsAlertsMatchWindowsInterop.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Security.Authentication; using Xunit; namespace System.Net.Security.Tests @@ -38,5 +40,23 @@ public void TlsAlertEnums_MatchWindowsInterop_Ok() Assert.Equal((int)TlsAlertMessage.NoRenegotiation, Interop.SChannel.TLS1_ALERT_NO_RENEGOTIATION); Assert.Equal((int)TlsAlertMessage.UnsupportedExtension, Interop.SChannel.TLS1_ALERT_UNSUPPORTED_EXT); } + + [Theory] + [MemberData(nameof(AlertFrameData))] + public void CreateAlertFrame_NonProtocolAlert_UsesRequestedVersion(SslProtocols protocol, byte minorVersion) + { + byte[] frame = TlsFrameHelper.CreateAlertFrame(protocol, TlsAlertDescription.UnknownCA); + + Assert.Equal(new byte[] { (byte)TlsContentType.Alert, 3, minorVersion, 0, 2, (byte)TlsAlertLevel.Fatal, (byte)TlsAlertDescription.UnknownCA }, frame); + } + + public static IEnumerable AlertFrameData() + { +#pragma warning disable SYSLIB0039 + yield return new object[] { SslProtocols.Tls, (byte)1 }; + yield return new object[] { SslProtocols.Tls11, (byte)2 }; +#pragma warning restore SYSLIB0039 + yield return new object[] { SslProtocols.Tls12, (byte)3 }; + } } } diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c index d1b4305dd0f7a3..7ddefdcca13db3 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -103,6 +103,7 @@ static const Entry s_cryptoAppleNative[] = DllImportEntry(AppleCryptoNative_SSLSetALPNProtocols) DllImportEntry(AppleCryptoNative_SslGetAlpnSelected) DllImportEntry(AppleCryptoNative_SslHandshake) + DllImportEntry(AppleCryptoNative_SslSetError) DllImportEntry(AppleCryptoNative_SslShutdown) DllImportEntry(AppleCryptoNative_SslGetProtocolVersion) DllImportEntry(AppleCryptoNative_SslGetCipherSuite) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.c b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.c index a296d6b63097ad..88898efc9b7b64 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.c @@ -338,6 +338,89 @@ PAL_TlsHandshakeState AppleCryptoNative_SslHandshake(SSLContextRef sslContext) } } +static OSStatus PalTlsAlertMessageToSslStatus(PAL_TlsAlertMessage alertMessage) +{ + switch (alertMessage) + { + case PAL_TlsAlertMessage_UnexpectedMessage: + return errSSLUnexpectedMessage; + case PAL_TlsAlertMessage_BadRecordMac: + return errSSLBadRecordMac; + case PAL_TlsAlertMessage_DecryptionFailed: + return errSSLDecryptionFail; + case PAL_TlsAlertMessage_RecordOverflow: + return errSSLRecordOverflow; + case PAL_TlsAlertMessage_DecompressionFail: + return errSSLDecompressFail; + case PAL_TlsAlertMessage_HandshakeFailure: + return errSSLHandshakeFail; + case PAL_TlsAlertMessage_BadCertificate: + return errSSLBadCert; + case PAL_TlsAlertMessage_UnsupportedCert: + return errSSLBadCert; + case PAL_TlsAlertMessage_CertificateRevoked: + return errSSLXCertChainInvalid; + case PAL_TlsAlertMessage_CertificateExpired: + return errSSLCertExpired; + case PAL_TlsAlertMessage_CertificateUnknown: + return errSSLXCertChainInvalid; + case PAL_TlsAlertMessage_IllegalParameter: + return errSSLIllegalParam; + case PAL_TlsAlertMessage_UnknownCA: + return errSSLUnknownRootCert; + case PAL_TlsAlertMessage_AccessDenied: + return errSSLPeerAccessDenied; + case PAL_TlsAlertMessage_DecodeError: + return errSSLDecodeError; + case PAL_TlsAlertMessage_DecryptError: + return errSSLPeerDecryptError; + case PAL_TlsAlertMessage_ExportRestriction: + return errSSLPeerExportRestriction; + case PAL_TlsAlertMessage_ProtocolVersion: + return errSSLPeerProtocolVersion; + case PAL_TlsAlertMessage_InsufficientSecurity: + return errSSLPeerInsufficientSecurity; + case PAL_TlsAlertMessage_InternalError: + return errSSLPeerInternalError; + case PAL_TlsAlertMessage_InappropriateFallback: + return errSSLInappropriateFallback; + case PAL_TlsAlertMessage_UserCanceled: + return errSSLPeerUserCancelled; + case PAL_TlsAlertMessage_NoRenegotiation: + return errSSLPeerNoRenegotiation; + case PAL_TlsAlertMessage_MissingExtension: + return errSSLMissingExtension; + case PAL_TlsAlertMessage_UnsupportedExtension: + return errSSLUnsupportedExtension; + case PAL_TlsAlertMessage_UnrecognizedName: + return errSSLUnrecognizedName; + case PAL_TlsAlertMessage_BadCertificateStatusResponse: + return errSSLBadCertificateStatusResponse; + case PAL_TlsAlertMessage_UnknownPskIdentity: + return errSSLUnknownPSKIdentity; + case PAL_TlsAlertMessage_CertificateRequired: + return errSSLCertificateRequired; + default: + return errSSLPeerInternalError; + } +} + +int32_t AppleCryptoNative_SslSetError(SSLContextRef sslContext, PAL_TlsAlertMessage alertMessage, int32_t* pOSStatus) +{ + if (pOSStatus != NULL) + *pOSStatus = noErr; + + if (sslContext == NULL || pOSStatus == NULL) + return -1; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + *pOSStatus = SSLSetError(sslContext, PalTlsAlertMessageToSslStatus(alertMessage)); +#pragma clang diagnostic pop + + return *pOSStatus == noErr; +} + static PAL_TlsIo OSStatusToPAL_TlsIo(OSStatus status) { switch (status) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.h b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.h index cc492417665f87..6413674d664124 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.h +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_ssl.h @@ -30,6 +30,40 @@ enum }; typedef int32_t PAL_TlsIo; +enum +{ + PAL_TlsAlertMessage_UnexpectedMessage = 10, + PAL_TlsAlertMessage_BadRecordMac = 20, + PAL_TlsAlertMessage_DecryptionFailed = 21, + PAL_TlsAlertMessage_RecordOverflow = 22, + PAL_TlsAlertMessage_DecompressionFail = 30, + PAL_TlsAlertMessage_HandshakeFailure = 40, + PAL_TlsAlertMessage_BadCertificate = 42, + PAL_TlsAlertMessage_UnsupportedCert = 43, + PAL_TlsAlertMessage_CertificateRevoked = 44, + PAL_TlsAlertMessage_CertificateExpired = 45, + PAL_TlsAlertMessage_CertificateUnknown = 46, + PAL_TlsAlertMessage_IllegalParameter = 47, + PAL_TlsAlertMessage_UnknownCA = 48, + PAL_TlsAlertMessage_AccessDenied = 49, + PAL_TlsAlertMessage_DecodeError = 50, + PAL_TlsAlertMessage_DecryptError = 51, + PAL_TlsAlertMessage_ExportRestriction = 60, + PAL_TlsAlertMessage_ProtocolVersion = 70, + PAL_TlsAlertMessage_InsufficientSecurity = 71, + PAL_TlsAlertMessage_InternalError = 80, + PAL_TlsAlertMessage_InappropriateFallback = 86, + PAL_TlsAlertMessage_UserCanceled = 90, + PAL_TlsAlertMessage_NoRenegotiation = 100, + PAL_TlsAlertMessage_MissingExtension = 109, + PAL_TlsAlertMessage_UnsupportedExtension = 110, + PAL_TlsAlertMessage_UnrecognizedName = 112, + PAL_TlsAlertMessage_BadCertificateStatusResponse = 113, + PAL_TlsAlertMessage_UnknownPskIdentity = 115, + PAL_TlsAlertMessage_CertificateRequired = 116, +}; +typedef int32_t PAL_TlsAlertMessage; + /* Create an SSL context, for the Server or Client role as determined by isServer. @@ -199,6 +233,17 @@ Returns an indication of what state the error is in. Any negative number means a */ PALEXPORT PAL_TlsHandshakeState AppleCryptoNative_SslHandshake(SSLContextRef sslContext); +/* +Set the TLS alert to send when the handshake is aborted. + +Returns 1 on success, 0 on failure, other values for invalid state. + +Output: +pOSStatus: Receives the value returned by SSLSetError. +*/ +PALEXPORT int32_t +AppleCryptoNative_SslSetError(SSLContextRef sslContext, PAL_TlsAlertMessage alertMessage, int32_t* pOSStatus); + /* Take bufLen bytes of cleartext data from buf and encrypt/frame the data. Processed data will then be sent into the write callback.