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:
public const string HashAlgorithm = "System.Security.Cryptography.SHA512CryptoServiceProvider";
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:
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:
public static byte[] SignRequest(Document request, Person person)
{
DigitalSignature digitalSignature = new DigitalSignature();
using (RSACryptoServiceProvider rsaProvider =
new RSACryptoServiceProvider(Constants.RsaKeySize))
{
using (SHA512CryptoServiceProvider hashProvider = new SHA512CryptoServiceProvider())
{
digitalSignature.PublicKey = Encoding.UTF8.GetBytes(
rsaProvider.ToXmlString(includePrivateParameters: false));
digitalSignature.Signature = rsaProvider.SignHash(
hashProvider.ComputeHash(Encoding.UTF8.GetBytes(
new JavaScriptSerializer().Serialize(request))),
Constants.HashAlgorithm);
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; 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 {
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; 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 {
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:
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())
{
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