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 @@ -45,6 +45,32 @@ private JavaProxy.RemoteCertificateValidationResult VerifyRemoteCertificate(IntP
};
}

private static bool CanQueryConnectionInfoDuringCertificateValidation()
{
if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
// API 29+ can read the negotiated cipher from the handshake session.
return true;
}

if (OperatingSystem.IsAndroidVersionAtLeast(27))
{
// API 27 can dereference a null cipher and cause SIGSEGV.
// API 28 reports SSL_NULL_WITH_NULL_NULL instead.
// Both must wait for handshake completion.
return false;
}

if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
// API 26 exposes an intermediate handshake session with a valid cipher.
return true;
}

// API 24-25 report (NONE) before certificate validation completes.
return false;
}

private bool ShouldRespectPlatformValidation()
{
// Android platform trust is part of default/callback-only validation, but explicit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1372,11 +1372,11 @@ internal static bool VerifyRemoteCertificateCore(
// which may access properties like SslProtocol or CipherAlgorithm.
// During inline cert validation the handshake hasn't completed yet, so
// connectionInfo may not have been set by ProcessHandshakeSuccess.
// Android 8.1's Conscrypt dereferences a null current cipher during this
// phase of the handshake. Defer its connection info query until handshake
// completion; earlier and later Android versions do not have this bug.
if (connectionInfo.Protocol == 0 && securityContext is not null &&
(!OperatingSystem.IsAndroidVersionAtLeast(27) || OperatingSystem.IsAndroidVersionAtLeast(28)))
if (connectionInfo.Protocol == 0 && securityContext is not null
#if TARGET_ANDROID
&& CanQueryConnectionInfoDuringCertificateValidation()
#endif
)
{
SslStreamPal.QueryContextConnectionInfo(securityContext, ref connectionInfo);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Security.Authentication;
Expand Down Expand Up @@ -28,17 +29,34 @@ namespace System.Net.Security.Tests
/// </summary>
public class AndroidPlatformTrustTests
{
[Fact]
public async Task SslStream_CustomRootTrust_NoChainErrors()
public static IEnumerable<object[]> ConnectionInfoProtocols()
{
foreach (SslProtocols protocol in new[] { SslProtocols.Tls12, SslProtocols.None, SslProtocols.Tls13 })
{
if (protocol != SslProtocols.Tls13 || PlatformDetection.SupportsTls13)
{
foreach (bool mutualAuthentication in new[] { false, true })
{
yield return new object[] { protocol, mutualAuthentication };
}
}
}
}

[Theory]
[MemberData(nameof(ConnectionInfoProtocols))]
public async Task SslStream_CustomRootTrust_NoChainErrors(SslProtocols protocol, bool mutualAuthentication)
{
// The server uses testservereku.contoso.com.pfx signed by the NDX Test Root CA.
// CustomRootTrust is configured with the NDX root so managed validation accepts.
// Explicit managed custom trust is authoritative, so platform rejection would not
// pre-seed RemoteCertificateChainErrors.

using X509Certificate2 rootCertificate = GetRootCertificate();
using X509Certificate2 clientCertificate = Configuration.Certificates.GetSelfSignedClientCertificate();
SslPolicyErrors? reportedErrors = null;
int callbacks = 0;
int serverCallbacks = 0;

(SslStream client, SslStream server) = GetConnectedSslStreams();
using (client)
Expand All @@ -48,33 +66,25 @@ public async Task SslStream_CustomRootTrust_NoChainErrors()
var serverOptions = new SslServerAuthenticationOptions
{
ServerCertificate = serverCertificate,
EnabledSslProtocols = SslProtocols.Tls12,
EnabledSslProtocols = protocol,
};

var clientOptions = new SslClientAuthenticationOptions
{
TargetHost = "testservereku.contoso.com",
EnabledSslProtocols = SslProtocols.Tls12,
EnabledSslProtocols = protocol,
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
callbacks++;
reportedErrors = sslPolicyErrors;
Assert.Same(client, sender);
Assert.NotNull(certificate);
Assert.Equal(serverCertificate.RawData, certificate.GetRawCertData());
Assert.NotNull(chain);
Assert.Equal(SslPolicyErrors.None, sslPolicyErrors);

if (OperatingSystem.IsAndroidVersionAtLeast(27) && !OperatingSystem.IsAndroidVersionAtLeast(28))
{
Assert.Throws<InvalidOperationException>(() => client.SslProtocol);
Assert.Throws<InvalidOperationException>(() => client.NegotiatedCipherSuite);
}
else
{
Assert.Equal(SslProtocols.Tls12, client.SslProtocol);
_ = client.NegotiatedCipherSuite;
}
AssertConnectionInfo(client);

callbacks++;
return sslPolicyErrors == SslPolicyErrors.None;
},
CertificateChainPolicy = new X509ChainPolicy
Expand All @@ -85,21 +95,88 @@ public async Task SslStream_CustomRootTrust_NoChainErrors()
},
};

await Task.WhenAll(
client.AuthenticateAsClientAsync(clientOptions),
server.AuthenticateAsServerAsync(serverOptions)).WaitAsync(TimeSpan.FromSeconds(30));
if (mutualAuthentication)
{
clientOptions.RemoteCertificateValidationCallback = null;
clientOptions.ClientCertificates = new X509CertificateCollection { clientCertificate };
serverOptions.ClientCertificateRequired = true;
serverOptions.CertificateChainPolicy = new X509ChainPolicy
{
RevocationMode = X509RevocationMode.NoCheck,
TrustMode = X509ChainTrustMode.CustomRootTrust,
CustomTrustStore = { clientCertificate },
};
serverOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
reportedErrors = errors;
Assert.Same(server, sender);
Assert.NotNull(certificate);
Assert.Equal(clientCertificate.RawData, certificate.GetRawCertData());
Assert.NotNull(chain);
Assert.Equal(SslPolicyErrors.None, errors);
AssertConnectionInfo(server);
serverCallbacks++;
return errors == SslPolicyErrors.None;
};
}

Task clientAuthentication = client.AuthenticateAsClientAsync(clientOptions);
Task serverAuthentication = server.AuthenticateAsServerAsync(serverOptions);
await TaskTimeoutExtensions.WhenAllOrAnyFailed(
clientAuthentication, serverAuthentication, millisecondsTimeout: 30_000);

Assert.Equal(1, callbacks);
Assert.True(client.IsAuthenticated);
Assert.True(server.IsAuthenticated);
Assert.Equal(SslProtocols.Tls12, client.SslProtocol);
Assert.NotEqual(SslProtocols.None, client.SslProtocol);
if (protocol != SslProtocols.None)
{
Assert.Equal(protocol, client.SslProtocol);
}
Assert.Equal(client.SslProtocol, server.SslProtocol);
Assert.NotEqual(default, client.NegotiatedCipherSuite);
Assert.True(Enum.IsDefined(client.NegotiatedCipherSuite));
Assert.Equal(server.NegotiatedCipherSuite, client.NegotiatedCipherSuite);

await VerifyDataExchange(client, server);
await VerifyDataExchange(server, client);

Assert.Equal(mutualAuthentication ? 0 : 1, callbacks);
Assert.Equal(mutualAuthentication ? 1 : 0, serverCallbacks);
}

