Skip to content

Commit 8c4b353

Browse files
[Mono.Android] Add support for AndroidMessageHandler ClientCertificates (#8961)
Fixes: #7274 Fixes: dotnet/runtime#78933 Context: dotnet/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 (dotnet/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
1 parent c57ca14 commit 8c4b353

File tree

5 files changed

+300
-87
lines changed

5 files changed

+300
-87
lines changed

.gdn/.gdnsuppress

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,69 @@
5757
],
5858
"justification": "Dummy test.keystore file used for testing.",
5959
"createdDate": "2024-02-21 20:58:02Z"
60+
},
61+
"ad733d624486984da63461d2a23f266714f76e1788c271d90d45687579f51099": {
62+
"signature": "ad733d624486984da63461d2a23f266714f76e1788c271d90d45687579f51099",
63+
"alternativeSignatures": [],
64+
"memberOf": [
65+
"default"
66+
],
67+
"justification": "release.keystore file created during test run.",
68+
"createdDate": "2024-06-14 18:52:00Z"
69+
},
70+
"e10f89d02383ffef3bdbf9c048a9e0f3bdab956a8e6e49817780b0c837a5bd6d": {
71+
"signature": "e10f89d02383ffef3bdbf9c048a9e0f3bdab956a8e6e49817780b0c837a5bd6d",
72+
"alternativeSignatures": [],
73+
"memberOf": [
74+
"default"
75+
],
76+
"justification": "False positive in linker-dependencies.xml file.",
77+
"createdDate": "2024-06-14 18:52:00Z"
78+
},
79+
"e73b15633b7cb1e9e735ce0fe78a6ce3c95c11a8888181eb3b0cb50c191da19e": {
80+
"signature": "e73b15633b7cb1e9e735ce0fe78a6ce3c95c11a8888181eb3b0cb50c191da19e",
81+
"alternativeSignatures": [],
82+
"memberOf": [
83+
"default"
84+
],
85+
"justification": "False positive in linker-dependencies.xml file.",
86+
"createdDate": "2024-06-14 18:52:00Z"
87+
},
88+
"e622e6a9a73c1856d399e753105be517d62ec1e62d13a15ab9ecef43e15590a9": {
89+
"signature": "e622e6a9a73c1856d399e753105be517d62ec1e62d13a15ab9ecef43e15590a9",
90+
"alternativeSignatures": [],
91+
"memberOf": [
92+
"default"
93+
],
94+
"justification": "False positive in linker-dependencies.xml file.",
95+
"createdDate": "2024-06-14 18:52:00Z"
96+
},
97+
"df428be5ce5ef90685e15981cf49e2af10de6d87544f437aa1722f84516d6fef": {
98+
"signature": "df428be5ce5ef90685e15981cf49e2af10de6d87544f437aa1722f84516d6fef",
99+
"alternativeSignatures": [],
100+
"memberOf": [
101+
"default"
102+
],
103+
"justification": "False positive in linker-dependencies.xml file.",
104+
"createdDate": "2024-06-14 18:52:00Z"
105+
},
106+
"247325bc1f0ff6899ae09b13e006ac35c7cae4ffee0749f139fd5100f85a162f": {
107+
"signature": "247325bc1f0ff6899ae09b13e006ac35c7cae4ffee0749f139fd5100f85a162f",
108+
"alternativeSignatures": [],
109+
"memberOf": [
110+
"default"
111+
],
112+
"justification": "False positive in linker-dependencies.xml file.",
113+
"createdDate": "2024-06-14 18:52:00Z"
114+
},
115+
"6d53f09942503c3f7eeccf23af43ae976431e8dbf2ad3d32be8af5bd37068d4d": {
116+
"signature": "6d53f09942503c3f7eeccf23af43ae976431e8dbf2ad3d32be8af5bd37068d4d",
117+
"alternativeSignatures": [],
118+
"memberOf": [
119+
"default"
120+
],
121+
"justification": "False positive in linker-dependencies.xml file.",
122+
"createdDate": "2024-06-14 18:52:00Z"
60123
}
61124
}
62125
}

src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Net.Http.Headers;
1010
using System.Net.Security;
1111
using System.Security.Authentication;
12+
using System.Security.Cryptography;
1213
using System.Security.Cryptography.X509Certificates;
1314
using System.Text;
1415
using System.Threading;
@@ -21,6 +22,7 @@
2122
using Java.Security;
2223
using Java.Security.Cert;
2324
using Javax.Net.Ssl;
25+
using JavaX509Certificate = Java.Security.Cert.X509Certificate;
2426

