Handling Self-Signed Certificates (Cocoa)

Remoting SDK's client channels for Cocoa have always supported SSL implicitly. Simply by using the appropriate https:// or superhttps:// URL Scheme, the clients will automatically choose Secure Socket Layer and communicate securely with the server.

For that, your server has to be set up with a "proper" certificate – that is, a certificate signed by (and usually bought from) a known certificate authority that is implicitly trusted by the OS. For production applications (especially those deployed via the App Store) that is recommended practice, but sometimes – for example in Enterprise deployment or during debugging – it is helpful to be able to work with "invalid" or self-signed certificates, just to get things going before spending money on a real certificate.

By default, if your application connects to a server with an invalid or unknown certificate, the channel will refuse to connect, and your requests will fail with an error such as:

HTTP error: The certificate for this server is invalid. You might be connecting to a server that is pretending to be “your.server.com” which could put your confidential information at risk.

This is good and essential, as accepting unknown certificates blindly would enable Man-in-the-middle Attacks and essentially negate any security you aim to gain by using SSL.

But it still can sometimes be helpful to use a self-signed certificate. There are two ways to make that work:

Option 1: Manually Installing Your Certificate

If you have access to the server's certificate, or a certificate for the root authority used to create it, you can manually register it on your system in order to make the certificate trusted. You can usually do this by browsing to the server in a web browser and choosing to always trust the certificate (depending on the browser), or by explicitly importing the root certificate (usually a ".cer" file) into the Key Chain.

This works on Mac (double-click a .cer file to add it to the Key Chain) and iOS devices (open a .cer file from any application that allows so, such as Mail or Safari, and you will get asked whether to add it to the list of trusted certificates; it will show up under "Settings|General|Profiles").

However, this technique does not work on the iOS Simulator, which puts a big dent in debugging, and it will also not work on tvOS or watchOS.

Option 2: Verifying Certificates in Your App

Remoting SDK for Cocoa has callbacks on the ROClientChannel, ROAsyncRequest and DARemoteDataAdapter delegates that get called whenever the client encounters an invalid and/or untrusted certificate. These callbacks are:

  • clientChannel:shouldAcceptUntrustedServerCertificate:
  • asyncRequest:shouldAcceptUntrustedServerCertificate:
  • remoteDataAdapter:shouldAcceptUntrustedServerCertificate:

and they all work the same, the only difference being that the "sender" object is passed in the first parameter.

By default (i.e. if not implemented), the channels will act as if the delegate callback returned NO/false and perform the default behavior described above: the request will fail with the "The certificate for this server is invalid" error.

If instead the callback returns YES/true, the certificate will be accepted and the request will proceed.

Of course, as discussed above, you should never just blindly return YES/true (except maybe in a very controlled debugging environment, and make sure the code does not slip into the production release). Instead, you can and should use the second parameter, which is of type ROCertificateInfo, to inspect the certificate and base the trust decision on that.

There are two core pieces of information on ROCertificateInfo that you will want to look at:

  • The SHA1 fingerprint of the certificate is provided via the sha1 (NSData) and sha1String (NSString) properties. This is a very short binary (or hex digit string) that you can use to see if the certificate is the one you expect.

  • The subject property gives the name of the certificate, which is usually the domain name that it has been issued for. You might want to compare this to the hostname used in the Target URL of your channel to see if it matches.

A common implementation of clientChannel:shouldAcceptUntrustedServerCertificate: might look like this:

#if TARGET_IPHONE_SIMULATOR == 1
- (BOOL)clientChannel:(ROClientChannel *)channel 
        shouldAcceptUntrustedServerCertificate:(ROCertificateInfo *)certificate
{
   NSLog(@"clientChannel:shouldAcceptUntrustedServerCertificate:%@", [certificate sha1String]);
   NSString goodSHA = @"29 94 b5 ce c6 69 29 db 73 a4 22 30 21 cc 81 2b b4 f4 c4 14";
   return
       [[certificate subject] isEqualToString:[[(ROHTTPClientChannel *)[[rda dataService] channel] targetURL] host]] &&
       [[certificate sha1String] isEqualToString:goodSHA];
}
#endif
#if TARGET_IPHONE_SIMULATOR == 1
func clientChannel(_ channel: ROChannel, 
     shouldAcceptUntrustedServerCertificate certificate: ROCertificateInfo) -> Bool {
   NSLog("clientChannel:shouldAcceptUntrustedServerCertificate:%@", certificate.sha1String)
   let goodSHA = "29 94 b5 ce c6 69 29 db 73 a4 22 30 21 cc 81 2b b4 f4 c4 14"
   return certificate.subject == (channel as? ROHTTPClientChannel)?.targetURL.host &&
          certificate.sha1String == goodSHA
}
#endif
{$IF SIMULATOR}
method ServerAccess.clientChannel(channel: ROChannel)
         shouldAcceptUntrustedServerCertificate)(certificate: ROCertificateInfo): Boolean
begin
  NSLog('clientChannel:shouldAcceptUntrustedServerCertificate:%@', certificate.sha1String);
  var goodSHA := "29 94 b5 ce c6 69 29 db 73 a4 22 30 21 cc 81 2b b4 f4 c4 14";
  result := certificate.subject = (channel as ROHTTPClientChannel).targetURL.host and
            certificate.sha1String = goodSHA;
end;
{$ENDIF}
#if SIMULATOR
bool clientChannel(ROChannel channel) 
     shouldAcceptUntrustedServerCertificate(ROCertificateInfo certificate)
{
    NSLog("clientChannel:shouldAcceptUntrustedServerCertificate:%@", certificate.sha1String);
    NSString goodSHA = "29 94 b5 ce c6 69 29 db 73 a4 22 30 21 cc 81 2b b4 f4 c4 14";
    return certificate.subject == (w(ROHTTPClientChannel)channel).targetURL.host &&
           certificate.sha1String == goodSHA;
}
#endif

This implementation shows a few important concepts:

  1. We surrounded it with an #if check for the Simulator, to ensure this check never makes it into production code (although that would be pretty safe, because we do perform a proper and safe check). Of course, whether this is feasible for you depends on whether you can install your certificate on your test devices. You might also choose to check for #ifdef DEBUG, instead.

  2. We print out the SHA1 fingerprint. This makes it easy to capture the fingerprint on first run (assuming we're running locally against a trusted server and validating it with the person who created the certificate) to paste it into the following check.

  3. We check the subject against the targetURL.host of the channel to make sure the server matches the certificate. (This is safe to skip for debugging purposes, if you want to, say, connect to "localhost" or an IP address.)
  4. Finally, we check the SHA1 fingerprint against a known good value (of course you'd replace the sample digits here with the proper values).

Going Deeper

If you want to go fancier and do more checks, the ROCertificateInfo class also exposes the raw ASN.1 data of the certificate via the data property. Using OpenSSL and some Googling, you can find ways to inspect the certificate in more detail, extract further information such as expiration dates, certificate authority names, etc.