The key to strong names

This article was originally published in VSJ, which is now part of Developer Fusion.
In Strength in Naming (VSJ, September 2006) I described the use of strong names in the .NET environment. Strong named assemblies have a cryptographic digital signature which is generated using a public/private key pair. These keys may be stored in a key container (provided by the Windows Cryptographic Service Provider or CSP), or in a key file (usually with the extension .snk). In the 2.0 version of the frame work there is a third option, which is to store the key in a .pfx file.

Working with keys

For a project I was working on recently I wished to be able to obtain the public key token that would appear on an assembly. One way of doing this is to load the assembly itself and get the AssemblyName object for it, which in turn provides a function to obtain the public key:
Assembly ass = Assembly.LoadFile(
	assemblyPath );
AssemblyName n =
		assembly.GetName();
byte [] token =
		n.GetPublicKeyToken();
This is very straightforward if you have an assembly that has been signed with the relevant key, but I also wanted to be able to obtain the public key token directly from a .snk file or container, as this is really where the public key token is determined.

The sn command line tool can read .snk files, but to generate the public key token it requires a file that contains only the public key. This means that where a private key .snk file is available, it must first be converted to public key format, and then the public key token requested. This is complicated, and the sn tool is only installed with the .NET development tools and therefore we can’t guarantee that it will be available. The sn tool does not offer any means to read keys from key containers either, only to install them.

It is possible to read SNK files using the unmanaged StrongName API, but dropping into unmanaged code has a performance penalty, and also requires an assembly to have a very high level of trust. I wanted an application that can read an SNK file with only a minimum of trust – i.e. it will need permission to read the file but should not require many further permissions. I therefore had to set out to read and decode the SNK file for myself.

File format

The first stage of the task is to understand the file format used in snk files. The .NET documentation implies that these files essentially contain key “BLOB” structures as defined in the Windows Cryptography API.

In fact, a little reverse engineering has shown that the sn tool uses slightly different formats for public and private key storage files (see Table 1).

Table 1: .snk file structures

Item Public Key Private Key
PublicKeyBlob structure Y N
BLOBHEADER structure Y Y
RSAPUBKEY structure Y Y
Modulus parameter Y Y
Private key parameters N Y

Intuitively, the only difference between the files would be the presence or absence of the private key parameters, however in practice the public key file has an additional header structure. The contents of these structural elements are described fully in Table 2.

Table 2: .snk file detail

Item Description C# Type Public Private
PublicKeyBlob Signing Algorithm ID UInt32 Y N
Hash Algorithm ID UInt32
Count of bytes in rest of BLOB UInt32
BLOBHEADER Type of Key byte Y Y
Version of Structure byte
Reserved UInt16
Algorithm ID UInt32
RSAPUBKEY Magic key identifier UInt32 Y Y
Bit Length of the key UInt32
Public Exponent UInt32
Modulus Modulus byte [] Y Y
Private key Prime1 byte [] N Y
Prime2 byte []
Exponent1 byte []
Exponent2 byte []
Coefficient byte []
Private Coefficient byte []

Binary files in .NET

Regardless of whether the SNK file contains just the public key or the public/private key pair, the format consists of a number of binary formatted elements. One way to read these elements in the .NET environment would be to use the FileStream class that allows you to read a byte, or an array of bytes, and to manually reconstruct the higher-level types from the bytes directly. The BinaryReader class (layered on top of a FileStream) is a better alternative in this case since it enables us to read the higher-level types directly from the file. It is not necessarily a good solution in all cases however, because it only works with Windows type little-endian encoded types.

This listing shows how the BinaryReader class can be used to read the elements of a public or private key .snk file:

public static void ReadPublicKeySNKFile(
	BinaryReader br) {
	br.BaseStream.Position = 0;
	// Read PublicKeyBlob
	UInt32 algorithmIdSignature =
		br.ReadUInt32();
	UInt32 algorithmIdHash = br.ReadUInt32();
	UInt32 countBlobBytes = br.ReadUInt32();
	// Read BLOBHEADER
	byte keyType = br.ReadByte();
	byte blobVersion = br.ReadByte();
	UInt16 reserverd = br.ReadUInt16();
	UInt32 algorithmID = br.ReadUInt32();
	// Read RSAPUBKEY
	string magic=new string(br.ReadChars(4));
	UInt32 keyBitLength = br.ReadUInt32();
	UInt32 rsaPublicExponent=br.ReadUInt32();
	// Read Modulus
	byte[] rsaModulus = br.ReadBytes((int)
		keyBitLength/8);
	// Reverse byte arrays so they are
	// formatted to read as a number
	Array.Reverse( rsaModulus );
}
public static void ReadPrivateKeySNKFile(
	BinaryReader br ) {
	br.BaseStream.Position = 0;
	// Read BLOBHEADER
	byte keyType = br.ReadByte();
	byte blobVersion = br.ReadByte();
	UInt16 reserverd = br.ReadUInt16();
	UInt32 algorithmID = br.ReadUInt32();
	// Read RSAPUBKEY
	string magic=new string(br.ReadChars(4));
	UInt32 keyBitLength = br.ReadUInt32();
	UInt32 rsaPublicExponent=br.ReadUInt32();
	// Read Modulus
	byte[] rsaModulus = br.ReadBytes(
		(int)keyBitLength / 8 );
	// Read Private Key Paremeters
	byte[] rsaPrime1 = br.ReadBytes(
		(int)keyBitLength / 16 );
	byte[] rsaPrime2 = br.ReadBytes(
		(int)keyBitLength / 16 );
	byte[] rsaExponent1 = br.ReadBytes(
		(int)keyBitLength / 16 );
	byte[] rsaExponent2 = br.ReadBytes(
		(int)keyBitLength / 16 );
	byte[] rsaCoefficient = br.ReadBytes(
		(int)keyBitLength / 16 );
	byte[] rsaPrivateExponent = br.ReadBytes(
		(int)keyBitLength / 8 );
	// Format to read as a number
	Array.Reverse( rsaModulus );
	Array.Reverse( rsaPrime1 );
	Array.Reverse( rsaPrime2 );
	Array.Reverse( rsaExponent1 );
	Array.Reverse( rsaExponent2 );
	Array.Reverse( rsaCoefficient );
	Array.Reverse( rsaPrivateExponent );
}
These functions don’t actually do anything with the data once it has been read, but demonstrate the principle. Note that the byte arrays are conventionally reversed – they represent numbers that are stored in little-endian form on disk but are used by cryptography components arranged in big-endian form.

If the key(s) were to be used for other cryptographic applications the next stage would be to convert them to the framework RSAParameters structure. This is actually a useful means of communicating key information within an application anyway even if the keys are not to be used with other cryptographic functions. The mappings between the names used in the blob format description in the MSDN documentation and the RSAParameters structure elements are shown in Table 3.

Table 3: Mapping a BLOB to RSAParameters

BLOB element RSAParameters element
PublicExponent Exponent
Modulus Modulus
Prime1 P
Prime2 Q
Exponent1 DP
Exponent2 DQ
Coefficient InverseQ
PrivateExponent D

Writing .snk files

The public key token is calculated on the binary data contained within the public key version of the .snk file format, and the cryptography functions in .NET all operate on a byte[]array. Where the source .snk file contains only the public key, we could create the required byte[] array by reading the whole file in one go with code like this:
byte[] data = new byte[fs.Length];
fs.Read( data , 0 , (int)fs.Length );
However, when the format of the source .snk file contains the private key too, we must re-format the data into the public key version of the format. This requires us to understand a little more detail about what the elements of the file mean, beyond the elements that form the RSA key itself. Table 4 describes these elements.

Table 4: .snk file element values

Description Value
Signing Algorithm ID always 0x00002400 (RSA)
Hash Algorithm ID always 0x00008004 (SHA)
Type of Key 0x06 (public) or 0x07 (private)
Version of Structure always 0x02
Reserved always 0x0000
Algorithm ID always 0x00002400 (RSA)
Magic key identifier “RSA1” (public)/“RSA2” (private)

To write values to a file we can use a BinaryWriter wrapping a FileStream in the reverse of the process we used above, using the information in Table 4 to fill in the correct values for a public key rather than a private one, where the formats differ.

In this case however, we actually want to end up with a byte[] array to pass to a cryptography function. We can achieve this by using a MemoryStream object underneath the BinaryWriter instead of a FileStream:

public static byte[]
	WritePublicKey(UInt32
	keyBitLength, UInt32
	publicExponent, byte[] modulus) {
	Debug.Assert( modulus.Length ==
		(keyBitLength/8),
		“Supplied modulus does not
		match keyBitLength” );
	MemoryStream ms =
		new MemoryStream();
	BinaryWriter bw =
		new BinaryWriter(ms);
	bw.Write((UInt32)0x00002400);
	bw.Write((UInt32)0x00008004);
	bw.Write( modulus.Length + 20 );
	bw.Write( (byte)0x06 );
	bw.Write( (byte)0x02 );
	bw.Write( (UInt16)0x0000 );
	bw.Write( (UInt32)0x2400 );
	bw.Write( “RSA1”.ToCharArray() );
	bw.Write( keyBitLength );
	bw.Write( publicExponent );
	// Convert number to little-
	// endian format for writing
	// to file
	byte[] m =
		(byte[])modulus.Clone();
	Array.Reverse( m );
	bw.Write( m );
	return ms.ToArray();
}

Calculating the Public Key Token

The Public Key Token is the last 8 bytes of the hash of the public key .snk file. It is usually displayed as the hexadecimal form of a UInt64 rather than simply as a sequence of bytes. More specifically, the hash algorithm used is SHA1.

In the .NET Framework there is an abstract class SHA1 that provides a base from which classes implementing the SHA1 algorithm can be derived. The framework also provides an implementation in the shape of the SHA1CryptoServiceProvider class, which we can use here to calculate the hash of the public key .snk file:

SHA1 crypto = new
SHA1CryptoServiceProvider();
byte [] hash =
	crypto.ComputeHash(data);
Only the last 8 bytes of the computed hash are used in the Public Key Token, and these must be transformed into an UInt64 for convenient use. To carry out this transformation we can use the reverse of the process we used to write a .snk file. This time, we wrap the byte[] with a MemoryStream, seek to the appropriate position, and then use a BinaryReader to read the data in the form we need it:
MemoryStream ms = new MemoryStream(hash);
ms.Seek(-8, SeekOrigin.End);
BinaryReader br = new BinaryReader(ms);
UInt64 token = br.ReadUInt64();
A complete function is:
public static UInt64 GetStrongNameToken(
	RSAParameters rsaParameters ) {
	MemoryStream msi = new MemoryStream();
	BinaryWriter bw = new BinaryWriter( msi );
	bw.Write( (UInt32)0x2400 );
	bw.Write( (UInt32)0x8004 );
	bw.Write( rsaParameters.Modulus.Length + ( 4 * 5 ) );
	bw.Write( (byte)0x06);
	bw.Write( (byte)0x02 );
	bw.Write( (UInt16)0x0000 );
	bw.Write( (UInt32)0x2400 );
	string magic = “RSA1”;
	bw.Write( magic.ToCharArray() );
	bw.Write((UInt32)(rsaParameters.Modulus.Length * 8));
	// Note that externally generated RSAParameters may
	// have missing leading 0 bytes!
	byte[] e = (byte[])rsaParameters.Exponent.Clone();
	Array.Reverse( e );
	byte[] rightlength_e = new byte[] { 0 , 0 , 0 , 0 };
	e.CopyTo( rightlength_e , 0 );
	bw.Write( rightlength_e );
	byte[] m = (byte[])rsaParameters.Modulus.Clone();
	Array.Reverse( m );
	bw.Write( m );
	SHA1 crypto = new SHA1CryptoServiceProvider();
	byte[] hash = crypto.ComputeHash( msi.ToArray() );
	msi.Close();
	MemoryStream mso = new MemoryStream( hash );
	mso.Seek( -8 , SeekOrigin.End );
	BinaryReader br = new BinaryReader( mso );
	UInt64 token = br.ReadUInt64();
	mso.Close();
	return token;
}

Keys in containers

The second part of my challenge was to calculate the public key token for a key stored in a key container. Key containers are essentially named “slots” in which a key can be stored. They are managed by the Windows Cryptography API, and their main benefit is that the key can be used without extracting it from the container. In fact, it is quite usual for keys to be stored in such a way that they can never be extracted! This means that once a key is installed it can be used by the (trusted) cryptography code in Windows, but cannot be read by other (potentially malicious) software. This makes it very difficult for someone to steal a copy of the key, even if they have access to the machine on which it is stored.

Containers may have either machine or user scope – i.e. they may be available to all users, or only to the user that installed the key. Each container may also store multiple keys with different numbers, and potentially different applications.

