Tuesday, September 27, 2011

Digitally Signing App.config


Ever thought of putting digital signatures on Application Configuration? Here is something we went through:


Scenario
We have some values in Application Configuration file which we do want user to play around with as some of them might lead to an alternate hack of the application. We need to ensure that App.config is not tampered when used by the application. First we thought of embedding the App.config file but this is not provisioned by CLR as the file is loaded during Application Startup, so if it’s missing then application will crash. We had no option but to digitally sign the App.config file and verify the signatures on Application load. 


What is the issue?
App.config file is not any other file in the application as we cannot embed it on application assembly. So, we are left with no option than to sign it digitally and check the signatures of config file on application load. Several issues are there:

  1. Config file cannot be signed using signtool, as this is an XML file.
  2. Application crashes if I just sign the config file as config file no more abide by the schema .net CRL expects.
  3. How to ensure the signature are not tampered.

How to resolve the issue?
As config file is nothing but an XML file, we will use XMLDSIG for signing the config file. We have some classes from the framework to support the same. So, before deploying the application we’ll sign the configuration file. 
The signing of the file, adds another section to the config file – “<Signature>”. But the application fails to load because of this, as config file no more abide by the schema that .net CRL expects. So, for resolving this we add a new section declaration in “<configSections>” element before signing the config file.


For signing the App.config we’ll use asymmetric keys. We’ll use pfx certificate file for the same. The process has two steps:

  1. Signing – For this we import the pfx file in Local Computer’s Personal certificate store and then use the private key of this certificate for signing the config file. 
  2. Verify – To verify the signatures we need the corresponding public key. So, we can embed public key in the assembly and retrieve that during application load and verify the signatures on the config file.

To simplify we need to do the following things in order to sign the config file:

  1. Import pfx certificate for signing the config file
  2. Add a new <section> item to <configSections> element
  3. Execute the code for signing the config file
  4. On application load retrieve the embedded public key (.cer file)
  5. Verify the signature on config file and return the result



Code for the solution

Import the pfx file in Local Computer’s Personal certificate store


Add the following section to the <configSections> element :


<configSections>
 <section name="Signature" type="System.Configuration.IgnoreSectionHandler" />
</configSections>



Sign the config file using the console application generated by compiling the following code:


namespace SigningAppConfigConsole
{
    using System;
    using System.Text;
    using System.IO;
    using System.Linq;
    using System.Xml;
    using System.Security.Cryptography.Xml;
    using System.Security.Cryptography.X509Certificates;
    using System.Security.Cryptography;

    class Program
    {
        private static void Main(string[] args)
        {
            // Expecting first argument as path of the config file
            string path = args[0];

            // Expecting second argument as certifacte's thumbprint which will be used for signing
            string thumbprint = args[1];

            // Sign the Xml File
            SignXml(path, thumbprint);
        }

        /// 
        /// Method for signing the xml file
        /// 
        /// Path of teh file</param>
        /// Thumbprint of the certificate which should be used for signing</param>
        private static void SignXml(string path, string thumbprint)
        {
            // Check if file exists
            if (File.Exists(path))
            {
                // Loading Applcaition Configuration file as an Xml Document
                var configurationFile = new XmlDocument { PreserveWhitespace = true };
                configurationFile.Load(path);

                // Adding the transform
                var transform = new XmlDsigEnvelopedSignatureTransform();
                var reference = new Reference { Uri = string.Empty };
                reference.AddTransform(transform);

                // Creating XMLDSIG processor  
                var xmldsig = new SignedXml(configurationFile);
                xmldsig.AddReference(reference);

                // Setting signature key  
                xmldsig.SigningKey = GetPrivateKey(thumbprint);

                // Computing the signature
                xmldsig.ComputeSignature();

                // Signature XML
                var signature = xmldsig.GetXml();

                // Node to be inserted in the configuration file
                var signatureNode = configurationFile.ImportNode(signature, true);

                // Adding node to teh configuration file
                configurationFile.DocumentElement.AppendChild(signatureNode);

                // Write back the configuraiton file
                GenerateFile(path, configurationFile.InnerXml);
            }
            else
            {
                Console.WriteLine("File does not exists.");
            }
        }

