-
Notifications
You must be signed in to change notification settings - Fork 561
[Mono.Android] Add support for AndroidMessageHandler ClientCertificates #8961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Mono.Android] Add support for AndroidMessageHandler ClientCertificates #8961
Conversation
|
The expected apk size increased, this is expected I think: Is this increase acceptable? |
| get | ||
| { | ||
| if (ClientCertificateOptions != ClientCertificateOption.Manual) { | ||
| throw new InvalidOperationException ($"Enable manual options first on {nameof (ClientCertificateOptions)}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this is somewhat contrary to the Framework design guidelines; from Property Design:
✔️ DO allow properties to be set in any order even if this results in a temporary invalid state of the object.
It is common for two or more properties to be interrelated to a point where some values of one property might be invalid given the values of other properties on the same object. In such cases, exceptions resulting from the invalid state should be postponed until the interrelated properties are actually used together by the object.
For example, consider this snippet:
var handler = new AndroidMessageHandler(…) {
ClientCertificates = {
certificate,
},
ClientCertificateOptions = ClientCertificateOption.Automatic,
};Because C# collection initializer syntax is repeated .Add() calls, the above is equivalent to:
var handler = new AndroidMessageHandler(…);
handler.ClientCertificates.Add(certificate);
handler.ClientCertificateOptions = ClientCertificateOption.Automatic,which is an invalid state: having any ClientCertificates should require ClientCertificateOption.Manual, as per the current check, but (as far as I can tell) nothing actually validates this ordering.
(And nothing should validate "ordering"!)
I think it would thus be preferable to delay the "consistency check" until…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks for the detailed feedback, @jonpryor!
You have a great point here, this design choice isn't great. I'm following HttpClientHandler which also throws. I would personally keep this in line with the runtime, but I don't have a strong preference and I can definitely modify the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the one hand, following what HttpClientHandler is doing makes sense.
On the other hand, I'm not convinced that actually fixes the implicit ordering issues.
A quick glance through the HttpClientHandler unit tests for ClientCertificates suggests that this ordering issue is not tested at all. Fortunately, I don't see any issues on dotnet/runtime about this either…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The required order makes sense to me. The error you get is helpful too and expected
| return; | ||
| } | ||
|
|
||
| for (int i = 0; i < _clientCertificates.Count; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…here?
if (ClientCertificateOptions != ClientCertificateOption.Manual) {
throw new InvalidOperationException ($"Use of {nameof(ClientCertificates)} requires that {nameof(ClientCertificateOptions)} be set to ClientCertificateOption.Manual");
}| { | ||
| gotCerts = false; | ||
|
|
||
| if (_clientCertificates is null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…and this should probably be:
if (_clientCertificates is null || _clientCertificates.Count == 0) {
return;
}| } | ||
| } | ||
|
|
||
| void LoadClientCertificates (KeyStore? keyStore, out bool gotCerts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why out bool gotCerts instead of bool return type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obvious response: because GetConfiguredKeyStoreInstance() has an out parameter.
Obvious counter-response: so?
KeyStore GetConfiguredKeyStoreInstance (out bool gotTrustedCerts, out bool gotClientCerts)
{
…
gotClientCerts = LoadClientCertificates (keyStore);
}I'm not sure what the out parameter is actually buying us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mostly just shuffled existing code around here, so I kept the out bool we already had. I'm not a big fan of out arguments myself so I'll look into ways of getting rid of it.
| return kmf; | ||
| } | ||
|
|
||
| KeyStore? GetConfiguredKeyStoreInstance (out bool gotTrustedCerts, out bool gotClientCerts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I like the implied semantics of the "wtf" path.
Assume that ClientCertificateOptions=ClientCertificateOptions.Manual and ClientCertificates is set to a non-empty collection. This presumably means "the certificates should be used".
Execution eventually hits this method, which calls KeyStore.GetInstance(KeyStore.DefaultType), which could return null. If that happens, then:
keyStore?.Load()is ignoredLoadClientCertificates(keyStore, out gotClientCerts)becomes an effective no-op, ignoring the contents ofClientCertificates(okay, not quite,X509Certificate2instances will be created, other logic happens, but nothing important happens to thekeyStoreinstance, as it'snull)- …and then…???
Should this happen? Of course not.
But if it does happen -- something goes FUBAR, reasons unknown -- then the downstream effect will be "as if" ClientCertificates weren't set at all, which in turn will likely result in a bunch of head scratching and cursing, or a security vulnerability. (I have an overactive imagination.)
I would prefer that we instead require that things work:
var keyStore = KeyStore.GetInstance (KeyStore.DefaultType) ??
throw new InvalidOperationException($"KeyStore.GetInstance(\"{KeyStore.DefaultType}\") returned null!");and then replace most of the keyStore?.Whatever with keyStore.Whatever, as we are requiring that KeyStore be non-null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thans for pointing this out. The GetInstance(DefaultType) call never returns null AFAIK (at least on any existing Android version), so ensuring it is null will clean up the code and make the intent much clearer. We might also avoid weird silent errors at some later point if the behavior of the method changes.
| "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { | ||
| "Size": 4020 | ||
| }, | ||
| "lib/arm64-v8a/lib_System.Runtime.Numerics.dll.so": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change seems odd; what's pulling in System.Runtime.Numerics.dll?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This the Asn1 DLLs are both dependencies of System.Security.Cryptography (https://github.com/dotnet/runtime/blob/e8b89a3fde2911c6cbac0488bf82c74329a7224a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj#L1785-L1798).
| "Size": 11963 | ||
| "Size": 11962 | ||
| }, | ||
| "lib/arm64-v8a/lib_System.Formats.Asn1.dll.so": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change seems odd; what's pulling in System.Formats.Asn1.dll?
| } | ||
|
|
||
| [Test] | ||
| public async Task AndroidMessageHandlerSendsClientCertificate () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a'la my previous comment, we should update this test to include setting ClientCertificateOptions:
[Test]
public async Task AndroidMessageHandlerSendsClientCertificate([Values(true, false)] bool manualOptions)
{
string testClientCertificate = …;
using var certificate = new X509Certificate(Convert.FromBase64String (testClientCertificate), "pass");
using var handler = new AndroidMessageHandler {
ClientCertificates = { certificate },
ClientCertificateOptions = manualOptions ? ClientCertificateOptions.Manual : ClientCertificateOptions.Automatic,
};
using var client = new HttpClient (handler);
try {
var response = await client.GetAsync ("https://corefx-net-tls.azurewebsites.net/EchoClientCertificate.ashx");
var content = await response.EnsureSuccessStatusCode ().Content.ReadAsStringAsync ();
if (!manualOptions) AssertNotReached();
}
catch (Exception e) {
if (manualOptions) AssertNotReached();
// assert that e is whatever it should be (InvalidOperationException?)
}
}|
@jonpryor could you please take another look? I hope we could get this ready in time for the next preview release. |
| } | ||
|
|
||
| [Test] | ||
| public async Task AndroidMessageHandlerRejectsClientCertificateOptionsAutomatic () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test concerns me, as it's (kinda, sorta) the "inverse" of my previous comment. This test asserts that when AndroidMessageHandler.ClientCertificateOptions is ClientCertificateOption.Automatic, accessing AndroidMessageHandler.ClientCertificates will throw.
Fair enough.
But what should happen when ClientCertificateOptions is set last?
var h = new AndroidMessageHandler {
ClientCertificates = { BuildClientCertificate () },
ClientCertificateOptions = ClientCertificateOption.Automatic,
};
var c = new HttpClient(h);
var r = await c.GetAsync (…);Because you've updated the default value of AndroidMessageHandler.ClientCertificateOptions to ClientCertificateOption.Manual, I believe that h will be constructed without error, because when h.ClientCertificates is accessed, ClientCertificateOptions will still be .Manual, so h.ClientCertificates shouldn't throw.
The result is that after construction, h is in an invalid state.
What will happen when we try to use h? What should happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What will happen when you try to use
h? What should happen?
-
When we set
ClientCertificateOptions.Automaticfirst and then we setClientCertificates, we know we're in an invalid state as soon as we setClientCertificates(they would be ignored). HttpClientHandler throws which breaks the design guideline that we should allow being temporarily in an invalid state.- I think we should follow the design of HttpClientHandler to keep the behavior consistent accross platforms since
AndroidMessageHandleris commonly used throughHttpClientHandler.
- I think we should follow the design of HttpClientHandler to keep the behavior consistent accross platforms since
-
In the other scenario, when we set
ClientCertificatesfirst and then we setClientCertificateOptions.Automatic, the HttpClientHandler/SocketsHttpHandler is not in an invalid state because it chooses to ignore theClientCertificatescompletely (certificates from the current user's X509 store are used instead).- We could maybe support the Automatic mode as well (using KeyChain.choosePrivateKeyAlias to show the user a UI to choose a certificate?) but definitely not in this PR. Until we support the
Automaticmode, I think it makes sense to throw once we are preparing the request here: https://github.com/xamarin/xamarin-android/pull/8961/files?diff=split&w=0#diff-5db393c58158459e46590be1423607c52488da9182e9e5e929d7b79e7179d13dR1219-R1221 Maybe this deserves its own unit test. - Alternatively, we could choose to throw as early as when the developer sets
ClientCertificateOptions.Automaticbecause we don't support it at all. If we want to go in this direction, we should coordinate this with the iOS/Mac implementation (cc @dotMorten)
- We could maybe support the Automatic mode as well (using KeyChain.choosePrivateKeyAlias to show the user a UI to choose a certificate?) but definitely not in this PR. Until we support the
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense: ClientCertificateOptions = ClientCertificateOption.Automatic means "ignore ClientCertificates", so the resulting state isn't "invalid", it's just "weird" (contains data that cannot be used until ClientCertificateOptions is changed again).
@simonrozsival: what's the process for getting documentation updated? The documentation for HttpClientHandler.ClientCertificates does not mention that it could throw an exception depending on the value of HttpClientHandler.ClientCertificateOptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the process for getting documentation updated?
AFAIK the source of the documentation is here: https://github.com/dotnet/dotnet-api-docs/blob/2cd326e62adfa53ec1df761f571d1f2b94c263ab/xml/System.Net.Http/HttpClientHandler.xml#L256-L353 and the process is described here: https://learn.microsoft.com/en-gb/contribute/content/dotnet/dotnet-contribute
|
@simonrozsival: please rebase this branch and resolve the merge conflict. |
5a8f2ef to
80062ee
Compare
80062ee to
7dabc57
Compare
…-message-handler-support-client-certificate
|
@jonpryor please take another look |
…-message-handler-support-client-certificate
…er-support-client-certificate
* main: (23 commits) Localized file check-in by OneLocBuild Task (#9129) [ci] Disable CodeQL on CI/PR pipelines (#9128) Refine 16k page alignment support (#9075) [build] fix `ConfigureLocalWorkload` target (#9124) Bump to NDK r27 (#9020) [ci] Use drop service for SDK insertion artifacts (#9116) Fix up all mapping paths (#9121) [ci] Fix maestro publishing for stable packages (#9118) Bump to dotnet/sdk@2f14fea98b 9.0.100-preview.7.24367.21 (#9108) Missing androidx.window.[extensions|sidecar] warnings (#9085) [ci] Use sign-artifacts template for macOS signing (#9091) [ci] Use DotNetCoreCLI to sign macOS files (#9102) [ci] Disable CodeQL on macOS, Linux, non-main jobs (#9111) [tests] re-enable `JavaAbstractMethodTest` (#9097) [Microsoft.Android.Sdk.ILLink] preserve types with `IJniNameProviderAttribute` (#9099) [Mono.Android] Data sharing and Close() overrides (#9103) [AndroidManifest] Add `Android.App.PropertyAttribute` (#9016) [Mono.Android] Add support for AndroidMessageHandler ClientCertificates (#8961) [Mono.Android] Bind and enumify API-35 (#9043) Bump to dotnet/java-interop@7a058c0e (#9066) ...
This PR updates the SSLSetup method of AndroidMessageHandler to load ClientCertificates into a KeyStore before making the request.
Closes #7274
Closes dotnet/runtime#78933
Related to and inspired by dotnet/macios#20434 (cc @dotMorten)