Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

FIPS Compliant Digital Signatures in an ASP.NET Web Application

0.00/5 (No votes)
17 Nov 2015 2  
In this article, I explain how I went about creating a FIPS compliant digital signature solution and the reasons for my chosen techniques.

Introduction

The following is an account of some recent adventures implementing digital signatures for a web application. Forcing the .NET Framework to use algorithms compliant with the Federal Information Processing Standards (FIPS) proved to be less than intuitive. The following offers up a solution as well as documenting in detail the techniques used and the reasons for choosing them.

The customer’s original intent was to use a private key on a smart card, but access to the private key is not possible without the installation of additional software (the CAPICOM ActiveX control allowed for this, but it is no longer included in more recent Windows operating systems and wasn’t a completely portable solution anyway). An alternative technique would be to create a public/private key pair on the server whenever a document is moved forward through its workflow and use the newly generated private key to create a digital signature.

RSA Cryptography and Digital Signatures

RSA is a public-key cryptosystem created at MIT in 1977 by Ron Rivest, Adi Shamir, and Leonard Adleman, their last names being the reason for the name RSA. Interestingly, an English mathematician named Clifford Cocks had developed a similar cryptosystem in 1973, but it remained classified by British Intelligence until 1997. The main concept behind RSA encryption is its use of very large prime numbers to create mathematically related values used to encrypt and decrypt data. These values are called public and private keys, and the public key is usually published in some kind of directory such as a Certificate Authority. The private key, along with a hash algorithm, can be used to create digital signatures.

I decided to store digital signatures in my web application as serialized objects, so I created a struct named DigitalSignature. The basic definition is as follows:

private struct DigitalSignature
{
    public byte[] PublicKey;
    public byte[] Signature;
    public byte[] UserId;
    public byte[] UserName;
}

As you can see, there are four parts to this struct. Two members store the current user’s name and ID. The ID can be a logon name, an e-mail, or anything else that identifies the user, and the name is the full name to display on a page. The Signature is an encrypted hash of the object being signed (details to follow). The PublicKey is a set of mathematical values needed to decrypt the hash (the “public” part of the RSA public/private key pair or, more technically, the modulus and the exponent). I put these into a struct so I could serialize and store them together into a single database column. When stored in the database, the signature will look something like this:

0x9F0100003C5253414B657956616C75653E3C4D6F64756C75733E7536413274792F7045383172464D796F71305371485430587A2F56307831692B554B3259557A636957426B4531656662756764526C444A54732B2F4B596538653447776735705557654D694F645A556B66326F7A46564256617143744B33526746722B6663385462792B7459364F31752F3857744C546450754D68336F30463749745930615971554132615130354843304B6D3838784D4B5967685543723830525941504E6E6B314D6941664B41317766666B2B41374262756541434B6C6963457A78496C3959676B4C717159634F374D6B34586F444551766F65744D4C7736724B6E3364613556562B676D52664C616B444B306B536C6E444B2F6B457034397030654857554577446B6F5156366B48635964724749543173493436415237513231572B4779627A4C646878456647755A2F3466434E516E575546766F66564A77526F6C383279743264726E39413663354F57364C513D3D3C2F4D6F64756C75733E3C4578706F6E656E743E415141423C2F4578706F6E656E743E3C2F5253414B657956616C75653E0001000024419DE220AFC0F77D54EF98D3D729F4F444D6AEA57E8F17ABA74EB03F67E1AA3E265AB21673F044AC5CD5DD32E25A970A1FD89B5EB662F908EDFD76F157A8542BC227C75F315236C7D42BBDD8E168327FA0B549BF7D73CC64F08FE8D2722FFFFDAB27DBAA7DA83F88EACA1BAD1F4690CB87AE91BDFCFA026BB034B6C45E240C1413105C3F87AA0408A8E04F17E8CFD454C2E5FBFB2174E0B3234C2C0CE29BDF4CEAFD5B378750E116577E4070410FD19169A1437473EA4971F4ABC75C806986210AAE812D4117FCB3944251DA1AD9B3457F8A8317B413E7B00DA51A38D61D25162CE942A2BF61911FB02E9531F1A91E603FE0525B5464EEE9D179A41E7C90ED00010000756610345159126C31BCE512AA3DBBAE585CAA06E3A21EC94AFDF836B626A6C6A10E1D13C19619ADFE6A2BAAD89F2E4BA158EC836C2813A41B4D423F5D5298C6B01C2F4151CB823AD59FB20D444DB2F9E217D15E395E7AB6A19EBC4F2827174D7238DFB373DA451961891758A585E8E3F4E132E70377234D1298147865F35E68D522909A076DE5A7DF1661809F9A07513A8291938E7B8CC2FE6F75E7244CD299D79D25AA0727EC0AD081BA8A9D8273D01DAB7FAFD72B7E1276817926F13EFC853152A6A2AA34D9FFD27025F886639F9DDFDF63626A2CC092BF2BCF6EF92D3515E87C854E6B40A0C510894E0FB35BB355B9E8559CE73C43F84FFFE27B590E159A00010000B8D3ECFA1B64E8A451E45C33CBE9CDB7A7F402C0541D40FC1FF2CC8305C4CC450C6191417307BF1D4A1A3B79F8237C829487E78D78DF0931ECF715ACFF7977272A05EC4A7359B79B208BF3BA63061F59C39F03EDB5E9D8787AF45C564BCD26A8F2548AE5F7EF1D4946387DCEE9198F9CEB2B74D67AF24483EBDE4F61F7CFB7D4E72A787F30CE223E9B4B1866E55479377A25ECF96C4E6C15D78296B9B5931EFABFED012339EA21861C41C0B961C7157C9323E5F5C9EBBDB2AA4E945085FE07CF0AC73E442612467340ACFAC45A661C6F4950E3325131EC60597FD2DD463A6448E63F62528BF4CB8AFDED6BD275A8CF2586B2936381CBADDB81F02B5D160088B5

