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].
Comments