Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal static unsafe partial bool Init(

// 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, IntPtr serverIdentity);
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, IntPtr serverIdentity, [MarshalAs(UnmanagedType.I4)] bool requireClientCert);

// Start the TLS handshake, notifications are received via the status callback (potentially from a different thread).
[LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionStart")]
Expand All @@ -52,7 +52,7 @@ internal static unsafe partial bool Init(

// 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);
internal static unsafe partial int GetConnectionInfo(SafeNwHandle connection, IntPtr context, out SslProtocols pProtocol, out TlsCipherSuite pCipherSuiteOut, byte* negotiatedAlpn, ref int negotiatedAlpnLength, byte* serverName, ref int serverNameLength);
}

// Status enumeration for Network Framework TLS operations
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,38 @@ namespace System.Net.Security
{
internal partial struct SslConnectionInfo
{
public void UpdateSslConnectionInfo(SafeDeleteContext context)
public void UpdateSslConnectionInfo(SafeDeleteContext context, out string? negotiatedServerName)
{
switch (context)
{
case SafeDeleteNwContext nwContext:
UpdateSslConnectionInfoNetworkFramework(nwContext);
negotiatedServerName = UpdateSslConnectionInfoNetworkFramework(nwContext);
break;
case SafeDeleteSslContext sslContext:
UpdateSslConnectionInfoAppleCrypto(sslContext);
negotiatedServerName = null;
break;
default:
throw new NotSupportedException("Unsupported context type.");
}
}

private unsafe void UpdateSslConnectionInfoNetworkFramework(SafeDeleteNwContext context)
private unsafe string? UpdateSslConnectionInfoNetworkFramework(SafeDeleteNwContext context)
{
SafeNwHandle nwContext = context.ConnectionHandle;
SslProtocols protocol;
TlsCipherSuite cipherSuite;

Span<byte> alpn = stackalloc byte[256]; // Ensure the stack is initialized for alpnPtr
Span<byte> alpn = stackalloc byte[256];
int alpnLength = alpn.Length;
Span<byte> serverName = stackalloc byte[256];
int serverNameLength = serverName.Length;

int osStatus;
unsafe
fixed (byte* alpnPtr = alpn)
fixed (byte* serverNamePtr = serverName)
{
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);
}
osStatus = Interop.NetworkFramework.Tls.GetConnectionInfo(nwContext, context.StateHandle, out protocol, out cipherSuite, alpnPtr, ref alpnLength, serverNamePtr, ref serverNameLength);
}

if (osStatus != 0)
Expand All @@ -63,6 +63,8 @@ private unsafe void UpdateSslConnectionInfoNetworkFramework(SafeDeleteNwContext
// cipher suite and ALPN; sec_protocol_metadata_get_early_data_accepted covers TLS 1.3
// 0-RTT only, not general resumption). Leave TlsResumed as false so that the peer
// certificate is always revalidated on this backend, matching the safe fallback.

return TlsFrameHelper.DecodeString(serverName.Slice(0, serverNameLength));
}

private void UpdateSslConnectionInfoAppleCrypto(SafeDeleteSslContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,11 @@ public static void QueryContextConnectionInfo(
SafeDeleteContext securityContext,
ref SslConnectionInfo connectionInfo)
{
connectionInfo.UpdateSslConnectionInfo(securityContext);
connectionInfo.UpdateSslConnectionInfo(securityContext, out string? serverName);
if (serverName is not null && securityContext is SafeDeleteNwContext { IsServer: true } nwContext)
{
nwContext.SetServerTargetHost(serverName);
}
}

public static bool TryUpdateClintCertificate(
Expand Down Expand Up @@ -359,6 +363,16 @@ private static ProtocolToken HandshakeInternal(
ProtocolToken token = default;
consumed = 0;

if (context is SafeDeleteNwContext)
{
// The only path that reaches here with a Network Framework context is the shutdown
// token, and ApplyShutdownToken has already cancelled the connection, which makes
// Network Framework emit close_notify itself. There is no token for SslStream to
// send, so report success rather than falling into the SecureTransport path.
token.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.OK);
return token;
}

try
{
if ((null == context) || context.IsInvalid)
Expand Down Expand Up @@ -494,14 +508,20 @@ private static bool ShouldUseNetworkFramework(
sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.AllowNoEncryption;
#pragma warning restore SYSLIB0040

return
bool useNetworkFramework =
SafeDeleteNwContext.IsNetworkFrameworkAvailable &&
!sslAuthenticationOptions.ForceSyncPal &&
encryptionPolicyOk &&
// Network Framework has no API for advertising a custom CA list in the
// CertificateRequest, so a server configured with SslCertificateTrust has to keep
// using SecureTransport rather than silently dropping the caller's trust list.
sslAuthenticationOptions.CertificateContext?.Trust == null &&
(sslAuthenticationOptions.IsClient || sslAuthenticationOptions.CertificateContext != null) &&
(sslAuthenticationOptions.EnabledSslProtocols == SslProtocols.None ||
sslAuthenticationOptions.EnabledSslProtocols == SslProtocols.Tls13 ||
(sslAuthenticationOptions.EnabledSslProtocols == (SslProtocols.Tls12 | SslProtocols.Tls13)));
sslAuthenticationOptions.EnabledSslProtocols == (SslProtocols.Tls12 | SslProtocols.Tls13));

return useNetworkFramework;
}

private static SafeDeleteNwContext CreateAsyncSecurityContext(SslStream stream)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ private static SslProtocols TlsMinorVersionToProtocol(byte value)
};
}

private static string? DecodeString(ReadOnlySpan<byte> bytes)
internal static string? DecodeString(ReadOnlySpan<byte> bytes)
{
// https://tools.ietf.org/html/rfc3546#section-3.1
// Per spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.DotNet.RemoteExecutor;
using Microsoft.DotNet.XUnitExtensions;
using Xunit;
using Xunit.Abstractions;
using Microsoft.DotNet.XUnitExtensions;

namespace System.Net.Security.Tests
{
Expand All @@ -38,7 +40,7 @@ public void Dispose()
_clientCertificate.Dispose();
}

[Theory]
[ConditionalTheory]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
Expand All @@ -50,6 +52,15 @@ public async Task CertificateSelectionCallback_DelayedCertificate_OK(bool delayC
throw new SkipTestException("Android does not support delayed certificate selection.");
}

if (PlatformDetection.IsNetworkFrameworkEnabled() && !sendClientCertificate)
{
// Network.framework enforces ClientCertificateRequired itself and fails the handshake
// when the client presents no certificate, so RemoteCertificateValidationCallback never
// gets the chance to accept the absence that this case relies on. The resulting
// behavior is asserted by NetworkFramework_MissingClientCertificate_FailsHandshake.
throw new SkipTestException("Network.framework cannot request an optional client certificate.");
}

X509Certificate? remoteCertificate = null;

(SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
Expand Down Expand Up @@ -96,9 +107,11 @@ public async Task CertificateSelectionCallback_DelayedCertificate_OK(bool delayC
};


Task clientTask = client.AuthenticateAsClientAsync(clientOptions);

await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
client.AuthenticateAsClientAsync(clientOptions),
server.AuthenticateAsServerAsync(serverOptions));
clientTask,
server.AuthenticateAsServerAsync(serverOptions));

// verify that the session is usable with or without client's certificate
await TestHelper.PingPong(client, server);
Expand All @@ -112,6 +125,60 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
}
}