        /// 
        /// Method for getting the private key of the certificate
        /// 
        /// Thumbprint of the certificate whose private key is required</param>
        /// 
        private static RSACryptoServiceProvider GetPrivateKey(string thumbprint)
        {
            // Removing spaces if thumbrint is supplied with spaces
            thumbprint = thumbprint.Replace(" ", string.Empty).ToLower();

            // StroreName.My = Personal Store, StoreLocation.LocalMachine = Local Computer Stores
            var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

            // Opening the certificate store
            store.Open(OpenFlags.ReadWrite);

            // Getting the cettificate with the mentioned thumbprint
            var x509Certificate2 = store.Certificates.Cast<X509Certificate2>().FirstOrDefault(certificate => certificate.Thumbprint.ToLower().Equals(thumbprint));

            // If certificate is not found in the store then throw exception that no certificate with
            // the mentioned thumbrint does not exists
            if (x509Certificate2 == null)
                throw new Exception("No Certificate exists with the thumbprint : " + thumbprint);

            // Returing the private key of the certificate
            if (x509Certificate2.HasPrivateKey)
            {
                var rsaCryptoServiceProvider = (RSACryptoServiceProvider)x509Certificate2.PrivateKey;

                if (rsaCryptoServiceProvider != null)
                    return rsaCryptoServiceProvider;
            }

            // Throw exception if certificate used does not have private key
            throw new Exception("Certificate used does not have private key");
        }

        /// 
        /// Method for genrating file with the passed contents
        /// 
        /// Path of the file</param>
        /// Contents</param>
        private static void GenerateFile(string filePath, string text)
        {
            // Delete file if the file with same name exists
            if (File.Exists(filePath))
            {
                File.Delete(filePath);
            }

            // Writing the contents to the file
            var fileContents = new StringBuilder();
            fileContents.AppendLine(text);
            using (var outFile = new StreamWriter(filePath))
            {
                outFile.WriteLine(fileContents.ToString());
            }
        }
    }
}

The above code takes config file path and certificate’s thumbprint as input. And as output it signs the configuration file.


On Application load get the embedded public key certificate. For more details on this task follow this link – I have explained there, how to embed and get the public key certificate and ensure that it is not tampered. Once you get the certificate verify the signatures using following code:


/// 
/// Checks if the digital signature on config are valid or not
/// 
/// Returns true if signatures are valid else returns false
public static bool CheckDigitalSignature()
{
 // Get the path of the configuration file
 string configFilePath = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;  
 
 // Loading configuration file as an Xml Document
 XmlDocument config = new XmlDocument();
 // Preserving whitespaces is important the signatures are computed considering each and every character in the file
 config.PreserveWhitespace = true;
 config.Load(configFilePath);

 // Creating XMLDSIG processor 
 SignedXml xmldsig = new SignedXml(config);

 // Getting the Signature element from the config
 XmlElement signature = (XmlElement)config.GetElementsByTagName("Signature")[0];

 // Loading XMLDSIG element into the XMLDSIG processor  
 xmldsig.LoadXml(signature);

 // Getting the public cerficate file
 X509Certificate2 certificate = new X509Certificate2(GetCertificateFile());

 // Public Key to verify the digital signatures
 RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PublicKey.Key;

 // Checking the signatures and returning the result
 return xmldsig.CheckSignature(rsa);
}

/// 
/// Get the public certificate file
/// 
/// Returns the path of the certificate file
private static string GetCertificateFile()
{
 // How to retrieve the embedded certificate can be seen in detail at :
 // http://www.rahulchugh.com/2011/09/ensuring-file-is-not-tampered-when-used.html
 string certificateFilePath = "D:\ApplicationFolder\Resources\Certificate.cer"
 return certificateFilePath;
}


Monday, September 26, 2011

Ensuring file is not tampered when used by .net code | For Thick Client Applications



Every now and then we use application files which are installed as part of your application on user’s machine. Recently, I came across the following scenario:

Scenario
We have a .net Think Client Application. Just take for example - We have a public key certificate (.cer) file which validates some signature. We want to ensure that this file is not tampered when we are using it. (I have just used certificate file as an example. It can be any file – say a pdf file which serves as a help file for my application.)

What is the issue?
As this file is part of our .net application which is installed on a user’s machine. User can change this certificate file with some of his own file, which may lead to security concerns. 

How to resolve the issue?
Let’s make it simple and split it into steps:
  1. We’ll embed the file in our assembly
  2.  Just before we want to use the file we’ll stream the file to user’s hard disk.
  3. The first time we stream the file we’ll store the MD5 hash value of the file’s content.
  4. Every time we use the file we’ll check the hash value:
a.       If it matches we are good to use the file
b.      If it doesn’t matches then we’ll delete this file and will go back to Step 2

Code for the solution
  1. Embedding file to the assembly : Add a “Resources” folder to your project. (optional – we’ll use this while retrieving file). Add the file Certificate.cer file to that folder and in the properties window of that file set the “Build Action” to “Embedded Resource".
  2. To retrieve and use this file use the following code :

  
namespace DefaultNamespace.FileHelper
{
    using System;
    using System.Configuration;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    /// The Certificate File Helper Class
    public class CertificateFileHelper
    {
        /// 
        /// For entry in assembly
        /// 
        private static Assembly _entryAssembly;

        /// 
        /// Syncronization Root
        /// 
        private static object _syncRoot = new object();

        /// 
        /// Hash of the help file
        /// 
        private static string _hashCertificate;

        /// 
        /// Gets the Certificate File Resource Namespace
        /// 
        public const string CertificateFileResourceNamespace = ".Resources.CertificateFile.cer";