When the sn tool is used to install a key into a container, it always installs it as non-exportable. Although in the context of RSA keys such as those used in strong naming the public key is exportable, it is just the private key parameters that are not. It also installs the key as number 2, which is used for signing keys, the default is 1 which is used for key exchange. These correspond to AT_SIGNATURE and AT_KEYEXCHANGE in the native Windows Cryptography API. The –m flag of the sn tool can be used to switch between user and machine stores, and to check which store is currently active, the default being to use the machine store.

Accessing key containers

Fortunately the .NET framework provides a mechanism for interacting with key containers, however, it is not a particularly intuitive part of the framework to use and is not particularly transparent. Reading and writing key containers us accomplished using the RSACryptoServiceProvider class, usually configured with a CspParamters passed to the constructor. For keys used in strong names, the KeyNumber property of CspParameters should be set to 2 and the KeyContainerName to the name of the container to use. To use the machine store you will also need to explicitly set Flags to CspProviderFlags.UseMachineKeyStore.

To read a key, all you need do is create a new RSACryptoServiceProvider instance using the appropriate CspParameters. The ExportParameters function will then read the public (or public and private) key into an RSAParameters structure. Writing a key is similar, except that ImportParameters is used instead of ExportParameters. Finally, delete a key by setting the RSACryptoServiceProvider.PersistKey flag to false, then calling the Clear function on it. Code for reading, writing and deleting a key is:

static RSACryptoServiceProvider GetProviderForContainer(
	string containerName , bool machineScope ) {
	CspParameters csp = new CspParameters();
	csp.KeyContainerName = containerName;
	csp.KeyNumber = 2;
	csp.Flags = CspProviderFlags.UseNonExportableKey;
	if ( machineScope ) 	{
		csp.Flags |= CspProviderFlags.UseMachineKeyStore;
	}
	return new RSACryptoServiceProvider( csp );
}
static RSAParameters GetStrongNamePublicKeyFromContainer(
		string continerName , bool machineScope ) {
	RSACryptoServiceProvider crypto =
	GetProviderForContainer(continerName, machineScope);
	RSAParameters p = crypto.ExportParameters( false );
	crypto.Clear();
	return p;
}
static void WriteStrongNameKeyToContainer(string
	continerName, bool machineScope, RSAParameters
	rsaParameters ) {
	RSACryptoServiceProvider crypto =
	GetProviderForContainer(continerName, machineScope);
	crypto.ImportParameters( rsaParameters );
}
static void DeleteStrongNameKeyContainer(
	string continerName , bool machineScope ) {
	RSACryptoServiceProvider crypto =
	GetProviderForContainer(continerName, machineScope);
	crypto.PersistKeyInCsp = false;
	crypto.Clear();
}
From the point of view of my app, I can use the GetPublicKeyTokenFromContainer function to retrieve the key in RSAParameters and then use the GetStrongNameToken function to get the public key token.

Changes in .NET 2.0

This code is compatible with .NET Framework 1.x. If I had been able to use 2.0, I would have had the benefit of the new ImportCspBlob and ExportCspBlob functions in the RSACryptoServiceProvider class, which would have saved me some of the binary formatting and decoding tasks. .NET 2.0 also introduces the possibility of using .pfx files in to sign assemblies, and my application will need to be able to read these too. pfx files are considerably more complicated than snk files however, and are always encrypted and password protected (at least when used in assembly signing). Fortunately, .NET 2.0 also provides a straightforward mechanism for reading pfx files:
private static void
	GetRSAParametersFromPFXFile(
	string fileName, string password)
{
	X509Certificate2 certificate =
		new X509Certificate2();
	certificate.Import(filename,
		password,
		X509KeyStorageFlags.
		MachineKeySet);
	RSACryptoServiceProvider rsa =
		(RSACryptoServiceProvider)
		certificate.PublicKey.Key;
	return rsa.ExportParameters(
		false);
}

Conclusions

Although reading .snk files and key containers, and getting the resulting public key token, are fairly specific requirements, these techniques have wider applicability for reading binary file formats (including those saved by MFC apps) and cryptography.


Ian Stevenson has been developing Windows software professionally for 10 years, in areas ranging from WDM device drivers through to rapid-prototyping of enterprise systems. Ian currently works as a consultant in the software industry, and can be contacted at [email protected].

You might also like...

Comments

About the author

Ian Stevenson United Kingdom

Ian Stevenson has been developing Windows software professionally for 10 years, in areas ranging from WDM device drivers through to rapid-prototyping of enterprise systems. Ian currently works a...

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Debuggers don't remove bugs. They only show them in slow motion.”