-
Notifications
You must be signed in to change notification settings - Fork 533
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
[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,X509Certificate2
instances will be created, other logic happens, but nothing important happens to thekeyStore
instance, 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
?
@@ -248,6 +248,22 @@ public async Task AndroidMessageHandlerFollows308PermanentRedirect () | |||
Assert.AreEqual ("https://www.microsoft.com/", result.RequestMessage.RequestUri.ToString ()); | |||
} | |||
|
|||
[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.Automatic
first 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
AndroidMessageHandler
is 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
ClientCertificates
first and then we setClientCertificateOptions.Automatic
, the HttpClientHandler/SocketsHttpHandler is not in an invalid state because it chooses to ignore theClientCertificates
completely (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
Automatic
mode, 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.Automatic
because 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 xamarin/xamarin-macios#20434 (cc @dotMorten)