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"