Working with Self Signed Certificates (Certificate Pinning) in iOS Application with Xamarin.Forms

Working with Self Signed Certificates (Certificate Pinning) in iOS Application with Xamarin.Forms

Next up in the sequence of posts talking about app security is looking at working with self-signed certificates in an iOS application. Previous posts in this sequence are:

Accessing ASP.NET Core API hosted on Kestrel over Https from iOS Simulator, Android Emulator and UWP Applications.
Publishing ASP.NET Core 3 Web API to Azure App Service with Http/2
Xamarin and the HttpClient For iOS, Android and Windows
Working with Self Signed Certificates (Certificate Pinning) in Windows (UWP) Application with Xamarin.Forms

In this post we’re going to cover 1) accessing non-secure services 2) trusting a self-signed certificate and 3) handling certificate validation. This gives you all the options you should need when accessing which security option to use during development and how you might want to implement certificate pinning in the production version of your app.

Non-Secure (i.e. Http) Services

iOS is secure by default – this means that by default an iOS application can only connect to services over Https with a certificate that can be verified against one of the well known certificate authorities. For most services this is not a problem, as most production services will use a certificate that has been issued by a well know certificate authority (eg when you deploy a service to Azure App Service the generated endpoint (eg myservices.azurewebsites.net) already has a Https endpoint with a certificate that is trusted). However, this may be an issue when you’re running services locally, or you’re attempting to connect to a service in an environment where either there is no https endpoint. In these cases, you may want to adjust the behaviour of the iOS application so that it can connect to a non-secure (ie http) endpoint.

Let’s see this in action by changing our the endpoint of our service request to http://192.168.1.107:5000 – when we setup the endpoint it was configured for both https on port 5001 and http on port 5000. If you happen to be trying this out with a new ASP.NET Core 3 project, don’t forget that the template comes with the line UseHttpRedirection in startup.cs so if you want to expose an http endpoint you’ll need to remove that line.

image

In the iOS application if you simply change to http://192.168.1.107:5000 the application will operate correctly, despite all the concern that http connections aren’t supported. This is because there’s a clear set of exceptions to the Https rule on iOS:

image

So what happens if you do want to use http but instead of using an IP address (ie the exclusion we just saw) you have a fully qualified domain name. Let’s try this by changing the endpoint to http://192.168.1.107.xip.io:5000. BIG shout out to the xip.io service which is super cool – you can use any ip address before the xip.io and the returned ip address from doing a DNS lookup will be the same ip address.

image

Now when we run the iOS application and it attempts to make the service call we get the following exception raised within Visual Studio:

Unhandled Exception:
System.Net.WebException: <Timeout exceeded getting exception details> occurred

This isn’t very meaningful. However, in the Output window there’s much more information:T

