Skip to content

Commit 48540d6

Browse files
[Mono.Android] custom validation callback for server certificates in HTTP handlers (#6665)
Context: dotnet/runtime#62966 The `AndroidClientHandler` and `AndroidMessageHandler` classes both have the `ServerCertificateCustomValidationCallback` property, which should be useful e.g. to allow running the Android app against a server with a self-signed SSL certificate during development, but the callback is never used. Unfortunatelly since .NET 6 the `System.Net.Http.SocketsHttpHandler` for Android doesn't support the use case anymore. That means that [the recommended way of connecting to local web server][0] won't work in MAUI. This PR introduces an implementation of `IX509TrustManger` which wraps the default Java X509 trust manager and calls the user's callback on top of the default validation. It turns out that `X509Chain` `Build` function doesn't work on Android, so I'm not calling it and I'm passing the chain to the callback directly. Additionally, we need a default proguard rule due to: https://github.com/xamarin/xamarin-android/blob/46002b49d8c0b7b1a17532a8e104b4d31afee7a6/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/GenerateProguardConfiguration.cs#L50-L57 -keep class xamarin.android.net.X509TrustManagerWithValidationCallback { *; <init>(...); } `Mono.Android.dll` is skipped during the `GenerateProguardConfiguration` linker step. It might be worth addressing this in a future PR. [0]: https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/connect-to-local-web-services
1 parent 46002b4 commit 48540d6

11 files changed

+843
-633
lines changed

src/Mono.Android/Mono.Android.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@
367367
<Compile Include="Xamarin.Android.Net\AuthModuleBasic.cs" />
368368
<Compile Include="Xamarin.Android.Net\AuthModuleDigest.cs" />
369369
<Compile Include="Xamarin.Android.Net\IAndroidAuthenticationModule.cs" />
370+
<Compile Include="Xamarin.Android.Net\X509TrustManagerWithValidationCallback.cs" />
370371
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
371372
</ItemGroup>
372373

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

+43-22
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,15 @@ public CookieContainer CookieContainer
157157

158158
public bool CheckCertificateRevocationList { get; set; } = false;
159159

160-
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateCustomValidationCallback { get; set; }
160+
X509TrustManagerWithValidationCallback.Helper? _callbackTrustManagerHelper = null;
161+
162+
public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback
163+
{
164+
get => _callbackTrustManagerHelper?.Callback;
165+
set {
166+
_callbackTrustManagerHelper = value != null ? new X509TrustManagerWithValidationCallback.Helper (value) : null;
167+
}
168+
}
161169

162170
// See: https://developer.android.com/reference/javax/net/ssl/SSLSocket#protocols
163171
public SslProtocols SslProtocols { get; set; } =
@@ -199,7 +207,7 @@ public int MaxAutomaticRedirections
199207
/// If the website requires authentication, this property will contain data about each scheme supported
200208
/// by the server after the response. Note that unauthorized request will return a valid response - you
201209
/// need to check the status code and and (re)configure AndroidMessageHandler instance accordingly by providing
202-
/// both the credentials and the authentication scheme by setting the <see cref="PreAuthenticationData"/>
210+
/// both the credentials and the authentication scheme by setting the <see cref="PreAuthenticationData"/>
203211
/// property. If AndroidMessageHandler is not able to detect the kind of authentication scheme it will store an
204212
/// instance of <see cref="AuthenticationData"/> with its <see cref="AuthenticationData.Scheme"/> property
205213
/// set to <c>AuthenticationScheme.Unsupported</c> and the application will be responsible for providing an
@@ -939,7 +947,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
939947

940948
// SSL context must be set up as soon as possible, before adding any content or
941949
// headers. Otherwise Java won't use the socket factory
942-
SetupSSL (httpConnection as HttpsURLConnection);
950+
SetupSSL (httpConnection as HttpsURLConnection, request);
943951
if (request.Content != null)
944952
AddHeaders (httpConnection, request.Content.Headers);
945953
AddHeaders (httpConnection, request.Headers);
@@ -997,7 +1005,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
9971005
internal SSLSocketFactory? ConfigureCustomSSLSocketFactoryInternal (HttpsURLConnection connection)
9981006
=> ConfigureCustomSSLSocketFactoryInternal (connection);
9991007

1000-
void SetupSSL (HttpsURLConnection? httpsConnection)
1008+
void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMessage)
10011009
{
10021010
if (httpsConnection == null)
10031011
return;
@@ -1017,35 +1025,48 @@ void SetupSSL (HttpsURLConnection? httpsConnection)
10171025
}
10181026
#endif
10191027

