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
10 changes: 6 additions & 4 deletions src/libraries/System.Net.Security/src/System.Net.Security.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,17 @@
Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(UseAndroidCrypto)' != 'true' and '$(UseAppleCrypto)' != 'true'" />
<Compile Include="System\Net\Security\TlsOperationStatus.cs" />
<Compile Include="System\Net\Security\TlsSession.cs"
Condition="'$(TargetPlatformIdentifier)' != '' and '$(UseAndroidCrypto)' != 'true'" />
Condition="'$(TargetPlatformIdentifier)' != ''" />
<Compile Include="System\Net\Security\TlsBufferSession.cs"
Condition="'$(TargetPlatformIdentifier)' != '' and '$(UseAndroidCrypto)' != 'true'" />
Condition="'$(TargetPlatformIdentifier)' != ''" />
<Compile Include="System\Net\Security\TlsSocketSession.cs"
Condition="'$(TargetPlatformIdentifier)' != '' and '$(UseAndroidCrypto)' != 'true'" />
Condition="'$(TargetPlatformIdentifier)' != ''" />
<Compile Include="System\Net\Security\TlsSession.Android.cs"
Condition="'$(UseAndroidCrypto)' == 'true'" />
<Compile Include="System\Net\Security\TlsSession.OpenSsl.cs"
Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows' and '$(UseAndroidCrypto)' != 'true' and '$(UseAppleCrypto)' != 'true'" />
<Compile Include="System\Net\Security\TlsSession.Stub.cs"
Condition="'$(TargetPlatformIdentifier)' == '' or '$(UseAndroidCrypto)' == 'true'" />
Condition="'$(TargetPlatformIdentifier)' == ''" />
<Compile Include="System\Net\Security\SslStreamCertificateContext.cs" />
<Compile Include="System\Net\Security\SslConnectionInfo.cs" />
<Compile Include="System\Net\Security\StreamSizes.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Diagnostics;
using System.Collections.Generic;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
Comment thread
wfurt marked this conversation as resolved.
using System.Security.Cryptography;
Expand All @@ -13,7 +12,7 @@
using PAL_KeyAlgorithm = Interop.AndroidCrypto.PAL_KeyAlgorithm;
using PAL_SSLStreamStatus = Interop.AndroidCrypto.PAL_SSLStreamStatus;