The length will vary a bit, but will always be around 1200 bytes because of the use of a 2048-bit key and SHA-512. The resulting public key information is around 415 bytes, each hash is exactly 256 bytes, and the rest is a tiny bit of overhead.

I also defined some constants for specifying the hash algorithm and the RSA key size as follows:

/// <summary>
///   The hash algorithm to use when generating digital signatures.
/// </summary>
public const string HashAlgorithm = "System.Security.Cryptography.SHA512CryptoServiceProvider";

/// <summary>
///   The number of bits to use for the key size when creating public/private
///   key pairs to use when digitally signing data.
/// </summary>
public const int RsaKeySize = 2048;

To create a new public/private key pair, I instantiated the RSACryptoServiceProvider class with the following block of code:

RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(Constants.RsaKeySize);

At a key size of 2048 bits, this line took two to three seconds to run on my workstation. Increasing the key size to 4096 resulted in a 20 second execute time. This is because of the mathematical acrobatics required when generating public/private key pairs. The provider will generate some very large numbers based on some very large primes, but pick the numbers randomly. Ensuring that the chosen numbers are prime takes some time. As an example, here is a set of numbers chosen with a key size of 384 bits:

e=65537
d=9773246456229309607437744074133646299943718950328390643269336611042842984820786028726742014932282000188069915373873
n=25316571264897243626191598157569042591281087306232084489652070244388241177057425104381474781040592424701059944739309

The modulus n is a number of the specified key size. The numbers n and d are mathematically related, and that relationship can be ascertained if one knows the two prime factors (p and q) that were used to create n. Since those prime factors are not published and since calculating large prime factors is extremely time consuming, computing d is practically impossible. Reverse engineering is made exponentially more difficult with larger key sizes – the numbers created for a 2048-bit key are unimaginably astronomical.

Learning RSA Cryptography

I created a tool for the purpose of experimenting with and learning about RSA cryptography. When using this tool, one can see all the numbers generated when creating a public/private key pair and some cryptography formulas in action. Here’s a screenshot:

FIPS Compliant Digital Signatures Screenshot

The user can choose a Key Strength with the drop-down at the upper-right corner then click the Generate Key Pair button to see the incredibly large numbers created with the chosen key strength (all numbers are converted and displayed in decimal). Be warned, generating keys with a key strength of 4096 or above may result in significant delays.

You can download the tool and its source code with the links at the top of this article.

Why is the exponent e always equal to 65537? RFC 4871 (section 3.3.1) suggests this number when using the RSA-SHA1 signing algorithm as a compromise between performance and security. It’s prime, big enough to make reverse engineering near impossible, and small enough to keep computations fast. If desired, a different exponent can be specified as part of a set of CspParameters passed to the RSACryptoServiceProvider constructor.

The numbers e and n together are the public key, while d and n together are the private key. Creating a digital signature involves hashing the data to be signed, encrypting the hash with the private key, and storing the result with the public key so that it can be decrypted and verified later.

Creating a Digital Signature