Unhandled Exception:
System.Net.WebException: The resource could not be loaded because the App Transport Security policy requires the use of a secure connection. —> Foundation.NSErrorException: Error Domain=NSURLErrorDomain Code=-1022 “The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.” UserInfo={NSLocalizedDescription=The resource could not be loaded because the App Transport Security policy requires the use of a secure connection., NSErrorFailingURLStringKey=http://192.168.1.107.xip.io:5000/api/values

This exception we can combat by including an exception in the Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
   <key>NSExceptionDomains</key>
   <dict>
     <key>192.168.1.107.xip.io</key>
     <dict>
       <key>NSExceptionAllowsInsecureHTTPLoads</key>
       <true />
     </dict>
   </dict>
</dict>

Adding this to the plist will exclude the listed domain from the App Transport Security policy. Another alternative is to use the NSAllowArbitraryLoads attribute – this is not recommended as it effectively disables the security policy for any endpoint that the app connects to.

<key>NSAppTransportSecurity</key>
<dict>
   <key>NSAllowsArbitraryLoads</key>
   <true />

</dict>

So that’s it for accessing non-secure, or Http, endpoints. Simply add the endpoint to the NSExceptionDomains element in the Info.plist file and you’re good to go.

Trusting Self-Signed Certificates

Now let’s go back to connecting to a secure endpoint but this time we’re going to keep with using a xip.io address to ensure any security policies are enforced. The secure endpoint would be https://192.168.1.107.xip.io:5001. Note that I’ve reissued the certificate used by the ASP.NET Core application to include 192.168.1.107.xip.io in the alternative names section:

image 

You’d imagine this would just work since the endpoint domain is already listed in the Info.plist file (see earlier on doing this using the NSExceptionDomains) but instead you get an error similar to the following.

System.Net.WebException: The certificate for this server is invalid. You might be connecting to a server that is pretending to be “192.168.1.107.xip.io” which could put your confidential information at risk.

The information in this error is only partially correct – the certificate for the server is actually value, it’s just that the app/device isn’t able to verify the integrity of the certificate. In order to use this endpoint we’re going to need to find a way for the device to trust the ceritifcate being returned by the server. By far the easiest way to get the certificate to be trust by applications running on an iOS device is to install the public key for the certificate onto the device. To do this we need the public key, which we can extract from the pfx file used by the ASP.NET Core service, using the following openssl command (This site is very useful for Openssl commands):

openssl pkcs12 -in kestrel.pfx -out kestrel.pem -nodes

This extracts the public key in pem format. iOS needs der format. Luckily there’s again an openssl command for converting the files.

openssl x509 -outform der -in kestrel.pem -out kestrel.der

To get the public key to the device you can either share a hyperlink (ie upload the der file and share a link) or email the file to your self and open it on the device. My preference is just to add the file to my dropbox and then open the corresponding link using Safari on the device. Select the Direct Download option and the file downloads, extracts and attempts to installs the certificate

image

After clicking on Direct download you should see a prompt from the OS about installing a profile that has been installed from a website, followed by a confirmation prompt indicating the Profile has been Downloaded.

imageimage

However, it’s important to read the second prompt closely because what it’s saying is that you still need to go to Settings in order to complete the installation of the profile (which in this case is just a certificate). When we go to Settings / General / Profile and then select the profile for 192.168.1.107.xip.io you’ll see information about the certificate and the ability to Install (top right corner) the certificate.

image

After clicking Install and following the prompts you’ll be returned to this screen but other than the Install changing to Done, there’s not difference – the certificate is still marked as Not Verified. This is because the root certificate, which in this case was the root certificate used by mkcert when creating the certificate, is not trusted by the device.

image

Unfortunately as the certificate isn’t trusted, the application will still fail to connect to this endpoint. For the moment we’ll remove this certificate as it’s not helpful.

Let’s repeat the process of installing the certificate but this time let’s install the root certificate used by mkcert. The public key can be found at C:Users[username]AppDataLocalmkcertrootCA.pem and when you attempt to install it on the device you should see something similar to

image image

Note the difference after the certificate has been installed – it is clearly marked as Verified in green.

To prevent profiles being accidentally downloaded and installed by users (it’s actually very easy to do) and for them to have full access to the device, it is now necessary to manually trust certificates. This can be found under Settings / General / About / Certificate Trust Settings. On this screen you can control which profiles (ie certificates) are fully trusted.

image

After toggling the full trust setting we’re good to try our application. Note that we’ve not had to make any changes to the application itself and yet because we’ve installed the correct certificate we’re now able to connect to the secured services. It’s also worth noting that with making the root certificate trusted on this device, it also removes the need for the NSExceptionDomains section in the Info.plist file (except if you still want to target a Http endpoint)

Validating Server Certificates (i.e. Certificate Pinning)

In this last section we’re going to look at how you can specify a certificate within the application itself to allow requests to be made to the service with the self-signed certificate. I’ve removed the trusted certificate that was installed in the previous option and have reinserted the NSExceptionDomains back into the Info.plist file.

Currently (at the time of writing) there is no way to override the certificate validation process for the out of the box NSUrlSessionHandler. There’s been work done in the past to provide a better alternative, such as the ModernHttpClient, however they do not seem to work with self-signed certificates. They may have worked well with self-signed certificates back when the library was created but as it’s no longer maintained it appears to not support self-signed certificates.

Even the sample project put together by Jonathan Peppers on SSLPinning doesn’t appear to work. Luckily with a minor tweak it’s possible to use the revised NSUrlSessionHandler to permit access to the self-signed service. Using the source code in the SSLPinning repository as the starting point, I’ve collated all the pieces of the alternative NSUrlSessionHAndler into a single file (Full source code). The main changes are:

– The addition of an UntrustedCertificate property which will accept the raw data from the .der public key

public NSData UntrustedCertificate { get; set; }

Modification to the processing included in the DidReceiveChallenge method. This essentially installs the root certificate so that it can be trusted by the application.

if (sessionHandler.UntrustedCertificate != null)
{
   var trust = challenge.ProtectionSpace.ServerSecTrust;
   
var rootCaData = sessionHandler.UntrustedCertificate;
    var x = new SecCertificate(rootCaData);


   
trust.SetAnchorCertificates(new[] { x });
     trust.SetAnchorCertificatesOnly(false);
    completionHandler(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential);
}
else
{
    completionHandler(NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, null);
}

In order to take advantage of the updated NSUrlSessionHandler we need to modify the setup.cs

Mvx.IoCProvider.LazyConstructAndRegisterSingleton<HttpMessageHandler, IServiceOptions>(options =>
{
     var handler = new Xamarin.SSLPinning.iOS.NSUrlSessionHandler
     {
         UntrustedCertificate = NSData.FromFile(“kestrel.der”)
     };
     return handler;
});

And of course we need to make sure we include the kestrel.der file in the Resources folder with Build Action set to BundleResource

image

Running this up and we get the response back from the service.

image

The big difference with this option is that there’s nothing outside of the application that needs to be modified on the device. All the setting up of the trust is done with the application, making it much easier to deploy to any device without having to worry about configuring the device.

Note that we didn’t strictly do certificate pinning – we just allowed the application to connect to a self-signed endpoint. To carry out certificate pinning you can make changes to the DidReceiveChallenge method and determine whether the certificate should be trusted. The ModernHttpClient does have an implementation of a callback that the application can register for in order to determine whether the certificate, and thus the endpoind, should be trusted.

Leave a Reply

Your email address will not be published. Required fields are marked *