An overview of digital signatures in .NET

Introduction

A digital signature in software has about the same role as a real signature on a document. It proves that a certain person has signed the document thereby authenticating it. A signature increases security around a document for both parties involved, i.e. the one who signed the document – the signee – and the one that uses the document afterwards. The one who signed can claim that the document belongs to them, i.e. it originates from them and cannot be used by another person. If you sign a bank loan request then you should receive the loan and not someone else. Also, the party that takes the document for further processing can be sure that it really originates from the person who signed it. The signee cannot claim that the signature belongs to some other person and they have nothing to do with the document. This latter is called non-repudiation. The signee cannot deny that the document originates from him or her.

Digital signatures in software are used to enhance messaging security. The receiver must be able to know for sure that the message originated with one specific sender and that the sender cannot claim that it was someone else who sent the message. While it is quite possible to copy someone’s signature on a paper document it is much harder to forge a strong digital signature.

In this post we’ll review how digital signatures are implemented in .NET

In software

First of all do not confuse digital signatures with digital certificates. They are similar concepts but digital certificates are generally more complex and more difficult to work with in code. If you want to know more about digital signatures then there are various posts dedicated to this topic on this blog:

Signatures in software bring together hashing and asymmetric encryption. Signature creation and verification are based on asymmetric cryptography with its pair of public and private keys. If John wants to send a message to Jane and wants to sign it then the flow is the following:

  • John hashes the message with some hashing algorithm like MD5 or SHA256
  • He also generates an asymmetric pair of keys
  • He then signs the hash of the message with the private key and sends the hashed message, the signature and the public key of the asymmetric key-pair to Jane
  • Jane uses the public key to verify the signature. If verification passes, i.e. the signatures are identical, then she’ll know that the message is authentic

The above flow is based on the fact that the public and private keys go hand in hand in asymmetric cryptography, i.e. they are not independent objects. If a digital signature is verified with the supplied public key then we know that it can only have been signed by the sender since only they have access to the private key. This is the basis for the trust between the sender and the receiver of the message.

Digital signatures require a basic understanding of hashing and asymmetric cryptography. If you’re not sure how they work then you can start here:

We’ll reuse much of the code presented in those posts.

The most widely used asymmetric encryption algorithm is RSA. Hence the the default choice for a digital signature implementation is also based on RSA. The class to build a signature of a hashed message is called RSAPKCS1SignatureFormatter. Similarly, the class for signature verification is called RSAPKCS1SignatureDeformatter. We’ll use the RSACryptoServiceProvider to create an asymmetric key pair for signing and verification. We’ll also see SHA256 class in action to hash the message to be sent to the receiver.

Basic objects for the demo

Let’s start with the hashing algorithm. We’ll have an interface for the hashing service…:

public interface IHashingService
{
	byte[] CalculateMessageDigest(string originalMessage);
	string GetHashAlgorithmDescription();
}

…and a SHA256-based concrete implementation:

public class Sha256HashingService : IHashingService
{
	public byte[] CalculateMessageDigest(string originalMessage)
	{
		SHA256 sha256 = SHA256.Create();
		return sha256.ComputeHash(Encoding.UTF8.GetBytes(originalMessage));
	}

	public string GetHashAlgorithmDescription()
	{
		return "SHA256";
	}
}

The GetHashAlgorithmDescription will be used for the RSAPKCS1SignatureFormatter and RSAPKCS1SignatureDeformatter classes. Unfortunately they require the hashing algorithm to be declared with a string. The hashing algorithm for hashing the message must match the algorithm declared when creating and verifying the signature. If the message is hashed with MD5 but we declare SHA512 as the hash algorithm for the signature then we’ll get an exception.

We’ll hide the digital signature service behind an interface as well:

public interface IDigitalSignatureService
{
	SignMessageResult SignMessage(string message);
	DigitalSignatureVerificationResult VerifySignature(SignMessageResult signMessageResult);
}

…where both SignMessageResult and DigitalSignatureVerificationResult are container objects for multiple return types and inherit from an abstract base class:

public abstract class OperationResult
{
	public bool Success { get; set; }
	public string ExceptionMessage { get; set; }
}

SignMessageResult includes the properties necessary for the receiver to verify the authenticity of the signature:

public class SignMessageResult : OperationResult
{
	public byte[] HashedMessage { get; set; }
	public byte[] Signature { get; set; }
	public string PublicSigningKeyXml { get; set; }
	public string SigningHashAlgorithm { get; set; }
}

As noted above it’s enough to export the public key for signature verification, the private key can stay with the sender. Sending the public key as XML is a convenient way to populate the RSACryptoServiceProvider object using its FromXmlString function as we’ll see later on.

DigitalSignatureVerificationResult holds the result of the signature verification:

public class DigitalSignatureVerificationResult : OperationResult
{
	public bool SignaturesMatch { get; set; }
}

The concrete digital signature service

We can now implement the IDigitalSignatureService interface:

public class RsaDigitalSignatureService : IDigitalSignatureService
{
	private readonly IHashingService _hashingService;

	public RsaDigitalSignatureService(IHashingService hashingService)
	{			
		if (hashingService == null) throw new ArgumentNullException("Hashing service");
		_hashingService = hashingService;
	}