2527
namespace Xamarin.Android.Net
2628
{
@@ -206,9 +208,22 @@ public CookieContainer CookieContainer
206208

207209
public bool AllowAutoRedirect { get; set; } = true;
208210

209-
public ClientCertificateOption ClientCertificateOptions { get; set; }
211+
public ClientCertificateOption ClientCertificateOptions { get; set; } = ClientCertificateOption.Manual;
210212

211-
public X509CertificateCollection? ClientCertificates { get; set; }
213+
private X509CertificateCollection? _clientCertificates;
214+
public X509CertificateCollection? ClientCertificates
215+
{
216+
get
217+
{
218+
if (ClientCertificateOptions != ClientCertificateOption.Manual) {
219+
throw new InvalidOperationException ($"Enable manual options first on {nameof (ClientCertificateOptions)}");
220+
}
221+
222+
return _clientCertificates ?? (_clientCertificates = new X509CertificateCollection ());
223+
}
224+
225+
set => _clientCertificates = value;
226+
}
212227

213228
public ICredentials? DefaultProxyCredentials { get; set; }
214229

@@ -1151,49 +1166,115 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
11511166
return;
11521167
}
11531168

1154-
var keyStore = InitializeKeyStore (out bool gotCerts);
1155-
keyStore = ConfigureKeyStore (keyStore);
1156-
var kmf = ConfigureKeyManagerFactory (keyStore);
1157-
var tmf = ConfigureTrustManagerFactory (keyStore);
1169+
KeyStore keyStore = GetConfiguredKeyStoreInstance ();
1170+
KeyManagerFactory? kmf = GetConfiguredKeyManagerFactory (keyStore);
1171+
TrustManagerFactory? tmf = ConfigureTrustManagerFactory (keyStore);
1172+
1173+
// If there is no customization there is no point in changing the behavior of the default SSL socket factory.
1174+
if (tmf is null && kmf is null && !HasTrustedCerts && !HasServerCertificateCustomValidationCallback && !HasClientCertificates) {
1175+
return;
1176+
}
11581177

1159-
if (tmf == null) {
1160-
// If there are no trusted certs, no custom trust manager factory or custom certificate validation callback
1161-
// there is no point in changing the behavior of the default SSL socket factory
1162-
if (!gotCerts && _serverCertificateCustomValidator is null)
1163-
return;
1178+
var context = SSLContext.GetInstance ("TLS") ?? throw new InvalidOperationException ("Failed to get the SSLContext instance for TLS");
1179+
var trustManagers = GetTrustManagers (tmf, keyStore, requestMessage);
1180+
context.Init (kmf?.GetKeyManagers (), trustManagers, null);
1181+
httpsConnection.SSLSocketFactory = context.SocketFactory;
1182+
}
11641183

1165-
tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm);
1166-
tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
1184+
[MemberNotNullWhen (true, nameof(TrustedCerts))]
1185+
bool HasTrustedCerts => TrustedCerts?.Count > 0;
1186+
1187+
[MemberNotNullWhen (true, nameof(_serverCertificateCustomValidator))]
1188+
bool HasServerCertificateCustomValidationCallback => _serverCertificateCustomValidator is not null;
1189+
1190+
[MemberNotNullWhen (true, nameof(_clientCertificates))]
1191+
bool HasClientCertificates => _clientCertificates?.Count > 0;
1192+
1193+
KeyManagerFactory? GetConfiguredKeyManagerFactory (KeyStore keyStore)
1194+
{
1195+
var kmf = ConfigureKeyManagerFactory (keyStore);
1196+
1197+
if (kmf is null && HasClientCertificates) {
1198+
kmf = KeyManagerFactory.GetInstance ("PKIX") ?? throw new InvalidOperationException ("Failed to get the KeyManagerFactory instance for PKIX");
1199+
kmf.Init (keyStore, null);
11671200
}
11681201

1169-
ITrustManager[]? trustManagers = tmf?.GetTrustManagers ();
1202+
return kmf;
1203+
}
1204+
1205+
KeyStore GetConfiguredKeyStoreInstance ()
1206+
{
1207+
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType) ?? throw new InvalidOperationException ("Failed to get the default KeyStore instance");
1208+
keyStore.Load (null, null);
11701209