The RSACryptoServiceProvider class provides a SignData() method that accepts the data to be signed and a string representing the name of a hash algorithm to use. This method is very convenient, and accepting the hash algorithm to use as a string allows developers to specify the algorithm name in a config file and easily change it later. Unfortunately, when executing on a machine set to only use FIPS compliant algorithms, using the SignData() method will always result in a FIPS error, even when specifying a FIPS compliant hash algorithm. Determining the reason why involved inspecting some .NET Framework code.

The SignData() method uses reflection to call a static Create() method on the class associated with the specified hash algorithm name. For example, specifying a hash algorithm of "SHA512CryptoServiceProvider" will result in the execution of the SHA512CryptoServiceProvider.Create() method. The SHA512CryptoServiceProvider class inherits its Create() method from the SHA512 class, which contains the following code:

public static SHA512 Create()
{
    return SHA512.Create("System.Security.Cryptography.SHA512");
}

So even though the name "System.Security.Cryptography.SHA512CryptoServiceProvider" was specified, "System.Security.Cryptography.SHA512" will be instantiated instead. The SHA512 class is not FIPS compliant, thus the reason for the error.

The workaround is to instantiate the SHA512CryptoServiceProvider directly in your code and call the ComputeHash() method to create a hash of your data. Then, instead of calling the RSACryptoServiceProvider’s SignData() method, call the RSACryptoServiceProvider’s SignHash() method to sign the hash you just generated. Here is some code that does just that:

/// <summary>
///   Uses RSA to digitally sign supplied data.
/// </summary>
/// <param name="request">The <see cref="Document"/> 
/// object containing the data to process.</param>
/// <returns>A byte stream representing a digital signature.</returns>
public static byte[] SignRequest(Document request, Person person)
{
    DigitalSignature digitalSignature = new DigitalSignature();

    // Create a public/private key pair
    using (RSACryptoServiceProvider rsaProvider =
       new RSACryptoServiceProvider(Constants.RsaKeySize))
    {
        // Use a FIPS compliant hash algorithm
        using (SHA512CryptoServiceProvider hashProvider = new SHA512CryptoServiceProvider())
        {
            // Get the public key to be stored with the signature and used later for verification
            digitalSignature.PublicKey = Encoding.UTF8.GetBytes(
                rsaProvider.ToXmlString(includePrivateParameters: false));

            // Create the hash with a specific provider for FIPS compliance (using the rsaProvider
            // SignData method will cause hashing to occur with the SHA512Managed class, which is
            // not FIPS compliant).
            digitalSignature.Signature = rsaProvider.SignHash(
                hashProvider.ComputeHash(Encoding.UTF8.GetBytes(
                    new JavaScriptSerializer().Serialize(request))),
                    Constants.HashAlgorithm);

            // Store the name and ID of the person doing the signing for non-repudiation
            digitalSignature.UserId = rsaProvider.SignHash(
                hashProvider.ComputeHash(Encoding.UTF8.GetBytes(person.LogOnName)),
                                         Constants.HashAlgorithm);
            digitalSignature.UserName = rsaProvider.SignHash(
                hashProvider.ComputeHash(Encoding.UTF8.GetBytes(person.FullName)),
                                         Constants.HashAlgorithm);
        }
    }

    return digitalSignature.ToArray();
}

The data being signed is contained within the Document domain object. Serializing this object creates a string of data that can be easily hashed. I used the JavaScriptSerializer because it was convenient, but any serializer will do. Since the ComputeHash() method needs a byte array for input, I encoded the resulting string with UTF8 encoding, ensuring the smallest byte array possible but also supporting Unicode characters if necessary.

Since I wanted to store the resulting digital signature with the public key into a single database field, I needed to convert the whole mess into an array of bytes. Thus, the need for a DigitalSignature struct. The struct I defined also contains methods for converting all contained data to a byte array and for creating an instance of the struct from a byte array (used later for verification). I tried various serializers, but using a BinaryWriter actually resulted in a much smaller byte array. For instance, using the JavaScriptSerializer would result in a byte array of over 3K, while using the BinaryWriter kept the size down to around 1200 bytes. The complete code for the DigitalSignature struct is as follows:

private struct DigitalSignature
{
    public byte[] PublicKey;
    public byte[] Signature;
    public byte[] UserId;
    public byte[] UserName;