1020-
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType);
1021-
keyStore?.Load (null, null);
1022-
bool gotCerts = TrustedCerts?.Count > 0;
1023-
if (gotCerts) {
1024-
for (int i = 0; i < TrustedCerts!.Count; i++) {
1025-
Certificate cert = TrustedCerts [i];
1026-
if (cert == null)
1027-
continue;
1028-
keyStore?.SetCertificateEntry ($"ca{i}", cert);
1029-
}
1030-
}
1028+
var keyStore = InitializeKeyStore (out bool gotCerts);
10311029
keyStore = ConfigureKeyStore (keyStore);
10321030
var kmf = ConfigureKeyManagerFactory (keyStore);
10331031
var tmf = ConfigureTrustManagerFactory (keyStore);
10341032

10351033
if (tmf == null) {
1036-
// If there are no certs and no trust manager factory, we can't use a custom manager
1037-
// because it will cause all the HTTPS requests to fail because of unverified trust
1038-
// chain
1039-
if (!gotCerts)
1034+
// If there are no trusted certs, no custom trust manager factory or custom certificate validation callback
1035+
// there is no point in changing the behavior of the default SSL socket factory
1036+
if (!gotCerts && _callbackTrustManagerHelper == null)
10401037
return;
10411038

10421039
tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm);
1043-
tmf?.Init (keyStore);
1040+
tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
1041+
}
1042+
1043+
ITrustManager[]? trustManagers = tmf?.GetTrustManagers ();
1044+
1045+
if (_callbackTrustManagerHelper != null) {
1046+
trustManagers = _callbackTrustManagerHelper.Inject (trustManagers, requestMessage);
10441047
}
10451048

10461049
var context = SSLContext.GetInstance ("TLS");
1047-
context?.Init (kmf?.GetKeyManagers (), tmf?.GetTrustManagers (), null);
1050+
context?.Init (kmf?.GetKeyManagers (), trustManagers, null);
10481051
httpsConnection.SSLSocketFactory = context?.SocketFactory;
1052+
1053+
KeyStore? InitializeKeyStore (out bool gotCerts)
1054+
{
1055+
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType);
1056+
keyStore?.Load (null, null);
1057+
gotCerts = TrustedCerts?.Count > 0;
1058+
1059+
if (gotCerts) {
1060+
for (int i = 0; i < TrustedCerts!.Count; i++) {
1061+
Certificate cert = TrustedCerts [i];
1062+
if (cert == null)
1063+
continue;
1064+
keyStore?.SetCertificateEntry ($"ca{i}", cert);
1065+
}
1066+
}
1067+
1068+
return keyStore;
1069+
}
10491070
}
10501071

10511072
void HandlePreAuthentication (HttpURLConnection httpConnection)
@@ -1116,4 +1137,4 @@ void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage requ
11161137
}
11171138
}
11181139

