SSL Certificate Validation

Hi there,

how is it possible to validate a SSL Certificate in Unity? I've come so far that the check works in the Unity game window, but not on real iOS or Android devices...

ServicePointManager.CertificatePolicy = new SSLTest();
try {
        WebRequest wr = WebRequest.Create(url);
        Stream stream = wr.GetResponse ().GetResponseStream ();

        Console.WriteLine (new StreamReader (stream).ReadToEnd ());
}
catch (WebException we) {
    if (we != null  we.InnerException != null){
        if (we.InnerException.Message == "The authentication or decryption has failed.")
        {
            // ssl certificate validation failed
        }
    }
}

SSLTest:

public class SSLTest : ICertificatePolicy {

    public bool CheckValidationResult (ServicePoint sp, X509Certificate certificate, WebRequest request, int error)
    {
        if (error == 0)
        {
            return true;
        }
        return false;
    }

}

It's possible that your exception message check isn't passing... you shouldn't check against the text of the message. You're catching your WebException, so what you want to do is check the Status property of the WebException:

//...
catch (WebException ex)
{
          if(ex.Status == WebExceptionStatus.TrustFailure)
         {
                ///The SSL validation has failed.
         }

}

Thank you for your answer, Dustin ... I tried that check first, but the thrown exception had the status SendFailure, not TrustFailure. So I had to look at the Message in InnerException.
I know this is not pretty, but it was the only way that it worked in the game window for a start.

Ok... what's the base url you're trying to hit so I can look at the cert? Is it a self-signed cert?

Also, have you tried the callback for cert validation? Not sure if it works on mobile or not but worth a shot.

http://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.servercertificatevalidationcallback(v=vs.90).aspx

And sorry to spam the post... one more thing to try. :) Add this in your code before you make the web call:

var aesCrypto  = new System.Security.Cryptography.AesCryptoServiceProvider();

You won't actually be using it but from what I read it's a possibly related mono bug and creating an instance of the provider will prevent the linker from tossing it during JIT / AOT.

Hey Dustin,

you got a mail with the base url. It seems I am a little bit closer ... I hope.

With your help and some research about ServerCertificateValidationCallback I changed my code to:

public static bool ValidateServerCertificate(
   object sender,
   System.Security.Cryptography.X509Certificates.X509Certificate certificate,
   X509Chain chain,
   System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
   if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
        return true;

    Debug.Log("Certificate error: " + sslPolicyErrors);
    return false;
}



// in my main function:
System.Net.Security.RemoteCertificateValidationCallback orgCallback = ServicePointManager.ServerCertificateValidationCallback;
try
{
    var aesCrypto  = new System.Security.Cryptography.AesCryptoServiceProvider();

    ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(ValidateServerCertificate);
    ServicePointManager.Expect100Continue = true;

    WebRequest wr = WebRequest.Create(url);
    Stream stream = wr.GetResponse ().GetResponseStream (); 
    Debug.Log (new StreamReader (stream).ReadToEnd ());
}
catch (WebException we)
{
    Debug.Log ("status: "+we.Status);
    if (we != null  we.InnerException != null)
    {
        if (we.InnerException.Message == "The authentication or decryption has failed.")
        {
            sslCheckState = SSLCheckState.NOT_OK;
        }
    }
}
finally
{
    ServicePointManager.ServerCertificateValidationCallback = orgCallback;
}
}

Calling the base url I've sent you, PolicyErrors contains "RemoteCertificateChainErrors" ... (output at line 10)
If I call for example a test site with an expired SSL certificate like "https://testssl-expire.disig.sk/", PolicyErrors contains "RemoteCertificateNameMismath, RemoteCertificateChainErrors". So at least there is some difference...

Ok... What you want to look at in your hander is:

chain.ChainStatus

This will return an array... you want to look at the "Status" and "Status Information" properties. There are two errors being returned:

RevocationStatusUnknown

and

UntrustedRoot

There may be something wrong with the cert... or it's not able to get the revocation status from the certificate authority for some reason.

Ok I think I may have a solution for you. Remove the certificate validation handler for now and just go back to your webrequest in your Try/Catch block. I did some additional testing. I first created a site with a self-signed cert to verify and I got the same error. Then I changed the validation method to just return true and it worked perfectly, so it does seem to be a certificate validation error but I don't think the certificate is the problem because I replaced your URL with "https://www.google.com" and "https://www.microsoft.com" and I got the exact same error.

