Skip to content
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

X509Chain does not populate ChainElements (Android) #84202

Open
borrrden opened this issue Apr 1, 2023 · 15 comments
Open

X509Chain does not populate ChainElements (Android) #84202

borrrden opened this issue Apr 1, 2023 · 15 comments

Comments

@borrrden
Copy link

borrrden commented Apr 1, 2023

Description

I am using SslStream with a remote certificate validation callback on many platforms. The only one that has an issue is .NET 6 Android (Xamarin Android is OK, FYI). I am already painfully aware of this issue which is currently kneecapping the custom certificate validation of our SDK, but I did find a way to work around that by forcing Android to trust my cert using network-security-config.

That being said, now that the callback is finally being called, the chain argument has 0 elements. On .NET 6 Console, it is properly populated with 3 elements. I also saw this happen on Xamarin / Mono back in the day but as a workaround I would take the received cert and use X509Chain.Build() to reconstruct the chain so that I could examine it. The problem is that on .NET 6 Android this also does not work. The resulting chain still has 0 elements.

Reproduction Steps

I'm unsure of how I could present this because it requires a server running https using a cert that is not trusted by stock Android, but assuming that such a server is running these are the steps to take:

  1. Start server (In my case I have the server set up to serve the entire chain, but not sure if this is required or not)

  2. Create a maui project (or .NET 6 Android at least)

  3. Add the cert that the server is using (not sure if the whole chain is needed or not, but I used a PEM file with the three concatenated certificates inside) to Resources/raw/cert.pem

  4. Add the following as Resources/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="@raw/cert"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>
  1. Annotate the MauiApplication with the following: [Application(NetworkSecurityConfig = "@xml/network_security_config")]
  2. Open a TcpClient to the server in question (_client.ConnectAsync(host, port))
  3. Wrap the TCP client in SslStream -> var sslStream = new SslStream(_client.GetStream(), false, ValidateServerCert);
  4. Implement ValidateServerCert with simply return true or anything you want
  5. Call sslStream.AuthenticateAsClientAsync(host, null, SslProtocols.Tls12, false)
  6. Inspect the third argument of ValidateServerCert

Expected behavior

The chain passed into the validation callback with the proper ChainElements

Actual behavior

ChainElements is length 0

Regression?

Not sure how to categorize this but at least with the mono workaround in place this works on the following platforms:

  • .NET 6 Windows Console
  • .NET 6 WinUI
  • .NET 6 iOS
  • .NET 6 Mac Catalyst
  • Xamarin iOS
  • Xamarin Android
  • .NET Framework 4.6.2

Known Workarounds

Unknown at this time

Configuration

.NET 6 on Android API 26 x64 emulator

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Apr 1, 2023
@ghost
Copy link

ghost commented Apr 1, 2023

Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

I am using SslStream with a remote certificate validation callback on many platforms. The only one that has an issue is .NET 6 Android (Xamarin Android is OK, FYI). I am already painfully aware of this issue which is currently kneecapping the custom certificate validation of our SDK, but I did find a way to work around that by forcing Android to trust my cert using network-security-config.

That being said, now that the callback is finally being called, the chain argument has 0 elements. On .NET 6 Console, it is properly populated with 3 elements. I also saw this happen on Xamarin / Mono back in the day but as a workaround I would take the received cert and use X509Chain.Build() to reconstruct the chain so that I could examine it. The problem is that on .NET 6 Android this also does not work. The resulting chain still has 0 elements.

Reproduction Steps

I'm unsure of how I could present this because it requires a server running https using a cert that is not trusted by stock Android, but assuming that such a server is running these are the steps to take:

  1. Start server (In my case I have the server set up to serve the entire chain, but not sure if this is required or not)

  2. Create a maui project (or .NET 6 Android at least)

  3. Add the cert that the server is using (not sure if the whole chain is needed or not, but I used a PEM file with the three concatenated certificates inside) to Resources/raw/cert.pem

  4. Add the following as Resources/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="@raw/cert"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>
  1. Annotate the MauiApplication with the following: [Application(NetworkSecurityConfig = "@xml/network_security_config")]
  2. Open a TcpClient to the server in question (_client.ConnectAsync(host, port))
  3. Wrap the TCP client in SslStream -> var sslStream = new SslStream(_client.GetStream(), false, ValidateServerCert);
  4. Implement ValidateServerCert with simply return true or anything you want
  5. Call sslStream.AuthenticateAsClientAsync(host, null, SslProtocols.Tls12, false)
  6. Inspect the third argument of ValidateServerCert

Expected behavior

The chain passed into the validation callback with the proper ChainElements

Actual behavior

ChainElements is length 0

Regression?

Not sure how to categorize this but at least with the mono workaround in place this works on the following platforms:

  • .NET 6 Windows Console
  • .NET 6 WinUI
  • .NET 6 iOS
  • .NET 6 Mac Catalyst
  • Xamarin iOS
  • Xamarin Android
  • .NET Framework 4.6.2

Known Workarounds

Unknown at this time

Configuration

.NET 6 on Android API 26 x64 emulator