namespace System.Net
namespace System.Net.Security
{
internal sealed class SafeDeleteSslContext : SafeDeleteContext
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ internal sealed class JavaProxy : IDisposable
{
private static bool s_initialized;

private readonly SslStream _sslStream;
private readonly Func<IntPtr, RemoteCertificateValidationResult> _validator;
private GCHandle? _handle;

public IntPtr Handle
Expand All @@ -79,14 +79,21 @@ public IntPtr Handle
public Exception? ValidationException { get; private set; }
public RemoteCertificateValidationResult? ValidationResult { get; private set; }

public JavaProxy(SslStream sslStream)
public JavaProxy(Func<IntPtr, RemoteCertificateValidationResult> validator)
{
ArgumentNullException.ThrowIfNull(validator);

RegisterRemoteCertificateValidationCallback();

_sslStream = sslStream;
_validator = validator;
_handle = GCHandle.Alloc(this);
}

public JavaProxy(SslStream sslStream)
: this(sslStream.VerifyRemoteCertificate)
{
}

public void Dispose()
{
_handle?.Free();
Expand All @@ -111,7 +118,7 @@ private static bool VerifyRemoteCertificate(IntPtr sslStreamProxyHandle, IntPtr

try
{
proxy.ValidationResult = proxy._sslStream.VerifyRemoteCertificate(platformValidationError);
proxy.ValidationResult = proxy._validator(platformValidationError);
return proxy.ValidationResult.IsValid;
}
catch (Exception exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static SecurityStatusPal SelectApplicationProtocol(
}

public static ProtocolToken AcceptSecurityContext(
ref SafeFreeCredentials credential,
ref SafeFreeCredentials? credential,
ref SafeDeleteSslContext? context,
ReadOnlySpan<byte> inputBuffer,
out int consumed,
Expand All @@ -57,7 +57,7 @@ public static ProtocolToken AcceptSecurityContext(
}

public static ProtocolToken InitializeSecurityContext(
ref SafeFreeCredentials credential,
ref SafeFreeCredentials? credential,
ref SafeDeleteSslContext? context,
string? targetName,
ReadOnlySpan<byte> inputBuffer,
Expand Down Expand Up @@ -211,7 +211,7 @@ public static bool TryUpdateClintCertificate(
}

private static ProtocolToken HandshakeInternal(
SafeFreeCredentials credential,
SafeFreeCredentials? credential,
ref SafeDeleteSslContext? context,
ReadOnlySpan<byte> inputBuffer,
out int consumed,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography.X509Certificates;

namespace System.Net.Security
{
public abstract partial class TlsSession
{
private bool _platformChainRejected;

partial void InitializePlatformSpecificSessionState()
{
// In wedge mode the options bag is shared with SslStream, which installs its own
// proxy in its constructor. Only supply one when the bag does not already have it,
// so SslStream's callback routing stays intact and its JavaProxy is not leaked.
_options.SslStreamProxy ??= new SslStream.JavaProxy(AcceptAndDeferPlatformValidation);
}

partial void SeedPlatformValidationErrors(ref SslPolicyErrors sslPolicyErrors)
{
if (_platformChainRejected)
{
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
}
}

// Invoked synchronously from Android's DotnetProxyTrustManager. Always accepts so
// the handshake progresses; the platform verdict (if respected) is recorded and
// surfaced later through AcceptWithDefaultValidation. The verdict is assigned rather
// than latched so a later validation (e.g. renegotiation with a different chain) is
// not tainted by an earlier rejection.
private SslStream.JavaProxy.RemoteCertificateValidationResult AcceptAndDeferPlatformValidation(IntPtr platformValidationError)
Comment on lines +28 to +33

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember that always accepting during handshake and only surfacing the result later did not work on Android and was causing all sorts of problem. That's the reason why we even needed the JavaProxy and hacking a way to integrate it in SslStream. I would prefer if we would not do it this way in TlsSession. Is that possible? Is that something that could be done later? Wouldn't it be better to do the actual validation before the handshake completes on other platforms too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have specific examples? Maybe you can ping me privately. I'm open to what ever this needs. As I mentioned , the goal is to isolate TLS functionality here and make SslStream consumer of it. So in general it needs to do everything SslStream needs. I used my local setup with emulator and the CI Android pipeline for verification. But I can broaden that as needed. Also we may do follow-up fixes if/when need to. This is primarily for the completeness so we can start on the SslStream internal cleanup.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did little bit more digging @simonrozsival

The fundamental problem is that the trust manager runs inside the PAL call on the caller's thread. To block it until the caller posts a verdict, the caller can't be the blocked thread, or it can never return from Handshake() to supply one. And since we can't know which frame carries the peer Certificate, we can't offload just that call — the session would have to drive all PAL interaction on a dedicated thread from the start.

So it did work for SslStream with synchronous callback. But that is problematic in general as the validation may need to fetch intermediates or do other IO (like revocation check) so to do it right would should probably add some ways hot to support asynchronous validation in SslStream.

@rzikm did some work for OpenSSL but that still have some caveats. And this goes possibly beyond handshake: with TLS 1.3 post-handshake auth the peer certificate arrives during application data, so the trust manager can fire inside Read/Write/RequestClientCertificate as well. So every PAL entry point becomes a cross-thread handoff, with the worker JNI-attached for the session's lifetime.

I'm not sure what would be good way out of this and I'm open to suggestions.

{
bool rejected = platformValidationError != IntPtr.Zero && ShouldRespectPlatformValidation();
_platformChainRejected = rejected;

if (rejected && NetEventSource.Log.IsEnabled())
{
string? validationError = Interop.AndroidCrypto.GetPlatformValidationError(platformValidationError);
NetEventSource.Error(this, $"The Android platform trust manager rejected the remote certificate chain: {validationError}");
}

return new SslStream.JavaProxy.RemoteCertificateValidationResult
{
IsValid = true,
SslPolicyErrors = SslPolicyErrors.None,
ChainStatus = default,
AlertToken = default,
};
}

private bool ShouldRespectPlatformValidation()
{
return _options.CertificateChainPolicy is not null
? _options.CertificateChainPolicy.TrustMode != X509ChainTrustMode.CustomRootTrust
: _options.CertificateContext?.Trust is null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,15 @@ private void InitializeFromContext(TlsContext context)
_ownsSessionCertificateContext = _options.OwnsCertificateContext;
_options.OwnsCertificateContext = false;

InitializePlatformSpecificSessionState();

OnContextInitialized();
}

partial void InitializePlatformSpecificSessionState();

partial void SeedPlatformValidationErrors(ref SslPolicyErrors sslPolicyErrors);

internal virtual void OnContextInitialized()
{
}
Expand Down Expand Up @@ -371,6 +377,7 @@ public SslPolicyErrors AcceptWithDefaultValidation()

ProtocolToken alertToken = default;
SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None;
SeedPlatformValidationErrors(ref sslPolicyErrors);
bool ok;
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void Dispose()
_clientCertificate.Dispose();
}

[Theory]
[ConditionalTheory]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.DotNet.XUnitExtensions;

using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -242,6 +243,7 @@ public static IEnumerable<object[]> Alpn_TestData()
}

[ConditionalFact(nameof(BackendSupportsAlpn))]
[SkipOnPlatform(TestPlatforms.Android, "JSSE rejects the oversized ALPN list during the handshake, surfacing AuthenticationException instead of ArgumentException.")]
public async Task SslStream_StreamToStream_AlpnListTotalSizeExceedsLimit_Throws()
{
// Each protocol is 255 bytes, serialized with a 1-byte length prefix = 256 bytes each.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public async Task Handshake_Success(FramingType framingType, SslProtocols sslPro
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsTls13))]
[SkipOnPlatform(TestPlatforms.Android, "SslStream hangs waiting for more data instead of detecting the complete 5-byte TLS frame.")]
public async Task Read_ExactlyFiveByteTlsRecord_DetectedAsCompleteFrame()
{
// Regression test: a TLS record that is exactly the 5-byte header with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<Compile Include="SslStreamFramingTest.cs" />
<Compile Include="SslStreamMutualAuthenticationTest.cs" />
<Compile Include="TlsSessionTests.cs"
Condition="'$(TargetPlatformIdentifier)' == 'unix' or '$(TargetPlatformIdentifier)' == 'windows' or '$(TargetPlatformIdentifier)' == 'osx'" />
Condition="'$(TargetPlatformIdentifier)' == 'unix' or '$(TargetPlatformIdentifier)' == 'windows' or '$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'android'" />
Comment thread
wfurt marked this conversation as resolved.
<Compile Include="TransportContextTest.cs" />
<!-- NegotiateAuthentication Tests -->
<Compile Include="NegotiateAuthenticationKerberosTest.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal static class TestConfiguration
public static bool SupportsHandshakeAlerts { get { return OperatingSystem.IsLinux() || OperatingSystem.IsWindows() || OperatingSystem.IsFreeBSD() || OperatingSystem.IsOpenBSD(); } }
public static bool SupportsRenegotiation { get { return OperatingSystem.IsWindows() || ((OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD() || OperatingSystem.IsOpenBSD()) && PlatformDetection.OpenSslVersion >= new Version(1, 1, 1)); } }

public static bool SupportsUniqueChannelBinding => PlatformDetection.IsNotMobile && !PlatformDetection.IsApplePlatform;

public static readonly X509Certificate2 ServerCertificate = System.Net.Test.Common.Configuration.Certificates.GetServerCertificate();

public static Task WhenAllOrAnyFailedWithTimeout(params Task[] tasks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace System.Net.Security.Tests
{
[PlatformSpecific(TestPlatforms.Linux | TestPlatforms.FreeBSD | TestPlatforms.Windows | TestPlatforms.OSX)]
[PlatformSpecific(TestPlatforms.Linux | TestPlatforms.FreeBSD | TestPlatforms.Windows | TestPlatforms.OSX | TestPlatforms.Android)]
public class TlsSessionTests
{
private const int CipherBufSize = 32 * 1024;
Expand Down Expand Up @@ -169,6 +169,7 @@ public async Task ServerSession_DeferredOptions_SelectedFromSni_Succeeds()
[InlineData(SslProtocols.Tls12, false)]
[InlineData(SslProtocols.Tls13, true)]
[InlineData(SslProtocols.Tls13, false)]
[SkipOnPlatform(TestPlatforms.Android, "Each Android session builds its own SSLContext, so the JSSE server session cache is never shared between connections and the second handshake cannot resume.")]
public async Task ServerSession_TlsResume_HonorsAllowTlsResumeOption(SslProtocols protocol, bool allowResume)
{
if (OperatingSystem.IsMacOS())
Expand Down Expand Up @@ -480,6 +481,7 @@ public async Task ServerSession_MutualAuth_InitialHandshake_InvokesValidator()
[Theory]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.Android, "JSSE's trust manager has no retry-verify equivalent, so the rejection is deferred and no alert reaches the client.")]
public async Task SslStreamServer_RejectsClientCert_ClientObservesAlert(SslProtocols protocol)
{
if (protocol == SslProtocols.Tls13 && !PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -708,7 +710,7 @@ await DriveHandshakeWithExternalValidationAsync(
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows))]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not surface a deferred client-credential prompt; SslStream supplies the certificate up-front.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Deferred client credentials are an OpenSSL-only flow: JSSE takes the KeyManagers up-front at SSLContext.init and the Android PAL never reports CredentialsNeeded, so no mid-handshake CertificateRequest is surfaced.")]
public async Task ClientSession_WantCredentials_SetClientCertificateContext_ResumesHandshake(SslProtocols protocol)
{
// Server (SslStream) demands a client certificate. The client TlsContext is
Expand Down Expand Up @@ -891,8 +893,7 @@ public async Task ServerSession_OptionalClientCert_NoCertSent_HandshakeCompletes
}
}

[Fact]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not expose the TLS exporter required to compute tls-server-end-point channel binding here.")]
[ConditionalFact(typeof(TestConfiguration), nameof(TestConfiguration.SupportsUniqueChannelBinding))]
public async Task ServerSession_ChannelBinding_MatchesSslStreamClient()
{
using X509Certificate2 serverCert = TestCertificates.GetServerCertificate();
Expand Down Expand Up @@ -1957,8 +1958,7 @@ public async Task ServerSession_ServerCertificateSelectionCallback_InvokedWithSn
}
}

[Fact]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not support post-handshake renegotiation.")]
[ConditionalFact(typeof(TestConfiguration), nameof(TestConfiguration.SupportsRenegotiation))]
public async Task ServerSession_RequestClientCertificate_Tls12_ProducesHandshakeBytes()
{
using X509Certificate2 serverCert = TestCertificates.GetServerCertificate();
Expand Down Expand Up @@ -2022,7 +2022,7 @@ public async Task ServerSession_RequestClientCertificate_Tls12_ProducesHandshake
[Theory]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not support post-handshake client authentication.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Neither SecureTransport nor JSSE supports post-handshake client authentication.")]
public async Task ServerSession_RequestClientCertificate_DrivesSecondHandshakeToCompletion(SslProtocols protocol)
{
if (protocol == SslProtocols.Tls13 && !PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -2107,7 +2107,7 @@ public async Task ServerSession_RequestClientCertificate_DrivesSecondHandshakeTo
[Theory]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not support post-handshake client authentication.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Neither SecureTransport nor JSSE supports post-handshake client authentication.")]
public async Task ServerSession_RequestClientCertificate_SessionRemainsUsable(SslProtocols protocol)
{
if (protocol == SslProtocols.Tls13 && !PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -2179,7 +2179,7 @@ public async Task ServerSession_RequestClientCertificate_SessionRemainsUsable(Ss
[Theory]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not support post-handshake client authentication.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Neither SecureTransport nor JSSE supports post-handshake client authentication.")]
public async Task ServerSession_RequestClientCertificate_ReadWriteDuringSecondHandshake_Throws(SslProtocols protocol)
{
if (protocol == SslProtocols.Tls13 && !PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -2799,7 +2799,7 @@ public async Task SocketBoundSession_MutualAuth_InitialHandshake_SurfacesSuspens
[Theory]
[InlineData(SslProtocols.Tls12)]
[InlineData(SslProtocols.Tls13)]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not support post-handshake client authentication.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Neither SecureTransport nor JSSE supports post-handshake client authentication.")]
public async Task SocketBoundSession_RequestClientCertificate_DrivesSecondHandshakeToCompletion(SslProtocols protocol)
{
if (protocol == SslProtocols.Tls13 && !PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -3615,6 +3615,7 @@ public async Task SocketBoundSession_DeferredOptions_WithAlpn_NegotiatesProtocol
// with the client's ClientHello must fail the handshake cleanly (no crash, no hang)
// via the socket-replay BIO path.
[Fact]
[SkipOnPlatform(TestPlatforms.Android, "The deferred-options protocol mismatch does not surface as a handshake failure on Android; both peers stall and the test times out. Root cause not yet established.")]
public async Task SocketBoundSession_DeferredOptions_ProtocolMismatch_Fails()
{
if (!PlatformDetection.SupportsTls13)
Expand Down Expand Up @@ -3970,7 +3971,7 @@ private static async Task RunServerTenantSessionAsync(
// TlsContext, each supplying a distinct cert via SetClientCertificateContext,
// and verify every server sees the correct client cert.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows))]
[SkipOnPlatform(TestPlatforms.OSX, "SecureTransport does not surface deferred client-credential prompts.")]
[SkipOnPlatform(TestPlatforms.OSX | TestPlatforms.Android, "Depends on the deferred client-credential flow, which the Android PAL does not report (no CredentialsNeeded).")]
public async Task SetClientCertificateContext_ConcurrentSessionsOnSharedContext_DoNotRace()
{
using X509Certificate2 serverCert = TestCertificates.GetServerCertificate();
Expand Down
1 change: 0 additions & 1 deletion src/libraries/tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Console\tests\System.Console.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.IO.FileSystem.Tests\System.IO.FileSystem.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.IO.Ports\tests\System.IO.Ports.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Security\tests\FunctionalTests\System.Net.Security.Tests.csproj" />
</ItemGroup>

<ItemGroup Condition="('$(TargetOS)' == 'android' or '$(TargetsLinuxBionic)' == 'true') and '$(TargetArchitecture)' == 'arm' and '$(RunDisabledAndroidTests)' != 'true'">
Expand Down
Loading