I think the error is misleading and is actually caused by a policy restriction. First, change your code to this so you can see the actual exceptions being thrown:

        try
        {
            WebRequest wr = WebRequest.Create("your-url-here");

            Stream stream = wr.GetResponse().GetResponseStream();
            Debug.Log("Success");
            Debug.Log(new StreamReader(stream).ReadToEnd());
        }
        catch(WebException we)
        {
            var ex = we as Exception;

            while (ex != null)
            {
                Debug.Log(ex.ToString());
                ex = ex.InnerException;
            }
        }

You'll see three exceptions going back to the Invalid Server Certificate exception. I think this is because there is no crossdomain.xml file on your host. Create a crossdomain.xml file with the following. Easiest is to create it in Notepad++ and made sure it is UTF-8 encoded by clicking the Encoding menu and selecting "Encoding in UTF-8". It grants access to port 443 (the SSL Port).

<?xml version="1.0" ?>
<cross-domain-policy>
    <allow-access-from domain="*" to-ports="443" />
</cross-domain-policy>

Problem solved! After your suggestion with Crossdomain.xml I talked to the person responsible for the server where that file should have been copied. He did not want to go that way (security concerns) and we found a way that finally everything works fine. Now I will ship the certificates in my app, and at program start a X509Certificate2Collection takes the certificates in a static variable.
When the ssl callback method is called, a new chain is build by that collection and checked.

in ValidateServerCertificate:

        X509Chain tempChain = new X509Chain();
        tempChain.ChainPolicy.RevocationMode = chain.ChainPolicy.RevocationMode;    

        X509Certificate2Collection cert2Collection = tempChain.ChainPolicy.ExtraStore;
        cert2Collection.AddRange(myStaticX509Collection);

        return = tempChain.Build(new X509Certificate2(certificate));

First why doesn't this work on my Android? Is it a Unity Pro thing?

Second thanks for all your posts!, I was able to get something working for myself :) But it does not work on my Android Device, is this a Unity Pro function? I only have the free version thanks! Here's the code, what I had to do was that is a little different from the previous examples is include a client cert... Jogo I couldn't get Validate Cert to return true so I don't know what I'm doing wrong there so in my example it just always returns true.. so I'm still trying to figure that out but I am able to get my response back so I'm like 65% happy

using UnityEngine;
using System.Collections;
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Xml;

public class newhttptest : MonoBehaviour
{
    string s = "not set";
// Use this for initialization
        void Start ()
        {
                X509Certificate2 adminClient = new X509Certificate2 ("Assets/Scripts/adminClient.p12", "xxxxxx");


                ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
                try {


                        HttpWebRequest request = (HttpWebRequest)WebRequest.Create ("https://xxxx/rest/user-message");
                        request.AuthenticationLevel = AuthenticationLevel.MutualAuthRequested;

                        request.ClientCertificates.Add (adminClient);
                        HttpWebResponse response = (HttpWebResponse)request.GetResponse ();


                        //XmlDocument xmlDoc = new XmlDocument();
                        //xmlDoc.Load(response.GetResponseStream());

                        Debug.Log(new StreamReader(response.GetResponseStream()).ReadToEnd());


                        Debug.Log ("Success");

                        s = "yippy!";

                } catch (WebException we) {
                        var ex = we as Exception;
                        s = "fail";
                        while (ex != null) {
                                Debug.Log (ex.ToString ());
                                ex = ex.InnerException;
                        }
                }




        }


        public static bool ValidateServerCertificate (
        object sender,
        System.Security.Cryptography.X509Certificates.X509Certificate certificate,
        X509Chain chain,
        System.Net.Security.SslPolicyErrors sslPolicyErrors)
        {

                return true;
        }

    void OnGUI(){
        GUI.Label (new Rect (0, 0, 100, 100), s);
        if (GUI.Button (new Rect (20, 20, 100, 100), s)) {
            Application.Quit();
        }
    }

// Update is called once per frame
        void Update ()
        {

        }


}

What happens when you try to run it on Android? Is there an error or does it just do nothing? And what version of Unity are you using?

When I run it on Android nothing appears to happen, I set the button in the scene to get text "yippy!" or "fail" depending on the results of the try/catch statement but it keeps its default value "not set". I'm using Unity 4.5.0f6. The scene is empty except for the main camera and a cube with the script attached. I also made sure that the manifest file has

is there another permission I need since I'm also using a file to create my x.509 certs?

Ok I added a new permission to my android manifest file like so
[quote]

[/quote]
and I also added some comments around this line