1119-
}
1140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Net.Security;
6+
using System.Security.Cryptography.X509Certificates;
7+
8+
using Javax.Net.Ssl;
9+
10+
using JavaCertificateException = Java.Security.Cert.CertificateException;
11+
using JavaX509Certificate = Java.Security.Cert.X509Certificate;
12+
13+
namespace Xamarin.Android.Net
14+
{
15+
internal sealed class X509TrustManagerWithValidationCallback : Java.Lang.Object, IX509TrustManager
16+
{
17+
internal sealed class Helper
18+
{
19+
public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> Callback { get; }
20+
21+
public Helper (Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> callback)
22+
{
23+
Callback = callback;
24+
}
25+
26+
public ITrustManager[] Inject (
27+
ITrustManager[]? trustManagers,
28+
HttpRequestMessage requestMessage)
29+
{
30+
IX509TrustManager? x509TrustManager = trustManagers?.OfType<IX509TrustManager> ().FirstOrDefault ();
31+
IEnumerable<ITrustManager> otherTrustManagers = trustManagers?.Where (manager => manager != x509TrustManager) ?? Enumerable.Empty<ITrustManager> ();
32+
var trustManagerWithCallback = new X509TrustManagerWithValidationCallback (x509TrustManager, requestMessage, Callback);
33+
return otherTrustManagers.Prepend (trustManagerWithCallback).ToArray ();
34+
}
35+
}
36+
37+
private readonly IX509TrustManager? _internalTrustManager;
38+
private readonly HttpRequestMessage _request;
39+
private readonly Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> _serverCertificateCustomValidationCallback;
40+
41+
private X509TrustManagerWithValidationCallback (
42+
IX509TrustManager? internalTrustManager,
43+
HttpRequestMessage request,
44+
Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
45+
{
46+
_request = request;
47+
_internalTrustManager = internalTrustManager;
48+
_serverCertificateCustomValidationCallback = serverCertificateCustomValidationCallback;
49+
}
50+
51+
public void CheckServerTrusted (JavaX509Certificate[] javaChain, string authType)
52+
{
53+
var sslPolicyErrors = SslPolicyErrors.None;
54+
var certificates = ConvertCertificates (javaChain);
55+
56+
try {
57+
_internalTrustManager?.CheckServerTrusted (javaChain, authType);
58+
} catch (JavaCertificateException) {
59+
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
60+
}
61+
62+
X509Certificate2? certificate = certificates.FirstOrDefault ();
63+
using X509Chain chain = CreateChain (certificates);
64+
65+
if (certificate == null) {
66+
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable;
67+
}
68+
69+
if (!_serverCertificateCustomValidationCallback (_request, certificate, chain, sslPolicyErrors)) {
70+
throw new JavaCertificateException ("The remote certificate was rejected by the provided RemoteCertificateValidationCallback.");
71+
}
72+
}
73+
74+
public void CheckClientTrusted (JavaX509Certificate[] chain, string authType)
75+
=> _internalTrustManager?.CheckClientTrusted (chain, authType);
76+
77+
public JavaX509Certificate[] GetAcceptedIssuers ()
78+
=> _internalTrustManager?.GetAcceptedIssuers () ?? Array.Empty<JavaX509Certificate> ();
79+
80+
private static X509Chain CreateChain (X509Certificate2[] certificates)
81+
{
82+
// the chain initialization is based on dotnet/runtime implementation in System.Net.Security.SecureChannel
83+
var chain = new X509Chain ();
84+
85+
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
86+
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
87+
88+
chain.ChainPolicy.ExtraStore.AddRange (certificates);
89+
90+
return chain;
91+
}
92+
93+
private static X509Certificate2[] ConvertCertificates (JavaX509Certificate[] certificates)
94+
=> certificates.Select (cert => new X509Certificate2 (cert.GetEncoded ()!)).ToArray ();
95+
}
96+
}

src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
-keep class opentk_1_0.platform.android.AndroidGameView { *; <init>(...); }
1616
-keep class opentk_1_0.GameViewBase { *; <init>(...); }
1717
-keep class com.xamarin.java_interop.ManagedPeer { *; <init>(...); }
18+
-keep class xamarin.android.net.X509TrustManagerWithValidationCallback { *; <init>(...); }
1819

1920
-keep class android.runtime.** { <init>(...); }
2021
-keep class assembly_mono_android.android.runtime.** { <init>(...); }

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

+10-10
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,37 @@
55
"Size": 3032
66
},
77
"assemblies/Java.Interop.dll": {
8-
"Size": 55106
8+
"Size": 55099
99
},
1010
"assemblies/Mono.Android.dll": {
11-
"Size": 88461
11+
"Size": 88852
1212
},
1313
"assemblies/rc.bin": {
1414
"Size": 1083
1515
},
1616
"assemblies/System.Linq.dll": {
17-
"Size": 10120
17+
"Size": 10112
1818
},
1919
"assemblies/System.Private.CoreLib.dll": {
20-
"Size": 519314
20+
"Size": 519255
2121
},
2222
"assemblies/System.Runtime.CompilerServices.Unsafe.dll": {
2323
"Size": 1165
2424
},
2525
"assemblies/System.Runtime.dll": {
26-
"Size": 2374
26+
"Size": 2369
2727
},
2828
"assemblies/UnnamedProject.dll": {
29-
"Size": 3546
29+
"Size": 3543
3030
},
3131
"classes.dex": {
32-
"Size": 345328
32+
"Size": 344840
3333
},
3434
"lib/arm64-v8a/libmonodroid.so": {
35-
"Size": 382304
35+
"Size": 382480
3636
},
3737
"lib/arm64-v8a/libmonosgen-2.0.so": {
38-
"Size": 3192432
38+
"Size": 3176080
3939
},
4040
"lib/arm64-v8a/libSystem.IO.Compression.Native.so": {
4141
"Size": 776216
@@ -47,7 +47,7 @@
4747
"Size": 150032
4848
},
4949
"lib/arm64-v8a/libxamarin-app.so": {
50-
"Size": 9424
50+
"Size": 9328
5151
},
5252
"META-INF/BNDLTOOL.RSA": {
5353
"Size": 1213

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

+12-12
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,43 @@
55
"Size": 2604
66
},
77
"assemblies/Java.Interop.dll": {
8-
"Size": 67956
8+
"Size": 67947
99
},
1010
"assemblies/Mono.Android.dll": {
11-
"Size": 256630
11+
"Size": 257171
1212
},
1313
"assemblies/mscorlib.dll": {
14-
"Size": 769015
14+
"Size": 769010
1515
},
1616
"assemblies/System.Core.dll": {
17-
"Size": 28199
17+
"Size": 28190
1818
},
1919
"assemblies/System.dll": {
20-
"Size": 9180
20+
"Size": 9178
2121
},
2222
"assemblies/UnnamedProject.dll": {
23-
"Size": 2881
23+
"Size": 2871
2424
},
2525
"classes.dex": {
26-
"Size": 347796
26+
"Size": 349528
2727
},
2828
"lib/arm64-v8a/libmono-btls-shared.so": {
2929
"Size": 1613872
3030
},
31+
"lib/arm64-v8a/libmonodroid.so": {
32+
"Size": 296448
33+
},
3134
"lib/arm64-v8a/libmono-native.so": {
3235
"Size": 750976
3336
},
34-
"lib/arm64-v8a/libmonodroid.so": {
35-
"Size": 296192
36-
},
3737
"lib/arm64-v8a/libmonosgen-2.0.so": {
3838
"Size": 4030448
3939
},
4040
"lib/arm64-v8a/libxa-internal-api.so": {
4141
"Size": 65512
4242
},
4343
"lib/arm64-v8a/libxamarin-app.so": {
44-
"Size": 19960
44+
"Size": 19864
4545
},
4646
"META-INF/ANDROIDD.RSA": {
4747
"Size": 1213
@@ -74,5 +74,5 @@
7474
"Size": 1724
7575
}
7676
},
77-
"PackageSize": 4011732
77+
"PackageSize": 4015828
7878
}

0 commit comments

Comments
 (0)