// Network.framework requires a client certificate once ClientCertificateRequired is set: it
// fails the handshake itself rather than surfacing SslPolicyErrors.RemoteCertificateNotAvailable
// to RemoteCertificateValidationCallback, because the API that would request a certificate
// optionally (sec_protocol_options_set_peer_authentication_optional) is unavailable on macOS.
Comment thread
rzikm marked this conversation as resolved.
// This documents that divergence: the handshake fails even when the callback accepts.
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[PlatformSpecific(TestPlatforms.OSX)]
[InlineData(true)]
[InlineData(false)]
public async Task NetworkFramework_MissingClientCertificate_FailsHandshake(bool acceptMissingCertificate)
{
var psi = new ProcessStartInfo
{
Environment = { { "DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK", "1" } }
};

await RemoteExecutor.Invoke(static async (string acceptMissingCertificateString) =>
{
bool acceptMissingCertificate = bool.Parse(acceptMissingCertificateString);
bool validationCallbackInvoked = false;
(SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
using (client)
using (server)
using (X509Certificate2 serverCertificate = Configuration.Certificates.GetServerCertificate())
{
var clientOptions = new SslClientAuthenticationOptions
{
TargetHost = "localhost",
RemoteCertificateValidationCallback = TestHelper.AllowAnyServerCertificate,
};
var serverOptions = new SslServerAuthenticationOptions
{
ServerCertificate = serverCertificate,
ClientCertificateRequired = true,
RemoteCertificateValidationCallback = (_, _, _, _) =>
{
validationCallbackInvoked = true;
return acceptMissingCertificate;
},
};

Task clientTask = client.AuthenticateAsClientAsync(clientOptions);

// The verdict of the callback is irrelevant here - Network.framework has already
// aborted the handshake by the time managed validation would run.
await Assert.ThrowsAsync<AuthenticationException>(
() => server.AuthenticateAsServerAsync(serverOptions));
Assert.False(validationCallbackInvoked);

await clientTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
}, acceptMissingCertificate.ToString(), new RemoteInvokeOptions { StartInfo = psi }).DisposeAsync();
}

public enum ClientCertSource
{
ClientCertificate,
Expand Down
Loading
Loading