0

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;
        }
curiosolio
  • 11
  • 2
  • In your Attempt 1, when doing certificate pinning you need to download the server's certificate and put it in your application bundle, did you download the certificate? At runtime, the application compares the server's certificate with your embedded certificate. You can check the answer in the thread "[Certificate pinning xamarin forms](https://stackoverflow.com/questions/50511532/certificate-pinning-xamarin-forms)", hope it will help you。 – Zack Feb 01 '23 at 02:27
  • Hi @DongzhiWang-MSFT , I am attempting to use public key pinning (not certificate pinning). Am I still supposed to bundle the certificate with the app? If so, how would I be able to do it? – curiosolio Feb 03 '23 at 02:32
  • You need to extract the public key from the certificate and add your public key to your info.plist file. You can refer to the answer: ["A Certificate is not (just) a public key"](https://stackoverflow.com/questions/40404963/how-do-i-get-public-key-hash-for-ssl-pinning#:~:text=A%20Certificate%20is%20not%20(just)%20a%20public%20key),For more information about fixed public key, you can refer to Apple's doc:[Identity Pinning](https://developer.apple.com/news/?id=g9ejcf8y) – Zack Feb 03 '23 at 07:31
  • Thanks, @DongzhiWang-MSFT. As part of attempt 1 above, I did try to pin the certificate using NSAppTransportSecurity and NSPinnedCAIdentities. But I get an exception saying "Certificate not trusted". I understand that this requires the certificate to be installed and trusted on the client device. As per requirement, the app doesn't need user to install certificate, but just checking the public key to prevent MITM attacks. – curiosolio Feb 06 '23 at 04:18
  • Did you add the public key to the info.plist file, you said: "And I removed info.plist changes." Did you add it when you tried to pinning the public key later. – Zack Feb 06 '23 at 08:00

0 Answers0