1171-
var customValidator = _serverCertificateCustomValidator;
1172-
if (customValidator is not null) {
1173-
trustManagers = customValidator.ReplaceX509TrustManager (trustManagers, requestMessage);
1210+
if (HasTrustedCerts) {
1211+
for (int i = 0; i < TrustedCerts!.Count; i++) {
1212+
if (TrustedCerts [i] is Certificate cert) {
1213+
keyStore.SetCertificateEntry ($"ca{i}", cert);
1214+
}
1215+
}
11741216
}
11751217

1176-
var context = SSLContext.GetInstance ("TLS");
1177-
context?.Init (kmf?.GetKeyManagers (), trustManagers, null);
1178-
httpsConnection.SSLSocketFactory = context?.SocketFactory;
1218+
if (HasClientCertificates) {
1219+
if (ClientCertificateOptions != ClientCertificateOption.Manual) {
1220+
throw new InvalidOperationException ($"Use of {nameof(ClientCertificates)} requires that {nameof(ClientCertificateOptions)} be set to ClientCertificateOption.Manual");
1221+
}
11791222

1180-
KeyStore? InitializeKeyStore (out bool gotCerts)
1181-
{
1182-
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType);
1183-
keyStore?.Load (null, null);
1184-
gotCerts = TrustedCerts?.Count > 0;
1185-
1186-
if (gotCerts) {
1187-
for (int i = 0; i < TrustedCerts!.Count; i++) {
1188-
Certificate cert = TrustedCerts [i];
1189-
if (cert == null)
1190-
continue;
1191-
keyStore?.SetCertificateEntry ($"ca{i}", cert);
1223+
for (int i = 0; i < _clientCertificates.Count; i++) {
1224+
var keyEntry = GetKeyEntry (new X509Certificate2 (_clientCertificates [i]));
1225+
if (keyEntry is var (key, chain)) {
1226+
keyStore.SetKeyEntry ($"key{i}", key, null, chain);
11921227
}
11931228
}
1229+
}
1230+
1231+
return ConfigureKeyStore (keyStore) ?? throw new InvalidOperationException ($"{nameof(ConfigureKeyStore)} unexpectedly returned null");
1232+
}
1233+
1234+
ITrustManager[]? GetTrustManagers (TrustManagerFactory? tmf, KeyStore keyStore, HttpRequestMessage requestMessage)
1235+
{
1236+
if (tmf is null) {
1237+
tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm) ?? throw new InvalidOperationException ("Failed to get the default TrustManagerFactory instance");
1238+
tmf.Init (HasTrustedCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
1239+
}
1240+
1241+
ITrustManager[]? trustManagers = tmf.GetTrustManagers ();
1242+
1243+
if (HasServerCertificateCustomValidationCallback) {
1244+
trustManagers = _serverCertificateCustomValidator.ReplaceX509TrustManager (trustManagers, requestMessage);
1245+
}
11941246

1195-
return keyStore;
1247+
return trustManagers;
1248+
}
1249+
1250+
static (IPrivateKey, Certificate[])? GetKeyEntry (X509Certificate2 clientCertificate)
1251+
{
1252+
if (!clientCertificate.HasPrivateKey) {
1253+
return null;
11961254
}
1255+
1256+
AsymmetricAlgorithm? key = null;
1257+
string? algorithmName = null;
1258+
1259+
if (clientCertificate.GetRSAPrivateKey () is {} rsa) {
1260+
(key, algorithmName) = (rsa, "RSA");
1261+
} else if (clientCertificate.GetECDsaPrivateKey () is {} ec) {
1262+
(key, algorithmName) = (ec, "EC");
1263+
} else if (clientCertificate.GetDSAPrivateKey () is {} dsa) {
1264+
(key, algorithmName) = (dsa, "DSA");
1265+
} else {
1266+
return null;
1267+
}
1268+
1269+
var keyFactory = KeyFactory.GetInstance (algorithmName) ?? throw new InvalidOperationException ($"Failed to get the KeyFactory instance for algorithm {algorithmName}");
1270+
var privateKey = keyFactory.GeneratePrivate (new Java.Security.Spec.PKCS8EncodedKeySpec (key.ExportPkcs8PrivateKey ()));
1271+
var certificate = Java.Lang.Object.GetObject<Certificate> (clientCertificate.Handle, JniHandleOwnership.DoNotTransfer);
1272+
1273+
if (privateKey is null || certificate is null) {
1274+
return null;
1275+
}
1276+
1277+
return (privateKey, new Certificate [] { certificate });
11971278
}
11981279

11991280
void HandlePreAuthentication (HttpURLConnection httpConnection)

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
"Size": 1114
1212
},
1313
"lib/arm64-v8a/lib_Java.Interop.dll.so": {
14-
"Size": 66243
14+
"Size": 66250
1515
},
1616
"lib/arm64-v8a/lib_Mono.Android.dll.so": {
17-
"Size": 94712
17+
"Size": 94741
1818
},
1919
"lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": {
20-
"Size": 5320
20+
"Size": 5367
2121
},
2222
"lib/arm64-v8a/lib_System.Console.dll.so": {
2323
"Size": 7226
@@ -35,7 +35,7 @@
3535
"Size": 4475
3636
},
3737
"lib/arm64-v8a/lib_UnnamedProject.dll.so": {
38-
"Size": 2932
38+
"Size": 3059
3939
},
4040
"lib/arm64-v8a/libarc.bin.so": {
4141
"Size": 1546
@@ -44,7 +44,7 @@
4444
"Size": 87432
4545
},
4646
"lib/arm64-v8a/libmonodroid.so": {
47-
"Size": 492344
47+
"Size": 492280
4848
},
4949
"lib/arm64-v8a/libmonosgen-2.0.so": {
5050
"Size": 3163208
@@ -62,10 +62,10 @@
6262
"Size": 159544
6363
},
6464
"lib/arm64-v8a/libxamarin-app.so": {
65-
"Size": 17960
65+
"Size": 18008
6666
},
6767
"META-INF/BNDLTOOL.RSA": {
68-
"Size": 1221
68+
"Size": 1213
6969
},
7070
"META-INF/BNDLTOOL.SF": {
7171
"Size": 3266

0 commit comments

Comments
 (0)