s = "making cert";
X509Certificate2 adminClient = new X509Certificate2 ("Assets/Scripts/adminClient.p12", "xxxxx");
s = "cert created";

And what I found was is that the Android gets to the "making cert" line and quits so it's a good bet that this is where the error is thrown. So at least now I know where the problem is; so now I just need to figure out a fix. Let me know if you have an idea :)

It fails when trying to create the new X509Certificate2 on line 3? And are you sure it's not a path problem accessing that file?

Hi Dustin!
So after I tried everything ... putting it the resources trying Application.persistentDataPath and storing the .p12 with a txt and bytes extension and then trying Resources.Load... nothing worked! Then finally I just used a www call to down load the .p12 from a server and then store it in Application.persistentDataPath and guess what? It worked!!!! I'll post all the details later today :) for now I'm taking a break! Thanks for your help with this :)

No problem. I was guessing that was probably your issue is that it was something to do with accessing the file data.

As promised here's what my final solution looks like

using UnityEngine;
using System.Collections;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Xml;

public class RestCalls : MonoBehaviour
{

        X509Certificate2 adminClient;
        string clientCertPath="";
        static string baseUrl = "https://"someurl"/rest/";
        void Start ()
        {
                clientCertPath = Application.persistentDataPath + "/adminClient.p12";
                StartCoroutine (GetCert ());   
        }

        private IEnumerator GetCert ()
        {
                if (File.Exists (clientCertPath)) {
                        print ("I have the file");
                        MakeRestCall();
                } else {
                        WWW download = new WWW ("http://"pathtocert"/adminClient.p12");
                        yield return download;
                        if (download.error != null) {
                                print ("Error downloading: " + download.error);
                        } else {
                                File.WriteAllBytes (clientCertPath, download.bytes);

                        }
                }
        }

        public XmlDocument MakeRestCall()
        {
            ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback (ValidateServerCertificate);
            adminClient = new X509Certificate2 (clientCertPath, "xxxxx");
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create (baseUrl + "user-message");
            request.AuthenticationLevel = AuthenticationLevel.MutualAuthRequested;


            request.ClientCertificates.Add (adminClient);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse ();

            XmlDocument xmlDoc = new XmlDocument ();
            xmlDoc.Load (response.GetResponseStream ());

            return xmlDoc;


        }

        public static bool ValidateServerCertificate (
            object sender,
            System.Security.Cryptography.X509Certificates.X509Certificate certificate,
            X509Chain chain,
            System.Net.Security.SslPolicyErrors sslPolicyErrors)
        {
           //TODO Make this more secure
            return true;
        }


}
2 Likes

Hi,

I using #18 same code for SSL validation, but i am getting below error on line no. 48 in #18 code, in editor. I am loading my .p12 certificate file from persistent storage. Please help

IOException: BeginWrite failure
System.Net.Sockets.NetworkStream.BeginWrite (System.Byte[] buffer, Int32 offset, Int32 size, System.AsyncCallback callback, System.Object state)
Mono.Security.Protocol.Tls.RecordProtocol.BeginSendRecord (ContentType contentType, System.Byte[] recordData, System.AsyncCallback callback, System.Object state)
Mono.Security.Protocol.Tls.RecordProtocol.SendRecord (ContentType contentType, System.Byte[] recordData)
Mono.Security.Protocol.Tls.RecordProtocol.SendAlert (Mono.Security.Protocol.Tls.Alert alert)
Mono.Security.Protocol.Tls.RecordProtocol.SendAlert (AlertDescription description)
Mono.Security.Protocol.Tls.SslStreamBase.AsyncHandshakeCallback (IAsyncResult asyncResult)
Rethrow as WebException: Error getting response stream (Write: BeginWrite failure): SendFailure
System.Net.HttpWebRequest.EndGetResponse (IAsyncResult asyncResult)
System.Net.HttpWebRequest.GetResponse ()

How to validate SSL certificates when using HttpWebRequest

ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback;

public bool MyRemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    bool isOk = true;
    // If there are errors in the certificate chain, look at each error to determine the cause.
    if (sslPolicyErrors != SslPolicyErrors.None) {
        for (int i = 0; i < chain.ChainStatus.Length; i++) {
            if (chain.ChainStatus[i].Status != X509ChainStatusFlags.RevocationStatusUnknown) {
                chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
                chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
                chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0);
                chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
                bool chainIsValid = chain.Build((X509Certificate2)certificate);
                if (!chainIsValid) {
                    isOk = false;
                }
            }
        }
    }
    return isOk;
}