I am new to Xamarin Forms and also SSL Pinning. I am looking at an issue regarding SSL pinning in a preexisting app at work.

The idea is that, with server certificate (or public key) pinned, the app should close when a proxy (middle man) is connected.

I have checked many tutorials and have tried many solutions but the app seems to work normally confirming the public key even with proxy setup.

Attempt 1:

The code that was given to me is below and it uses ServerCertificateCustomValidationCallback and HttpClientHandler. I learnt that the ValidateRemoteCertificate method has the details of the server certificate, and the chain of certs associated to the remote cert (server). https://learn.microsoft.com/en-gb/dotnet/api/system.net.security.remotecertificatevalidationcallback?view=net-7.0

// Main.cs

const string MyPublicKey = "YOUR_PUBLIC_KEY_STRING"

public async Task<HttpContent> CheckCertificate()
        {
            var destUri = new Uri("YOUR_URL");
            var handler = new HttpClientHandler();
            handler.ServerCertificateCustomValidationCallback = ValidateRemoteCertificate;
            using (HttpClient client = new HttpClient(handler))
            {
                using (HttpResponseMessage result = await client.GetAsync(destUri))
                {
                    return result.Content;
                }
            }
        }

public bool ValidateRemoteCertificate(object sender,
                                            X509Certificate cert,
                                            X509Chain chain,
                                            SslPolicyErrors policyErrors)
        {
            byte[] foundCert = null;
            if (chain != null)
            {
                if (chain.ChainElements != null && chain.ChainElements.Count > 0)
                {
                    if (chain.ChainElements[1].Certificate != null)
                    {
                        foundCert = chain.ChainElements[1].Certificate.RawData;
                    }
                }
            }
            if (foundCert != null)
            {
                var key = cert.GetPublicKeyString();
                if (!MyPublicKey.Equals(key))
                {
                    // PublicKey mismatch, Exiting...
                    CloseApp();
                    return false;
                }
                else
                {
                    // "Public key matches
   
                }
                return true;
            }
            return false;
        }

This returns an exception with "Certificate Unknown". I found that the chain is not null, but ChainElements is empty.

I read many articles about certificate pinning and added the following code in Info.plist.

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSPinnedDomains</key>
    <dict>
        <key>YOUR_URL_HERE</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSPinnedCAIdentities</key>
            <array>
                <dict>
                    <key>SPKI-SHA256-BASE64</key>                    
                    <string>YOUR_KEY</string>
                </dict>
            </array>
        </dict>
    </dict>
</dict>

And I receive an exception that certificate is not trusted. I assume that this code in info.plist expects a trusted CA relating to this certificate to be trusted by the device owner. Which is not the target of this task.

Attempt 2:

I decided to just use public key pinning instead of certificate. So I changed ValidateRemoteCertificate code like this:

                var key = cert.GetPublicKeyString();
                if (!MyPublicKey.Equals(key))
                {
                    // PublicKey mismatch, Exiting...
                    CloseApp();
                    return false;
                }
                else
                {
                    // "Public key matches
   
                }
                return true;

And I removed info.plist changes. This allows the app to run smoothly. But the problem is, when a proxy is connected, it still works, meaning the cert still comes with the correct public key.

I am trying to figure out if I am missing something in this implementation or if the methods used here is even valid. I checked how it is handled in Android, I see the exact same implementation as my first code block, and with proxy, the app fails to launch (handshake failed; Trust anchor for certification path not found).

If this is an iOS issue, is there any other form of implementation that I need to use? Or is there a better way to pin cert (perhaps bundling the cert with the app)?

Update 1:

I have analysed the certificate chain validation a little further and observed these:

  1. As mentioned in my Attempt 1, the chain received in ValidateRemoteCertificate method is empty (with or without proxy). Someone suggested that to build the chain using the certificate if this happened, in order to validate the chain.
  2. I attempted to do "Build the chain" using this code. chain.Build(cert)) But I get an error "operation not permitted on this platform" suggesting that "chain.Build" doesn't work for Xamarin.iOS.

Update 2:

I did manage to get the chain built using the following code and everything seems to work normally. But the public key also matches when a proxy is connected and is able to crawl iOS traffic successfully. Hence, making the app function normally in case of man in the middle.

public bool ValidateRemoteCertificate(object sender,
                                        X509Certificate cert,
                                        X509Chain chain,
                                        SslPolicyErrors policyErrors)
        {
            if (policyErrors == SslPolicyErrors.None)
            {
                String actualPKString = cert.GetPublicKeyString();

                // validate public key
                if (!MyPublicKey.SequenceEqual(actualPKString))
                {
                    Console.WriteLine("Security: public key mismatched.");
                    CloseApp();
                    return false;
                }

                //validate chain
                if (chain != null)
                {
                        if (!(chain.ChainElements != null && chain.ChainElements.Count > 0))
                    {
                      try
                        {
                            chain.Build(new X509Certificate2(cert));
                            Console.WriteLine("Security: built chain");
                        } catch(Exception e)
                        {
                            Console.WriteLine("Security: issues building chain {0}", e);
                        }
                    }
                    if (chain.ChainElements == null || chain.ChainElements.Count == 0)
                    {
                        Console.WriteLine("Security: Chain elements still null");
                        CloseApp();
                        return false;
                    }
                }
                return true;
            }
            Console.WriteLine("Security: error occured");
            CloseApp();
            return false;
        }
0

There are 0 best solutions below