Udostępnij za pośrednictwem


PKI Authentication as a Azure Web App

Hi everyone,

So recently I had to port a solution for authenticating users which use a PKI token. The solution was onsite and relied heavily on IIS for chain trusts, CRL checks, and more.

In Azure when porting the STS (Security Token Service) there are several elements missing.

Review: https://azure.microsoft.com/en-us/blog/enabling-client-certificate-authentication-for-an-azure-web-app/

This allows any Azure web app service to be client certificate enabled.

What you don't get:

  • No chain trust verification, this means that it will not verify the trust chain, you cannot upload CA's into Azure in SaaS.
  • No CRL check, there is no mechanism in place to verify if the client certificate is revoked.
  • No check on expire/valid state.

What this means that any certificate can be used that was issued by a CA that is in the Azure Root CA store from a public CA.

How to solve the Problems

Validate the Certificate dates:  

           if (DateTime.Compare(DateTime.Now, cert.NotBefore) < 0 || DateTime.Compare(DateTime.Now, cert.NotAfter) > 0) return false;

Validate the policy chain:

           X509Chain ch = new X509Chain();

           ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            ch.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
            ch.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(1000);
            ch.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
            ch.ChainPolicy.VerificationTime = DateTime.Now;

            //build chain
            ch.Build(cert);

            //validate issuer, check the chain trusts
            var certArray = ch.ChainElements.Cast<X509ChainElement>().ToArray();
            if (!Array.TrueForAll(certArray, validatechainelements)) return false;

The function (validatechainelements) is where each CA in the policy chain is verified against a list.  The "Build" method is just used to build the policy but no validation is used, as it will always fail. What it does is builds the chain elements which allow us to validate what's in the chain.

Notice the usage of Array.TrueForAll, this is a very interesting solution, basically this allows each item to be used against a function, like a foreach loop.

        private static bool validatechainelements(X509ChainElement el)
        {
           //Part of the chain is the client cert it's self so we just return true if the client cert matches the cert being passed
            if (el.Certificate.Thumbprint.ToLower().Contains(clientCert.Thumbprint.ToLower())) return true;

            //The getCAThumbprints below pulls a CSV file of trusted thumbprints

            System.Collections.Generic.List<string> thumbprints = GetCAThumbprints();
            if (thumbprints.Contains(el.Certificate.Thumbprint.ToLower())) return true;
            return false;
        }

As you can see, the thumbprints are pulled from another function (not shown) which is just a list of strings of thumbprints of CA's we trust. If the chain element thumbprint matches a trusted CA thumbprint then return true, else false.

Validate CRL:

//check CRL list
X509CertificateParser pr = new X509CertificateParser();
string crlurl = GetCrlDistributionPoints(certArray[1].Certificate)[0];
if (IsCertRevoked(pr.ReadCertificate(cert.RawData),crlurl)) return false;

The above calls two different functions, the first gets the distribution URL, here I  am using the API BouncyCastle.Crypto to parse the certificate.

We use the second item in the array (1, as is zero based) which is the CA up the chain from the client certificate.

          /// <summary>
        /// Returns an array of CRL distribution points for X509Certificate2 object.
        /// </summary>
        /// <param name="certificate">X509Certificate2 object.</param>
        /// <returns>Array of CRL distribution points.</returns>
        public static string[] GetCrlDistributionPoints(this X509Certificate2 certificate)
        {
            X509Extension ext = certificate.Extensions.Cast<X509Extension>().FirstOrDefault(
                e => e.Oid.Value == "2.5.29.31");

            if (ext == null || ext.RawData == null || ext.RawData.Length < 11)
                return null;

            int prev = -2;
            List<string> items = new List<string>();
            while (prev != -1 && ext.RawData.Length > prev + 1)
            {
                int next = IndexOf(ext.RawData, 0x86, prev == -2 ? 8 : prev + 1);
                if (next == -1)
                {
                    if (prev >= 0)
                    {
                        string item = Encoding.UTF8.GetString(ext.RawData, prev + 2, ext.RawData.Length - (prev + 2));
                        items.Add(item);
                    }

                    break;
                }

                if (prev >= 0 && next > prev)
                {
                    string item = Encoding.UTF8.GetString(ext.RawData, prev + 2, next - (prev + 2));
                    items.Add(item);
                }

                prev = next;
            }

            return items.ToArray();
        }

        private static int IndexOf(byte[] instance, byte item, int start)
        {
            for (int i = start, l = instance.Length; i < l; i++)
                if (instance[i] == item)
                    return i;

            return -1;
        }

Now that we have the distribution point we can validate the client certificate.

      //reference library BouncyCastle.Crypto
        //https://www.bouncycastle.org/csharp/
        //Load CRL file and access its properties
        private static bool IsCertRevoked(Org.BouncyCastle.X509.X509Certificate cert,string url)
        {

                byte[] buf = GetCacheCRL(url);
                X509CrlParser xx = new X509CrlParser();
                X509Crl ss = xx.ReadCrl(buf);
                var nextupdate = DateTime.Parse(ss.NextUpdate.ToString());
                return ss.IsRevoked(cert);

        }

Above the CLR is held in cache, and updated based on the nextupdate property. Although it would not be needed this section is the important part of the "GetCacheCRL".

byte[] data;

                System.Net.WebRequest httpRequest = System.Net.WebRequest.Create(CRLFileURL);
                httpRequest.Timeout = 10000;

                System.Net.WebResponse webResponse = httpRequest.GetResponse();

                System.IO.Stream strm  = webResponse.GetResponseStream();

                byte[] buffer = new byte[16 * 1024];
                using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
                {
                    int read;
                    while ((read = strm.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        ms.Write(buffer, 0, read);
                    }
                    data= ms.ToArray();
                }

                strm.Close();
                webResponse.Close();

The last element to get the solution working when dealing with client certificates:

Application Settings as follows:

  • WEBSITE_LOAD_USER_PROFILE and set it to 1
  • WEBSITE_LOAD_CERTIFICATES with its value set to the thumbprint of the certificate will make it accessible to your web application.

Now that the above is met, then you now have the following validating:

  1. What you have (the PKI token)
  2. What you know (PIN)
  3. Is it valid (by date expire)
  4. Is it issued by a trusted CA(s)
  5. Has it been revoked?

Hope this helps others, as working though everything above was a lot of fun.