	public SignMessageResult SignMessage(string message)
	{
		SignMessageResult result = new SignMessageResult();
		try
		{
			int rsaKeySizeBits = 2048;
			RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(rsaKeySizeBits);
			string publicKeyXml = rsaProvider.ToXmlString(false);
			RSAPKCS1SignatureFormatter signatureFormatter =
				new RSAPKCS1SignatureFormatter(rsaProvider);
			string signingHashAlgorithm = _hashingService.GetHashAlgorithmDescription();
			signatureFormatter.SetHashAlgorithm(signingHashAlgorithm);
			byte[] hashedMessage = _hashingService.CalculateMessageDigest(message);
			byte[] signature = signatureFormatter.CreateSignature(hashedMessage);
			result.PublicSigningKeyXml = publicKeyXml;
			result.Success = true;
			result.HashedMessage = hashedMessage;
			result.Signature = signature;
			result.SigningHashAlgorithm = signingHashAlgorithm;
		}
		catch (Exception ex)
		{
			result.ExceptionMessage = ex.Message;
		}

		return result;
	}

	public DigitalSignatureVerificationResult VerifySignature(SignMessageResult signMessageResult)
	{			
		DigitalSignatureVerificationResult result = new DigitalSignatureVerificationResult();
		try
		{
			RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider();
			rsaProvider.FromXmlString(signMessageResult.PublicSigningKeyXml);
			RSAPKCS1SignatureDeformatter signatureDeformatter = new RSAPKCS1SignatureDeformatter(rsaProvider);
			signatureDeformatter.SetHashAlgorithm(signMessageResult.SigningHashAlgorithm);
			bool signatureOk = signatureDeformatter.VerifySignature(signMessageResult.HashedMessage, 
				signMessageResult.Signature);
			result.SignaturesMatch = signatureOk;
			result.Success = true;
		}
		catch (Exception ex)
		{
			result.ExceptionMessage = ex.Message;
		}
		return result;
	}
}

RsaDigitalSignatureService needs an IHashingService in order to hash the message to be signed.

In SignMessage we generate an asymmetric key-pair of 2048 bits or 256 bytes and export the public key using ToXmlString. The false boolean parameter means that we don’t want to export the private key. We then construct an RSAPKCS1SignatureFormatter object and supply the RSA provider as the underlying asymmetric encryption algorithm. We also need to set the hash algorithm for RSAPKCS1SignatureFormatter which we get from the hashing service implementation. The message is hashed using CalculateMessageDigest in the hashing service and is signed with RSAPKCS1SignatureFormatter.CreateSignature. Finally we populate the various fields of the SignMessageResult object.

In VerifySignature we create an RSACryptoServiceProvider using the public key from SignMessageResult. RSAPKCS1SignatureDeformatter also needs an asymmetric encryption algorithm and a hashing algorithm. The VerifySignature function accepts the hashed message and the signature as byte arrays.

Usage

The following method demonstrates how the above components can be linked together:

public void TestDigitalSignatures()
{
	IDigitalSignatureService ser = new RsaDigitalSignatureService(new Sha256HashingService());
	string message = "Hello dear receiver";
	SignMessageResult signMessageResult = ser.SignMessage(message);
	if (signMessageResult.Success)
	{
		Console.WriteLine($"Message signed. Signature base b64: {Environment.NewLine}{Convert.ToBase64String(signMessageResult.Signature)}");
		DigitalSignatureVerificationResult signatureVerificationResult = ser.VerifySignature(signMessageResult);
		if (signatureVerificationResult.Success)
		{
			Console.WriteLine($"Signatures match: {signatureVerificationResult.SignaturesMatch}");
		}
		else
		{
			Console.WriteLine($"Signature verification failure: {signatureVerificationResult.ExceptionMessage}");
		}
	}
	else
	{
		Console.WriteLine($"Message signing failure: {signMessageResult.ExceptionMessage}");
	}
}

Running the above example code produces an output similar to the following:

Message signed. Signature base b64:
NYBqKocZajqfp9cFbEiVrTcrqvoxzhauk/DBg36DBaWYzv5OLmDOKee1yc8tPqZFaLVqU/yTVsoGsbMb4IPE6vHaCS/lLQF8b48zxT1h4ALvxBU7eaRvKa/x2Ht+WKp09bnuLA4cJG4tD3+dVIvyGKgafr9WLat/TMEJwgEhzW7bgt8B94uJSfKw9qUjF7u70g5YObs7TBkDQLwAEC6X4pSCyMViP39YddkqbVSzsn6i/Zagk+elduBqd4cqImxTX+FhSyDcPpkoR94kW7sAs67gIVeT6QAPArLvcDrIhlDKx9vqnSN5XxG20MfgO8usZIQk9NmZckfeA7ie/Z5GwA==
Signatures match: True

You can simulate a signature mismatch by stopping code execution at…:

result.Signature = signature;

…in RsaDigitalSignatureService.SignMessage and change a couple of bytes in the “signature” byte array in Visual Studio. The VerifySignature function will return false in that case.

You can view the list of posts on Security and Cryptography here.

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a comment

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.