diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets index b655d3879ac449..b76e5aca25ace1 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets @@ -201,6 +201,7 @@ The .NET Foundation licenses this file to you under the MIT license. + diff --git a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs index b82c030e4232d4..f3951931e03910 100644 --- a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs @@ -14,6 +14,7 @@ internal static partial class Libraries internal const string OpenLdap = "libldap.dylib"; internal const string SystemConfigurationLibrary = "/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration"; internal const string AppleCryptoNative = "libSystem.Security.Cryptography.Native.Apple"; + internal const string NetworkFramework = "/System/Library/Frameworks/Network.framework/Network"; internal const string MsQuic = "libmsquic.dylib"; } } diff --git a/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.Tls.cs b/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.Tls.cs new file mode 100644 index 00000000000000..ee216667d6a06a --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.Tls.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + // TLS 1.3 specific Network Framework implementation for macOS + internal static partial class NetworkFramework + { + internal static partial class Tls + { + // Initialize internal shim for NetworkFramework integration + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_Init")] + [return: MarshalAs(UnmanagedType.I4)] + internal static unsafe partial bool Init( + delegate* unmanaged statusCallback, + delegate* unmanaged writeCallback, + delegate* unmanaged challengeCallback); + + // Create a new connection context + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionCreate", StringMarshalling = StringMarshalling.Utf8)] + internal static unsafe partial SafeNwHandle NwConnectionCreate([MarshalAs(UnmanagedType.I4)] bool isServer, IntPtr context, string targetName, byte* alpnBuffer, int alpnLength, SslProtocols minTlsProtocol, SslProtocols maxTlsProtocol, uint* cipherSuites, int cipherSuitesLength); + + // Start the TLS handshake, notifications are received via the status callback (potentially from a different thread). + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionStart")] + internal static partial int NwConnectionStart(SafeNwHandle connection, IntPtr context); + + // takes encrypted input from underlying stream and feed it to the connection. + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwFramerDeliverInput")] + internal static unsafe partial int NwFramerDeliverInput(SafeNwHandle framer, IntPtr context, byte* buffer, int bufferLength, delegate* unmanaged completionCallback); + + // sends plaintext data to the connection. + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionSend")] + internal static unsafe partial void NwConnectionSend(SafeNwHandle connection, IntPtr context, void* buffer, int bufferLength, delegate* unmanaged completionCallback); + + // read plaintext data from the connection. + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionReceive")] + internal static unsafe partial void NwConnectionReceive(SafeNwHandle connection, IntPtr context, int length, delegate* unmanaged readCompletionCallback); + + // starts connection cleanup + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionCancel")] + internal static partial void NwConnectionCancel(SafeNwHandle connection); + + // gets TLS connection information + [LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_GetConnectionInfo")] + internal static unsafe partial int GetConnectionInfo(SafeNwHandle connection, IntPtr context, out SslProtocols pProtocol, out TlsCipherSuite pCipherSuiteOut, byte* negotiatedAlpn, ref int negotiatedAlpnLength); + } + + // Status enumeration for Network Framework TLS operations + internal enum StatusUpdates + { + UnknownError = 0, + FramerStart = 1, + HandshakeFinished = 3, + ConnectionFailed = 4, + ConnectionCancelled = 103, + CertificateAvailable = 104, + DebugLog = 200, + } + } + + // Safe handle classes for Network Framework TLS resources + internal sealed class SafeNwHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeNwHandle() : base(ownsHandle: true) { } + + public SafeNwHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + NetworkFramework.Release(handle); + SetHandle(IntPtr.Zero); + return true; + } + } +} diff --git a/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.cs b/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.cs new file mode 100644 index 00000000000000..d5eb743a333b35 --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.NetworkFramework.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class NetworkFramework + { + // Network Framework reference counting functions + [LibraryImport(Libraries.NetworkFramework, EntryPoint = "nw_retain")] + internal static partial IntPtr Retain(IntPtr obj); + + [LibraryImport(Libraries.NetworkFramework, EntryPoint = "nw_release")] + internal static partial void Release(IntPtr obj); + + // Network Framework error domains + internal enum NetworkFrameworkErrorDomain + { + Invalid = 0, + POSIX = 1, + DNS = 2, + TLS = 3 + } + + internal enum NWErrorDomainPOSIX + { + OperationCanceled = 89, // ECANCELED + } + + internal sealed class NetworkFrameworkException : Exception + { + public int ErrorCode { get; } + public NetworkFrameworkErrorDomain ErrorDomain { get; } + + internal NetworkFrameworkException() + { + } + + internal NetworkFrameworkException(int errorCode, NetworkFrameworkErrorDomain errorDomain, string? message) + : base(message ?? $"Network Framework error {errorCode} in domain {errorDomain}") + { + HResult = errorCode; + ErrorCode = errorCode; + ErrorDomain = errorDomain; + } + + internal NetworkFrameworkException(int errorCode, NetworkFrameworkErrorDomain errorDomain, string? message, Exception innerException) + : base(message ?? $"Network Framework error {errorCode} in domain {errorDomain}", innerException) + { + HResult = errorCode; + ErrorCode = errorCode; + ErrorDomain = errorDomain; + } + + public override string ToString() + { + return $"{base.ToString()}, ErrorCode: {ErrorCode}, ErrorDomain: {ErrorDomain}"; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct NetworkFrameworkError + { + public int ErrorCode; + public int ErrorDomain; + public IntPtr ErrorMessage; // C string of NULL + } + + internal static Exception CreateExceptionForNetworkFrameworkError(in NetworkFrameworkError error) + { + string? message = null; + NetworkFrameworkErrorDomain domain = (NetworkFrameworkErrorDomain)error.ErrorDomain; + + if (error.ErrorMessage != IntPtr.Zero) + { + message = Marshal.PtrToStringUTF8(error.ErrorMessage); + } + + return new NetworkFrameworkException(error.ErrorCode, domain, message); + } + } +} diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.OSStatus.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.OSStatus.cs new file mode 100644 index 00000000000000..ac271a5b4e0f8c --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.OSStatus.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class AppleCrypto + { + internal static class OSStatus + { + public const int NoErr = 0; + public const int ReadErr = -19; + public const int WritErr = -20; + public const int EOFErr = -39; + public const int SecUserCanceled = -128; + public const int ErrSSLWouldBlock = -9803; + } + } +} diff --git a/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs b/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs index 78fe5e65c4d49f..7019d6f1da4917 100644 --- a/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs +++ b/src/libraries/Common/src/System/Net/ReadWriteAdapter.cs @@ -14,6 +14,8 @@ internal interface IReadWriteAdapter static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken); static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken); static abstract Task WaitAsync(TaskCompletionSource waiter); + static abstract Task WaitAsync(Task task); + static abstract ValueTask WaitAsync(ValueTask task); } internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter @@ -30,6 +32,8 @@ public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory buffer, C public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) => stream.FlushAsync(cancellationToken); public static Task WaitAsync(TaskCompletionSource waiter) => waiter.Task; + public static Task WaitAsync(Task task) => task; + public static ValueTask WaitAsync(ValueTask task) => task; } internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter @@ -57,5 +61,16 @@ public static Task WaitAsync(TaskCompletionSource waiter) waiter.Task.GetAwaiter().GetResult(); return Task.CompletedTask; } + + public static Task WaitAsync(Task task) + { + task.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + + public static ValueTask WaitAsync(ValueTask task) + { + return ValueTask.FromResult(task.AsTask().GetAwaiter().GetResult()); + } } } diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 9a7ef0734ad082..ebdd48c679fc97 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -547,7 +547,7 @@ private static bool GetSsl3Support() } - return (IsOSX || (IsLinux && OpenSslVersion < new Version(1, 0, 2) && !IsDebian)); + return ((IsOSX && !IsNetworkFrameworkEnabled()) || (IsLinux && OpenSslVersion < new Version(1, 0, 2) && !IsDebian)); } private static bool OpenSslGetTlsSupport(SslProtocols protocol) @@ -569,7 +569,7 @@ private static bool AndroidGetSslProtocolSupport(SslProtocols protocol) private static bool GetTls10Support() { // on macOS and Android TLS 1.0 is supported. - if (IsApplePlatform || IsAndroid) + if ((IsApplePlatform && !IsNetworkFrameworkEnabled()) || IsAndroid) { return true; } @@ -580,7 +580,7 @@ private static bool GetTls10Support() return GetProtocolSupportFromWindowsRegistry(SslProtocols.Tls, defaultProtocolSupport: true) && !IsWindows10Version20348OrGreater; } - return OpenSslGetTlsSupport(SslProtocols.Tls); + return IsOpenSslSupported && OpenSslGetTlsSupport(SslProtocols.Tls); } private static bool GetTls11Support() @@ -597,12 +597,12 @@ private static bool GetTls11Support() return GetProtocolSupportFromWindowsRegistry(SslProtocols.Tls11, defaultProtocolSupport: true) && !IsWindows10Version20348OrGreater; } // on macOS and Android TLS 1.1 is supported. - else if (IsApplePlatform || IsAndroid) + else if ((IsApplePlatform && !IsNetworkFrameworkEnabled()) || IsAndroid) { return true; } - return OpenSslGetTlsSupport(SslProtocols.Tls11); + return IsOpenSslSupported && OpenSslGetTlsSupport(SslProtocols.Tls11); } #pragma warning restore SYSLIB0039 @@ -665,6 +665,31 @@ private static bool GetTls13Support() return false; } + /// + /// Determines if Network.framework is enabled for SSL/TLS operations on Apple platforms. + /// This can be controlled via AppContext switch or environment variable. + /// + /// True if Network.framework is enabled, false otherwise. + public static bool IsNetworkFrameworkEnabled() + { + // Check AppContext switch first (highest priority) + if (AppContext.TryGetSwitch("System.Net.Security.UseNetworkFramework", out bool isEnabled)) + { + return isEnabled; + } + + // Fall back to environment variable + string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK"); + if (!string.IsNullOrEmpty(envVar)) + { + return envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + // Default is disabled + return false; + } + + private static bool GetSendsCAListByDefault() { if (IsWindows) diff --git a/src/libraries/Common/tests/TestUtilities/TestEventListener.cs b/src/libraries/Common/tests/TestUtilities/TestEventListener.cs index 772dc40dfd1dd3..bdfae390d47732 100644 --- a/src/libraries/Common/tests/TestUtilities/TestEventListener.cs +++ b/src/libraries/Common/tests/TestUtilities/TestEventListener.cs @@ -129,7 +129,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) } } #endif - sb.Append($"[{eventData.EventName}] "); + sb.Append($"[{eventData.EventName}] "); for (int i = 0; i < eventData.Payload?.Count; i++) { diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index c82d7803598c49..fe26b4a0a12905 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -15,6 +15,7 @@ ExcludeApiList.PNSE.txt $(DefineConstants);TARGET_WINDOWS $(DefineConstants);TARGET_ANDROID + $(DefineConstants);TARGET_APPLE true true true @@ -440,6 +441,12 @@ Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.Ssl.cs" /> + + + + + + diff --git a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs index 48ece23743274d..213ee2f42698e5 100644 --- a/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/CertificateValidationPal.OSX.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; +using SafeNwHandle = Interop.SafeNwHandle; namespace System.Net { @@ -31,16 +33,16 @@ internal static SslPolicyErrors VerifyCertificateProperties( return null; } - SafeSslHandle sslContext = ((SafeDeleteSslContext)securityContext).SslContext; + X509Certificate2? result = null; - if (sslContext == null) + SafeX509ChainHandle chainHandle = securityContext switch { - return null; - } + SafeDeleteNwContext nwContext => nwContext.PeerX509ChainHandle!, + SafeDeleteSslContext sslContext => Interop.AppleCrypto.SslCopyCertChain(sslContext.SslContext), + _ => throw new ArgumentException("Invalid context type", nameof(securityContext)) + }; - X509Certificate2? result = null; - - using (SafeX509ChainHandle chainHandle = Interop.AppleCrypto.SslCopyCertChain(sslContext)) + try { long chainSize = Interop.AppleCrypto.X509ChainGetChainSize(chainHandle); @@ -69,6 +71,13 @@ internal static SslPolicyErrors VerifyCertificateProperties( result = new X509Certificate2(certHandle); } } + finally + { + if (securityContext is SafeDeleteSslContext) + { + chainHandle.Dispose(); + } + } if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.RemoteCertificate(result); @@ -76,15 +85,35 @@ internal static SslPolicyErrors VerifyCertificateProperties( } // This is only called when we selected local client certificate. - // Currently this is only when Apple crypto asked for it. - internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _1, SafeDeleteContext? _2) => true; + // We need to check if the server actually requested it during handshake. + internal static bool IsLocalCertificateUsed(SafeFreeCredentials? _, SafeDeleteContext? context) + { + return context switch + { + // For Network Framework, we need to check if the server actually requested + // a client certificate during the handshake. + SafeDeleteNwContext nwContext => nwContext.ClientCertificateRequested, + SafeDeleteSslContext => true, + _ => true + }; + } // // Used only by client SSL code, never returns null. // internal static string[] GetRequestCertificateAuthorities(SafeDeleteContext securityContext) { - SafeSslHandle sslContext = ((SafeDeleteSslContext)securityContext).SslContext; + return securityContext switch + { + SafeDeleteNwContext nwContext => nwContext.AcceptableIssuers, + SafeDeleteSslContext sslContext => GetRequestCertificateAuthorities(sslContext), + _ => throw new ArgumentException("Invalid context type", nameof(securityContext)) + }; + } + + private static string[] GetRequestCertificateAuthorities(SafeDeleteSslContext securityContext) + { + SafeSslHandle sslContext = securityContext.SslContext; if (sslContext == null) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Managed/SslProtocolsValidation.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Managed/SslProtocolsValidation.cs index b5943e7ba5e76b..f98f4e396d55ef 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Managed/SslProtocolsValidation.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Managed/SslProtocolsValidation.cs @@ -8,7 +8,7 @@ namespace System.Net { internal static class SslProtocolsValidation { - public static (int MinIndex, int MaxIndex) ValidateContiguous(this SslProtocols protocols, SslProtocols[] orderedSslProtocols) + public static (int MinIndex, int MaxIndex) ValidateContiguous(this SslProtocols protocols, ReadOnlySpan orderedSslProtocols) { // A contiguous range of protocols is required. Find the min and max of the range, // or throw if it's non-contiguous or if no protocols are specified. diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteNwContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteNwContext.cs new file mode 100644 index 00000000000000..c5b8d4201c225f --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteNwContext.cs @@ -0,0 +1,813 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Runtime.ExceptionServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using SafeNwHandle = Interop.SafeNwHandle; +using SafeCFStringHandle = Microsoft.Win32.SafeHandles.SafeCFStringHandle; +using NetworkFrameworkStatusUpdates = Interop.NetworkFramework.StatusUpdates; +using NwOSStatus = Interop.AppleCrypto.OSStatus; +using ResettableValueTaskSource = System.Net.Quic.ResettableValueTaskSource; + +namespace System.Net.Security +{ + /// + /// Network Framework-specific SSL/TLS context implementation for macOS. + /// This class provides secure connection management using Apple's modern + /// Network Framework APIs while presenting a synchronous interface + /// consistent with SecureTransport. + /// + internal sealed class SafeDeleteNwContext : SafeDeleteContext + { + // AppContext switch to enable Network Framework usage + internal static bool IsSwitchEnabled { get; } = AppContextSwitchHelper.GetBooleanConfig( + "System.Net.Security.UseNetworkFramework", + "DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK"); + private static readonly Lazy s_isNetworkFrameworkAvailable = new Lazy(CheckNetworkFrameworkAvailability); + + private const int InitialReceiveBufferSize = 2 * 1024; + + // backreference to the SslStream instance + private readonly SslStream _sslStream; + private Stream TransportStream => _sslStream.InnerStream; + private SslAuthenticationOptions SslAuthenticationOptions => _sslStream._sslAuthenticationOptions; + + // Underlying nw_connection_t handle + internal readonly SafeNwHandle ConnectionHandle; + // nw_framer_t handle for tunneling messages + private SafeNwHandle? _framerHandle; + + // Temporary storage for data that are available only during callback + private string[] _acceptableIssuers = Array.Empty(); + internal string[] AcceptableIssuers => _acceptableIssuers; + + // peer certificate chain (obtained from callback once available) + private SafeX509ChainHandle? _peerCertChainHandle; + internal SafeX509ChainHandle? PeerX509ChainHandle => _peerCertChainHandle; + + // Provides backreference from native code. This GC handle is expected + // to be always valid and is only freed once we are sure there will be + // no more callbacks from the native code + private readonly GCHandle _thisHandle; + + internal IntPtr StateHandle => _thisHandle.IsAllocated ? GCHandle.ToIntPtr(_thisHandle) : IntPtr.Zero; + + private TaskCompletionSource _handshakeCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private Task? _transportReadTask; + private ResettableValueTaskSource _transportReadTcs = new ResettableValueTaskSource() + { + CancellationAction = target => + { + if (target is SafeDeleteNwContext nwContext) + { + nwContext._transportReadTcs.TrySetException(new OperationCanceledException()); + } + } + }; + private ArrayBuffer _appReceiveBuffer = new(InitialReceiveBufferSize); + private ResettableValueTaskSource _appReceiveBufferTcs = new ResettableValueTaskSource(); + private Task? _pendingAppReceiveBufferFillTask; + private readonly TaskCompletionSource _connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private CancellationTokenSource _shutdownCts = new CancellationTokenSource(); + + private bool _disposed; + private int _challengeCallbackCompleted; // 0 = not called, 1 = called + private IntPtr _selectedClientCertificate; // Cached result from challenge callback + + private ResettableValueTaskSource _appWriteTcs = new ResettableValueTaskSource() + { + CancellationAction = target => + { + if (target is SafeDeleteNwContext nwContext) + { + nwContext._appWriteTcs.TrySetException(new OperationCanceledException()); + } + } + }; + + private TaskCompletionSource? _currentWriteCompletionSource; + + public SafeDeleteNwContext(SslStream stream) : base(IntPtr.Zero) + { + _sslStream = stream; + ValidateSslAuthenticationOptions(SslAuthenticationOptions); + _thisHandle = GCHandle.Alloc(this, GCHandleType.Normal); + ConnectionHandle = CreateConnectionHandle(SslAuthenticationOptions, _thisHandle); + + if (ConnectionHandle.IsInvalid) + { + throw new Exception("Failed to create Network Framework connection"); // TODO: Make this string resource + } + } + + public static bool IsNetworkFrameworkAvailable => IsSwitchEnabled && s_isNetworkFrameworkAvailable.Value; + + internal bool ClientCertificateRequested => _challengeCallbackCompleted == 1 && _selectedClientCertificate != IntPtr.Zero; + + internal async Task HandshakeAsync(CancellationToken cancellationToken) + { + Interop.NetworkFramework.Tls.NwConnectionStart(ConnectionHandle, StateHandle); + + using CancellationTokenRegistration registration = cancellationToken.UnsafeRegister(static (state, token) => + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, "Handshake cancellation requested"); + ((TaskCompletionSource)state!).TrySetCanceled(token); + }, _handshakeCompletionSource); + + _transportReadTask = Task.Run(async () => + { + try + { + byte[] buffer = ArrayPool.Shared.Rent(16 * 1024); + try + { + Memory readBuffer = new Memory(buffer); + + while (!_shutdownCts.IsCancellationRequested) + { + // Read data from the transport stream + int bytesRead = await TransportStream.ReadAsync(readBuffer, _shutdownCts.Token).ConfigureAwait(false); + + if (bytesRead > 0) + { + // Process the read data + await WriteInboundWireDataAsync(readBuffer.Slice(0, bytesRead)).ConfigureAwait(false); + } + else + { + // EOF reached, signal completion + _transportReadTcs.TrySetResult(final: true); + + // TODO: can this race with actual handshake completion? + Interop.NetworkFramework.Tls.NwConnectionCancel(ConnectionHandle); + break; + } + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + catch (OperationCanceledException) + { + // Handle cancellation gracefully + } + catch (Exception ex) + { + // Propagate transport stream exceptions to the handshake + _handshakeCompletionSource.TrySetException(ex); + _currentWriteCompletionSource?.TrySetException(ex); + } + }, cancellationToken); + + return await _handshakeCompletionSource.Task.ConfigureAwait(false); + } + + internal async Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"App sending {buffer.Length} bytes"); + + TaskCompletionSource transportWriteCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _currentWriteCompletionSource = transportWriteCompletion; + + bool success = _appWriteTcs.TryGetValueTask(out ValueTask valueTask, this, CancellationToken.None); + Debug.Assert(success, "Concurrent WriteAsync detected"); + + using MemoryHandle memoryHandle = buffer.Pin(); + unsafe + { + Interop.NetworkFramework.Tls.NwConnectionSend(ConnectionHandle, StateHandle, memoryHandle.Pointer, buffer.Length, &CompletionCallback); + } + try + { + await valueTask.ConfigureAwait(false); + } + catch (Exception ex) + { + _currentWriteCompletionSource = null; + transportWriteCompletion.TrySetException(ex); + } + + // Wait for the transport write to complete + await transportWriteCompletion.Task.ConfigureAwait(false); + + [UnmanagedCallersOnly] + static unsafe void CompletionCallback(IntPtr context, Interop.NetworkFramework.NetworkFrameworkError* error) + { + SafeDeleteNwContext thisContext = ResolveThisHandle(context); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(thisContext, $"Completing WriteAsync", nameof(WriteAsync)); + + if (error != null) + { + thisContext._appWriteTcs.TrySetException(Interop.NetworkFramework.CreateExceptionForNetworkFrameworkError(in *error)); + } + else + { + thisContext._appWriteTcs.TrySetResult(); + } + } + } + + internal ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_pendingAppReceiveBufferFillTask == null && _appReceiveBuffer.ActiveLength > 0) + { + // fast path, data available + int length = Math.Min(_appReceiveBuffer.ActiveLength, buffer.Length); + _appReceiveBuffer.ActiveSpan.Slice(0, length).CopyTo(buffer.Span); + _appReceiveBuffer.Discard(length); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Read {length} bytes"); + return ValueTask.FromResult(length); + } + + return ReadAsyncInternal(buffer, cancellationToken); + + async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) + { + // Create a linked token that respects both the user's cancellation token and our shutdown token + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCts.Token); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Internal buffer empty, refilling."); + + // Since the native nw_connection_receive is asynchronous and + // not cancellable, we save reference to the pending task. In + // case of cancellation, we cancel only waiting on the task, and + // on the next ReadAsync call, we don't issue another native + // nw_connection_receive call, but rather wait for the pending + // task to complete. + _pendingAppReceiveBufferFillTask ??= FillAppDataBufferAsync(); + try + { + // Wait for the pending task to complete, which will fill the buffer + await _pendingAppReceiveBufferFillTask.WaitAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Throw with correct cancellation token if cancellation was + // requested by user + cancellationToken.ThrowIfCancellationRequested(); + + // otherwise we are tearing down the connection, simulate EOS + Debug.Assert(_shutdownCts.IsCancellationRequested, "Expected shutdown cancellation token to be triggered"); + + ObjectDisposedException.ThrowIf(_disposed, _sslStream); + } + // other exception types are expected to be fatal and it is okay to not + // clear the pending task and rethrow the same exception + + _pendingAppReceiveBufferFillTask = null; + + if (_appReceiveBuffer.ActiveLength == 0) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "ReadAsync returning 0 bytes, end of stream reached"); + _appReceiveBufferTcs.TrySetResult(final: true); + return 0; // EOF + } + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"ReadAsync filled buffer with {_appReceiveBuffer.ActiveLength} bytes"); + + int length = Math.Min(_appReceiveBuffer.ActiveLength, buffer.Length); + _appReceiveBuffer.ActiveSpan.Slice(0, length).CopyTo(buffer.Span); + _appReceiveBuffer.Discard(length); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Read {length} bytes"); + return length; + } + } + + internal Task FillAppDataBufferAsync() + { + bool success = _appReceiveBufferTcs.TryGetValueTask(out ValueTask valueTask, this, CancellationToken.None); + Debug.Assert(success, "Concurrent FillAppDataBufferAsync detected"); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Waiting for read from connection"); + unsafe + { + Interop.NetworkFramework.Tls.NwConnectionReceive(ConnectionHandle, StateHandle, 16 * 1024, &CompletionCallback); + } + + return _pendingAppReceiveBufferFillTask = valueTask.AsTask(); + + [UnmanagedCallersOnly] + static unsafe void CompletionCallback(IntPtr context, Interop.NetworkFramework.NetworkFrameworkError* error, byte* data, int length) + { + SafeDeleteNwContext thisContext = ResolveThisHandle(context); + Debug.Assert(thisContext != null, "Expected thisContext to be non-null"); + + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(thisContext, $"Completing ConnectionRead, status: {(error != null ? error->ErrorCode : 0)}, len: {length}", nameof(FillAppDataBufferAsync)); + + ref ArrayBuffer buffer = ref thisContext._appReceiveBuffer; + + if (error != null && error->ErrorCode != 0) + { + if (error->ErrorDomain == (int)Interop.NetworkFramework.NetworkFrameworkErrorDomain.POSIX && + error->ErrorCode == (int)Interop.NetworkFramework.NWErrorDomainPOSIX.OperationCanceled) + { + // We cancelled the connection, so this is expected as pending read will be cancelled. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(thisContext, "Connection read cancelled, no data to process"); + thisContext._appReceiveBufferTcs.TrySetResult(); + return; + } + thisContext._appReceiveBufferTcs.TrySetException(ExceptionDispatchInfo.SetCurrentStackTrace(Interop.NetworkFramework.CreateExceptionForNetworkFrameworkError(in *error))); + } + else + { + buffer.EnsureAvailableSpace(length); + new Span(data, length).CopyTo(buffer.AvailableSpan); + buffer.Commit(length); + thisContext._appReceiveBufferTcs.TrySetResult(); + } + } + } + + internal void Shutdown() + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Shutting down Network Framework context"); + _shutdownCts.Cancel(); + Interop.NetworkFramework.Tls.NwConnectionCancel(ConnectionHandle); + } + + private static bool CheckNetworkFrameworkAvailability() + { + try + { + unsafe + { + // Call Init with null callbacks to check if Network Framework is available + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, "Checking Network Framework availability..."); + return !Interop.NetworkFramework.Tls.Init(&StatusUpdateCallback, &WriteOutboundWireData, &ChallengeCallback); + } + } + catch + { + return false; + } + } + + private static void ValidateSslAuthenticationOptions(SslAuthenticationOptions options) + { + switch (options.EncryptionPolicy) + { + case EncryptionPolicy.RequireEncryption: +#pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete + case EncryptionPolicy.AllowNoEncryption: + // SecureTransport doesn't allow TLS_NULL_NULL_WITH_NULL, but + // since AllowNoEncryption intersect OS-supported isn't nothing, + // let it pass. + break; +#pragma warning restore SYSLIB0040 + default: + throw new PlatformNotSupportedException(SR.Format(SR.net_encryptionpolicy_notsupported, options.EncryptionPolicy)); + } + } + + private static SafeNwHandle CreateConnectionHandle(SslAuthenticationOptions options, GCHandle thisHandle) + { + int alpnLength = GetAlpnProtocolListSerializedLength(options.ApplicationProtocols); + + SslProtocols minProtocol = SslProtocols.None; + SslProtocols maxProtocol = SslProtocols.None; + + if (options.EnabledSslProtocols != SslProtocols.None) + { + (minProtocol, maxProtocol) = GetMinMaxProtocols(options.EnabledSslProtocols); + } + + byte[]? alpnBuffer = null; + try + { + const int StackAllocThreshold = 256; + Span alpn = alpnLength == 0 + ? Span.Empty + : alpnLength <= StackAllocThreshold + ? stackalloc byte[StackAllocThreshold] + : (alpnBuffer = ArrayPool.Shared.Rent(alpnLength)); + + if (alpnLength > 0) + { + SerializeAlpnProtocolList(options.ApplicationProtocols!, alpn.Slice(0, alpnLength)); + } + + Span ciphers = options.CipherSuitesPolicy is null + ? Span.Empty + : options.CipherSuitesPolicy.Pal.TlsCipherSuites; + + string idnHost = TargetHostNameHelper.NormalizeHostName(options.TargetHost); + + unsafe + { + fixed (byte* alpnPtr = alpn) + fixed (uint* ciphersPtr = ciphers) + { + return Interop.NetworkFramework.Tls.NwConnectionCreate(options.IsServer, GCHandle.ToIntPtr(thisHandle), idnHost, alpnPtr, alpnLength, minProtocol, maxProtocol, ciphersPtr, ciphers.Length); + } + } + } + finally + { + if (alpnBuffer != null) + { + ArrayPool.Shared.Return(alpnBuffer); + } + } + + // + // Native API accepts only a single ALPN protocol at a time + // (null-terminated string). We serialize all used app protocols + // into a single buffer in the format <0> + // + + static int GetAlpnProtocolListSerializedLength(List? applicationProtocols) + { + if (applicationProtocols is null) + { + return 0; + } + + int protocolSize = 0; + + foreach (SslApplicationProtocol protocol in applicationProtocols) + { + protocolSize += protocol.Protocol.Length + 2; + } + + return protocolSize; + } + + static void SerializeAlpnProtocolList(List applicationProtocols, Span buffer) + { + Debug.Assert(GetAlpnProtocolListSerializedLength(applicationProtocols) == buffer.Length); + + int offset = 0; + + foreach (SslApplicationProtocol protocol in applicationProtocols) + { + buffer[offset] = (byte)protocol.Protocol.Length; // preffix len + protocol.Protocol.Span.CopyTo(buffer.Slice(offset + 1)); // ALPN + buffer[offset + protocol.Protocol.Length + 1] = 0; // null-terminator + + offset += protocol.Protocol.Length + 2; + } + } + + static (SslProtocols, SslProtocols) GetMinMaxProtocols(SslProtocols protocols) + { + ReadOnlySpan orderedProtocols = [ +#pragma warning disable 0618 + SslProtocols.Ssl2, + SslProtocols.Ssl3, +#pragma warning restore 0618 +#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete + SslProtocols.Tls, + SslProtocols.Tls11, +#pragma warning restore SYSLIB0039 + SslProtocols.Tls12, + SslProtocols.Tls13 + ]; + + (int minIndex, int maxIndex) = protocols.ValidateContiguous(orderedProtocols); + SslProtocols minProtocolId = orderedProtocols[minIndex]; + SslProtocols maxProtocolId = orderedProtocols[maxIndex]; + + return (minProtocolId, maxProtocolId); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + Shutdown(); + + // Wait for the transport read task to complete + if (_transportReadTask is Task transportTask) + { + // Ignore exceptions from the transport task + transportTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing).GetAwaiter().GetResult(); + } + + + // Wait for any pending app receive tasks so that we may safely dispose the app receive buffer. + if (_pendingAppReceiveBufferFillTask is Task t) + { + t.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing).GetAwaiter().GetResult(); + } + + _appReceiveBuffer.Dispose(); + + // wait for callback signalling connection has been truly closed. + _connectionClosedTcs.Task.GetAwaiter().GetResult(); + // Complete all pending operations with ObjectDisposedException + var disposedException = new ObjectDisposedException(nameof(SafeDeleteNwContext)); + + _appReceiveBufferTcs.TrySetException(disposedException); + _transportReadTcs.TrySetException(disposedException); + _handshakeCompletionSource.TrySetException(disposedException); + + // Complete any pending writes with disposed exception + TaskCompletionSource? writeCompletion = _currentWriteCompletionSource; + if (writeCompletion != null) + { + _currentWriteCompletionSource = null; + writeCompletion.TrySetException(new ObjectDisposedException(nameof(SafeDeleteNwContext))); + } + + ConnectionHandle.Dispose(); + _framerHandle?.Dispose(); + _peerCertChainHandle?.Dispose(); + _shutdownCts?.Dispose(); + + // now that we know all callbacks are done, we can free the handle + _thisHandle.Free(); + } + base.Dispose(disposing); + } + + private static SafeDeleteNwContext ResolveThisHandle(IntPtr thisHandle) + { + GCHandle handle = GCHandle.FromIntPtr(thisHandle); + return (SafeDeleteNwContext)handle.Target!; + } + + [UnmanagedCallersOnly] + private static unsafe void WriteOutboundWireData(IntPtr thisHandle, byte* data, ulong dataLength) + { + SafeDeleteNwContext? nwContext = null; + try + { + nwContext = ResolveThisHandle(thisHandle); + Debug.Assert(dataLength <= int.MaxValue); + + nwContext.WriteOutboundWireData(new ReadOnlySpan(data, (int)dataLength)); + } + catch (Exception e) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(nwContext, $"WriteOutboundWireData Failed: {e.Message}"); + } + + // Complete the write operation with the exception + TaskCompletionSource? writeCompletion = nwContext?._currentWriteCompletionSource; + if (writeCompletion != null) + { + nwContext?._currentWriteCompletionSource = null; + writeCompletion.TrySetException(e); + } + } + finally + { + nwContext?._currentWriteCompletionSource?.TrySetResult(); + nwContext?._currentWriteCompletionSource = null; + } + } + + [UnmanagedCallersOnly] + private static IntPtr ChallengeCallback(IntPtr thisHandle, IntPtr acceptableIssuersHandle) + { + try + { + SafeDeleteNwContext nwContext = ResolveThisHandle(thisHandle); + + // the callback may end up being called multiple times for some reason. + // check if we've already processed the challenge callback + if (Interlocked.CompareExchange(ref nwContext._challengeCallbackCompleted, 1, 0) != 0) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(nwContext, $"ChallengeCallback already processed, returning cached result: {nwContext._selectedClientCertificate}"); + } + return nwContext._selectedClientCertificate; + } + + nwContext._acceptableIssuers = ExtractAcceptableIssuers(acceptableIssuersHandle); + Debug.Assert(nwContext._peerCertChainHandle != null, "Peer certificate chain handle should be set before challenge callback"); + + byte[]? dummy = null; + nwContext._sslStream.AcquireClientCredentials(ref dummy, true); + return nwContext._selectedClientCertificate = nwContext.SslAuthenticationOptions.CertificateContext?.TargetCertificate.Handle ?? IntPtr.Zero; + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(null, $"ChallengeCallback exception: {ex}"); + } + return IntPtr.Zero; + } + } + + private void WriteOutboundWireData(ReadOnlySpan data) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Sending {data.Length} bytes"); + + TransportStream.Write(data); + } + + private async Task WriteInboundWireDataAsync(ReadOnlyMemory buf) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Receiving {buf.Length} bytes"); + + if (_framerHandle != null && buf.Length > 0) + { + // the data needs to be pinned until the callback fires + using MemoryHandle memoryHandle = buf.Pin(); + + bool success = _transportReadTcs.TryGetValueTask(out ValueTask valueTask, this, CancellationToken.None); + Debug.Assert(success, "Concurrent WriteInboundWireDataAsync detected"); + + unsafe + { + Interop.NetworkFramework.Tls.NwFramerDeliverInput(_framerHandle, StateHandle, (byte*)memoryHandle.Pointer, buf.Length, &CompletionCallback); + } + + await valueTask.ConfigureAwait(false); + } + + [UnmanagedCallersOnly] + static unsafe void CompletionCallback(IntPtr context, Interop.NetworkFramework.NetworkFrameworkError* error) + { + Debug.Assert(error == null || error->ErrorCode == 0, $"CompletionCallback called with error {(error != null ? error->ErrorCode : 0)}"); + + SafeDeleteNwContext thisContext = ResolveThisHandle(context); + Debug.Assert(thisContext != null, "Expected thisContext to be non-null"); + + thisContext._transportReadTcs.TrySetResult(); + } + } + + public override bool IsInvalid => ConnectionHandle.IsInvalid || (_framerHandle?.IsInvalid ?? true); + + [UnmanagedCallersOnly] + private static unsafe void StatusUpdateCallback(IntPtr thisHandle, NetworkFrameworkStatusUpdates statusUpdate, IntPtr data, IntPtr data2, Interop.NetworkFramework.NetworkFrameworkError* error) + { + try + { + SafeDeleteNwContext? nwContext = null; + + if (thisHandle != IntPtr.Zero) + { + nwContext = ResolveThisHandle(thisHandle); + } + + if (nwContext == null) + { + Debug.Assert(statusUpdate == NetworkFrameworkStatusUpdates.DebugLog, + "StatusUpdateCallback called with null thisHandle, but not a DebugLog update"); + } + + if (NetEventSource.Log.IsEnabled() && statusUpdate != NetworkFrameworkStatusUpdates.DebugLog) + NetEventSource.Info(nwContext, $"Received status update: {statusUpdate}"); + + switch (statusUpdate) + { + case NetworkFrameworkStatusUpdates.FramerStart: + nwContext!.FramerStartCallback(new SafeNwHandle(Interop.NetworkFramework.Retain(data), true)); + break; + case NetworkFrameworkStatusUpdates.HandshakeFinished: + nwContext!.HandshakeFinished(); + break; + case NetworkFrameworkStatusUpdates.ConnectionFailed: + Debug.Assert(error != null, "ConnectionFailed should have an error"); + nwContext!.ConnectionFailed(in *error); + break; + case NetworkFrameworkStatusUpdates.ConnectionCancelled: + nwContext!.ConnectionClosed(); + break; + case NetworkFrameworkStatusUpdates.CertificateAvailable: + global::System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller.ManagedToUnmanagedOut marshaller = new(); + marshaller.FromUnmanaged(data); + nwContext!.CertificateAvailable(marshaller.ToManaged()); + marshaller.Free(); + break; + case NetworkFrameworkStatusUpdates.DebugLog: + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(nwContext, Marshal.PtrToStringAnsi(data)!, "Native"); + break; + default: // We shouldn't hit here. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(nwContext, $"Unknown status update: {statusUpdate}"); + Debug.Assert(false); + break; + } + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(null, $"StatusUpdateCallback failed for {statusUpdate}: {ex}"); + } + } + } + private void FramerStartCallback(SafeNwHandle framerHandle) + { + _framerHandle = framerHandle; + } + + private void HandshakeFinished() + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "TLS handshake completed successfully"); + _handshakeCompletionSource.TrySetResult(null); + } + + private void ConnectionFailed(in Interop.NetworkFramework.NetworkFrameworkError error) + { + Exception ex = Interop.NetworkFramework.CreateExceptionForNetworkFrameworkError(in error); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"TLS handshake failed with error: {ex.Message}"); + _handshakeCompletionSource.TrySetResult(ExceptionDispatchInfo.SetCurrentStackTrace(ex)); + } + + private void ConnectionClosed() + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Connection was cancelled"); + _connectionClosedTcs.TrySetResult(); + _handshakeCompletionSource.TrySetResult(ExceptionDispatchInfo.SetCurrentStackTrace( + new IOException(SR.net_io_eof))); + // Complete any pending writes with connection closed exception + TaskCompletionSource? writeCompletion = _currentWriteCompletionSource; + if (writeCompletion != null) + { + _currentWriteCompletionSource = null; + writeCompletion.TrySetException(new IOException(SR.net_io_eof)); + } + } + + private void CertificateAvailable(SafeX509ChainHandle peerCertChainHandle) + { + _peerCertChainHandle = peerCertChainHandle; + } + + private static string[] ExtractAcceptableIssuers(IntPtr acceptableIssuersHandle) + { + if (acceptableIssuersHandle == IntPtr.Zero) + { + return Array.Empty(); + } + + var issuers = new List(); + + try + { + using var arrayHandle = new Microsoft.Win32.SafeHandles.SafeCFArrayHandle(acceptableIssuersHandle, ownsHandle: false); + int count = (int)Interop.CoreFoundation.CFArrayGetCount(arrayHandle); + + for (int i = 0; i < count; i++) + { + IntPtr dataRef = Interop.CoreFoundation.CFArrayGetValueAtIndex(arrayHandle, i); + if (dataRef != IntPtr.Zero) + { + // Create a non-owning handle for the CFData + using var dataHandle = new Microsoft.Win32.SafeHandles.SafeCFDataHandle(dataRef, ownsHandle: false); + // Get the DER-encoded DN data + byte[] derData = Interop.CoreFoundation.CFGetData(dataHandle); + + if (derData.Length > 0) + { + // Convert DER-encoded DN to string using X500DistinguishedName + try + { + X500DistinguishedName dn = new X500DistinguishedName(derData); + string issuerDn = dn.Name; + issuers.Add(issuerDn); + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(null, $"Failed to parse DN at index {i}: {ex.Message}"); + } + } + } + } + } + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(null, $"Failed to extract acceptable issuers: {ex.Message}"); + } + } + + return issuers.ToArray(); + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs index f1945c428363cd..9b672d7d6c3a6d 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs @@ -8,16 +8,13 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; +using OSStatus = Interop.AppleCrypto.OSStatus; namespace System.Net { internal sealed class SafeDeleteSslContext : SafeDeleteContext { // mapped from OSX error codes - private const int OSStatus_writErr = -20; - private const int OSStatus_readErr = -19; - private const int OSStatus_noErr = 0; - private const int OSStatus_errSSLWouldBlock = -9803; private const int InitialBufferSize = 2048; private readonly SafeSslHandle _sslContext; private ArrayBuffer _inputBuffer = new ArrayBuffer(InitialBufferSize); @@ -241,14 +238,14 @@ private static unsafe int WriteToConnection(IntPtr connection, byte* data, void* context._outputBuffer.Commit(toWrite); // Since we can enqueue everything, no need to re-assign *dataLength. - return OSStatus_noErr; + return OSStatus.NoErr; } } catch (Exception e) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(context, $"WritingToConnection failed: {e.Message}"); - return OSStatus_writErr; + return OSStatus.WritErr; } } @@ -266,7 +263,7 @@ private static unsafe int ReadFromConnection(IntPtr connection, byte* data, void if (toRead == 0) { - return OSStatus_noErr; + return OSStatus.NoErr; } uint transferred = 0; @@ -274,7 +271,7 @@ private static unsafe int ReadFromConnection(IntPtr connection, byte* data, void if (context._inputBuffer.ActiveLength == 0) { *dataLength = (void*)0; - return OSStatus_errSSLWouldBlock; + return OSStatus.ErrSSLWouldBlock; } int limit = Math.Min((int)toRead, context._inputBuffer.ActiveLength); @@ -284,14 +281,14 @@ private static unsafe int ReadFromConnection(IntPtr connection, byte* data, void transferred = (uint)limit; *dataLength = (void*)transferred; - return OSStatus_noErr; + return OSStatus.NoErr; } } catch (Exception e) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(context, $"ReadFromConnectionfailed: {e.Message}"); - return OSStatus_readErr; + return OSStatus.ReadErr; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.OSX.cs index 06546583205831..4d9492a77b59cd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.OSX.cs @@ -4,12 +4,62 @@ using System.Collections.Generic; using System.Diagnostics; using System.Security.Authentication; +using SafeNwHandle = Interop.SafeNwHandle; namespace System.Net.Security { internal partial struct SslConnectionInfo { - public void UpdateSslConnectionInfo(SafeDeleteSslContext context) + public void UpdateSslConnectionInfo(SafeDeleteContext context) + { + switch (context) + { + case SafeDeleteNwContext nwContext: + UpdateSslConnectionInfoNetworkFramework(nwContext); + break; + case SafeDeleteSslContext sslContext: + UpdateSslConnectionInfoAppleCrypto(sslContext); + break; + default: + throw new NotSupportedException("Unsupported context type."); + } + } + + private void UpdateSslConnectionInfoNetworkFramework(SafeDeleteNwContext context) + { + SafeNwHandle nwContext = context.ConnectionHandle; + SslProtocols protocol; + TlsCipherSuite cipherSuite; + + Span alpn = stackalloc byte[256]; // Ensure the stack is initialized for alpnPtr + int alpnLength = alpn.Length; + + int osStatus; + unsafe + { + fixed (byte* alpnPtr = alpn) + { + // Call the native method to get connection info + osStatus = Interop.NetworkFramework.Tls.GetConnectionInfo(nwContext, context.StateHandle, out protocol, out cipherSuite, alpnPtr, ref alpnLength); + } + } + + if (osStatus != 0) + { + throw Interop.AppleCrypto.CreateExceptionForOSStatus(osStatus); + } + + if (alpnLength > 0) + { + ApplicationProtocol = alpn.Slice(0, alpnLength).ToArray(); + } + + Protocol = (int)protocol; + TlsCipherSuite = cipherSuite; + MapCipherSuite(cipherSuite); + } + + private void UpdateSslConnectionInfoAppleCrypto(SafeDeleteSslContext context) { SafeSslHandle sslContext = context.SslContext; SslProtocols protocol; 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 eac06538b92119..cda4db5e7f7cad 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 @@ -15,7 +15,8 @@ namespace System.Net.Security { public partial class SslStream { - private readonly SslAuthenticationOptions _sslAuthenticationOptions = new SslAuthenticationOptions(); + internal readonly SslAuthenticationOptions _sslAuthenticationOptions = new SslAuthenticationOptions(); + internal new Stream InnerStream => base.InnerStream; private NestedState _nestedAuth; private bool _isRenego; @@ -295,6 +296,30 @@ private async Task ForceAuthenticationAsync(bool receiveFirst, byte[ } try { +#if TARGET_APPLE + if (SslStreamPal.ShouldUseAsyncSecurityContext(_sslAuthenticationOptions)) + { + Debug.Assert(_sslAuthenticationOptions.IsClient); + byte[]? dummy = null; + AcquireClientCredentials(ref dummy, true); + + Task handshakeTask = SslStreamPal.AsyncHandshakeAsync(ref _securityContext, this, cancellationToken); + await TIOAdapter.WaitAsync(handshakeTask).ConfigureAwait(false); + if (await handshakeTask.ConfigureAwait(false) is Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex, "Async handshake failed"); + if (ex is ArgumentException or IOException) + { + throw ex; + } + throw new AuthenticationException(SR.net_auth_SSPI, ex); + } + + CompleteHandshake(_sslAuthenticationOptions); + return; + } +#endif // TARGET_APPLE + if (!receiveFirst) { token = NextMessage(reAuthenticationData, out int consumed); @@ -820,7 +845,6 @@ private SecurityStatusPal DecryptData(int frameSize) private async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) where TIOAdapter : IReadWriteAdapter { - // Throw first if we already have exception. // Check for disposal is not atomic so we will check again below. ThrowIfExceptionalOrNotAuthenticated(); @@ -833,6 +857,15 @@ private async ValueTask ReadAsyncInternal(Memory buffer, try { + +#if TARGET_APPLE + if (SslStreamPal.IsAsyncSecurityContext(_securityContext!)) + { + ValueTask task = SslStreamPal.AsyncReadAsync(_securityContext!, buffer, cancellationToken); + return await TIOAdapter.WaitAsync(task).ConfigureAwait(false); + } +#endif // TARGET_APPLE + int processedLength = 0; int nextTlsFrameLength = UnknownTlsFrameLength; @@ -974,6 +1007,15 @@ private async ValueTask WriteAsyncInternal(ReadOnlyMemory buff try { +#if TARGET_APPLE + if (SslStreamPal.IsAsyncSecurityContext(_securityContext!)) + { + Task task = SslStreamPal.AsyncWriteAsync(_securityContext!, buffer, cancellationToken); + await TIOAdapter.WaitAsync(task).ConfigureAwait(false); + return; + } +#endif // TARGET_APPLE + ValueTask t = buffer.Length < MaxDataSize ? WriteSingleChunk(buffer, cancellationToken) : WriteAsyncChunked(buffer, cancellationToken); 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 4e9f4327ee9e22..f170be43aedfb3 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 @@ -50,7 +50,13 @@ internal static bool DisableTlsResume private SafeFreeCredentials? _credentialsHandle; + +#if TARGET_APPLE + // on OSX, we have two implementations of SafeDeleteContext, so store a reference to the base class + private SafeDeleteContext? _securityContext; +#else private SafeDeleteSslContext? _securityContext; +#endif private SslConnectionInfo _connectionInfo; private X509Certificate? _selectedClientCertificate; @@ -568,7 +574,7 @@ This will not restart a session but helps minimizing the number of handles we cr --*/ - private bool AcquireClientCredentials(ref byte[]? thumbPrint, bool newCredentialsRequested = false) + internal bool AcquireClientCredentials(ref byte[]? thumbPrint, bool newCredentialsRequested = false) { // Acquire possible Client Certificate information and set it on the handle. bool cachedCred = false; // this is a return result from this method. @@ -1135,7 +1141,13 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot if (!success) { - CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); +#pragma warning disable CS0162 // unreachable code detected (compile time const) + if (SslStreamPal.CanGenerateCustomAlerts) + { + CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); + } +#pragma warning restore CS0162 // unreachable code detected (compile time const) + if (chain != null) { foreach (X509ChainStatus status in chain.ChainStatus) 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 4a5de17a2a221f..f662e1fcbd4309 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 @@ -21,6 +21,7 @@ public static Exception GetException(SecurityStatusPal status) internal const bool StartMutualAuthAsAnonymous = false; internal const bool CanEncryptEmptyMessage = false; + internal const bool CanGenerateCustomAlerts = false; public static void VerifyPackageInfo() { @@ -230,6 +231,7 @@ public static SecurityStatusPal ApplyAlertToken( { // There doesn't seem to be an exposed API for writing an alert. // The API seems to assume that all alerts are generated internally. + Debug.Assert(CanGenerateCustomAlerts); return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); } 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 add3454a6f14eb..4da95d52028243 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 @@ -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.IO; using System.Buffers; using System.Collections.Generic; using System.ComponentModel; @@ -9,6 +10,8 @@ using System.Security.Authentication; using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; using PAL_TlsHandshakeState = Interop.AppleCrypto.PAL_TlsHandshakeState; using PAL_TlsIo = Interop.AppleCrypto.PAL_TlsIo; @@ -28,14 +31,20 @@ 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; public static void VerifyPackageInfo() { } + public static bool IsAsyncSecurityContext(SafeDeleteContext securityContext) + { + return securityContext is SafeDeleteNwContext; + } + public static SecurityStatusPal SelectApplicationProtocol( SafeFreeCredentials? _, - SafeDeleteSslContext context, + SafeDeleteContext securityContext, SslAuthenticationOptions sslAuthenticationOptions, ReadOnlySpan clientProtocols) { @@ -46,6 +55,8 @@ public static SecurityStatusPal SelectApplicationProtocol( return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); } + SafeDeleteSslContext context = (SafeDeleteSslContext)securityContext; + // We do server side ALPN e.g. walk the intersect in server order foreach (SslApplicationProtocol applicationProtocol in sslAuthenticationOptions.ApplicationProtocols) { @@ -88,7 +99,7 @@ public static SecurityStatusPal SelectApplicationProtocol( #pragma warning disable IDE0060 public static ProtocolToken AcceptSecurityContext( ref SafeFreeCredentials credential, - ref SafeDeleteSslContext? context, + ref SafeDeleteContext? context, ReadOnlySpan inputBuffer, out int consumed, SslAuthenticationOptions sslAuthenticationOptions) @@ -98,7 +109,7 @@ public static ProtocolToken AcceptSecurityContext( public static ProtocolToken InitializeSecurityContext( ref SafeFreeCredentials credential, - ref SafeDeleteSslContext? context, + ref SafeDeleteContext? context, string? _ /*targetName*/, ReadOnlySpan inputBuffer, out int consumed, @@ -109,7 +120,7 @@ public static ProtocolToken InitializeSecurityContext( public static ProtocolToken Renegotiate( ref SafeFreeCredentials? credentialsHandle, - ref SafeDeleteSslContext? context, + ref SafeDeleteContext? context, SslAuthenticationOptions sslAuthenticationOptions) { throw new PlatformNotSupportedException(); @@ -123,18 +134,21 @@ public static ProtocolToken Renegotiate( #pragma warning restore IDE0060 public static ProtocolToken EncryptMessage( - SafeDeleteSslContext securityContext, + SafeDeleteContext securityContext, ReadOnlyMemory input, int _ /*headerSize*/, int _1 /*trailerSize*/) { - ProtocolToken token = default; - Debug.Assert(input.Length > 0, $"{nameof(input.Length)} > 0 since {nameof(CanEncryptEmptyMessage)} is false"); + Debug.Assert(securityContext is SafeDeleteSslContext, "SafeDeleteSslContext expected"); + SafeDeleteSslContext sslContext = (SafeDeleteSslContext)securityContext; + + ProtocolToken token = default; + try { - SafeSslHandle sslHandle = securityContext.SslContext; + SafeSslHandle sslHandle = sslContext.SslContext; unsafe { @@ -169,7 +183,7 @@ public static ProtocolToken EncryptMessage( break; } - securityContext.ReadPendingWrites(ref token); + sslContext.ReadPendingWrites(ref token); } finally { @@ -186,18 +200,22 @@ public static ProtocolToken EncryptMessage( } public static SecurityStatusPal DecryptMessage( - SafeDeleteSslContext securityContext, + SafeDeleteContext securityContext, Span buffer, out int offset, out int count) { + Debug.Assert(securityContext is SafeDeleteSslContext, "SafeDeleteSslContext expected"); + SafeDeleteSslContext sslContext = (SafeDeleteSslContext)securityContext; + offset = 0; count = 0; try { - SafeSslHandle sslHandle = securityContext.SslContext; - securityContext.Write(buffer); + SafeSslHandle sslHandle = sslContext.SslContext; + + sslContext.Write(buffer); unsafe { @@ -261,7 +279,7 @@ public static void QueryContextStreamSizes( } public static void QueryContextConnectionInfo( - SafeDeleteSslContext securityContext, + SafeDeleteContext securityContext, ref SslConnectionInfo connectionInfo) { connectionInfo.UpdateSslConnectionInfo(securityContext); @@ -269,10 +287,23 @@ public static void QueryContextConnectionInfo( public static bool TryUpdateClintCertificate( SafeFreeCredentials? _, - SafeDeleteSslContext? context, + SafeDeleteContext? context, SslAuthenticationOptions sslAuthenticationOptions) { - SafeDeleteSslContext? sslContext = ((SafeDeleteSslContext?)context); + if (context == null) + { + return false; + } + + if (context is SafeDeleteNwContext) + { + // We are being called from Network Framework, we will retrieve + // the selected certificate from higher frame in the callstack + // and return it as return value of the callback + return true; + } + + SafeDeleteSslContext sslContext = ((SafeDeleteSslContext)context); if (sslAuthenticationOptions.CertificateContext != null) { @@ -283,7 +314,7 @@ public static bool TryUpdateClintCertificate( } private static ProtocolToken HandshakeInternal( - ref SafeDeleteSslContext? context, + ref SafeDeleteContext? context, ReadOnlySpan inputBuffer, out int consumed, SslAuthenticationOptions sslAuthenticationOptions) @@ -293,23 +324,27 @@ private static ProtocolToken HandshakeInternal( try { - SafeDeleteSslContext? sslContext = ((SafeDeleteSslContext?)context); - if ((null == context) || context.IsInvalid) { - sslContext = new SafeDeleteSslContext(sslAuthenticationOptions); - context = sslContext; + Debug.Assert(!ShouldUseAsyncSecurityContext(sslAuthenticationOptions)); + + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(null, $"Using SecureTransport (SafeDeleteSslContext) for TLS connection - Protocols: {sslAuthenticationOptions.EnabledSslProtocols}, IsClient: {sslAuthenticationOptions.IsClient}, NetworkFrameworkAvailable: {SafeDeleteNwContext.IsNetworkFrameworkAvailable}"); + + context = new SafeDeleteSslContext(sslAuthenticationOptions); } + Debug.Assert(context is SafeDeleteSslContext, "SafeDeleteSslContext expected"); + SafeDeleteSslContext sslContext = (SafeDeleteSslContext)context; + if (inputBuffer.Length > 0) { - sslContext!.Write(inputBuffer); + sslContext.Write(inputBuffer); } consumed = inputBuffer.Length; - SafeSslHandle sslHandle = sslContext!.SslContext; - token.Status = PerformHandshake(sslHandle); + token.Status = PerformHandshake(sslContext.SslContext); if (token.Status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded) { @@ -370,14 +405,23 @@ public static SecurityStatusPal ApplyAlertToken( // 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); } #pragma warning restore IDE0060 public static SecurityStatusPal ApplyShutdownToken( - SafeDeleteSslContext securityContext) + SafeDeleteContext securityContext) { - SafeSslHandle sslHandle = securityContext.SslContext; + if (securityContext is SafeDeleteNwContext nwContext) + { + nwContext.Shutdown(); + return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); + } + + Debug.Assert(securityContext is SafeDeleteSslContext, "SafeDeleteSslContext expected"); + SafeDeleteSslContext context = (SafeDeleteSslContext)securityContext; + SafeSslHandle sslHandle = context.SslContext; int osStatus = Interop.AppleCrypto.SslShutdown(sslHandle); @@ -390,5 +434,60 @@ public static SecurityStatusPal ApplyShutdownToken( SecurityStatusPalErrorCode.InternalError, Interop.AppleCrypto.CreateExceptionForOSStatus(osStatus)); } + + internal static bool ShouldUseAsyncSecurityContext(SslAuthenticationOptions sslAuthenticationOptions) + { + return ShouldUseNetworkFramework(sslAuthenticationOptions); + } + + private static bool ShouldUseNetworkFramework( + SslAuthenticationOptions sslAuthenticationOptions) + { + return + sslAuthenticationOptions.IsClient && + SafeDeleteNwContext.IsNetworkFrameworkAvailable && + (sslAuthenticationOptions.EnabledSslProtocols == SslProtocols.None || + sslAuthenticationOptions.EnabledSslProtocols == SslProtocols.Tls13 || + (sslAuthenticationOptions.EnabledSslProtocols == (SslProtocols.Tls12 | SslProtocols.Tls13))); + } + + private static SafeDeleteNwContext CreateAsyncSecurityContext(SslStream stream) + { + Debug.Assert(ShouldUseAsyncSecurityContext(stream._sslAuthenticationOptions), + "ShouldUseAsyncSecurityContext should be true when creating an async security context."); + + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(null, $"Using Network Framework (SafeDeleteNwContext) for TLS connection - Protocols: {stream._sslAuthenticationOptions.EnabledSslProtocols}"); + return new SafeDeleteNwContext(stream); + } + + internal static Task AsyncHandshakeAsync(ref SafeDeleteContext? context, SslStream stream, CancellationToken cancellationToken) + { + Debug.Assert(context == null); + try + { + SafeDeleteNwContext nwContext = CreateAsyncSecurityContext(stream); + context = nwContext; + return nwContext.HandshakeAsync(cancellationToken); + } + catch (Exception e) + { + return Task.FromResult(e); + } + } + + internal static Task AsyncWriteAsync(SafeDeleteContext securityContext, ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + Debug.Assert(securityContext is SafeDeleteNwContext, "SafeDeleteNwContext expected for async write"); + SafeDeleteNwContext nwContext = (SafeDeleteNwContext)securityContext; + return nwContext.WriteAsync(buffer, cancellationToken); + } + + internal static ValueTask AsyncReadAsync(SafeDeleteContext securityContext, Memory buffer, CancellationToken cancellationToken) + { + Debug.Assert(securityContext is SafeDeleteNwContext, "SafeDeleteNwContext expected for async read"); + SafeDeleteNwContext nwContext = (SafeDeleteNwContext)securityContext; + return nwContext.ReadAsync(buffer, cancellationToken); + } } } 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 700531aa49d35d..1de318873fca10 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 @@ -19,6 +19,7 @@ public static Exception GetException(SecurityStatusPal status) internal const bool StartMutualAuthAsAnonymous = false; internal const bool CanEncryptEmptyMessage = false; + internal const bool CanGenerateCustomAlerts = false; public static void VerifyPackageInfo() { @@ -242,6 +243,7 @@ public static SecurityStatusPal ApplyAlertToken(SafeDeleteContext? securityConte // 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); } #pragma warning restore IDE0060 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 61bc2fd2e71c04..2f29cc44c2583e 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 @@ -45,6 +45,7 @@ public static Exception GetException(SecurityStatusPal status) internal const bool StartMutualAuthAsAnonymous = true; internal const bool CanEncryptEmptyMessage = true; + internal const bool CanGenerateCustomAlerts = true; private static readonly byte[] s_sessionTokenBuffer = InitSessionTokenBuffer(); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/LoggingTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/LoggingTest.cs index 52a8282409aacd..46fe333f26860e 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/LoggingTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/LoggingTest.cs @@ -28,6 +28,10 @@ public void EventSource_ExistsWithCorrectId() [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "X509 certificate store is not supported on iOS or tvOS.")] // Match SslStream_StreamToStream_Authentication_Success public async Task EventSource_EventsRaisedAsExpected() { + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + throw new SkipTestException("We'll deal with EventSources later."); + } await RemoteExecutor.Invoke(async () => { try diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamConformanceTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamConformanceTests.cs index c89c4802c8d467..dd01c99c4b5a49 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamConformanceTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamConformanceTests.cs @@ -7,6 +7,8 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; +using Microsoft.DotNet.XUnitExtensions; namespace System.Net.Security.Tests { @@ -48,6 +50,23 @@ await new[] return new StreamPair(ssl1, ssl2); } + + [ConditionalTheory] + [InlineData(ReadWriteMode.SyncArray)] + [InlineData(ReadWriteMode.SyncSpan)] + [InlineData(ReadWriteMode.AsyncArray)] + [InlineData(ReadWriteMode.AsyncMemory)] + [InlineData(ReadWriteMode.SyncAPM)] + [InlineData(ReadWriteMode.AsyncAPM)] + public override Task ZeroByteRead_PerformsZeroByteReadOnUnderlyingStreamWhenDataNeeded(ReadWriteMode mode) + { + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + throw new SkipTestException("NetworkFramework works in Async and does not issue zero-byte reads to underlying stream."); + } + + return base.ZeroByteRead_PerformsZeroByteReadOnUnderlyingStreamWhenDataNeeded(mode); + } } public sealed class SslStreamMemoryConformanceTests : SslStreamConformanceTests diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamDisposeTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamDisposeTest.cs index 9864029c7a0b34..4d2e1e30807f37 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamDisposeTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamDisposeTest.cs @@ -92,8 +92,8 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( { // This will read everything into internal buffer. Following ReadAsync will not need IO. task = client.ReadAsync(readBuffer, 0, 4, cts.Token); - client.Dispose(); int readLength = await task.ConfigureAwait(false); + client.Dispose(); Assert.Equal(4, readLength); } else diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNegotiatedCipherSuiteTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNegotiatedCipherSuiteTest.cs index 38da62537c6401..79ff6cb6120c31 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNegotiatedCipherSuiteTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNegotiatedCipherSuiteTest.cs @@ -55,6 +55,12 @@ public class NegotiatedCipherSuiteTest if (PlatformDetection.IsAndroid) return false; + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + // Network.framework CipherSuite APIs doesn't enforce the given list. + return false; + } + try { new CipherSuitesPolicy(Array.Empty()); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 7bbd1359d04b55..520db2e7000b56 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs @@ -274,13 +274,18 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( Assert.Equal(rawHostname, client.TargetHostName); } - [Theory] + [ConditionalTheory] [InlineData("www-.volal.cz")] [InlineData("www-.colorhexa.com")] [InlineData("xn--www-7m0a.thegratuit.com")] [SkipOnPlatform(TestPlatforms.Android, "Safe invalid IDN hostnames are not supported on Android")] public async Task SslStream_SafeInvalidIdn_Success(string name) { + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + throw new SkipTestException("Safe invalid IDN hostnames are not supported on Network.framework"); + } + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); using (client) using (server) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs index d495f9dfc268b7..494080f0c794a0 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamStreamToStreamTest.cs @@ -133,10 +133,15 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout(client.AuthenticateAsClien } } - [Fact] + [ConditionalFact] [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "X509 certificate store is not supported on iOS or tvOS.")] public async Task Read_CorrectlyUnlocksAfterFailure() { + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + throw new SkipTestException("Reads and writes to inner streams are happening on different thread, so the exception does not propagate"); + } + (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); var clientStream = new ThrowingDelegatingStream(stream1); using (var clientSslStream = new SslStream(clientStream, false, AllowAnyServerCertificate)) @@ -210,10 +215,15 @@ public async Task Read_InvokedSynchronously() } } - [Fact] + [ConditionalFact] [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "X509 certificate store is not supported on iOS or tvOS.")] public async Task Write_InvokedSynchronously() { + if (PlatformDetection.IsNetworkFrameworkEnabled()) + { + throw new SkipTestException("Reads and writes to inner streams are happening on different thread, so we're calling InnerStream Read/Write async."); + } + (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); var clientStream = new PreReadWriteActionDelegatingStream(stream1); using (var clientSslStream = new SslStream(clientStream, false, AllowAnyServerCertificate)) diff --git a/src/mono/msbuild/apple/build/AppleBuild.targets b/src/mono/msbuild/apple/build/AppleBuild.targets index c686768b27621a..8127cc0c86a686 100644 --- a/src/mono/msbuild/apple/build/AppleBuild.targets +++ b/src/mono/msbuild/apple/build/AppleBuild.targets @@ -84,6 +84,7 @@ <_CommonLinkerArgs Include="-lswiftCore" /> <_CommonLinkerArgs Include="-lswiftFoundation" /> <_CommonLinkerArgs Include="-framework Foundation" /> + <_CommonLinkerArgs Include="-framework Network" /> <_CommonLinkerArgs Include="-framework Security" /> <_CommonLinkerArgs Include="-framework CryptoKit" /> <_CommonLinkerArgs Include="-framework UIKit" /> diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Apple/CMakeLists.txt index bc333326b9837e..876a5c47e845ad 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/CMakeLists.txt @@ -21,6 +21,7 @@ set(NATIVECRYPTO_SOURCES pal_x509.c pal_x509chain.c pal_swiftbindings.o + pal_networkframework.m ) if (CLR_CMAKE_TARGET_MACCATALYST OR CLR_CMAKE_TARGET_IOS OR CLR_CMAKE_TARGET_TVOS) 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 ef27e7bb832afa..d968ed2e127cf3 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -23,6 +23,7 @@ #include "pal_x509.h" #include "pal_x509_macos.h" #include "pal_x509chain.h" +#include "pal_networkframework.h" static const Entry s_cryptoAppleNative[] = { @@ -136,6 +137,14 @@ static const Entry s_cryptoAppleNative[] = DllImportEntry(AppleCryptoNative_X509StoreRemoveCertificate) DllImportEntry(AppleCryptoNative_Pbkdf2) DllImportEntry(AppleCryptoNative_X509GetSubjectSummary) + DllImportEntry(AppleCryptoNative_Init) + DllImportEntry(AppleCryptoNative_NwConnectionCreate) + DllImportEntry(AppleCryptoNative_NwConnectionStart) + DllImportEntry(AppleCryptoNative_NwFramerDeliverInput) + DllImportEntry(AppleCryptoNative_NwConnectionSend) + DllImportEntry(AppleCryptoNative_NwConnectionReceive) + DllImportEntry(AppleCryptoNative_NwConnectionCancel) + DllImportEntry(AppleCryptoNative_GetConnectionInfo) }; EXTERN_C const void* CryptoAppleResolveDllImport(const char* name); diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/extra_libs.cmake b/src/native/libs/System.Security.Cryptography.Native.Apple/extra_libs.cmake index d220db67479ac5..adeb619b299171 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/extra_libs.cmake +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/extra_libs.cmake @@ -2,7 +2,8 @@ macro(append_extra_cryptography_apple_libs NativeLibsExtra) find_library(COREFOUNDATION_LIBRARY CoreFoundation) find_library(SECURITY_LIBRARY Security) + find_library(NETWORK_LIBRARY Network) find_library(CRYPTOKIT_LIBRARY CryptoKit) - list(APPEND ${NativeLibsExtra} ${COREFOUNDATION_LIBRARY} ${SECURITY_LIBRARY} ${CRYPTOKIT_LIBRARY} -L/usr/lib/swift -lobjc -lswiftCore -lswiftFoundation) + list(APPEND ${NativeLibsExtra} ${COREFOUNDATION_LIBRARY} ${SECURITY_LIBRARY} ${NETWORK_LIBRARY} ${CRYPTOKIT_LIBRARY} -L/usr/lib/swift -lobjc -lswiftCore -lswiftFoundation) endmacro() diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.h b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.h new file mode 100644 index 00000000000000..c3bf1ca0181df6 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.h @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "pal_compiler.h" +#include +#include +#include +#include // for intptr_t + +#ifdef __OBJC__ +#import +#else +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Status update enumeration for TLS operations +typedef enum +{ + PAL_NwStatusUpdates_UnknownError = 0, + PAL_NwStatusUpdates_FramerStart = 1, + PAL_NwStatusUpdates_HandshakeFinished = 3, + PAL_NwStatusUpdates_ConnectionFailed = 4, + PAL_NwStatusUpdates_ConnectionCancelled = 103, + PAL_NwStatusUpdates_CertificateAvailable = 104, + + PAL_NwStatusUpdates_DebugLog = 200, +} PAL_NwStatusUpdates; + +// Error information structure +typedef struct +{ + int32_t errorCode; + int32_t errorDomain; + const char* errorMessage; +} PAL_NetworkFrameworkError; + +// Callback type definitions that match the implementation usage +typedef void (*StatusUpdateCallback)(void* context, PAL_NwStatusUpdates status, size_t data1, size_t data2, PAL_NetworkFrameworkError* error); +typedef int32_t (*WriteCallback)(void* context, uint8_t* buffer, uint64_t length); +typedef void (*CompletionCallback)(void* context, PAL_NetworkFrameworkError* error); +typedef void (*ReadCompletionCallback)(void* context, PAL_NetworkFrameworkError* error, const uint8_t* data, size_t length); +typedef void* (*ChallengeCallback)(void* context, CFArrayRef acceptableIssuers); + +// Initializes global state +PALEXPORT int32_t AppleCryptoNative_Init(StatusUpdateCallback statusFunc, WriteCallback writeFunc, ChallengeCallback challengeFunc); + +PALEXPORT nw_connection_t AppleCryptoNative_NwConnectionCreate(int32_t isServer, void* context, char* targetName, const uint8_t * alpnBuffer, int alpnLength, PAL_SslProtocol minTlsProtocol, PAL_SslProtocol maxTlsProtocol, uint32_t* cipherSuites, int cipherSuitesLength); +PALEXPORT int32_t AppleCryptoNative_NwConnectionStart(nw_connection_t connection, void* context); +PALEXPORT void AppleCryptoNative_NwConnectionSend(nw_connection_t connection, void* context, uint8_t* buffer, int length, CompletionCallback completionCallback); +PALEXPORT void AppleCryptoNative_NwConnectionReceive(nw_connection_t connection, void* context, uint32_t length, ReadCompletionCallback readCompletionCallback); +PALEXPORT void AppleCryptoNative_NwConnectionCancel(nw_connection_t connection); + +PALEXPORT int32_t AppleCryptoNative_NwFramerDeliverInput(nw_framer_t framer, void* context, const uint8_t* data, int dataLength, CompletionCallback completionCallback); + +PALEXPORT int32_t AppleCryptoNative_GetConnectionInfo(nw_connection_t connection, void* context, PAL_SslProtocol* pProtocol, uint16_t* pCipherSuiteOut, char* negotiatedAlpn, int32_t* negotiatedAlpnLength); + +#ifdef __cplusplus +} +#endif diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.m b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.m new file mode 100644 index 00000000000000..159fa373f1d88e --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_networkframework.m @@ -0,0 +1,606 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// Network.framework requires an underlying network connection for TLS operations, +// but we need to perform TLS without an actual connection so that we can expose +// it as the SslStream abstraction. This implementation uses a workaround: we +// create a dummy UDP connection that will never be used. +// +// The trick works by layering a custom framer on top of this dummy connection, +// then adding TLS on top of the framer. The framer intercepts the raw TLS data +// and exposes it to SslStream, preventing it from ever reaching the underlying +// connection. +// + +#include "pal_networkframework.h" +#include +#include +#include + +static WriteCallback _writeFunc; +static StatusUpdateCallback _statusFunc; +static ChallengeCallback _challengeFunc; +static nw_protocol_definition_t _framerDefinition; +static nw_protocol_definition_t _tlsDefinition; +static dispatch_queue_t _tlsQueue; +static dispatch_queue_t _inputQueue; +static nw_endpoint_t _endpoint; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + +#define LOG_IMPL_(context, isError, ...) \ + do { \ + char buff[256]; \ + snprintf(buff, sizeof(buff), __VA_ARGS__); \ + _statusFunc(context, PAL_NwStatusUpdates_DebugLog, (size_t)(buff), (size_t)(isError), NULL); \ + } while (0) + +#ifdef DEBUG +#define LOG_INFO(context, ...) LOG_IMPL_(context, 0, __VA_ARGS__) +#else +#define LOG_INFO(context, ...) do { (void)context; } while (0) +#endif + +#define LOG_ERROR(context, ...) LOG_IMPL_(context, 1, __VA_ARGS__) + +#define MANAGED_CONTEXT_KEY "GCHANDLE" + +static void* FramerGetManagedContext(nw_framer_t framer) +{ + void* ptr = NULL; + + if (__builtin_available(macOS 12.3, iOS 15.4, tvOS 15.4, watchOS 8.4, *)) + { + nw_protocol_options_t framer_options = nw_framer_copy_options(framer); + assert(framer_options != NULL); + + NSNumber* num = nw_framer_options_copy_object_value(framer_options, MANAGED_CONTEXT_KEY); + assert(num != NULL); + [num getValue:&ptr]; + [num release]; + + nw_release(framer_options); + } + + return ptr; +} + +static void FramerOptionsSetManagedContext(nw_protocol_options_t framer_options, void* context) +{ + if (__builtin_available(macOS 12.3, iOS 15.4, tvOS 15.4.0, watchOS 8.4, *)) + { + NSNumber *ref = [NSNumber numberWithLong:(long)context]; + nw_framer_options_set_object_value(framer_options, MANAGED_CONTEXT_KEY, ref); + [ref release]; + } +} + +static tls_protocol_version_t PalSslProtocolToTlsProtocolVersion(PAL_SslProtocol palProtocolId) +{ + switch (palProtocolId) + { + case PAL_SslProtocol_Tls13: + return tls_protocol_version_TLSv13; + case PAL_SslProtocol_Tls12: + return tls_protocol_version_TLSv12; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + case PAL_SslProtocol_Tls11: + return tls_protocol_version_TLSv11; + case PAL_SslProtocol_Tls10: + return tls_protocol_version_TLSv10; + case tls_protocol_version_DTLSv10: + default: + break; +#pragma clang diagnostic pop + } + + return (tls_protocol_version_t)0; +} + + +// Helper function to extract error information from nw_error_t +// Returns CFStringRef that needs to be released after use, or NULL if no CFString was created +static CFStringRef ExtractNetworkFrameworkError(nw_error_t error, PAL_NetworkFrameworkError* outError) +{ + if (error == NULL || outError == NULL) + { + if (outError != NULL) + { + outError->errorCode = 0; + outError->errorDomain = 0; + outError->errorMessage = NULL; + } + return NULL; + } + + outError->errorCode = nw_error_get_error_code(error); + nw_error_domain_t domain = nw_error_get_error_domain(error); + outError->errorDomain = (int32_t)domain; + + if (domain == nw_error_domain_posix) + { + outError->errorMessage = strerror(outError->errorCode); + return NULL; + } + + // Get error message from CoreFoundation error if available + CFStringRef descriptionToRelease = NULL; + CFErrorRef cfError = nw_error_copy_cf_error(error); + if (cfError != NULL) + { + CFStringRef description = CFErrorCopyDescription(cfError); + if (description != NULL) + { + outError->errorMessage = CFStringGetCStringPtr(description, kCFStringEncodingUTF8); + if (outError->errorMessage == NULL) + { + // If direct pointer access fails, we'll leave it as NULL + CFRelease(description); + } + else + { + // We got a direct pointer, so we need to keep the CFString alive + descriptionToRelease = description; + } + } + CFRelease(cfError); + } + else + { + outError->errorMessage = NULL; + } + + return descriptionToRelease; +} + +PALEXPORT nw_connection_t AppleCryptoNative_NwConnectionCreate(int32_t isServer, void* context, char* targetName, const uint8_t * alpnBuffer, int alpnLength, PAL_SslProtocol minTlsProtocol, PAL_SslProtocol maxTlsProtocol, uint32_t* cipherSuites, int cipherSuitesLength) +{ + if (isServer != 0) // the current implementation only supports client + return NULL; + + nw_parameters_t parameters = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" + //return connection; + + nw_protocol_options_t tls_options = nw_tls_create_options(); + sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options); + if (targetName != NULL) + { + sec_protocol_options_set_tls_server_name(sec_options, targetName); + } + + tls_protocol_version_t version = PalSslProtocolToTlsProtocolVersion(minTlsProtocol); + if ((int)version != 0) + { + LOG_INFO(context, "Min TLS version: %d", version); + sec_protocol_options_set_min_tls_protocol_version(sec_options, version); + } + + version = PalSslProtocolToTlsProtocolVersion(maxTlsProtocol); + if ((int)version != 0) + { + LOG_INFO(context, "Max TLS version: %d", version); + sec_protocol_options_set_max_tls_protocol_version(sec_options, version); + } + + if (alpnBuffer != NULL) + { + int offset = 0; + while (offset < alpnLength) + { + uint8_t length = alpnBuffer[offset]; + const char* alpn = (const char*) &alpnBuffer[offset + 1]; + LOG_INFO(context, "Appending ALPN: %s", alpn); + sec_protocol_options_add_tls_application_protocol(sec_options, alpn); + offset += length + 2; + } + } + + if (cipherSuites != NULL && cipherSuitesLength > 0) + { + for (int i = 0; i < cipherSuitesLength; i++) + { + uint16_t cipherSuite = (uint16_t)cipherSuites[i]; + LOG_INFO(context, "Appending cipher suite: 0x%04x", cipherSuite); + sec_protocol_options_append_tls_ciphersuite(sec_options, cipherSuite); + } + } + + // Set up challenge block to detect when server requests client certificate + sec_protocol_options_set_challenge_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_protocol_challenge_complete_t complete) + { + // Extract acceptable issuers from metadata + CFMutableArrayRef acceptableIssuers = NULL; + + if (metadata != NULL) + { + // Create array to hold distinguished names + acceptableIssuers = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + + // Access distinguished names from the metadata + sec_protocol_metadata_access_distinguished_names(metadata, ^(dispatch_data_t dn) + { + // Convert dispatch_data to CFData + const void* dnBytes = NULL; + size_t dnLength = 0; + dispatch_data_t contiguousDN = dispatch_data_create_map(dn, &dnBytes, &dnLength); + + if (dnBytes != NULL && dnLength > 0) + { + CFDataRef dnData = CFDataCreate(NULL, (const UInt8*)dnBytes, (CFIndex)dnLength); + if (dnData != NULL) + { + CFArrayAppendValue(acceptableIssuers, dnData); + CFRelease(dnData); + } + } + + if (contiguousDN != NULL) + { + dispatch_release(contiguousDN); + } + }); + } + + // Call the managed callback to get the client identity + void* identity = _challengeFunc(context, acceptableIssuers); + + // Clean up + CFRelease(acceptableIssuers); + + if (identity != NULL) + { + // Convert to sec_identity_t and set it + SecIdentityRef secIdentityRef = (SecIdentityRef)identity; + sec_identity_t sec_identity = sec_identity_create(secIdentityRef); + if (sec_identity != NULL) + { + complete(sec_identity); + sec_release(sec_identity); + } + + return; + } + + complete(NULL); + }, _tlsQueue); + + // we accept all certificates here and we will do validation later + sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust_ref, sec_protocol_verify_complete_t complete) + { + LOG_INFO(context, "Cert validation callback called"); + + SecTrustRef chain = sec_trust_copy_ref(trust_ref); + + _statusFunc(context, PAL_NwStatusUpdates_CertificateAvailable, (size_t)chain, 0, NULL); + + (void)metadata; + (void)trust_ref; + complete(true); + }, _tlsQueue); + + nw_release(sec_options); + + nw_protocol_options_t framer_options = nw_framer_create_options(_framerDefinition); + FramerOptionsSetManagedContext(framer_options, context); + + nw_protocol_stack_t protocol_stack = nw_parameters_copy_default_protocol_stack(parameters); + nw_protocol_stack_prepend_application_protocol(protocol_stack, framer_options); + nw_protocol_stack_prepend_application_protocol(protocol_stack, tls_options); + + nw_release(framer_options); + nw_release(protocol_stack); + nw_release(tls_options); + + nw_connection_t connection = nw_connection_create(_endpoint, parameters); + + nw_release(parameters); + + if (connection == NULL) + { + LOG_ERROR(context, "Failed to create Network Framework connection"); + return NULL; + } + + return connection; +} + +// This writes encrypted TLS frames to the safe handle. It is executed on NW Thread pool +static nw_framer_output_handler_t framer_output_handler = ^(nw_framer_t framer, nw_framer_message_t message, size_t message_length, bool is_complete) +{ + if (__builtin_available(macOS 12.3, iOS 15.4, tvOS 15.4, watchOS 2.0, *)) + { + void* context = FramerGetManagedContext(framer); + size_t size = message_length; + + nw_framer_parse_output(framer, 1, message_length, NULL, ^size_t(uint8_t *buffer, size_t buffer_length, bool is_complete2) + { + (_writeFunc)(context, buffer, buffer_length); + (void)is_complete2; + (void)message; + return buffer_length; + }); + } + else + { + assert(0); + } + (void)is_complete; +}; + +static nw_framer_stop_handler_t framer_stop_handler = ^bool(nw_framer_t framer) +{ + (void)framer; + return TRUE; +}; + +static nw_framer_cleanup_handler_t framer_cleanup_handler = ^(nw_framer_t framer) +{ + (void)framer; +}; + +// This is called when connection start to set up framer +static nw_framer_start_handler_t framer_start = ^nw_framer_start_result_t(nw_framer_t framer) +{ + assert(_statusFunc != NULL); + + void* context = FramerGetManagedContext(framer); + + // Notify managed code with framer reference so we can submit to it directly. + (_statusFunc)(context, PAL_NwStatusUpdates_FramerStart, (size_t)framer, 0, NULL); + + nw_framer_set_output_handler(framer, framer_output_handler); + + nw_framer_set_stop_handler(framer, framer_stop_handler); + nw_framer_set_cleanup_handler(framer, framer_cleanup_handler); + + return nw_framer_start_result_ready; +}; + + +// this takes encrypted input from underlying stream and feeds it to nw_connection. +PALEXPORT int32_t AppleCryptoNative_NwFramerDeliverInput(nw_framer_t framer, void* context, const uint8_t* buffer, int bufferLength, CompletionCallback completionCallback) +{ + assert(framer != NULL); + if (framer == NULL) + { + LOG_ERROR(context, "NwFramerDeliverInput called with NULL framer"); + return -1; + } + + nw_framer_message_t message = nw_framer_message_create(framer); + + // There is a race condition when connection can fail or be canceled and if it does we fail to create the message here. + if (message == NULL) + { + LOG_ERROR(context, "NwFramerDeliverInput failed to create message"); + return -1; + } + + nw_framer_async(framer, ^(void) + { + nw_framer_deliver_input(framer, buffer, (size_t)bufferLength, message, bufferLength > 0 ? FALSE : TRUE); + completionCallback(context, NULL); + nw_release(message); + }); + + return 0; +} + +// This starts TLS handshake. For client, it will produce ClientHello and call output handler (on thread pool) +// important part here is the context handler that will get asynchronous notifications about progress. +PALEXPORT int AppleCryptoNative_NwConnectionStart(nw_connection_t connection, void* context) +{ + if (connection == NULL) + { + LOG_ERROR(context, "NwConnectionStart called with NULL connection"); + return -1; + } + + nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t status, nw_error_t error) + { + PAL_NetworkFrameworkError errorInfo; + CFStringRef cfStringToRelease = ExtractNetworkFrameworkError(error, &errorInfo); + LOG_INFO(context, "Connection context changed: %d, errorCode: %d", (int)status, errorInfo.errorCode); + switch (status) + { + case nw_connection_state_preparing: + case nw_connection_state_waiting: + case nw_connection_state_failed: + { + if (errorInfo.errorCode != 0 || status == nw_connection_state_failed) + { + (_statusFunc)(context, PAL_NwStatusUpdates_ConnectionFailed, 0, 0, &errorInfo); + } + } + break; + case nw_connection_state_ready: + { + (_statusFunc)(context, PAL_NwStatusUpdates_HandshakeFinished, 0, 0, NULL); + } + break; + case nw_connection_state_cancelled: + { + (_statusFunc)(context, PAL_NwStatusUpdates_ConnectionCancelled, 0, 0, NULL); + } + break; + case nw_connection_state_invalid: + { + (_statusFunc)(context, PAL_NwStatusUpdates_UnknownError, 0, 0, NULL); + } + break; + } + + // Release CFString if we created one + if (cfStringToRelease != NULL) + { + CFRelease(cfStringToRelease); + } + }); + + nw_connection_set_queue(connection, _tlsQueue); + nw_connection_start(connection); + + return 0; +} + +// This will start connection cleanup +PALEXPORT void AppleCryptoNative_NwConnectionCancel(nw_connection_t connection) +{ + nw_connection_cancel(connection); +} + +// this is used by encrypt. We write plain text to the connection and it will be handound out encrypted via output handler +PALEXPORT void AppleCryptoNative_NwConnectionSend(nw_connection_t connection, void* context, uint8_t* buffer, int length, CompletionCallback completionCallback) +{ + dispatch_data_t data = dispatch_data_create(buffer, (size_t)length, _inputQueue, ^() + { + // we specify empty destructor instead of DISPATCH_DATA_DESTRUCTOR_DEFAULT to avoid creating + // an internal copy of the data. The caller ensures the buffer is valid until we call completionCallback. + }); + + nw_connection_send(connection, data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, FALSE, ^(nw_error_t error) + { + PAL_NetworkFrameworkError errorInfo; + CFStringRef cfStringToRelease = ExtractNetworkFrameworkError(error, &errorInfo); + completionCallback(context, error != NULL ? &errorInfo : NULL); + + // Release CFString if we created one + if (cfStringToRelease != NULL) + { + CFRelease(cfStringToRelease); + } + }); + + // Release our reference to dispatch_data - nw_connection_send retains it + dispatch_release(data); +} + +// This is used by decrypt. We feed data in via AppleCryptoNative_NwProcessInputData and we try to read from the connection. +PALEXPORT void AppleCryptoNative_NwConnectionReceive(nw_connection_t connection, void* context, uint32_t length, ReadCompletionCallback readCompletionCallback) +{ + nw_connection_receive(connection, 0, length, ^(dispatch_data_t content, nw_content_context_t ctx, bool is_complete, nw_error_t error) + { + PAL_NetworkFrameworkError errorInfo; + + if (error != NULL) + { + CFStringRef cfStringToRelease = ExtractNetworkFrameworkError(error, &errorInfo); + readCompletionCallback(context, &errorInfo, NULL, 0); + + // Release CFString if we created one + if (cfStringToRelease != NULL) + { + CFRelease(cfStringToRelease); + } + return; + } + + if (content != NULL) + { + const void *buffer; + size_t bufferLength; + dispatch_data_t tmp = dispatch_data_create_map(content, &buffer, &bufferLength); + readCompletionCallback(context, NULL, (const uint8_t*)buffer, bufferLength); + dispatch_release(tmp); + return; + } + + if (is_complete || content == NULL) + { + readCompletionCallback(context, NULL, NULL, 0); + return; + } + + (void)ctx; + }); +} + +// This wil get TLS details after handshake is finished +PALEXPORT int32_t AppleCryptoNative_GetConnectionInfo(nw_connection_t connection, void* context, PAL_SslProtocol* protocol, uint16_t* pCipherSuiteOut, char* negotiatedAlpn, int32_t* negotiatedAlpnLength) +{ + nw_protocol_metadata_t meta = nw_connection_copy_protocol_metadata(connection, _tlsDefinition); + + if (meta == NULL) + { + LOG_ERROR(context, "nw_connection_copy_protocol_metadata returned null"); + return -1; + } + + sec_protocol_metadata_t secMeta = nw_tls_copy_sec_protocol_metadata(meta); + + const char* alpn = sec_protocol_metadata_get_negotiated_protocol(secMeta); + if (alpn != NULL) + { + strcpy(negotiatedAlpn, alpn); + *negotiatedAlpnLength = (int32_t)strlen(alpn); + } + else + { + negotiatedAlpn[0] = '\0'; + *negotiatedAlpnLength = 0; + } + + tls_protocol_version_t version = sec_protocol_metadata_get_negotiated_tls_protocol_version(secMeta); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + switch (version) + { + case tls_protocol_version_TLSv10: + *protocol = PAL_SslProtocol_Tls10; + break; + case tls_protocol_version_TLSv11: + *protocol = PAL_SslProtocol_Tls11; + break; + case tls_protocol_version_TLSv12: + *protocol = PAL_SslProtocol_Tls12; + break; + case tls_protocol_version_TLSv13: + *protocol = PAL_SslProtocol_Tls13; + break; + case tls_protocol_version_DTLSv10: + case tls_protocol_version_DTLSv12: + default: + *protocol = PAL_SslProtocol_None; + break; + } +#pragma clang diagnostic pop + + *pCipherSuiteOut = sec_protocol_metadata_get_negotiated_tls_ciphersuite(secMeta); + + nw_release(meta); + sec_release(secMeta); + return 0; +} + +// this is called once to set everything up +PALEXPORT int32_t AppleCryptoNative_Init(StatusUpdateCallback statusFunc, WriteCallback writeFunc, ChallengeCallback challengeFunc) +{ + assert(statusFunc != NULL); + assert(writeFunc != NULL); + + if (__builtin_available(macOS 12.3, iOS 15.4, tvOS 15.4.0, watchOS 8.4, *)) + { + _writeFunc = writeFunc; + _statusFunc = statusFunc; + _challengeFunc = challengeFunc; + _framerDefinition = nw_framer_create_definition("com.dotnet.networkframework.tlsframer", + NW_FRAMER_CREATE_FLAGS_DEFAULT, framer_start); + _tlsDefinition = nw_protocol_copy_tls_definition(); + _tlsQueue = dispatch_queue_create("com.dotnet.networkframework.tlsqueue", NULL); + _inputQueue = _tlsQueue; + + // The endpoint values (127.0.0.1:42) are arbitrary - they just need to be + // syntactically and semantically valid since the connection is never established. + _endpoint = nw_endpoint_create_host("127.0.0.1", "42"); + + return 0; + } + + return 1; +} diff --git a/src/tasks/AppleAppBuilder/Templates/CMakeLists-librarymode.txt.template b/src/tasks/AppleAppBuilder/Templates/CMakeLists-librarymode.txt.template index bda7c7fb3a30be..c2d4e9c004cde4 100644 --- a/src/tasks/AppleAppBuilder/Templates/CMakeLists-librarymode.txt.template +++ b/src/tasks/AppleAppBuilder/Templates/CMakeLists-librarymode.txt.template @@ -61,6 +61,7 @@ target_link_libraries( %ProjectName% PRIVATE "-framework Foundation" + "-framework Network" "-framework Security" "-framework CryptoKit" "-framework UIKit" diff --git a/src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template b/src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template index 300f2fc5414f4b..850825f2039e0b 100644 --- a/src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template +++ b/src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template @@ -70,6 +70,7 @@ target_link_libraries( %ProjectName% PRIVATE "-framework Foundation" + "-framework Network" "-framework Security" "-framework CryptoKit" "-framework UIKit" diff --git a/src/tasks/LibraryBuilder/Templates/CMakeLists.txt.template b/src/tasks/LibraryBuilder/Templates/CMakeLists.txt.template index 2af12ae8cf85a0..d7d0406de4fe83 100644 --- a/src/tasks/LibraryBuilder/Templates/CMakeLists.txt.template +++ b/src/tasks/LibraryBuilder/Templates/CMakeLists.txt.template @@ -42,6 +42,7 @@ if(TARGETS_ANDROID) elseif(TARGETS_APPLE_MOBILE) set(MOBILE_SYSTEM_LIBS "-framework Foundation" + "-framework Network" "-framework Security" "-framework CryptoKit" "-framework UIKit"