diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 15768278534e1b..9f29ab2f4849de 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -179,6 +179,11 @@ $(LibrariesNativeArtifactsPath)*.pdb" IsNative="true" Exclude="@(ExcludeNativeLibrariesRuntimeFiles)" /> + + + diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs index e0a103c0e13979..5f3ee7ac00b1e8 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Ssl.cs @@ -29,18 +29,26 @@ internal enum PAL_SSLStreamStatus }; [LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreate")] - internal static partial SafeSslHandle SSLStreamCreate(); + private static partial SafeSslHandle SSLStreamCreate(IntPtr sslStreamProxyHandle); + internal static SafeSslHandle SSLStreamCreate(SslStream.JavaProxy sslStreamProxy) + => SSLStreamCreate(sslStreamProxy.Handle); [LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamCreateWithCertificates")] private static partial SafeSslHandle SSLStreamCreateWithCertificates( + IntPtr sslStreamProxyHandle, ref byte pkcs8PrivateKey, int pkcs8PrivateKeyLen, PAL_KeyAlgorithm algorithm, IntPtr[] certs, int certsLen); - internal static SafeSslHandle SSLStreamCreateWithCertificates(ReadOnlySpan pkcs8PrivateKey, PAL_KeyAlgorithm algorithm, IntPtr[] certificates) + internal static SafeSslHandle SSLStreamCreateWithCertificates( + SslStream.JavaProxy sslStreamProxy, + ReadOnlySpan pkcs8PrivateKey, + PAL_KeyAlgorithm algorithm, + IntPtr[] certificates) { return SSLStreamCreateWithCertificates( + sslStreamProxy.Handle, ref MemoryMarshal.GetReference(pkcs8PrivateKey), pkcs8PrivateKey.Length, algorithm, @@ -48,6 +56,10 @@ ref MemoryMarshal.GetReference(pkcs8PrivateKey), certificates.Length); } + [LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_RegisterRemoteCertificateValidationCallback")] + internal static unsafe partial void RegisterRemoteCertificateValidationCallback( + delegate* unmanaged verifyRemoteCertificate); + [LibraryImport(Interop.Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_SSLStreamInitialize")] private static unsafe partial int SSLStreamInitializeImpl( SafeSslHandle sslHandle, diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs index cf9af001342177..b449f5025276dc 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs @@ -96,7 +96,6 @@ await TestHelper.WhenAllCompletedOrAnyFailed( [OuterLoop] [ConditionalTheory(nameof(ClientSupportsDHECipherSuites))] [MemberData(nameof(InvalidCertificateServers))] - [SkipOnPlatform(TestPlatforms.Android, "Android rejects the certificate, the custom validation callback in .NET cannot override OS behavior in the current implementation")] public async Task InvalidCertificateServers_CertificateValidationDisabled_Succeeds(string url) { using (HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true)) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs index 2962e25c6cd599..eca2101b78edbb 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -286,7 +286,6 @@ private async Task UseCallback_BadCertificate_ExpectedPolicyErrors_Helper(string [OuterLoop("Uses external servers")] [Theory] [MemberData(nameof(CertificateValidationServersAndExpectedPolicies))] - [SkipOnPlatform(TestPlatforms.Android, "Android rejects the certificate, the custom validation callback in .NET cannot override OS behavior in the current implementation")] public async Task UseCallback_BadCertificate_ExpectedPolicyErrors(string url, SslPolicyErrors expectedErrors) { const int SEC_E_BUFFER_TOO_SMALL = unchecked((int)0x80090321); @@ -310,7 +309,6 @@ public async Task UseCallback_BadCertificate_ExpectedPolicyErrors(string url, Ss } [Fact] - [SkipOnPlatform(TestPlatforms.Android, "Android rejects the certificate, the custom validation callback in .NET cannot override OS behavior in the current implementation")] public async Task UseCallback_SelfSignedCertificate_ExpectedPolicyErrors() { using (HttpClientHandler handler = CreateHttpClientHandler()) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 91ea0ae621bffe..f9f6de4931b9dd 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -153,7 +153,7 @@ public void Properties_AddItemToDictionary_ItemPresent() [ConditionalFact] [SkipOnPlatform(TestPlatforms.Browser, "ServerCertificateCustomValidationCallback not supported on Browser")] - [SkipOnPlatform(TestPlatforms.Android, "IPv6 loopback with SSL doesn't work on Android")] + [SkipOnPlatform(TestPlatforms.Android, "TargetHost cannot be set to an IPv6 address on Android because the string doesn't conform to the STD 3 ASCII rules")] public async Task GetAsync_IPv6LinkLocalAddressUri_Success() { if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) @@ -205,7 +205,7 @@ public async Task GetAsync_IPBasedUri_Success(IPAddress address) if (PlatformDetection.IsAndroid && options.UseSsl && address == IPAddress.IPv6Loopback) { - throw new SkipTestException("IPv6 loopback with SSL doesn't work on Android"); + throw new SkipTestException("TargetHost cannot be set to an IPv6 address on Android because the string doesn't conform to the STD 3 ASCII rules"); } await LoopbackServerFactory.CreateServerAsync(async (server, url) => @@ -287,7 +287,7 @@ public async Task GetAsync_SecureAndNonSecureIPBasedUri_CorrectlyFormatted(IPAdd if (PlatformDetection.IsAndroid && useSsl && address == IPAddress.IPv6Loopback) { - throw new SkipTestException("IPv6 loopback with SSL doesn't work on Android"); + throw new SkipTestException("TargetHost cannot be set to an IPv6 address on Android because the string doesn't conform to the STD 3 ASCII rules"); } var options = new LoopbackServer.Options { Address = address, UseSsl = useSsl }; diff --git a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs index 8525ed8c1b2974..fd19ace2704b39 100644 --- a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs +++ b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs @@ -173,9 +173,6 @@ public static SocketsHttpHandler CreateSocketsHttpHandler(bool allowAllCertifica // Browser doesn't support ServerCertificateCustomValidationCallback if (allowAllCertificates && PlatformDetection.IsNotBrowser) { - // On Android, it is not enough to set the custom validation callback, the certificates also need to be trusted by the OS. - // See HttpClientHandlerTestBase.SocketsHttpHandler.cs:CreateHttpClientHandler for more details. - handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs index 855dc25acb2d08..602d177f5f1be1 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs @@ -37,13 +37,6 @@ protected static HttpClientHandler CreateHttpClientHandler(Version useVersion = // Browser doesn't support ServerCertificateCustomValidationCallback if (allowAllCertificates && PlatformDetection.IsNotBrowser) { - // On Android, it is not enough to set the custom validation callback, the certificates also need to be trusted by the OS. - // The public keys of our self-signed certificates that are used by the loopback server are part of the System.Net.TestData - // package and they can be included in a the Android test apk by adding the following property to the test's .csproj: - // - // true - // - handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index bc14a2900f9440..4ad0546969f0c0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -4076,7 +4076,6 @@ public abstract class SocketsHttpHandler_SecurityTest : HttpClientHandlerTestBas public SocketsHttpHandler_SecurityTest(ITestOutputHelper output) : base(output) { } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] public async Task SslOptions_CustomTrust_Ok() { X509Certificate2Collection caCerts = new X509Certificate2Collection(); @@ -4113,7 +4112,6 @@ await LoopbackServerFactory.CreateClientAndServerAsync( } [Fact] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] public async Task SslOptions_InvalidName_Throws() { X509Certificate2Collection caCerts = new X509Certificate2Collection(); @@ -4144,7 +4142,6 @@ await LoopbackServerFactory.CreateClientAndServerAsync( } [Fact] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] public async Task SslOptions_CustomPolicy_IgnoresNameMismatch() { X509Certificate2Collection caCerts = new X509Certificate2Collection(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs index b7e6995f999809..a70f1c4cb5979c 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs @@ -37,7 +37,7 @@ public async Task TestLoopbackAsync(string scheme, bool useSsl, bool useAuth, st if (PlatformDetection.IsAndroid && useSsl && host == "::1") { - throw new SkipTestException("IPv6 loopback with SSL doesn't work on Android"); + throw new SkipTestException("TargetHost cannot be set to an IPv6 address on Android because the string doesn't conform to the STD 3 ASCII rules"); } await LoopbackServerFactory.CreateClientAndServerAsync( diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index f4480104d296d1..90c828e26b56b6 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -9,8 +9,6 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-osx true true - - true true diff --git a/src/libraries/System.Net.Requests/tests/System.Net.Requests.Tests.csproj b/src/libraries/System.Net.Requests/tests/System.Net.Requests.Tests.csproj index c7c5bf5f83cdc8..1a2818addac82b 100644 --- a/src/libraries/System.Net.Requests/tests/System.Net.Requests.Tests.csproj +++ b/src/libraries/System.Net.Requests/tests/System.Net.Requests.Tests.csproj @@ -7,8 +7,6 @@ true $(NoWarn);SYSLIB0014 - - true true true 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 af280c25779ed9..27f71a0b0eed99 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -11,6 +11,7 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) SR.SystemNetSecurity_PlatformNotSupported $(DefineConstants);TARGET_WINDOWS + $(DefineConstants);TARGET_ANDROID true true true @@ -102,7 +103,7 @@ Link="Common\System\Net\SecurityStatusPal.cs" /> - @@ -382,6 +383,7 @@ + diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs index a07f84a1440e01..50b0eff0aab14f 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Android/SafeDeleteSslContext.cs @@ -34,14 +34,19 @@ internal sealed class SafeDeleteSslContext : SafeDeleteContext private ArrayBuffer _inputBuffer = new ArrayBuffer(InitialBufferSize); private ArrayBuffer _outputBuffer = new ArrayBuffer(InitialBufferSize); + public SslStream.JavaProxy SslStreamProxy { get; } + public SafeSslHandle SslContext => _sslContext; public SafeDeleteSslContext(SslAuthenticationOptions authOptions) : base(IntPtr.Zero) { + SslStreamProxy = authOptions.SslStreamProxy + ?? throw new ArgumentNullException(nameof(authOptions.SslStreamProxy)); + try { - _sslContext = CreateSslContext(authOptions); + _sslContext = CreateSslContext(SslStreamProxy, authOptions); InitializeSslContext(_sslContext, authOptions); } catch (Exception ex) @@ -58,8 +63,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - SafeSslHandle sslContext = _sslContext; - if (sslContext != null) + if (_sslContext is SafeSslHandle sslContext) { _inputBuffer.Dispose(); _outputBuffer.Dispose(); @@ -145,11 +149,11 @@ internal int ReadPendingWrites(byte[] buf, int offset, int count) return limit; } - private static SafeSslHandle CreateSslContext(SslAuthenticationOptions authOptions) + private static SafeSslHandle CreateSslContext(SslStream.JavaProxy sslStreamProxy, SslAuthenticationOptions authOptions) { if (authOptions.CertificateContext == null) { - return Interop.AndroidCrypto.SSLStreamCreate(); + return Interop.AndroidCrypto.SSLStreamCreate(sslStreamProxy); } SslStreamCertificateContext context = authOptions.CertificateContext; @@ -169,7 +173,7 @@ private static SafeSslHandle CreateSslContext(SslAuthenticationOptions authOptio ptrs[i + 1] = context.IntermediateCertificates[i].Handle; } - return Interop.AndroidCrypto.SSLStreamCreateWithCertificates(keyBytes, algorithm, ptrs); + return Interop.AndroidCrypto.SSLStreamCreateWithCertificates(sslStreamProxy, keyBytes, algorithm, ptrs); } private static AsymmetricAlgorithm GetPrivateKeyAlgorithm(X509Certificate2 cert, out PAL_KeyAlgorithm algorithm) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs index 523cbdd1522bf8..261b356472bcf1 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs @@ -182,5 +182,9 @@ private static SslProtocols FilterOutIncompatibleSslProtocols(SslProtocols proto internal object? UserState { get; set; } internal ServerOptionsSelectionCallback? ServerOptionDelegate { get; set; } internal X509ChainPolicy? CertificateChainPolicy { get; set; } + +#if TARGET_ANDROID + internal SslStream.JavaProxy? SslStreamProxy { get; set; } +#endif } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs new file mode 100644 index 00000000000000..f35aafe9a66d29 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Android.cs @@ -0,0 +1,110 @@ +// 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.Diagnostics; +using System.Net.Security; +using System.Threading; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + public partial class SslStream + { + private JavaProxy.RemoteCertificateValidationResult VerifyRemoteCertificate() + { + ProtocolToken? alertToken = null; + var isValid = VerifyRemoteCertificate( + _sslAuthenticationOptions.CertValidationDelegate, + _sslAuthenticationOptions.CertificateContext?.Trust, + ref alertToken, + out SslPolicyErrors sslPolicyErrors, + out X509ChainStatusFlags chainStatus); + + return new() + { + IsValid = isValid, + SslPolicyErrors = sslPolicyErrors, + ChainStatus = chainStatus, + AlertToken = alertToken, + }; + } + + private bool TryGetRemoteCertificateValidationResult(out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus, out ProtocolToken? alertToken, out bool isValid) + { + JavaProxy.RemoteCertificateValidationResult? validationResult = _securityContext?.SslStreamProxy.ValidationResult; + sslPolicyErrors = validationResult?.SslPolicyErrors ?? default; + chainStatus = validationResult?.ChainStatus ?? default; + isValid = validationResult?.IsValid ?? default; + alertToken = validationResult?.AlertToken; + return validationResult is not null; + } + + internal sealed class JavaProxy : IDisposable + { + private static bool s_initialized; + + private readonly SslStream _sslStream; + private GCHandle? _handle; + + public IntPtr Handle + => _handle is GCHandle handle + ? GCHandle.ToIntPtr(handle) + : throw new ObjectDisposedException(nameof(JavaProxy)); + + public Exception? ValidationException { get; private set; } + public RemoteCertificateValidationResult? ValidationResult { get; private set; } + + public JavaProxy(SslStream sslStream) + { + RegisterRemoteCertificateValidationCallback(); + + _sslStream = sslStream; + _handle = GCHandle.Alloc(this); + } + + public void Dispose() + { + _handle?.Free(); + _handle = null; + } + + private static unsafe void RegisterRemoteCertificateValidationCallback() + { + if (!s_initialized) + { + Interop.AndroidCrypto.RegisterRemoteCertificateValidationCallback(&VerifyRemoteCertificate); + s_initialized = true; + } + } + + [UnmanagedCallersOnly] + private static unsafe bool VerifyRemoteCertificate(IntPtr sslStreamProxyHandle) + { + var proxy = (JavaProxy?)GCHandle.FromIntPtr(sslStreamProxyHandle).Target; + Debug.Assert(proxy is not null); + Debug.Assert(proxy.ValidationResult is null); + + try + { + proxy.ValidationResult = proxy._sslStream.VerifyRemoteCertificate(); + return proxy.ValidationResult.IsValid; + } + catch (Exception exception) + { + proxy.ValidationException = exception; + return false; + } + } + + internal sealed class RemoteCertificateValidationResult + { + public bool IsValid { get; init; } + public SslPolicyErrors SslPolicyErrors { get; init; } + public X509ChainStatusFlags ChainStatus { get; init; } + public ProtocolToken? AlertToken { get; init; } + } + } + } +} 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 310ef723c48d0c..e18a73fbb3d919 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 @@ -512,6 +512,28 @@ private bool CompleteHandshake(ref ProtocolToken? alertToken, out SslPolicyError return true; } + if (_selectedClientCertificate != null && !CertificateValidationPal.IsLocalCertificateUsed(_securityContext!)) + { + // We may select client cert but it may not be used. + // This is primarily an issue on Windows with credential caching. + _selectedClientCertificate = null; + } + +#if TARGET_ANDROID + // On Android, the remote certificate verification can be invoked from Java TrustManager's callback + // during the handshake process. If that has occurred, we shouldn't run the validation again and + // return the existing validation result. + // + // The Java TrustManager callback is called only when the peer has a certificate. It's possible that + // the peer didn't provide any certificate (for example when the peer is the client) and the validation + // result hasn't been set. In that case we still need to run the verification at this point. + if (TryGetRemoteCertificateValidationResult(out sslPolicyErrors, out chainStatus, out alertToken, out bool isValid)) + { + _handshakeCompleted = isValid; + return isValid; + } +#endif + if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus)) { _handshakeCompleted = false; 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 62edaff0a451a2..cf6e16e9e501d3 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 @@ -119,6 +119,10 @@ internal void CloseContext() _securityContext?.Dispose(); _credentialsHandle?.Dispose(); + +#if TARGET_ANDROID + _sslAuthenticationOptions.SslStreamProxy?.Dispose(); +#endif } // @@ -1009,13 +1013,6 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot } _remoteCertificate = certificate; - if (_selectedClientCertificate != null && !CertificateValidationPal.IsLocalCertificateUsed(_securityContext!)) - { - // We may select client cert but it may not be used. - // This is primarily issue on Windows with credential caching - _selectedClientCertificate = null; - } - if (_remoteCertificate == null) { if (NetEventSource.Log.IsEnabled() && RemoteCertRequired) NetEventSource.Error(this, $"Remote certificate required, but no remote certificate received"); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs index e609d3dbdccafd..395ab232576e0d 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs @@ -214,6 +214,10 @@ public SslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificat _sslAuthenticationOptions.CertValidationDelegate = userCertificateValidationCallback; _sslAuthenticationOptions.CertSelectionDelegate = userCertificateSelectionCallback; +#if TARGET_ANDROID + _sslAuthenticationOptions.SslStreamProxy = new SslStream.JavaProxy(sslStream: this); +#endif + if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.SslStreamCtor(this, innerStream); } 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 fe3a22c02b3b50..53b8f5ce77dc34 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 @@ -189,7 +189,7 @@ private static SecurityStatusPal HandshakeInternal( { SafeDeleteSslContext? sslContext = ((SafeDeleteSslContext?)context); - if ((context == null) || context.IsInvalid) + if (context == null || context.IsInvalid) { context = new SafeDeleteSslContext(sslAuthenticationOptions); sslContext = context; @@ -212,7 +212,8 @@ private static SecurityStatusPal HandshakeInternal( outputBuffer = sslContext.ReadPendingWrites(); - return new SecurityStatusPal(statusCode); + Exception? validationException = sslContext?.SslStreamProxy.ValidationException; + return new SecurityStatusPal(statusCode, validationException); } catch (Exception exc) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs index 1530d5a33b7b10..2d4b1ba09ff08c 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs @@ -95,7 +95,6 @@ public async Task DefaultConnect_EndToEnd_Ok(string host) [Theory] [InlineData(true)] [InlineData(false)] - [SkipOnPlatform(TestPlatforms.Android, "The invalid certificate is rejected by Android and the .NET validation code isn't reached")] [ActiveIssue("https://github.com/dotnet/runtime/issues/70981", TestPlatforms.OSX)] public Task ConnectWithRevocation_WithCallback(bool checkRevocation) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs index d8746a5d81afef..f1350954763201 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs @@ -17,7 +17,6 @@ namespace System.Net.Security.Tests public class SslStreamCredentialCacheTest { [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] public async Task SslStream_SameCertUsedForClientAndServer_Ok() { (Stream stream1, Stream stream2) = TestHelper.GetConnectedStreams(); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs index 58d6dfb0ff76b1..0c9a9ee22183f1 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs @@ -754,7 +754,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( [Theory] [InlineData(true)] [InlineData(false)] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] public async Task SslStream_ServerUntrustedCaWithCustomTrust_OK(bool usePartialChain) { int split = Random.Shared.Next(0, _certificates.serverChain.Count - 1); @@ -854,7 +854,7 @@ private async Task SslStream_ClientSendsChain_Core(SslClientAuthenticationOption } [Fact] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] [ActiveIssue("https://github.com/dotnet/runtime/issues/73862", TestPlatforms.OSX)] public async Task SslStream_ClientCertificate_SendsChain() { @@ -915,7 +915,7 @@ public async Task SslStream_ClientCertificate_SendsChain() } [Fact] - [SkipOnPlatform(TestPlatforms.Android, "Self-signed certificates are rejected by Android before the .NET validation is reached")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] public async Task SslStream_ClientCertificateContext_SendsChain() { (X509Certificate2 clientCertificate, X509Certificate2Collection clientChain) = TestHelper.GenerateCertificates(nameof(SslStream_ClientCertificateContext_SendsChain), serverCertificate: false); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs index 9d4aa07ff92991..944ab95699cce6 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs @@ -18,7 +18,7 @@ public class SslStreamSniTest { [Theory] [MemberData(nameof(HostNameData))] - [SkipOnPlatform(TestPlatforms.Android, "Host name is not sent on Android")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] public async Task SslStream_ClientSendsSNIServerReceives_Ok(string hostName) { using X509Certificate serverCert = Configuration.Certificates.GetSelfSignedServerCertificate(); @@ -96,7 +96,7 @@ public async Task SslStream_ServerCallbackAndLocalCertificateSelectionSet_Throws [Theory] [MemberData(nameof(HostNameData))] - [SkipOnPlatform(TestPlatforms.Android, "TODO: this test would work with GetServerCertificate(). Is there something wrong with the PEMs?")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)] public async Task SslStream_ServerCallbackNotSet_UsesLocalCertificateSelection(string hostName) { using X509Certificate serverCert = Configuration.Certificates.GetSelfSignedServerCertificate(); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index ecee9fc385253f..56e1f32c48f0b1 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -5,8 +5,6 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-ios true true - - true true true diff --git a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj index d022835499355f..690a3d4584485f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj +++ b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj @@ -7,8 +7,6 @@ $(DefineConstants);NETSTANDARD false - - true diff --git a/src/libraries/native-binplace.proj b/src/libraries/native-binplace.proj index 82427d8f27b01c..867fadd3c4637e 100644 --- a/src/libraries/native-binplace.proj +++ b/src/libraries/native-binplace.proj @@ -22,10 +22,12 @@ + + - \ No newline at end of file + diff --git a/src/libraries/pretest.proj b/src/libraries/pretest.proj index bbe56c8bcd5c1f..27c862055389ab 100644 --- a/src/libraries/pretest.proj +++ b/src/libraries/pretest.proj @@ -47,6 +47,8 @@ + + diff --git a/src/mono/msbuild/android/build/AndroidApp.targets b/src/mono/msbuild/android/build/AndroidApp.targets index cafe0ea419cad9..7c4545dac726a7 100644 --- a/src/mono/msbuild/android/build/AndroidApp.targets +++ b/src/mono/msbuild/android/build/AndroidApp.targets @@ -1,5 +1,5 @@ - @@ -45,7 +45,7 @@ - + @@ -63,7 +63,7 @@ - <_AotInputAssemblies Include="@(_AndroidAssembliesInternal)" + <_AotInputAssemblies Include="@(_AndroidAssembliesInternal)" Condition="'%(_AndroidAssembliesInternal._InternalForceInterpret)' != 'true'"> $(AotArguments) $(ProcessArguments) @@ -72,7 +72,7 @@ <_AOT_InternalForceInterpretAssemblies Include="@(_AndroidAssembliesInternal->WithMetadataValue('_InternalForceInterpret', 'true'))" /> <_AndroidAssembliesInternal Remove="@(_AndroidAssembliesInternal)" /> - + @@ -132,7 +132,6 @@ MonoRuntimeHeaders="$(MicrosoftNetCoreAppRuntimePackNativeDir)include\mono-2.0" Assemblies="@(_AndroidAssembliesInternal)" MainLibraryFileName="$(MainLibraryFileName)" - IncludeNetworkSecurityConfig="$(IncludeNetworkSecurityConfig)" EnvironmentVariables="@(AndroidEnv)" ForceAOT="$(RunAOTCompilation)" ForceFullAOT="$(ForceFullAOT)" @@ -152,7 +151,7 @@ - + - \ No newline at end of file + diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt index c4136a588f8565..36a0827e25d13c 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt @@ -24,6 +24,7 @@ set(NATIVECRYPTO_SOURCES pal_signature.c pal_ssl.c pal_sslstream.c + pal_trust_manager.c pal_x509.c pal_x509chain.c pal_x509store.c diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/DotnetProxyTrustManager.java b/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/DotnetProxyTrustManager.java new file mode 100644 index 00000000000000..38454a3d289d14 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/DotnetProxyTrustManager.java @@ -0,0 +1,39 @@ +package net.dot.android.crypto; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.X509TrustManager; + +/** + * This class is meant to replace the built-in X509TrustManager. + * Its sole responsibility is to invoke the C# code in the SslStream + * class during TLS handshakes to perform the validation of the remote + * peer's certificate. + */ +public final class DotnetProxyTrustManager implements X509TrustManager { + private final long sslStreamProxyHandle; + + public DotnetProxyTrustManager(long sslStreamProxyHandle) { + this.sslStreamProxyHandle = sslStreamProxyHandle; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + if (!verifyRemoteCertificate(sslStreamProxyHandle)) { + throw new CertificateException(); + } + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + if (!verifyRemoteCertificate(sslStreamProxyHandle)) { + throw new CertificateException(); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + static native boolean verifyRemoteCertificate(long sslStreamProxyHandle); +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c index 1fdffac63e034f..4455dd62fd4d38 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c @@ -426,8 +426,8 @@ jclass g_SSLEngine; jmethodID g_SSLEngineBeginHandshake; jmethodID g_SSLEngineCloseOutbound; jmethodID g_SSLEngineGetApplicationProtocol; -jmethodID g_SSLEngineGetHandshakeStatus; jmethodID g_SSLEngineGetHandshakeSession; +jmethodID g_SSLEngineGetHandshakeStatus; jmethodID g_SSLEngineGetSession; jmethodID g_SSLEngineGetSSLParameters; jmethodID g_SSLEngineGetSupportedProtocols; @@ -481,6 +481,13 @@ jmethodID g_KeyAgreementInit; jmethodID g_KeyAgreementDoPhase; jmethodID g_KeyAgreementGenerateSecret; +// javax/net/ssl/TrustManager +jclass g_TrustManager; + +// net/dot/android/crypto/DotnetProxyTrustManager +jclass g_DotnetProxyTrustManager; +jmethodID g_DotnetProxyTrustManagerCtor; + jobject ToGRef(JNIEnv *env, jobject lref) { if (lref) @@ -1021,9 +1028,9 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_SSLEngineBeginHandshake = GetMethod(env, false, g_SSLEngine, "beginHandshake", "()V"); g_SSLEngineCloseOutbound = GetMethod(env, false, g_SSLEngine, "closeOutbound", "()V"); g_SSLEngineGetApplicationProtocol = GetOptionalMethod(env, false, g_SSLEngine, "getApplicationProtocol", "()Ljava/lang/String;"); + g_SSLEngineGetHandshakeSession = GetOptionalMethod(env, false, g_SSLEngine, "getHandshakeSession", "()Ljavax/net/ssl/SSLSession;"); g_SSLEngineGetHandshakeStatus = GetMethod(env, false, g_SSLEngine, "getHandshakeStatus", "()Ljavax/net/ssl/SSLEngineResult$HandshakeStatus;"); g_SSLEngineGetSession = GetMethod(env, false, g_SSLEngine, "getSession", "()Ljavax/net/ssl/SSLSession;"); - g_SSLEngineGetHandshakeSession = GetOptionalMethod(env, false, g_SSLEngine, "getHandshakeSession", "()Ljavax/net/ssl/SSLSession;"); g_SSLEngineGetSSLParameters = GetMethod(env, false, g_SSLEngine, "getSSLParameters", "()Ljavax/net/ssl/SSLParameters;"); g_SSLEngineGetSupportedProtocols = GetMethod(env, false, g_SSLEngine, "getSupportedProtocols", "()[Ljava/lang/String;"); g_SSLEngineSetEnabledProtocols = GetMethod(env, false, g_SSLEngine, "setEnabledProtocols", "([Ljava/lang/String;)V"); @@ -1071,5 +1078,10 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_KeyAgreementDoPhase = GetMethod(env, false, g_KeyAgreementClass, "doPhase", "(Ljava/security/Key;Z)Ljava/security/Key;"); g_KeyAgreementGenerateSecret = GetMethod(env, false, g_KeyAgreementClass, "generateSecret", "()[B"); + g_TrustManager = GetClassGRef(env, "javax/net/ssl/TrustManager"); + + g_DotnetProxyTrustManager = GetClassGRef(env, "net/dot/android/crypto/DotnetProxyTrustManager"); + g_DotnetProxyTrustManagerCtor = GetMethod(env, false, g_DotnetProxyTrustManager, "", "(J)V"); + return JNI_VERSION_1_6; } diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h index 7933a8eac3fa21..341a274ba110ee 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h @@ -440,8 +440,8 @@ extern jclass g_SSLEngine; extern jmethodID g_SSLEngineBeginHandshake; extern jmethodID g_SSLEngineCloseOutbound; extern jmethodID g_SSLEngineGetApplicationProtocol; -extern jmethodID g_SSLEngineGetHandshakeStatus; extern jmethodID g_SSLEngineGetHandshakeSession; +extern jmethodID g_SSLEngineGetHandshakeStatus; extern jmethodID g_SSLEngineGetSession; extern jmethodID g_SSLEngineGetSSLParameters; extern jmethodID g_SSLEngineGetSupportedProtocols; @@ -495,6 +495,13 @@ extern jmethodID g_KeyAgreementInit; extern jmethodID g_KeyAgreementDoPhase; extern jmethodID g_KeyAgreementGenerateSecret; +// javax/net/ssl/TrustManager +extern jclass g_TrustManager; + +// net/dot/android/crypto/DotnetProxyTrustManager +extern jclass g_DotnetProxyTrustManager; +extern jmethodID g_DotnetProxyTrustManagerCtor; + // Compatibility macros #if !defined (__mallocfunc) #if defined (__clang__) || defined (__GNUC__) diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c index 637fa7fa89fe74..090bf1ea1e9c32 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c @@ -3,6 +3,7 @@ #include "pal_sslstream.h" #include "pal_ssl.h" +#include "pal_trust_manager.h" // javax/net/ssl/SSLEngineResult$HandshakeStatus enum @@ -46,14 +47,27 @@ ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoHandshake(JNIEnv* env, SSLStream* ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoWrap(JNIEnv* env, SSLStream* sslStream, int* handshakeStatus); ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoUnwrap(JNIEnv* env, SSLStream* sslStream, int* handshakeStatus); +ARGS_NON_NULL_ALL static int GetHandshakeStatus(JNIEnv* env, SSLStream* sslStream) +{ + // int handshakeStatus = sslEngine.getHandshakeStatus().ordinal(); + int handshakeStatus = GetEnumAsInt(env, (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetHandshakeStatus)); + if (CheckJNIExceptions(env)) + return -1; + + return handshakeStatus; +} + static bool IsHandshaking(int handshakeStatus) { return handshakeStatus != HANDSHAKE_STATUS__NOT_HANDSHAKING && handshakeStatus != HANDSHAKE_STATUS__FINISHED; } -static jobject GetSslSessionForHandshakeStatus(JNIEnv* env, SSLStream* sslStream, int handshakeStatus) +ARGS_NON_NULL(1, 2) static jobject GetSslSession(JNIEnv* env, SSLStream* sslStream, int handshakeStatus) { - // SSLEngine.getHandshakeSession() is available since API 24 + // During the initial handshake our sslStream->sslSession doesn't have access to the peer certificates + // which we need for hostname verification. Luckily, the SSLEngine has a getter for the handshake SSLSession. + // SSLEngine.getHandshakeSession() is available since API 24. + jobject sslSession = IsHandshaking(handshakeStatus) && g_SSLEngineGetHandshakeSession != NULL ? (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetHandshakeSession) : (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetSession); @@ -63,13 +77,13 @@ static jobject GetSslSessionForHandshakeStatus(JNIEnv* env, SSLStream* sslStream return sslSession; } -static jobject GetCurrentSslSession(JNIEnv* env, SSLStream* sslStream) +ARGS_NON_NULL_ALL static jobject GetCurrentSslSession(JNIEnv* env, SSLStream* sslStream) { - int handshakeStatus = GetEnumAsInt(env, (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetHandshakeStatus)); - if (CheckJNIExceptions(env)) + int handshakeStatus = GetHandshakeStatus(env, sslStream); + if (handshakeStatus == -1) return NULL; - return GetSslSessionForHandshakeStatus(env, sslStream, handshakeStatus); + return GetSslSession(env, sslStream, handshakeStatus); } ARGS_NON_NULL_ALL static PAL_SSLStreamStatus Close(JNIEnv* env, SSLStream* sslStream) @@ -149,8 +163,10 @@ ARGS_NON_NULL_ALL static jobject EnsureRemaining(JNIEnv* env, jobject oldBuffer, // There has been a change in the SSLEngineResult.Status enum between API 23 and 24 that changed // the order/interger values of the enum options. -static int MapLegacySSLEngineResultStatus(int legacyStatus) { - switch (legacyStatus) { +static int MapLegacySSLEngineResultStatus(int legacyStatus) +{ + switch (legacyStatus) + { case LEGACY__STATUS__BUFFER_OVERFLOW: return STATUS__BUFFER_OVERFLOW; case LEGACY__STATUS__BUFFER_UNDERFLOW: @@ -185,7 +201,8 @@ ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoWrap(JNIEnv* env, SSLStream* sslS int status = GetEnumAsInt(env, (*env)->CallObjectMethod(env, result, g_SSLEngineResultGetStatus)); (*env)->DeleteLocalRef(env, result); - if (g_SSLEngineResultStatusLegacyOrder) { + if (g_SSLEngineResultStatusLegacyOrder) + { status = MapLegacySSLEngineResultStatus(status); } @@ -236,6 +253,7 @@ ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoUnwrap(JNIEnv* env, SSLStream* ss PAL_SSLStreamStatus status = sslStream->streamReader(sslStream->managedContextHandle, tmpNative, &count); if (status != SSLStreamStatus_OK) { + free(tmpNative); (*env)->DeleteLocalRef(env, tmp); return status; } @@ -264,7 +282,8 @@ ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoUnwrap(JNIEnv* env, SSLStream* ss int status = GetEnumAsInt(env, (*env)->CallObjectMethod(env, result, g_SSLEngineResultGetStatus)); (*env)->DeleteLocalRef(env, result); - if (g_SSLEngineResultStatusLegacyOrder) { + if (g_SSLEngineResultStatusLegacyOrder) + { status = MapLegacySSLEngineResultStatus(status); } @@ -307,8 +326,9 @@ ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoUnwrap(JNIEnv* env, SSLStream* ss ARGS_NON_NULL_ALL static PAL_SSLStreamStatus DoHandshake(JNIEnv* env, SSLStream* sslStream) { PAL_SSLStreamStatus status = SSLStreamStatus_OK; - int handshakeStatus = - GetEnumAsInt(env, (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetHandshakeStatus)); + int handshakeStatus = GetHandshakeStatus(env, sslStream); + assert(handshakeStatus >= 0); + while (IsHandshaking(handshakeStatus) && status == SSLStreamStatus_OK) { switch (handshakeStatus) @@ -343,17 +363,79 @@ ARGS_NON_NULL_ALL static void FreeSSLStream(JNIEnv* env, SSLStream* sslStream) free(sslStream); } -SSLStream* AndroidCryptoNative_SSLStreamCreate(void) +ARGS_NON_NULL_ALL static jobject GetSSLContextInstance(JNIEnv* env) +{ + jobject sslContext = NULL; + + // sslContext = SSLContext.getInstance("TLSv1.3"); + jstring tls13 = make_java_string(env, "TLSv1.3"); + sslContext = (*env)->CallStaticObjectMethod(env, g_SSLContext, g_SSLContextGetInstanceMethod, tls13); + if (TryClearJNIExceptions(env)) + { + // TLSv1.3 is only supported on API level 29+ - fall back to TLSv1.2 (which is supported on API level 16+) + // sslContext = SSLContext.getInstance("TLSv1.2"); + jstring tls12 = make_java_string(env, "TLSv1.2"); + sslContext = (*env)->CallStaticObjectMethod(env, g_SSLContext, g_SSLContextGetInstanceMethod, tls12); + ReleaseLRef(env, tls12); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + } + +cleanup: + ReleaseLRef(env, tls13); + return sslContext; +} + +ARGS_NON_NULL_ALL static jobject GetKeyStoreInstance(JNIEnv* env) +{ + jobject keyStore = NULL; + jstring ksType = NULL; + + // String ksType = KeyStore.getDefaultType(); + // KeyStore keyStore = KeyStore.getInstance(ksType); + // keyStore.load(null, null); + // return keyStore; + + ksType = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetDefaultType); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + keyStore = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetInstance, ksType); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + (*env)->CallVoidMethod(env, keyStore, g_KeyStoreLoad, NULL, NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + +cleanup: + ReleaseLRef(env, ksType); + return keyStore; +} + +SSLStream* AndroidCryptoNative_SSLStreamCreate(intptr_t sslStreamProxyHandle) { + abort_unless(sslStreamProxyHandle != 0, "invalid pointer to the .NET SslStream proxy"); + + SSLStream* sslStream = NULL; JNIEnv* env = GetJNIEnv(); - // SSLContext sslContext = SSLContext.getDefault(); - jobject sslContext = (*env)->CallStaticObjectMethod(env, g_SSLContext, g_SSLContextGetDefault); - if (CheckJNIExceptions(env)) - return NULL; + INIT_LOCALS(loc, sslContext, trustManagers); + + loc[sslContext] = GetSSLContextInstance(env); + if (!loc[sslContext]) + goto cleanup; + + loc[trustManagers] = GetTrustManagers(env, sslStreamProxyHandle); + if (!loc[trustManagers]) + goto cleanup; + + // sslContext.init(null, trustManagers, null); + (*env)->CallVoidMethod(env, loc[sslContext], g_SSLContextInitMethod, NULL, loc[trustManagers], NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - SSLStream* sslStream = xcalloc(1, sizeof(SSLStream)); - sslStream->sslContext = ToGRef(env, sslContext); + sslStream = xcalloc(1, sizeof(SSLStream)); + sslStream->sslContext = ToGRef(env, loc[sslContext]); + loc[sslContext] = NULL; + +cleanup: + RELEASE_LOCALS(loc, env); return sslStream; } @@ -420,38 +502,27 @@ static int32_t AddCertChainToStore(JNIEnv* env, return ret; } -SSLStream* AndroidCryptoNative_SSLStreamCreateWithCertificates(uint8_t* pkcs8PrivateKey, +SSLStream* AndroidCryptoNative_SSLStreamCreateWithCertificates(intptr_t sslStreamProxyHandle, + uint8_t* pkcs8PrivateKey, int32_t pkcs8PrivateKeyLen, PAL_KeyAlgorithm algorithm, jobject* /*X509Certificate[]*/ certs, int32_t certsLen) { + abort_unless(sslStreamProxyHandle != 0, "invalid pointer to the .NET SslStream proxy"); + SSLStream* sslStream = NULL; JNIEnv* env = GetJNIEnv(); - INIT_LOCALS(loc, tls13, sslContext, ksType, keyStore, kmfType, kmf, keyManagers); + INIT_LOCALS(loc, sslContext, keyStore, kmfType, kmf, keyManagers, trustManagers); - // SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); - loc[tls13] = make_java_string(env, "TLSv1.3"); - loc[sslContext] = (*env)->CallStaticObjectMethod(env, g_SSLContext, g_SSLContextGetInstanceMethod, loc[tls13]); - if (TryClearJNIExceptions(env)) - { - // TLSv1.3 is only supported on API level 29+ - fall back to TLSv1.2 (which is supported on API level 16+) - // sslContext = SSLContext.getInstance("TLSv1.2"); - jobject tls12 = make_java_string(env, "TLSv1.2"); - loc[sslContext] = (*env)->CallStaticObjectMethod(env, g_SSLContext, g_SSLContextGetInstanceMethod, tls12); - ReleaseLRef(env, tls12); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - } + loc[sslContext] = GetSSLContextInstance(env); + if (!loc[sslContext]) + goto cleanup; - // String ksType = KeyStore.getDefaultType(); - // KeyStore keyStore = KeyStore.getInstance(ksType); - // keyStore.load(null, null); - loc[ksType] = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetDefaultType); - loc[keyStore] = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetInstance, loc[ksType]); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - (*env)->CallVoidMethod(env, loc[keyStore], g_KeyStoreLoad, NULL, NULL); - ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + loc[keyStore] = GetKeyStoreInstance(env); + if (!loc[keyStore]) + goto cleanup; int32_t status = AddCertChainToStore(env, loc[keyStore], pkcs8PrivateKey, pkcs8PrivateKeyLen, algorithm, certs, certsLen); @@ -469,10 +540,16 @@ SSLStream* AndroidCryptoNative_SSLStreamCreateWithCertificates(uint8_t* pkcs8Pri ON_EXCEPTION_PRINT_AND_GOTO(cleanup); // KeyManager[] keyManagers = kmf.getKeyManagers(); - // sslContext.init(keyManagers, null, null); loc[keyManagers] = (*env)->CallObjectMethod(env, loc[kmf], g_KeyManagerFactoryGetKeyManagers); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - (*env)->CallVoidMethod(env, loc[sslContext], g_SSLContextInitMethod, loc[keyManagers], NULL, NULL); + + // TrustManager[] trustManagers = GetTrustManagers(sslStreamProxyHandle); + loc[trustManagers] = GetTrustManagers(env, sslStreamProxyHandle); + if (!loc[trustManagers]) + goto cleanup; + + // sslContext.init(keyManagers, trustManagers, null); + (*env)->CallVoidMethod(env, loc[sslContext], g_SSLContextInitMethod, loc[keyManagers], loc[trustManagers], NULL); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); sslStream = xcalloc(1, sizeof(SSLStream)); @@ -521,8 +598,7 @@ int32_t AndroidCryptoNative_SSLStreamInitialize( // int applicationBufferSize = sslSession.getApplicationBufferSize(); // int packetBufferSize = sslSession.getPacketBufferSize(); - int32_t applicationBufferSize = - (*env)->CallIntMethod(env, sslStream->sslSession, g_SSLSessionGetApplicationBufferSize); + int32_t applicationBufferSize = (*env)->CallIntMethod(env, sslStream->sslSession, g_SSLSessionGetApplicationBufferSize); int32_t packetBufferSize = (*env)->CallIntMethod(env, sslStream->sslSession, g_SSLSessionGetPacketBufferSize); // ByteBuffer appInBuffer = ByteBuffer.allocate(Math.max(applicationBufferSize, appBufferSize)); @@ -625,8 +701,12 @@ PAL_SSLStreamStatus AndroidCryptoNative_SSLStreamHandshake(SSLStream* sslStream) abort_if_invalid_pointer_argument (sslStream); JNIEnv* env = GetJNIEnv(); - int handshakeStatus = GetEnumAsInt(env, (*env)->CallObjectMethod(env, sslStream->sslEngine, g_SSLEngineGetHandshakeStatus)); - if (!IsHandshaking(handshakeStatus)) { + int handshakeStatus = GetHandshakeStatus(env, sslStream); + if (handshakeStatus == -1) + return SSLStreamStatus_Error; + + if (!IsHandshaking(handshakeStatus)) + { // sslEngine.beginHandshake(); (*env)->CallVoidMethod(env, sslStream->sslEngine, g_SSLEngineBeginHandshake); if (CheckJNIExceptions(env)) @@ -808,18 +888,21 @@ int32_t AndroidCryptoNative_SSLStreamGetCipherSuite(SSLStream* sslStream, uint16 JNIEnv* env = GetJNIEnv(); int32_t ret = FAIL; *out = NULL; + INIT_LOCALS(loc, sslSession, cipherSuite); + + loc[sslSession] = GetCurrentSslSession(env, sslStream); + if (loc[sslSession] == NULL) + goto cleanup; // String cipherSuite = sslSession.getCipherSuite(); - jobject sslSession = GetCurrentSslSession(env, sslStream); - jstring cipherSuite = (*env)->CallObjectMethod(env, sslSession, g_SSLSessionGetCipherSuite); + loc[cipherSuite] = (*env)->CallObjectMethod(env, loc[sslSession], g_SSLSessionGetCipherSuite); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - *out = AllocateString(env, cipherSuite); + *out = AllocateString(env, loc[cipherSuite]); ret = SUCCESS; cleanup: - ReleaseLRef(env, sslSession); - ReleaseLRef(env, cipherSuite); + RELEASE_LOCALS(loc, env); return ret; } @@ -831,21 +914,43 @@ int32_t AndroidCryptoNative_SSLStreamGetProtocol(SSLStream* sslStream, uint16_t* JNIEnv* env = GetJNIEnv(); int32_t ret = FAIL; *out = NULL; + INIT_LOCALS(loc, sslSession, protocol); + + loc[sslSession] = GetCurrentSslSession(env, sslStream); + if (loc[sslSession] == NULL) + goto cleanup; // String protocol = sslSession.getProtocol(); - jobject sslSession = GetCurrentSslSession(env, sslStream); - jstring protocol = (*env)->CallObjectMethod(env, sslSession, g_SSLSessionGetProtocol); + loc[protocol] = (*env)->CallObjectMethod(env, loc[sslSession], g_SSLSessionGetProtocol); ON_EXCEPTION_PRINT_AND_GOTO(cleanup); - *out = AllocateString(env, protocol); + *out = AllocateString(env, loc[protocol]); ret = SUCCESS; cleanup: - ReleaseLRef(env, sslSession); - ReleaseLRef(env, protocol); + RELEASE_LOCALS(loc, env); return ret; } +ARGS_NON_NULL_ALL static jobject GetPeerCertificates(JNIEnv* env, SSLStream* sslStream) +{ + jobject certificates = NULL; + INIT_LOCALS(loc, sslSession); + + loc[sslSession] = GetCurrentSslSession(env, sslStream); + if (loc[sslSession] == NULL) + goto cleanup; + + // Certificate[] certificates = sslSession.getPeerCertificates(); + certificates = (*env)->CallObjectMethod(env, loc[sslSession], g_SSLSessionGetPeerCertificates); + // If there are no peer certificates, getPeerCertificates will throw. Return null to indicate no certificates. + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + +cleanup: + RELEASE_LOCALS(loc, env); + return certificates; +} + jobject /*X509Certificate*/ AndroidCryptoNative_SSLStreamGetPeerCertificate(SSLStream* sslStream) { abort_if_invalid_pointer_argument (sslStream); @@ -853,15 +958,11 @@ jobject /*X509Certificate*/ AndroidCryptoNative_SSLStreamGetPeerCertificate(SSLS JNIEnv* env = GetJNIEnv(); jobject ret = NULL; - // Certificate[] certs = sslSession.getPeerCertificates(); - // out = certs[0]; - jobject sslSession = GetCurrentSslSession(env, sslStream); - jobjectArray certs = (*env)->CallObjectMethod(env, sslSession, g_SSLSessionGetPeerCertificates); - - // If there are no peer certificates, getPeerCertificates will throw. Return null to indicate no certificate. - if (TryClearJNIExceptions(env)) + jobject certs = GetPeerCertificates(env, sslStream); + if (certs == NULL) goto cleanup; + // out = certs[0]; jsize len = (*env)->GetArrayLength(env, certs); if (len > 0) { @@ -871,7 +972,6 @@ jobject /*X509Certificate*/ AndroidCryptoNative_SSLStreamGetPeerCertificate(SSLS } cleanup: - ReleaseLRef(env, sslSession); ReleaseLRef(env, certs); return ret; } @@ -886,17 +986,13 @@ void AndroidCryptoNative_SSLStreamGetPeerCertificates(SSLStream* sslStream, jobj *out = NULL; *outLen = 0; - // Certificate[] certs = sslSession.getPeerCertificates(); + jobjectArray certs = GetPeerCertificates(env, sslStream); + if (certs == NULL) + goto cleanup; + // for (int i = 0; i < certs.length; i++) { // out[i] = certs[i]; // } - jobject sslSession = GetCurrentSslSession(env, sslStream); - jobjectArray certs = (*env)->CallObjectMethod(env, sslSession, g_SSLSessionGetPeerCertificates); - - // If there are no peer certificates, getPeerCertificates will throw. Return null and length of zero to indicate no certificates. - if (TryClearJNIExceptions(env)) - goto cleanup; - jsize len = (*env)->GetArrayLength(env, certs); *outLen = len; if (len > 0) @@ -910,7 +1006,6 @@ void AndroidCryptoNative_SSLStreamGetPeerCertificates(SSLStream* sslStream, jobj } cleanup: - ReleaseLRef(env, sslSession); ReleaseLRef(env, certs); } @@ -930,7 +1025,8 @@ int32_t AndroidCryptoNative_SSLStreamSetApplicationProtocols(SSLStream* sslStrea abort_if_invalid_pointer_argument (sslStream); abort_if_invalid_pointer_argument (protocolData); - if (!AndroidCryptoNative_SSLSupportsApplicationProtocolsConfiguration()) { + if (!AndroidCryptoNative_SSLSupportsApplicationProtocolsConfiguration()) + { LOG_ERROR ("SSL does not support application protocols configuration"); return FAIL; } @@ -1027,14 +1123,20 @@ bool AndroidCryptoNative_SSLStreamVerifyHostname(SSLStream* sslStream, char* hos bool ret = false; INIT_LOCALS(loc, name, verifier, sslSession); + loc[sslSession] = GetCurrentSslSession(env, sslStream); + if (loc[sslSession] == NULL) + goto cleanup; + // HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier(); - // return verifier.verify(hostname, sslSession); loc[name] = make_java_string(env, hostname); - loc[sslSession] = GetCurrentSslSession(env, sslStream); - loc[verifier] = - (*env)->CallStaticObjectMethod(env, g_HttpsURLConnection, g_HttpsURLConnectionGetDefaultHostnameVerifier); + loc[verifier] = (*env)->CallStaticObjectMethod(env, g_HttpsURLConnection, g_HttpsURLConnectionGetDefaultHostnameVerifier); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + // return verifier.verify(hostname, sslSession); ret = (*env)->CallBooleanMethod(env, loc[verifier], g_HostnameVerifierVerify, loc[name], loc[sslSession]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); +cleanup: RELEASE_LOCALS(loc, env); return ret; } diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h index 51c692440337e9..fa3a884d68adfa 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.h @@ -44,14 +44,15 @@ Create an SSL context Returns NULL on failure */ -PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreate(void); +PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreate(intptr_t sslStreamProxyHandle); /* Create an SSL context with the specified certificates Returns NULL on failure */ -PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreateWithCertificates(uint8_t* pkcs8PrivateKey, +PALEXPORT SSLStream* AndroidCryptoNative_SSLStreamCreateWithCertificates(intptr_t sslStreamProxyHandle, + uint8_t* pkcs8PrivateKey, int32_t pkcs8PrivateKeyLen, PAL_KeyAlgorithm algorithm, jobject* /*X509Certificate[]*/ certs, diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.c new file mode 100644 index 00000000000000..c0097c9f3b998a --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.c @@ -0,0 +1,36 @@ +#include "pal_trust_manager.h" + +static RemoteCertificateValidationCallback verifyRemoteCertificate; + +ARGS_NON_NULL_ALL void AndroidCryptoNative_RegisterRemoteCertificateValidationCallback(RemoteCertificateValidationCallback callback) +{ + abort_unless(verifyRemoteCertificate == NULL, "AndroidCryptoNative_RegisterRemoteCertificateValidationCallback can only be used once"); + verifyRemoteCertificate = callback; +} + +ARGS_NON_NULL_ALL jobjectArray GetTrustManagers(JNIEnv* env, intptr_t sslStreamProxyHandle) +{ + // X509TrustManager dotnetProxyTrustManager = new DotnetProxyTrustManager(sslStreamProxyHandle); + // TrustManager[] trustManagers = new TrustManager[] { dotnetProxyTrustManager }; + // return trustManagers; + + jobjectArray trustManagers = NULL; + INIT_LOCALS(loc, dotnetProxyTrustManager); + + loc[dotnetProxyTrustManager] = (*env)->NewObject(env, g_DotnetProxyTrustManager, g_DotnetProxyTrustManagerCtor, (jlong)sslStreamProxyHandle); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + trustManagers = make_java_object_array(env, 1, g_TrustManager, loc[dotnetProxyTrustManager]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + +cleanup: + RELEASE_LOCALS(loc, env); + return trustManagers; +} + +ARGS_NON_NULL_ALL jboolean Java_net_dot_android_crypto_DotnetProxyTrustManager_verifyRemoteCertificate( + JNIEnv* env, jobject thisHandle, jlong sslStreamProxyHandle) +{ + abort_unless(verifyRemoteCertificate, "verifyRemoteCertificate callback has not been registered"); + return verifyRemoteCertificate((intptr_t)sslStreamProxyHandle); +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.h new file mode 100644 index 00000000000000..e4f09118492327 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_trust_manager.h @@ -0,0 +1,10 @@ +#include "pal_jni.h" + +typedef bool (*RemoteCertificateValidationCallback)(intptr_t); + +PALEXPORT void AndroidCryptoNative_RegisterRemoteCertificateValidationCallback(RemoteCertificateValidationCallback callback); + +jobjectArray GetTrustManagers(JNIEnv* env, intptr_t sslStreamProxyHandle); + +JNIEXPORT jboolean JNICALL Java_net_dot_android_crypto_DotnetProxyTrustManager_verifyRemoteCertificate( + JNIEnv *env, jobject thisHandle, jlong sslStreamProxyHandle); diff --git a/src/native/libs/build-native.proj b/src/native/libs/build-native.proj index 00ac5897cc1c73..7199673717bc33 100644 --- a/src/native/libs/build-native.proj +++ b/src/native/libs/build-native.proj @@ -6,7 +6,8 @@ .NET Runtime <_BuildNativeTargetOS>$(TargetOS) <_BuildNativeTargetOS Condition="'$(TargetsLinuxBionic)' == 'true'">linux-bionic - <_BuildNativeArgs>$(TargetArchitecture) $(Configuration) outconfig $(NetCoreAppCurrent)-$(TargetOS)-$(Configuration)-$(TargetArchitecture) -os $(_BuildNativeTargetOS) + <_BuildNativeOutConfig>$(NetCoreAppCurrent)-$(TargetOS)-$(Configuration)-$(TargetArchitecture) + <_BuildNativeArgs>$(TargetArchitecture) $(Configuration) outconfig $(_BuildNativeOutConfig) -os $(_BuildNativeTargetOS) <_BuildNativeArgs Condition="'$(OfficialBuildId)' != ''">$(_BuildNativeArgs) /p:OfficialBuildId="$(OfficialBuildId)" @@ -53,4 +54,17 @@ + + + + + + diff --git a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs index 0f409c1745f7ca..bf10dc962c942b 100644 --- a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs +++ b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.cs @@ -85,8 +85,6 @@ public class AndroidAppBuilderTask : Task /// public string? NativeMainSource { get; set; } - public bool IncludeNetworkSecurityConfig { get; set; } - public string? KeyStorePath { get; set; } public bool ForceInterpreter { get; set; } @@ -112,7 +110,6 @@ public override bool Execute() apkBuilder.BuildToolsVersion = BuildToolsVersion; apkBuilder.StripDebugSymbols = StripDebugSymbols; apkBuilder.NativeMainSource = NativeMainSource; - apkBuilder.IncludeNetworkSecurityConfig = IncludeNetworkSecurityConfig; apkBuilder.KeyStorePath = KeyStorePath; apkBuilder.ForceInterpreter = ForceInterpreter; apkBuilder.ForceAOT = ForceAOT; diff --git a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj index 139d5672ee01a0..3dd38caa3c8809 100644 --- a/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj +++ b/src/tasks/AndroidAppBuilder/AndroidAppBuilder.csproj @@ -17,6 +17,11 @@ + + + + + diff --git a/src/tasks/AndroidAppBuilder/AndroidLibBuilderTask.cs b/src/tasks/AndroidAppBuilder/AndroidLibBuilderTask.cs new file mode 100644 index 00000000000000..81d9062c6f0eae --- /dev/null +++ b/src/tasks/AndroidAppBuilder/AndroidLibBuilderTask.cs @@ -0,0 +1,80 @@ +// 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 Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class AndroidLibBuilderTask : Task +{ + [Required] + public string JavaSourceDirectory { get; set; } = ""!; + + [Required] + public string OutputDir { get; set; } = ""!; + + [Required] + public string DexFileName { get; set; } = ""!; + + [Required] + public string JarFileName { get; set; } = ""!; + + public string? AndroidSdk { get; set; } + + public string? BuildApiLevel { get; set; } + + public string? BuildToolsVersion { get; set; } + + public override bool Execute() + { + var androidSdk = new AndroidSdkHelper( + androidSdkPath: AndroidSdk, + buildApiLevel: BuildApiLevel, + buildToolsVersion: BuildToolsVersion); + + var objDir = Path.Combine(OutputDir, "obj"); + Directory.CreateDirectory(objDir); + + try + { + CompileJava(objDir, androidSdk); + BuildJar(objDir); + BuildDex(objDir, androidSdk); + + return true; + } + finally + { + Directory.Delete(objDir, recursive: true); + } + } + + private void CompileJava(string objDir, AndroidSdkHelper androidSdk) + { + var compiler = new JavaCompiler(Log, androidSdk, workingDir: JavaSourceDirectory); + string[] javaFiles = Directory.GetFiles(JavaSourceDirectory, "*.java", SearchOption.AllDirectories); + foreach (var file in javaFiles) + { + compiler.Compile(file, outputDir: objDir); + } + } + + private void BuildJar(string objDir) + { + var jarBuilder = new JarBuilder(Log); + var jarFilePath = Path.Combine(OutputDir, JarFileName); + jarBuilder.Build(inputDir: objDir, outputFileName: jarFilePath); + + Log.LogMessage(MessageImportance.High, $"Built {jarFilePath}"); + } + + private void BuildDex(string objDir, AndroidSdkHelper androidSdk) + { + var dexBuilder = new DexBuilder(Log, androidSdk, workingDir: OutputDir); + var dexFilePath = Path.Combine(OutputDir, DexFileName); + dexBuilder.Build(inputDir: objDir, outputFileName: dexFilePath); + + Log.LogMessage(MessageImportance.High, $"Built {dexFilePath}"); + } +} diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index 4cfc8d0d1cac9b..ba1b7b3a8cdf0e 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -24,7 +24,6 @@ public class ApkBuilder public string OutputDir { get; set; } = ""!; public bool StripDebugSymbols { get; set; } public string? NativeMainSource { get; set; } - public bool IncludeNetworkSecurityConfig { get; set; } public string? KeyStorePath { get; set; } public bool ForceInterpreter { get; set; } public bool ForceAOT { get; set; } @@ -59,12 +58,6 @@ public ApkBuilder(TaskLoggingHelper logger) throw new ArgumentException($"MainLibraryFileName='{mainLibraryFileName}' was not found in AppDir='{AppDir}'"); } - var networkSecurityConfigFilePath = Path.Combine(AppDir, "res", "xml", "network_security_config.xml"); - if (IncludeNetworkSecurityConfig && !File.Exists(networkSecurityConfigFilePath)) - { - throw new ArgumentException($"IncludeNetworkSecurityConfig is set but the file '{networkSecurityConfigFilePath}' was not found"); - } - if (string.IsNullOrEmpty(abi)) { throw new ArgumentException("abi should not be empty (e.g. x86, x86_64, armeabi-v7a or arm64-v8a"); @@ -180,9 +173,8 @@ public ApkBuilder(TaskLoggingHelper logger) Directory.CreateDirectory(Path.Combine(OutputDir, "obj")); Directory.CreateDirectory(Path.Combine(OutputDir, "assets-tozip")); Directory.CreateDirectory(Path.Combine(OutputDir, "assets")); - Directory.CreateDirectory(Path.Combine(OutputDir, "res")); - var extensionsToIgnore = new List { ".so", ".a" }; + var extensionsToIgnore = new List { ".so", ".a", ".dex", ".jar" }; if (StripDebugSymbols) { extensionsToIgnore.Add(".pdb"); @@ -209,20 +201,9 @@ public ApkBuilder(TaskLoggingHelper logger) // aapt complains on such files return false; } - if (file.Contains("/res/")) - { - // exclude everything in the `res` folder - return false; - } return true; }); - // copy the res directory as is - if (Directory.Exists(Path.Combine(AppDir, "res"))) - { - Utils.DirectoryCopy(Path.Combine(AppDir, "res"), Path.Combine(OutputDir, "res")); - } - // add AOT .so libraries foreach (var aotlib in aotLibraryFiles) { @@ -247,7 +228,7 @@ public ApkBuilder(TaskLoggingHelper logger) if (!File.Exists(androidJar)) throw new ArgumentException($"API level={BuildApiLevel} is not downloaded in Android SDK"); - // 1. Build libmonodroid.so` via cmake + // 1. Build libmonodroid.so via cmake string nativeLibraries = ""; string monoRuntimeLib = ""; @@ -399,11 +380,6 @@ public ApkBuilder(TaskLoggingHelper logger) if (!string.IsNullOrEmpty(NativeMainSource)) File.Copy(NativeMainSource, javaActivityPath, true); - string networkSecurityConfigAttribute = - IncludeNetworkSecurityConfig - ? "a:networkSecurityConfig=\"@xml/network_security_config\"" - : string.Empty; - string envVariables = ""; foreach (ITaskItem item in EnvironmentVariables) { @@ -421,7 +397,6 @@ public ApkBuilder(TaskLoggingHelper logger) File.WriteAllText(Path.Combine(OutputDir, "AndroidManifest.xml"), Utils.GetEmbeddedResource("AndroidManifest.xml") .Replace("%PackageName%", packageId) - .Replace("%NetworkSecurityConfig%", networkSecurityConfigAttribute) .Replace("%MinSdkLevel%", MinApiLevel)); string javaCompilerArgs = $"-d obj -classpath src -bootclasspath {androidJar} -source 1.8 -target 1.8 "; @@ -446,8 +421,7 @@ public ApkBuilder(TaskLoggingHelper logger) string debugModeArg = StripDebugSymbols ? string.Empty : "--debug-mode"; string apkFile = Path.Combine(OutputDir, "bin", $"{ProjectName}.unaligned.apk"); - string resources = IncludeNetworkSecurityConfig ? "-S res" : string.Empty; - Utils.RunProcess(logger, aapt, $"package -f -m -F {apkFile} -A assets {resources} -M AndroidManifest.xml -I {androidJar} {debugModeArg}", workingDir: OutputDir); + Utils.RunProcess(logger, aapt, $"package -f -m -F {apkFile} -A assets -M AndroidManifest.xml -I {androidJar} {debugModeArg}", workingDir: OutputDir); var dynamicLibs = new List(); dynamicLibs.Add(Path.Combine(OutputDir, "monodroid", "libmonodroid.so")); @@ -507,6 +481,17 @@ public ApkBuilder(TaskLoggingHelper logger) } Utils.RunProcess(logger, aapt, $"add {apkFile} classes.dex", workingDir: OutputDir); + // Include prebuilt .dex files + int sequence = 2; + var dexFiles = Directory.GetFiles(AppDir, "*.dex"); + foreach (var dexFile in dexFiles) + { + var classesFileName = $"classes{sequence++}.dex"; + File.Copy(dexFile, Path.Combine(OutputDir, classesFileName)); + logger.LogMessage(MessageImportance.High, $"Adding dex file {Path.GetFileName(dexFile)} as {classesFileName}"); + Utils.RunProcess(logger, aapt, $"add {apkFile} {classesFileName}", workingDir: OutputDir); + } + // 4. Align APK string alignedApk = Path.Combine(OutputDir, "bin", $"{ProjectName}.apk"); diff --git a/src/tasks/AndroidAppBuilder/Templates/AndroidManifest.xml b/src/tasks/AndroidAppBuilder/Templates/AndroidManifest.xml index befd2e446a650e..30ded9e1360480 100644 --- a/src/tasks/AndroidAppBuilder/Templates/AndroidManifest.xml +++ b/src/tasks/AndroidAppBuilder/Templates/AndroidManifest.xml @@ -1,5 +1,5 @@ - @@ -8,7 +8,6 @@ @@ -21,4 +20,4 @@ - \ No newline at end of file + diff --git a/src/tasks/Common/AndroidSdkHelper.cs b/src/tasks/Common/AndroidSdkHelper.cs new file mode 100644 index 00000000000000..e56e7d65699d2d --- /dev/null +++ b/src/tasks/Common/AndroidSdkHelper.cs @@ -0,0 +1,81 @@ +// 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.Linq; + +internal sealed class AndroidSdkHelper +{ + private readonly string _androidSdkPath; + private readonly string _buildToolsPath; + private readonly string _buildApiLevel; + + public AndroidSdkHelper( + string? androidSdkPath, + string? buildApiLevel, + string? buildToolsVersion) + { + if (string.IsNullOrEmpty(androidSdkPath)) + androidSdkPath = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT"); + + if (string.IsNullOrEmpty(androidSdkPath) || !Directory.Exists(androidSdkPath)) + throw new ArgumentException($"Android SDK='{androidSdkPath}' was not found or empty (can be set via ANDROID_SDK_ROOT envvar)."); + + _androidSdkPath = androidSdkPath; + + // Try to get the latest API level if not specified + if (string.IsNullOrEmpty(buildApiLevel)) + buildApiLevel = GetLatestApiLevel(_androidSdkPath); + + _buildApiLevel = buildApiLevel; + + // Try to get the latest build-tools version if not specified + if (string.IsNullOrEmpty(buildToolsVersion)) + buildToolsVersion = GetLatestBuildTools(_androidSdkPath); + + _buildToolsPath = Path.Combine(_androidSdkPath, "build-tools", buildToolsVersion); + + if (!Directory.Exists(_buildToolsPath)) + throw new ArgumentException($"{_buildToolsPath} was not found."); + } + + public string AndroidJarPath => Path.Combine(_androidSdkPath, "platforms", $"android-{_buildApiLevel}", "android.jar"); + + public bool HasD8 => File.Exists(D8Path); + public string D8Path => getToolPath("d8"); + public string DxPath => getToolPath("dx"); + + private string getToolPath(string tool) + => Path.Combine(_buildToolsPath, tool); + + /// + /// Scan android SDK for api levels (ignore preview versions) + /// + private static string GetLatestApiLevel(string androidSdkDir) + { + return Directory.GetDirectories(Path.Combine(androidSdkDir, "platforms")) + .Select(file => int.TryParse(Path.GetFileName(file).Replace("android-", ""), out int apiLevel) ? apiLevel : -1) + .OrderByDescending(v => v) + .FirstOrDefault() + .ToString(); + } + + /// + /// Scan android SDK for build tools (ignore preview versions) + /// + private static string GetLatestBuildTools(string androidSdkPath) + { + string? buildTools = Directory.GetDirectories(Path.Combine(androidSdkPath, "build-tools")) + .Select(Path.GetFileName) + .Where(file => !file!.Contains('-')) + .Select(file => { Version.TryParse(Path.GetFileName(file), out Version? version); return version; }) + .OrderByDescending(v => v) + .FirstOrDefault()?.ToString(); + + if (string.IsNullOrEmpty(buildTools)) + throw new ArgumentException($"Android SDK ({androidSdkPath}) doesn't contain build-tools."); + + return buildTools; + } +} diff --git a/src/tasks/Common/DexBuilder.cs b/src/tasks/Common/DexBuilder.cs new file mode 100644 index 00000000000000..a98da4e218e939 --- /dev/null +++ b/src/tasks/Common/DexBuilder.cs @@ -0,0 +1,56 @@ +// 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.Linq; +using Microsoft.Build.Utilities; + +internal sealed class DexBuilder +{ + private readonly string _workingDir; + private readonly AndroidSdkHelper _androidSdk; + private readonly TaskLoggingHelper _logger; + + public DexBuilder( + TaskLoggingHelper logger, + AndroidSdkHelper buildTools, + string workingDir) + { + _androidSdk = buildTools; + _workingDir = workingDir; + _logger = logger; + } + + public void Build(string inputDir, string outputFileName) + { + if (_androidSdk.HasD8) + { + BuildUsingD8(inputDir, outputFileName); + } + else + { + BuildUsingDx(inputDir, outputFileName); + } + } + + private void BuildUsingD8(string inputDir, string outputFilePath) + { + string[] classFiles = Directory.GetFiles(inputDir, "*.class", SearchOption.AllDirectories); + + if (!classFiles.Any()) + throw new InvalidOperationException("Didn't find any .class files"); + + Utils.RunProcess(_logger, _androidSdk.D8Path, $"--no-desugaring {string.Join(" ", classFiles)}", workingDir: _workingDir); + + File.Move( + sourceFileName: Path.Combine(_workingDir, "classes.dex"), + destFileName: outputFilePath, + overwrite: true); + } + + private void BuildUsingDx(string inputDir, string outputFileName) + { + Utils.RunProcess(_logger, _androidSdk.DxPath, $"--dex --output={outputFileName} {inputDir}", workingDir: _workingDir); + } +} diff --git a/src/tasks/Common/JarBuilder.cs b/src/tasks/Common/JarBuilder.cs new file mode 100644 index 00000000000000..f5e2a03215b3ff --- /dev/null +++ b/src/tasks/Common/JarBuilder.cs @@ -0,0 +1,27 @@ +// 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.Linq; +using System.Collections.Generic; +using Microsoft.Build.Utilities; + +internal sealed class JarBuilder +{ + private readonly TaskLoggingHelper _logger; + + public JarBuilder(TaskLoggingHelper logger) + { + _logger = logger; + } + + public void Build(string inputDir, string outputFileName) + { + IEnumerable classFiles = + Directory.GetFiles(inputDir, "*.class", SearchOption.AllDirectories) + .Select(classFile => Path.GetRelativePath(inputDir, classFile)); + + Utils.RunProcess(_logger, "jar", $"-cf {outputFileName} {string.Join(" ", classFiles)}", workingDir: inputDir); + } +} diff --git a/src/tasks/Common/JavaCompiler.cs b/src/tasks/Common/JavaCompiler.cs new file mode 100644 index 00000000000000..abeb7c6fd91556 --- /dev/null +++ b/src/tasks/Common/JavaCompiler.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.Build.Utilities; + +internal sealed class JavaCompiler +{ + private readonly string _javaCompilerArgs; + private readonly string _workingDir; + private readonly TaskLoggingHelper _logger; + + public JavaCompiler( + TaskLoggingHelper logger, + AndroidSdkHelper androidSdk, + string workingDir, + string javaVersion = "1.8") + { + _javaCompilerArgs = $"-classpath src -bootclasspath {androidSdk.AndroidJarPath} -source {javaVersion} -target {javaVersion}"; + _workingDir = workingDir; + _logger = logger; + } + + public void Compile(string javaSourceFile, string outputDir) + { + Utils.RunProcess(_logger, "javac", $"{_javaCompilerArgs} -d {outputDir} {javaSourceFile}", workingDir: _workingDir); + } +} diff --git a/src/tests/Directory.Build.targets b/src/tests/Directory.Build.targets index 29b8159ce3a32d..d5a29562a686b4 100644 --- a/src/tests/Directory.Build.targets +++ b/src/tests/Directory.Build.targets @@ -170,12 +170,17 @@ + + + + + diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Android.Device_Emulator.gRPC.Test.csproj b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Android.Device_Emulator.gRPC.Test.csproj index 93f5817bc2aad6..12755944bf324a 100644 --- a/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Android.Device_Emulator.gRPC.Test.csproj +++ b/src/tests/FunctionalTests/Android/Device_Emulator/gRPC/Android.Device_Emulator.gRPC.Test.csproj @@ -13,7 +13,7 @@ true true - + enable enable @@ -21,16 +21,8 @@ CS8981;SYSLIB0039 - - true - - - - - PreserveNewest - diff --git a/src/tests/build.proj b/src/tests/build.proj index e156a911555be0..bec907bd2afde5 100644 --- a/src/tests/build.proj +++ b/src/tests/build.proj @@ -210,7 +210,7 @@ - + @@ -238,7 +238,6 @@ RuntimeIdentifier="$(RuntimeIdentifier)" ProjectName="$(Category)" MonoRuntimeHeaders="$(MicrosoftNetCoreAppRuntimePackDir)/native/include/mono-2.0" - IncludeNetworkSecurityConfig="$(IncludeNetworkSecurityConfig)" RuntimeComponents="$(RuntimeComponents)" DiagnosticPorts="$(DiagnosticPorts)" StripDebugSymbols="$(StripDebugSymbols)"