Skip to content

Commit

Permalink
[Mono.Android] Add support for AndroidMessageHandler ClientCertificat…
Browse files Browse the repository at this point in the history
…es (#8961)

Fixes: #7274
Fixes: dotnet/runtime#78933

Context: xamarin/xamarin-macios#20434

Update `AndroidMessageHandler.SetupSSL()` to use the
`AndroidMessageHandler.ClientCertificates` collection when
`AndroidMessageHandler.ClientCertificateOptions` is
`ClientCertificateOption.Manual`, which is now the default.

This allows the following code to work as expected:

	var certificate = …
	var handler     = new AndroidMessageHandler {
	    ClientCertificates = {
	        certificate,
	    },
	};

	var client      = new HttpClient(handler);
	var result      = await client.GetAsync(…);

!!API BREAK!! the `AndroidMessageHandler.ClientCertificates` property
is updated to now throw an `InvalidOperationException` when
`AndroidMessageHandler.ClientCertificateOptions` is
`ClientCertificateOption.Automatic`, meaning the following code will
now throw, whereas it does *not* throw in .NET 8:

	var certificate = …
	var handler     = new AndroidMessageHandler();
	handler.ClientCertificateOptions = ClientCertificateOption.Automatic;

	// this now throws InvalidOperationException, new to .NET 9
	handler.ClientCertificates.Add(certificate);

This updated behavior is consistent with iOS
(xamarin/xamarin-macios#20434) and [dotnet/runtime][0]:

	handler.ClientCertificateOptions = ClientCertificateOption.Automatic;
	Assert.Throws<InvalidOperationException>(() => handler.ClientCertificates);

[0]: https://github.com/dotnet/runtime/blob/2ea6ae57874c452923af059cbcb57d109564353c/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ClientCertificates.cs#L60-L68
  • Loading branch information
simonrozsival authored Jul 10, 2024
1 parent c57ca14 commit 8c4b353
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 87 deletions.
63 changes: 63 additions & 0 deletions .gdn/.gdnsuppress
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
147 changes: 114 additions & 33 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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; }

Expand Down Expand Up @@ -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<Certificate> (clientCertificate.Handle, JniHandleOwnership.DoNotTransfer);

if (privateKey is null || certificate is null) {
return null;
}

return (privateKey, new Certificate [] { certificate });
}

void HandlePreAuthentication (HttpURLConnection httpConnection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -44,7 +44,7 @@
"Size": 87432
},
"lib/arm64-v8a/libmonodroid.so": {
"Size": 492344
"Size": 492280
},
"lib/arm64-v8a/libmonosgen-2.0.so": {
"Size": 3163208
Expand All @@ -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
Expand Down
Loading

0 comments on commit 8c4b353

Please sign in to comment.