Assert.NotNull(reportedErrors);
Assert.Equal(SslPolicyErrors.None, reportedErrors.Value & SslPolicyErrors.RemoteCertificateChainErrors);

void AssertConnectionInfo(SslStream stream)
{
if (!OperatingSystem.IsAndroidVersionAtLeast(26) ||
(OperatingSystem.IsAndroidVersionAtLeast(27) && !OperatingSystem.IsAndroidVersionAtLeast(29)))
{
Assert.Throws<InvalidOperationException>(() => stream.SslProtocol);
Assert.Throws<InvalidOperationException>(() => stream.NegotiatedCipherSuite);
}
else
{
Assert.NotEqual(SslProtocols.None, stream.SslProtocol);
if (protocol != SslProtocols.None)
{
Assert.Equal(protocol, stream.SslProtocol);
}
Assert.NotEqual(default, stream.NegotiatedCipherSuite);
Assert.True(Enum.IsDefined(stream.NegotiatedCipherSuite));
}
}

static async Task VerifyDataExchange(SslStream sender, SslStream receiver)
{
byte[] expected = { 1, 2, 3, 4 };
byte[] actual = new byte[expected.Length];
await TaskTimeoutExtensions.WhenAllOrAnyFailed(
sender.WriteAsync(expected).AsTask(),
receiver.ReadExactlyAsync(actual).AsTask(),
millisecondsTimeout: 30_000);
Assert.Equal(expected, actual);
}
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
Link="Common\System\Net\Configuration.Certificates.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Http.cs"
Link="Common\System\Net\Configuration.Http.cs" />
<Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs"
Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading