diff --git a/.gdn/.gdnsuppress b/.gdn/.gdnsuppress index fada1dfadd9..9de92281188 100644 --- a/.gdn/.gdnsuppress +++ b/.gdn/.gdnsuppress @@ -57,6 +57,69 @@ ], "justification": "Dummy test.keystore file used for testing.", "createdDate": "2024-02-21 20:58:02Z" + }, + "ad733d624486984da63461d2a23f266714f76e1788c271d90d45687579f51099": { + "signature": "ad733d624486984da63461d2a23f266714f76e1788c271d90d45687579f51099", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "release.keystore file created during test run.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "e10f89d02383ffef3bdbf9c048a9e0f3bdab956a8e6e49817780b0c837a5bd6d": { + "signature": "e10f89d02383ffef3bdbf9c048a9e0f3bdab956a8e6e49817780b0c837a5bd6d", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "e73b15633b7cb1e9e735ce0fe78a6ce3c95c11a8888181eb3b0cb50c191da19e": { + "signature": "e73b15633b7cb1e9e735ce0fe78a6ce3c95c11a8888181eb3b0cb50c191da19e", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "e622e6a9a73c1856d399e753105be517d62ec1e62d13a15ab9ecef43e15590a9": { + "signature": "e622e6a9a73c1856d399e753105be517d62ec1e62d13a15ab9ecef43e15590a9", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "df428be5ce5ef90685e15981cf49e2af10de6d87544f437aa1722f84516d6fef": { + "signature": "df428be5ce5ef90685e15981cf49e2af10de6d87544f437aa1722f84516d6fef", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "247325bc1f0ff6899ae09b13e006ac35c7cae4ffee0749f139fd5100f85a162f": { + "signature": "247325bc1f0ff6899ae09b13e006ac35c7cae4ffee0749f139fd5100f85a162f", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" + }, + "6d53f09942503c3f7eeccf23af43ae976431e8dbf2ad3d32be8af5bd37068d4d": { + "signature": "6d53f09942503c3f7eeccf23af43ae976431e8dbf2ad3d32be8af5bd37068d4d", + "alternativeSignatures": [], + "memberOf": [ + "default" + ], + "justification": "False positive in linker-dependencies.xml file.", + "createdDate": "2024-06-14 18:52:00Z" } } } diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index fef5fc2cc03..f3f880db28e 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -9,6 +9,7 @@ using System.Net.Http.Headers; using System.Net.Security; using System.Security.Authentication; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -21,6 +22,7 @@ using Java.Security; using Java.Security.Cert; using Javax.Net.Ssl; +using JavaX509Certificate = Java.Security.Cert.X509Certificate; namespace Xamarin.Android.Net { @@ -206,9 +208,22 @@ public CookieContainer CookieContainer public bool AllowAutoRedirect { get; set; } = true; - public ClientCertificateOption ClientCertificateOptions { get; set; } + public ClientCertificateOption ClientCertificateOptions { get; set; } = ClientCertificateOption.Manual; - public X509CertificateCollection? ClientCertificates { get; set; } + private X509CertificateCollection? _clientCertificates; + public X509CertificateCollection? ClientCertificates + { + get + { + if (ClientCertificateOptions != ClientCertificateOption.Manual) { + throw new InvalidOperationException ($"Enable manual options first on {nameof (ClientCertificateOptions)}"); + } + + return _clientCertificates ?? (_clientCertificates = new X509CertificateCollection ()); + } + + set => _clientCertificates = value; + } public ICredentials? DefaultProxyCredentials { get; set; } @@ -1151,49 +1166,115 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe return; } - var keyStore = InitializeKeyStore (out bool gotCerts); - keyStore = ConfigureKeyStore (keyStore); - var kmf = ConfigureKeyManagerFactory (keyStore); - var tmf = ConfigureTrustManagerFactory (keyStore); + KeyStore keyStore = GetConfiguredKeyStoreInstance (); + KeyManagerFactory? kmf = GetConfiguredKeyManagerFactory (keyStore); + TrustManagerFactory? tmf = ConfigureTrustManagerFactory (keyStore); + + // If there is no customization there is no point in changing the behavior of the default SSL socket factory. + if (tmf is null && kmf is null && !HasTrustedCerts && !HasServerCertificateCustomValidationCallback && !HasClientCertificates) { + return; + } - if (tmf == null) { - // If there are no trusted certs, no custom trust manager factory or custom certificate validation callback - // there is no point in changing the behavior of the default SSL socket factory - if (!gotCerts && _serverCertificateCustomValidator is null) - return; + var context = SSLContext.GetInstance ("TLS") ?? throw new InvalidOperationException ("Failed to get the SSLContext instance for TLS"); + var trustManagers = GetTrustManagers (tmf, keyStore, requestMessage); + context.Init (kmf?.GetKeyManagers (), trustManagers, null); + httpsConnection.SSLSocketFactory = context.SocketFactory; + } - tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); - tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs + [MemberNotNullWhen (true, nameof(TrustedCerts))] + bool HasTrustedCerts => TrustedCerts?.Count > 0; + + [MemberNotNullWhen (true, nameof(_serverCertificateCustomValidator))] + bool HasServerCertificateCustomValidationCallback => _serverCertificateCustomValidator is not null; + + [MemberNotNullWhen (true, nameof(_clientCertificates))] + bool HasClientCertificates => _clientCertificates?.Count > 0; + + KeyManagerFactory? GetConfiguredKeyManagerFactory (KeyStore keyStore) + { + var kmf = ConfigureKeyManagerFactory (keyStore); + + if (kmf is null && HasClientCertificates) { + kmf = KeyManagerFactory.GetInstance ("PKIX") ?? throw new InvalidOperationException ("Failed to get the KeyManagerFactory instance for PKIX"); + kmf.Init (keyStore, null); } - ITrustManager[]? trustManagers = tmf?.GetTrustManagers (); + return kmf; + } + + KeyStore GetConfiguredKeyStoreInstance () + { + var keyStore = KeyStore.GetInstance (KeyStore.DefaultType) ?? throw new InvalidOperationException ("Failed to get the default KeyStore instance"); + keyStore.Load (null, null); - var customValidator = _serverCertificateCustomValidator; - if (customValidator is not null) { - trustManagers = customValidator.ReplaceX509TrustManager (trustManagers, requestMessage); + if (HasTrustedCerts) { + for (int i = 0; i < TrustedCerts!.Count; i++) { + if (TrustedCerts [i] is Certificate cert) { + keyStore.SetCertificateEntry ($"ca{i}", cert); + } + } } - var context = SSLContext.GetInstance ("TLS"); - context?.Init (kmf?.GetKeyManagers (), trustManagers, null); - httpsConnection.SSLSocketFactory = context?.SocketFactory; + if (HasClientCertificates) { + if (ClientCertificateOptions != ClientCertificateOption.Manual) { + throw new InvalidOperationException ($"Use of {nameof(ClientCertificates)} requires that {nameof(ClientCertificateOptions)} be set to ClientCertificateOption.Manual"); + } - KeyStore? InitializeKeyStore (out bool gotCerts) - { - var keyStore = KeyStore.GetInstance (KeyStore.DefaultType); - keyStore?.Load (null, null); - gotCerts = TrustedCerts?.Count > 0; - - if (gotCerts) { - for (int i = 0; i < TrustedCerts!.Count; i++) { - Certificate cert = TrustedCerts [i]; - if (cert == null) - continue; - keyStore?.SetCertificateEntry ($"ca{i}", cert); + for (int i = 0; i < _clientCertificates.Count; i++) { + var keyEntry = GetKeyEntry (new X509Certificate2 (_clientCertificates [i])); + if (keyEntry is var (key, chain)) { + keyStore.SetKeyEntry ($"key{i}", key, null, chain); } } + } + + return ConfigureKeyStore (keyStore) ?? throw new InvalidOperationException ($"{nameof(ConfigureKeyStore)} unexpectedly returned null"); + } + + ITrustManager[]? GetTrustManagers (TrustManagerFactory? tmf, KeyStore keyStore, HttpRequestMessage requestMessage) + { + if (tmf is null) { + tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm) ?? throw new InvalidOperationException ("Failed to get the default TrustManagerFactory instance"); + tmf.Init (HasTrustedCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs + } + + ITrustManager[]? trustManagers = tmf.GetTrustManagers (); + + if (HasServerCertificateCustomValidationCallback) { + trustManagers = _serverCertificateCustomValidator.ReplaceX509TrustManager (trustManagers, requestMessage); + } - return keyStore; + return trustManagers; + } + + static (IPrivateKey, Certificate[])? GetKeyEntry (X509Certificate2 clientCertificate) + { + if (!clientCertificate.HasPrivateKey) { + return null; } + + AsymmetricAlgorithm? key = null; + string? algorithmName = null; + + if (clientCertificate.GetRSAPrivateKey () is {} rsa) { + (key, algorithmName) = (rsa, "RSA"); + } else if (clientCertificate.GetECDsaPrivateKey () is {} ec) { + (key, algorithmName) = (ec, "EC"); + } else if (clientCertificate.GetDSAPrivateKey () is {} dsa) { + (key, algorithmName) = (dsa, "DSA"); + } else { + return null; + } + + var keyFactory = KeyFactory.GetInstance (algorithmName) ?? throw new InvalidOperationException ($"Failed to get the KeyFactory instance for algorithm {algorithmName}"); + var privateKey = keyFactory.GeneratePrivate (new Java.Security.Spec.PKCS8EncodedKeySpec (key.ExportPkcs8PrivateKey ())); + var certificate = Java.Lang.Object.GetObject (clientCertificate.Handle, JniHandleOwnership.DoNotTransfer); + + if (privateKey is null || certificate is null) { + return null; + } + + return (privateKey, new Certificate [] { certificate }); } void HandlePreAuthentication (HttpURLConnection httpConnection) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc index 6fa30c70747..460971d4d88 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc @@ -11,13 +11,13 @@ "Size": 1114 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 66243 + "Size": 66250 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 94712 + "Size": 94741 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 5320 + "Size": 5367 }, "lib/arm64-v8a/lib_System.Console.dll.so": { "Size": 7226 @@ -35,7 +35,7 @@ "Size": 4475 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { - "Size": 2932 + "Size": 3059 }, "lib/arm64-v8a/libarc.bin.so": { "Size": 1546 @@ -44,7 +44,7 @@ "Size": 87432 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 492344 + "Size": 492280 }, "lib/arm64-v8a/libmonosgen-2.0.so": { "Size": 3163208 @@ -62,10 +62,10 @@ "Size": 159544 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 17960 + "Size": 18008 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1221 + "Size": 1213 }, "META-INF/BNDLTOOL.SF": { "Size": 3266 diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc index aa4c87ce038..fa599195f71 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc @@ -35,37 +35,37 @@ "Size": 8330 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 77616 + "Size": 77620 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 496752 + "Size": 500856 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 5397 + "Size": 5319 }, "lib/arm64-v8a/lib_mscorlib.dll.so": { - "Size": 4355 + "Size": 4356 }, "lib/arm64-v8a/lib_netstandard.dll.so": { - "Size": 5996 + "Size": 5993 }, "lib/arm64-v8a/lib_System.Collections.Concurrent.dll.so": { - "Size": 12694 + "Size": 12730 }, "lib/arm64-v8a/lib_System.Collections.dll.so": { - "Size": 17057 + "Size": 19239 }, "lib/arm64-v8a/lib_System.Collections.NonGeneric.dll.so": { "Size": 8684 }, "lib/arm64-v8a/lib_System.Collections.Specialized.dll.so": { - "Size": 6771 + "Size": 6768 }, "lib/arm64-v8a/lib_System.ComponentModel.dll.so": { - "Size": 2509 + "Size": 2510 }, "lib/arm64-v8a/lib_System.ComponentModel.Primitives.dll.so": { - "Size": 4234 + "Size": 4231 }, "lib/arm64-v8a/lib_System.ComponentModel.TypeConverter.dll.so": { "Size": 25312 @@ -74,25 +74,28 @@ "Size": 7339 }, "lib/arm64-v8a/lib_System.Core.dll.so": { - "Size": 2369 + "Size": 2368 }, "lib/arm64-v8a/lib_System.Diagnostics.DiagnosticSource.dll.so": { - "Size": 10965 + "Size": 10962 }, "lib/arm64-v8a/lib_System.Diagnostics.TraceSource.dll.so": { - "Size": 7612 + "Size": 7614 }, "lib/arm64-v8a/lib_System.dll.so": { - "Size": 2771 + "Size": 2772 }, "lib/arm64-v8a/lib_System.Drawing.dll.so": { - "Size": 2353 + "Size": 2354 }, "lib/arm64-v8a/lib_System.Drawing.Primitives.dll.so": { - "Size": 12571 + "Size": 12570 + }, + "lib/arm64-v8a/lib_System.Formats.Asn1.dll.so": { + "Size": 32794 }, "lib/arm64-v8a/lib_System.IO.Compression.Brotli.dll.so": { - "Size": 12424 + "Size": 12427 }, "lib/arm64-v8a/lib_System.IO.Compression.dll.so": { "Size": 16838 @@ -101,13 +104,13 @@ "Size": 11204 }, "lib/arm64-v8a/lib_System.Linq.dll.so": { - "Size": 21392 + "Size": 21391 }, "lib/arm64-v8a/lib_System.Linq.Expressions.dll.so": { "Size": 168729 }, "lib/arm64-v8a/lib_System.Net.Http.dll.so": { - "Size": 70637 + "Size": 70642 }, "lib/arm64-v8a/lib_System.Net.Primitives.dll.so": { "Size": 24046 @@ -116,58 +119,61 @@ "Size": 4475 }, "lib/arm64-v8a/lib_System.ObjectModel.dll.so": { - "Size": 9992 + "Size": 9990 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 929813 + "Size": 932380 }, "lib/arm64-v8a/lib_System.Private.DataContractSerialization.dll.so": { - "Size": 199665 + "Size": 199662 }, "lib/arm64-v8a/lib_System.Private.Uri.dll.so": { - "Size": 45092 + "Size": 45090 }, "lib/arm64-v8a/lib_System.Private.Xml.dll.so": { - "Size": 220084 + "Size": 220082 }, "lib/arm64-v8a/lib_System.Private.Xml.Linq.dll.so": { "Size": 18532 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 3121 + "Size": 3122 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { - "Size": 4505 + "Size": 4506 + }, + "lib/arm64-v8a/lib_System.Runtime.Numerics.dll.so": { + "Size": 37219 }, "lib/arm64-v8a/lib_System.Runtime.Serialization.dll.so": { - "Size": 2275 + "Size": 2276 }, "lib/arm64-v8a/lib_System.Runtime.Serialization.Formatters.dll.so": { - "Size": 3251 + "Size": 3253 }, "lib/arm64-v8a/lib_System.Runtime.Serialization.Primitives.dll.so": { "Size": 4377 }, "lib/arm64-v8a/lib_System.Security.Cryptography.dll.so": { - "Size": 10151 + "Size": 63163 }, "lib/arm64-v8a/lib_System.Text.RegularExpressions.dll.so": { - "Size": 163854 + "Size": 163853 }, "lib/arm64-v8a/lib_System.Xml.dll.so": { - "Size": 2174 + "Size": 2175 }, "lib/arm64-v8a/lib_System.Xml.Linq.dll.so": { "Size": 2186 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { - "Size": 5007 + "Size": 5005 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.Activity.dll.so": { - "Size": 17871 + "Size": 17872 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.AppCompat.AppCompatResources.dll.so": { - "Size": 7421 + "Size": 7425 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.AppCompat.dll.so": { "Size": 146152 @@ -176,7 +182,7 @@ "Size": 7469 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.CoordinatorLayout.dll.so": { - "Size": 18822 + "Size": 18823 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.Core.dll.so": { "Size": 134318 @@ -185,7 +191,7 @@ "Size": 10076 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.DrawerLayout.dll.so": { - "Size": 16853 + "Size": 16856 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.Fragment.dll.so": { "Size": 55435 @@ -206,16 +212,16 @@ "Size": 14499 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.RecyclerView.dll.so": { - "Size": 95160 + "Size": 95162 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.SavedState.dll.so": { "Size": 6050 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.SwipeRefreshLayout.dll.so": { - "Size": 14856 + "Size": 14858 }, "lib/arm64-v8a/lib_Xamarin.AndroidX.ViewPager.dll.so": { - "Size": 20962 + "Size": 20961 }, "lib/arm64-v8a/lib_Xamarin.Forms.Core.dll.so": { "Size": 563905 @@ -230,10 +236,10 @@ "Size": 63542 }, "lib/arm64-v8a/lib_Xamarin.Google.Android.Material.dll.so": { - "Size": 67669 + "Size": 67675 }, "lib/arm64-v8a/libarc.bin.so": { - "Size": 1621 + "Size": 1562 }, "lib/arm64-v8a/libmono-component-marshal-ilgen.so": { "Size": 87432 @@ -242,7 +248,7 @@ "Size": 492344 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3181136 + "Size": 3182104 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 67248 @@ -257,7 +263,7 @@ "Size": 159544 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 348552 + "Size": 349520 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -410,10 +416,10 @@ "Size": 6 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1223 + "Size": 1221 }, "META-INF/BNDLTOOL.SF": { - "Size": 98341 + "Size": 98577 }, "META-INF/com.android.tools/proguard/coroutines.pro": { "Size": 1345 @@ -440,7 +446,7 @@ "Size": 5 }, "META-INF/MANIFEST.MF": { - "Size": 98214 + "Size": 98450 }, "META-INF/maven/com.google.guava/listenablefuture/pom.properties": { "Size": 96 @@ -2480,5 +2486,5 @@ "Size": 812848 } }, - "PackageSize": 10415187 + "PackageSize": 10521867 } \ No newline at end of file diff --git a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs index a98ff834d20..afd3f18294f 100644 --- a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs +++ b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Http; using System.Net.Security; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -248,6 +249,36 @@ public async Task AndroidMessageHandlerFollows308PermanentRedirect () Assert.AreEqual ("https://www.microsoft.com/", result.RequestMessage.RequestUri.ToString ()); } + [Test] + public async Task AndroidMessageHandlerSendsClientCertificate ([Values(true, false)] bool setClientCertificateOptionsExplicitly) + { + using X509Certificate2 certificate = BuildClientCertificate (); + + using var handler = new AndroidMessageHandler (); + if (setClientCertificateOptionsExplicitly) { + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + } + handler.ClientCertificates.Add (certificate); + + using var client = new HttpClient (handler); + var response = await client.GetAsync ("https://corefx-net-tls.azurewebsites.net/EchoClientCertificate.ashx"); + var content = await response.EnsureSuccessStatusCode ().Content.ReadAsStringAsync (); + + X509Certificate2 certificate2 = new X509Certificate2 (global::System.Convert.FromBase64String (content)); + Assert.AreEqual (certificate.Thumbprint, certificate2.Thumbprint); + } + + [Test] + public async Task AndroidMessageHandlerRejectsClientCertificateOptionsAutomatic () + { + var handler = new AndroidMessageHandler + { + ClientCertificateOptions = ClientCertificateOption.Automatic, + }; + + Assert.Throws(() => handler.ClientCertificates.Add (BuildClientCertificate ())); + } + private async Task AssertRejectsRemoteCertificate (Func makeRequest) { // there is a difference between the exception that's thrown in the .NET build and the legacy Xamarin @@ -262,5 +293,37 @@ private async Task AssertRejectsRemoteCertificate (Func makeRequest) catch (System.Net.WebException) {} catch (System.Net.Http.HttpRequestException) {} } + + // Adapted from https://github.com/dotnet/runtime/blob/e8b89a3fde2911c6cbac0488bf82c74329a7224a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs#L797 + private static X509Certificate2 BuildClientCertificate () + { + DateTimeOffset start = DateTimeOffset.UtcNow; + DateTimeOffset end = start.AddMonths (3); + + using RSA rootKey = RSA.Create (keySizeInBits: 2048); + using RSA clientKey = RSA.Create (keySizeInBits: 2048); + + var rootReq = new CertificateRequest ("CN=Test Root, O=Test Root Organization", rootKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + rootReq.CertificateExtensions.Add (new X509BasicConstraintsExtension (certificateAuthority: true, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true)); + rootReq.CertificateExtensions.Add (new X509SubjectKeyIdentifierExtension (rootReq.PublicKey, critical: false)); + X509Certificate2 rootCert = rootReq.CreateSelfSigned (start.AddDays (-2), end.AddDays (2)); + + var clientReq = new CertificateRequest ("CN=Test End Entity, O=Test End Entity Organization", clientKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + clientReq.CertificateExtensions.Add (new X509BasicConstraintsExtension (certificateAuthority: false, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: false)); + clientReq.CertificateExtensions.Add (new X509KeyUsageExtension (X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DataEncipherment, critical: false)); + clientReq.CertificateExtensions.Add (new X509EnhancedKeyUsageExtension (enhancedKeyUsages: new OidCollection { new Oid ("1.3.6.1.5.5.7.3.2", null) }, critical: false)); // TLS client EKU + clientReq.CertificateExtensions.Add (new X509SubjectKeyIdentifierExtension (clientReq.PublicKey, critical: false)); + + var serial = new byte [sizeof (long)]; + RandomNumberGenerator.Fill (serial); + + X509Certificate2 clientCert = clientReq.Create (rootCert, start, end, serial); + + var tmp = clientCert; + clientCert = clientCert.CopyWithPrivateKey (clientKey); + tmp.Dispose (); + + return clientCert; + } } }