        /// 
        /// Gets the location of the Certificate file.
        /// 
        public const string CertificateFileLocation = @"Resources\CertificateFile.cer";

        /// 
        /// Get or Set Entry in Assembly
        /// 
        private static Assembly EntryAssembly
        {
            get
            {
                lock (_syncRoot)
                {
                    if (null == _entryAssembly)
                    {
                        _entryAssembly = Assembly.GetEntryAssembly();
                    }

                    // In a automated test scenario the Entry Assembly comes back null so grab yourself
                    if (null == _entryAssembly)
                    {
                        _entryAssembly = Assembly.GetExecutingAssembly();
                    }

                    return _entryAssembly;
                }
            }
            set
            {
                lock (_syncRoot)
                {
                    _entryAssembly = value;
                }
            }
        }

        /// 
        /// Checks if the digital signature on config are valid or not
        /// 
        /// Returns true if signatures are valid else returns false
        public static void DoWorkUsingCertificateFile()
        {
            // Your Code
            // Your Code
            // Your Code

            // Gets the cerficate file
            string file = GetCertificateFile();

            // Your Code
            // Your Code
            // Your Code
        }

        /// 
        /// Writes the cetificate file to disk if the file is not present.
        /// 
        /// Path at which file is written
        private static string GetCertificateFile()
        {
            // Certificate file location
            var certificateFile = string.Format(
                                    CultureInfo.CurrentCulture,
                                    "{0}\\{1}",
                                    Path.GetDirectoryName(EntryAssembly.Location),
                                    CertificateFileLocation);

            // Checks if certificate file already exists
            if (File.Exists(certificateFile))
            {
                // Returns the certificate file path if the hash file is not tampered
                if (!string.IsNullOrEmpty(_hashCertificate) && GetMD5Hash(GetBytesFromFile(certificateFile)).Equals(_hashCertificate))
                {
                    return certificateFile;
                }

                // Delete the file if the file is tampered
                File.Delete(certificateFile);
            }

            // Getting the default namespace
            // Common string across al the types that assembly has
            string defaultNamespace = EntryAssembly.GetTypes().Aggregate(
                                                                    string.Empty,
                                                                    (current, type) => string.IsNullOrEmpty(current) ? type.Namespace : GetCommonString(current, type.Namespace));

            // Getting the file stream embedded in the assembly using its resource information
            var certificateFileStream = EntryAssembly.GetManifestResourceStream(defaultNamespace + CertificateFileResourceNamespace);

            // Create a FileStream object to write a stream to a file
            using (FileStream fileStream = File.Create(certificateFile, (int)certificateFileStream.Length))
            {
                // Fill the bytes[] array with the stream data
                byte[] bytesInStream = new byte[certificateFileStream.Length];
                certificateFileStream.Read(bytesInStream, 0, bytesInStream.Length);

                // Storing the hash of the file
                _hashCertificate = GetMD5Hash(bytesInStream);

                // Use FileStream object to write to the specified file
                fileStream.Write(bytesInStream, 0, bytesInStream.Length);
            }

            return certificateFile;
        }


        /// 
        /// Gets the MD5 hash of the file.
        /// 
        /// The input bytes.</param>
        /// Hexadecimal hash string
        private static string GetMD5Hash(byte[] inputBytes)
        {
            var hashString = new StringBuilder();
            using (var cryptoServiceProvider = new MD5CryptoServiceProvider())
            {
                byte[] hash = cryptoServiceProvider.ComputeHash(inputBytes);

                foreach (byte b in hash)
                {
                    hashString.Append(b.ToString("x2").ToLower());
                }
            }
            return hashString.ToString();
        }


        /// 
        /// Gets the common string. (We are using this method for getting the default namespace)
        /// 
        /// The first string.</param>
        /// The second string.</param>
        /// common string
        private static string GetCommonString(string firstString, string secondString)
        {
            string commonString = string.Empty;
            int shortStringLength = firstString.Length < secondString.Length ? firstString.Length : secondString.Length;
            for (int i = 0; i < shortStringLength; i++)
            {
                if (!firstString[i].Equals(secondString[i]))
                {
                    break;
                }
                commonString = commonString + firstString[i];
            }

            // Removing trailing periods
            commonString = commonString.TrimEnd('.');

            return commonString;
        }

        /// 
        /// Gets the bytes from file.
        /// 
        /// The full file path.</param>
        /// File read in byte array
        private static byte[] GetBytesFromFile(string fullFilePath)
        {
            byte[] bytes;
            // this method is limited to 2^32 byte files (4.2 GB)
            using (FileStream fileStream = File.OpenRead(fullFilePath))
            {
                bytes = new byte[fileStream.Length];
                fileStream.Read(bytes, 0, Convert.ToInt32(fileStream.Length));
            }
            return bytes;
        }
    }
}