Other information

No response

Author: borrrden
Assignees: -
Labels:

area-System.Net.Security

Milestone: -

@ghost
Copy link

ghost commented Apr 1, 2023

Tagging subscribers to 'arch-android': @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

I am using SslStream with a remote certificate validation callback on many platforms. The only one that has an issue is .NET 6 Android (Xamarin Android is OK, FYI). I am already painfully aware of this issue which is currently kneecapping the custom certificate validation of our SDK, but I did find a way to work around that by forcing Android to trust my cert using network-security-config.

That being said, now that the callback is finally being called, the chain argument has 0 elements. On .NET 6 Console, it is properly populated with 3 elements. I also saw this happen on Xamarin / Mono back in the day but as a workaround I would take the received cert and use X509Chain.Build() to reconstruct the chain so that I could examine it. The problem is that on .NET 6 Android this also does not work. The resulting chain still has 0 elements.

Reproduction Steps

I'm unsure of how I could present this because it requires a server running https using a cert that is not trusted by stock Android, but assuming that such a server is running these are the steps to take:

  1. Start server (In my case I have the server set up to serve the entire chain, but not sure if this is required or not)

  2. Create a maui project (or .NET 6 Android at least)

  3. Add the cert that the server is using (not sure if the whole chain is needed or not, but I used a PEM file with the three concatenated certificates inside) to Resources/raw/cert.pem

  4. Add the following as Resources/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="@raw/cert"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>
  1. Annotate the MauiApplication with the following: [Application(NetworkSecurityConfig = "@xml/network_security_config")]
  2. Open a TcpClient to the server in question (_client.ConnectAsync(host, port))
  3. Wrap the TCP client in SslStream -> var sslStream = new SslStream(_client.GetStream(), false, ValidateServerCert);
  4. Implement ValidateServerCert with simply return true or anything you want
  5. Call sslStream.AuthenticateAsClientAsync(host, null, SslProtocols.Tls12, false)
  6. Inspect the third argument of ValidateServerCert

Expected behavior

The chain passed into the validation callback with the proper ChainElements

Actual behavior

ChainElements is length 0

Regression?

Not sure how to categorize this but at least with the mono workaround in place this works on the following platforms:

  • .NET 6 Windows Console
  • .NET 6 WinUI
  • .NET 6 iOS
  • .NET 6 Mac Catalyst
  • Xamarin iOS
  • Xamarin Android
  • .NET Framework 4.6.2

Known Workarounds

Unknown at this time

Configuration

.NET 6 on Android API 26 x64 emulator

Other information

No response

Author: borrrden
Assignees: -
Labels:

area-System.Net.Security, os-android, untriaged

Milestone: -

@wfurt
Copy link
Member

wfurt commented Apr 1, 2023

cc: @simonrozsival

@borrrden
Copy link
Author

borrrden commented Apr 1, 2023

I should also add that the .NET runtime does not seem aware of the network security config and still says that the result is "PartialChain". For more insight to how I am actually trying to use this here is a link to the actual production validation function.

Given that it won't even make it here in the first place unless there are some Android workarounds, should I just not bother with the custom validation and just assume that the fact that it made it to the validation function at all means that the user wants to trust it?

@simonrozsival
Copy link
Member

@borrrden Hi, thanks for reporting this issue. We've made some changes to how certificate validation works on Android and you won't need the workaround with the network security config in .NET 8 anymore and the validation callback will be called directly. The problem with X509Chain behaving differently from the other platforms hasn't been resolved so far though.

@wfurt It doesn't seem it's currently possible to build an X509Chain when the certificate isn't trusted by the OS at the moment. I'm not very familiar with the X509Chain class and I'm not sure how exactly it should behave in this case so I don't have any suggestions how to improve it.

if (!_isValid)
{
// Android always validates name, time, signature, and trusted root.
// There is no way bypass that validation and build a path.
ChainElements = Array.Empty<X509ChainElement>();
Interop.AndroidCrypto.ValidationError[] errors = Interop.AndroidCrypto.X509ChainGetErrors(_chainContext);
var chainStatus = new X509ChainStatus[errors.Length];
for (int i = 0; i < errors.Length; i++)
{
Interop.AndroidCrypto.ValidationError error = errors[i];
chainStatus[i] = ValidationErrorToChainStatus(error);
Marshal.FreeHGlobal(error.Message);
}
ChainStatus = chainStatus;
return;
}

@vcsjones
Copy link
Member

vcsjones commented Apr 3, 2023

My understanding of Android is that ChainElements on X509Chain are not populated unless the chain is valid. Contrast this with other operating systems where the chain elements is usually populated, even for partial or incomplete chains.

This is reflected in our unit tests:

https://github.com/dotnet/runtime/blob/7a4efb2a80083db1c1ee5d6f1ae1d0abc440a35e/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.cs#L503-L504

@borrrden
Copy link
Author

borrrden commented Apr 3, 2023

In this case, due to the network security config, the chain should be valid but it still reports as partial chain.

EDIT For informational purposes, the underlying Java API will validate the certificate correctly:

#if NET6_0_OR_GREATER && __ANDROID__
var tmf = Javax.Net.Ssl.TrustManagerFactory.GetInstance(Javax.Net.Ssl.TrustManagerFactory.DefaultAlgorithm);
tmf.Init(default(Java.Security.KeyStore));
Java.Security.Cert.CertificateFactory cf = Java.Security.Cert.CertificateFactory.GetInstance("X.509");
foreach (var tm in tmf.GetTrustManagers()) {
    if(tm is Javax.Net.Ssl.IX509TrustManager x509tm) {
        var javaCert = cf.GenerateCertificate(new MemoryStream(cert2.GetRawCertData())) as Java.Security.Cert.X509Certificate;
        try { 
            x509tm.CheckServerTrusted(new[] { javaCert }, "RSA"); 
        } catch(Exception) {
          continue;
        }

        return true;
    }
}
#endif

FURTHER EDIT I'm trying to get a chain to function with cert pinning but it seems like the ExtraStore property of the chain policy is ignored? Furthermore, add to CustomTrustRoot makes Build throw a puzzling Exception

An empty custom trust store is not supported on this platform.

Which I've learned is because the certificate I want to pin is not self-signed (I am trying to pin an intermediate cert)

@borrrden
Copy link
Author

borrrden commented Apr 5, 2023

My understanding of Android is that ChainElements on X509Chain are not populated unless the chain is valid.

Is there an alternative way to construct the chain so that I can examine it then? I'm trying to figure out if a programmatically provided certificate matches any cert in the chain.

@wfurt
Copy link
Member

wfurt commented Apr 7, 2023

Can we get the certificates from the the security config @simonrozsival ?
Traditionally, there is difference between being able to construct chain vs the chain is trusted. Browsers may present questions if to proceed or not.
If this does not work on Android because of platform limitations we should at least document it.

Note that the chain is nullable https://learn.microsoft.com/en-us/dotnet/api/system.net.security.remotecertificatevalidationcallback?view=net-6.0

We may simply not pass it but I don't think that would improve the situation here.

@steveisok steveisok removed the untriaged New issue has not been triaged by the area owner label Apr 19, 2023
@steveisok steveisok added this to the 8.0.0 milestone Apr 19, 2023
@steveisok
Copy link
Member

Can we get the certificates from the the security config @simonrozsival ? Traditionally, there is difference between being able to construct chain vs the chain is trusted. Browsers may present questions if to proceed or not. If this does not work on Android because of platform limitations we should at least document it.

@simonrozsival can you follow up on this when you have a moment?

@simonrozsival
Copy link
Member

simonrozsival commented Aug 7, 2023

Can we get the certificates from the the security config?

@wfurt My understanding is that certificates included via network security config should behave as if they were installed on the system and the certificate chain should be valid and the chain elements should be populated correctly. I'm not sure why Android doesn't trust the certificate in this case (and we therefore don't populate the chain elements).

I believe we'll need to revisit how the X509Chain is built on Android. It is one of the features of the PAL that's not fully implemented (see #45741). I'll move the issue now to the Future milestone for now (same as #45741).

@simonrozsival simonrozsival modified the milestones: 8.0.0, Future Aug 7, 2023
@wfurt
Copy link
Member

wfurt commented Aug 7, 2023

yes, that would be preferable IMHO @simonrozsival. The question is if .NET has any access to those certificates.
Ot feel it may be best to experiment with X5089Chain directly. on Linux X509Stiores simple iterate through files in given directory. If there is API to retrieve certificates we can probably plug that in easily.

@simonrozsival
Copy link
Member

Certificates that devs bundle in the app so that they can use them in the network_security_config.xml should be accessible in code as any other file AFAIK, so devs should be able to build the chain themselves 🤔 is that what you meant, @wfut?

@wfurt
Copy link
Member

wfurt commented Aug 7, 2023

yes. What we would need is to get the content via API or we would need to parse it ourselves and extract the certificates.

@Sibl-SimonBlack
Copy link

I don't know if you found a workaround, and I am not sure if this applies to the exact same use case but here goes:
I was having a similar issue with the build() returning false on Android and not Windows. I discovered the same that the ChainElements was length 0.

The use case is the service I call with the HttpClient, returns 2 certificates, one LeafCertificate and an Intermediary certificate, where I needed to verify these with a root certificate.
After playing a bit around I found that the following worked for me when implementing the

bool ServerCertificateCustomValidationCallback(HttpRequestMessage httpRequestMessage, X509Certificate? cert, X509Chain? cetChain, SslPolicyErrors sslPolicyErrors)

On the HttpHandler.

I implemented the following:

if (cert == null) return false;
if (cetChain == null) return false;
var RootCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(CertResources.RootCert);
cetChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
cetChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
cetChain.ChainPolicy.CustomTrustStore.AddRange(cetChain.ChainPolicy.ExtraStore);
cetChain.ChainPolicy.CustomTrustStore.Add(RootCert);
var chainBuidlResult = cetChain.Build(cert);

where CertResources is a resource file containing the .PEM Root CA certificate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants