Handling Self-Signed Certificates (.NET)

Remoting SDK's client channels for .NET allow to enable TLS/SSL protection quite easily: simply by using the appropriate https://, superhttps://, tcps:// or supertcps:// 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 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.

Depending on the target OS and used channel type (see below), the channel will either refuse to connect or just accept the formally invalid certificate. Unfortunately, accepting unknown certificates blindly could enable Man-in-the-middle Attacks and essentially negate any security you aim to gain by using SSL. Even more, blindly accepting a formally valid certificate is still not MITM-attacks proof.

So a more advanced certificate validity check is required for real-worlds applications.

In Remoting SDK for .NET there are 2 different kinds of channels, based on different server connection technology used: the first kind \ is based on Remoting SDK's own implementation of HTTP and TCP clients (based on Internet Pack); the seconds kind is based on .NET's HttpWebRequest classes.

These channel kinds require different approaches to handle self-signed certificates and will be covered separately.

Ip-based Client Channels

These channels (namely IpHttpClientChannel, IpSuperHttpClientChannel, IpTcpClientChannel and IpSuperTcpClientChannel) use Remoting SDK's own network stack implementation and provide some more advanced customization options.

Note: Up to and including Version 9.0, by default these channels will accept non-trusted SSL certificates. It is strongly recommended to always implement a custom certificate validation method as described. As of version 9.1, self-signed certificates will be rejected by default.

To validate the certificate, simply assign the following event handler, and check the certificate properties:

channel.SslOptions.ValidateRemoteCertificate += (sender, e) => {
    e.Cancel = e.Certificate.GetCertHashString() != "BED61....E5D0";
}
channel.SslOptions.ValidateRemoteCertificate += (sender, e) -> begin 
    e.Cancel := e.Certificate.GetCertHashString ≠ 'BED61....E5D0';
end;
channel.SslOptions.ValidateRemoteCertificate += { (sender, e) in
    e.Cancel = e.Certificate.GetCertHashString() != "BED61....E5D0"
}
AddHandler channel.SslOptions.ValidateRemoteCertificate,
    Sub(sender As Object, e As SslValidateCertificateEventArgs)
        e.Cancel = e.Certificate.GetCertHashString() <> "BED61....E5D0"
    End Sub

Also, starting with version 9.0.95, the Ip-based channels can also use the global certificate validation callback set via ServicePointManager's ServerCertificateValidationCallback event.

To confirm the certificate validity (or to reject it) on the full .NET framework or Mono, simply register a validation method:

System.Net.ServicePointManager.ServerCertificateValidationCallback = 
                                            ServerCertificateValidationCallback;

private static bool ServerCertificateValidationCallback(object sender, 
                                                        X509Certificate certificate, 
                                                        X509Chain chain, 
                                                        SslPolicyErrors sslPolicyErrors)
{
    // Very simple certificate hash check
    return certificate.GetCertHashString() == "BED6...E5D0";
}
System.Net.ServicePointManager.ServerCertificateValidationCallback := 
                                            ServerCertificateValidationCallback;

method ServerCertificateValidationCallback(sender: Object; 
                                           certificate: X509Certificate; 
                                           chain: X509Chain; 
                                           sslPolicyErrors: SslPolicyErrors): Boolean;
begin
    // Very simple certificate hash check
    result := certificate.GetCertHashString = 'BED6...E5D0';
end;
System.Net.ServicePointManager.ServerCertificateValidationCallback = 
                                            ServerCertificateValidationCallback;

func ServerCertificateValidationCallback(_ sender: Object, 
                                         _ certificate: X509Certificate, 
                                         _ chain: X509Chain, 
                                         _ sslPolicyErrors: SslPolicyErrors) -> Bool {
    // Very simple certificate hash check
    return certificate.GetCertHashString() == "BED6...E5D0"
}
System.Net.ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ServerCertificateValidationCallback)

Private Shared Function ServerCertificateValidationCallback(sender As Object, certificate As X509Certificate, chain As X509Chain, sslPolicyErrors As SslPolicyErrors) As Boolean
    Return certificate.GetCertHashString() = "BED61....E5D0"
End Function

HttpWebRequest based Client Channels

These include WinInetHttpClientChannel and WinInetSuperHttpClientChannel.

An attempt to access a server protected with a self-signed certificate with these channels will result in a System.Net.WebException exception with a message such as "Could not establish trust relationship for the SSL/TLS secure channel." or "The certificate authority is invalid or incorrect" for UWP apps.

The most straightforward way to resolve the issue is to register the self-signed certificate as valid in the client's OS. Unfortunately it is not always possible or feasible.

Full .NET Framework, Mono and Xamarin

To confirm the certificate validity (or to reject it) on the full .NET framework, Mono and on Xamarin, you can simply just register a validation method:

System.Net.ServicePointManager.ServerCertificateValidationCallback = 
                                            ServerCertificateValidationCallback;

private static bool ServerCertificateValidationCallback(object sender, 
                                                        X509Certificate certificate, 
                                                        X509Chain chain, 
                                                        SslPolicyErrors sslPolicyErrors)
{
    // Very simple certificate hash check
    return certificate.GetCertHashString() != "BED6...E5D0";
}
System.Net.ServicePointManager.ServerCertificateValidationCallback := 
                                            ServerCertificateValidationCallback;

method ServerCertificateValidationCallback(sender: Object; 
                                           certificate: X509Certificate; 
                                           chain: X509Chain; 
                                           sslPolicyErrors: SslPolicyErrors): Boolean;
begin
    // Very simple certificate hash check
    result := certificate.GetCertHashString ≠ 'BED6...E5D0';
end;
System.Net.ServicePointManager.ServerCertificateValidationCallback = 
                                            ServerCertificateValidationCallback;

func ServerCertificateValidationCallback(_ sender: Object, 
                                         _ certificate: X509Certificate, 
                                         _ chain: X509Chain, 
                                         _ sslPolicyErrors: SslPolicyErrors) -> Bool {
    // Very simple certificate hash check
    return certificate.GetCertHashString() != "BED6...E5D0"
}
System.Net.ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ServerCertificateValidationCallback)

Private Shared Function ServerCertificateValidationCallback(sender As Object, certificate As X509Certificate, chain As X509Chain, sslPolicyErrors As SslPolicyErrors) As Boolean
    Return certificate.GetCertHashString() <> "BED61....E5D0"
End Function

Universal Windows Applications

Unfortunately there is no easy way to add similar check to "Universal" Windows and/or Windows Phone 8.1 applications, so a bit more work is needed there.

First of all, make sure that the server's generated certificate's "issuer" and "subject" match to the hostname that will be used to access data by the Universal app.

If your server uses the auto-generated certificate, add the following event handler to the server startup code:

server.CertificateGenerating += (sender, e) => {
    e.Subject = "localhost";
    e.Issuer = e.Subject;
}
server.CertificateGenerating += (sender, e) -> {
    e.Subject := 'localhost';
    e.Issuer := e.Subject;
}
server.CertificateGenerating += { (sender, e) in
    e.Subject = "localhost"
    e.Issuer = e.Subject
}
AddHandler server.CertificateGenerating,
    Sub(sender As Object, e As CertificateGeneratingEventArgs)
        e.Subject = "localhost"
        e.Issuer = e.Subject
    End Sub

Then delete the already previously generated certificate file (it will be located next to the server's exe) and restart the server. (Please note: Just issuing the 'Clean Solution' or 'Rebuild Solution' commands in Visual Studio does NOT delete the auto-generated certificate, thus it won't be regenerated.)

Next, get the public key-only file that corresponds to the server certificate file. Still perhaps the easies way to get the certificate file that is sent to the clients is to use your browser.

Start the server app. Open your browser (the following steps use Chrome, and might be slightly different in other browsers) and go to the server's address. Press the "lock" icon next to the striked-out https:// label in the address bad. A "Connection info" window will appear, containing a "Certificate Information" link. Press it and a more detailed information dialog will appear. Its "Details" tab contains a "Copy to file..." button that you can use to ctore the .cer file.

In the Universal or Windows 8.x application project, go to the Package.appxmaniest file and open the "Declarations: tab. Select the "Certificates" declaration from the dropdown list and add it. Add a new certificate and set its "Store" name to Root In the "Content" edit box, select the .cer file acquired on the previous step so it will be added to the client app project.

Now the UWP client application is able to communicate with the server that uses a self-signed certificate to protect the connection.