    public static DigitalSignature FromArray(byte[] array)
    {
        DigitalSignature digitalSignature = new DigitalSignature();
        MemoryStream     stream = null;

        try
        {
            stream = new MemoryStream(array);
            using (BinaryReader reader = new BinaryReader(stream))
            {
                stream = null;  // The BinaryReader now controls the stream and will dispose of it
                digitalSignature.PublicKey = reader.ReadBytes(reader.ReadInt32());
                digitalSignature.Signature = reader.ReadBytes(reader.ReadInt32());
                digitalSignature.UserId = reader.ReadBytes(reader.ReadInt32());
                digitalSignature.UserName = reader.ReadBytes(reader.ReadInt32());
            }
        }
        finally  // This pattern prevents Code Analysis CA2202 warning
        {
            if (stream != null)
            {
                stream.Dispose();
            }
        }

        return digitalSignature;
    }

    public byte[] ToArray()
    {
        byte[]       array;
        MemoryStream stream = null;

        try
        {
            stream = new MemoryStream();
            using (BinaryWriter writer = new BinaryWriter(stream))
            {
                stream = null; // The BinaryWriter now controls the stream and will dispose of it
                writer.Write(PublicKey.Length);
                writer.Write(PublicKey);
                writer.Write(Signature.Length);
                writer.Write(Signature);
                writer.Write(UserId.Length);
                writer.Write(UserId);
                writer.Write(UserName.Length);
                writer.Write(UserName);
                writer.Flush();

                array = ((MemoryStream)writer.BaseStream).ToArray();
            }
        }
        finally  // This pattern prevents Code Analysis CA2202 warning
        {
            if (stream != null)
            {
                stream.Dispose();
            }
        }

        return array;
    }
}

Verifying a Digital Signature

Digital signatures are meant to ensure data integrity and non-repudiation. As a document moves through its approval workflow, it could possibly be changed by personnel with appropriate permissions. To account for this and to enable validation of digital signatures for documents as they existed in a certain point in time, the web application I designed worked much like a source control system in that it stored all versions of the document. The underlying database’s design allowed for easy loading of the data contained in a document at the time a certain person moved it forward in the approval process. This snapshot of the document could then be checked for data integrity, ensuring that the data displayed in the application remained an accurate picture of the data as it existed when it was signed.

To verify data integrity, the document snapshot was loaded and rehashed with the same hash algorithm used when the digital signature was created. The hash stored in the digital signature was then decrypted with the corresponding public key and compared to the new hash. If the hashes matched, integrity was ensured. The code below performs this function:

/// <summary>
///   Verifies that the supplied data is correctly represented in the supplied digital signature.
/// </summary>
/// <param name="request">The <see cref="Document"/> 
/// object containing the data to verify.</param>
/// <param name="signature">
///   An array of bytes representing a digital signature containing previously signed data.
/// </param>
/// <exception cref="ArgumentNullException">
///   Thrown if <paramref name="request"/> is null.
/// </exception>
/// <exception cref="ArgumentNullException">
///   Thrown if <paramref name="signature"/> is null or contains no data.
/// </exception>
/// <returns>True if the supplied data matches the digital signature; otherwise, false.</returns>
public static bool VerifySignatureData(Document request, byte[] signature)
{
   DigitalSignature digitalSignature;

    if (null == request)
    {
       throw new ArgumentNullException("request", "Data to be signed must be supplied.");
    }
    if (null == signature || signature.Length == 0)
    {
       throw new ArgumentNullException("signature", "Signature data be supplied.");
    }

    using (RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider())
    {
        // Use a FIPS compliant hash algorithm
        using (SHA512CryptoServiceProvider hashProvider = new SHA512CryptoServiceProvider())
        {
            digitalSignature = DigitalSignature.FromArray(signature);
            rsaProvider.FromXmlString(Encoding.UTF8.GetString(digitalSignature.PublicKey));
            return rsaProvider.VerifyHash(
                hashProvider.ComputeHash(Encoding.UTF8.GetBytes(
                    new JavaScriptSerializer().Serialize(request))),
                    Constants.HashAlgorithm, digitalSignature.Signature);
        }
    }
}

To ensure non-repudiation, the user’s name and ID were also stored in the digital signature and associated with the public key. As long as this digital signature is not altered in any way, it is not possible for a user to deny signing a document and moving it forward in the approval process. Since the user information is also hashed and encrypted before storage and can only be decrypted with the associated public key, altering the digital signature in any way would cause verification to fail.

Points of Interest

This article shares techniques that can be used to create digital signatures in an ASP.NET web application, as well as documenting a workaround that allows the use of FIPS compliant algorithms.

History

  • 16th November, 2015: First version
  • 17th November, 2015: Updated for formatting

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here