Parsing strong name signatures generated with sn.exe






4.58/5 (14 votes)
May 5, 2004
2 min read

116946

1635
This article shows how to parse Assembly "strong name keyfiles" generated with sn.exe
Introduction
This application shows how to parse Assembly "strong name keyfiles" generated with sn.exe.
Background
System.Security.Cryptography
provides functions to work with RSA keypairs. These functions are wrappers on the Win32 Crypto API. However, the methods provided in the 1.1 Framework do not easily parse SNK keyfiles created by sn.exe. The reason is that none of the methods will accept a PRIVATEKEYBLOB
or PUBLICKEYBLOB
structure directly. Therefore, it is necessary to write some extra code to parse those structures. Furthermore, there are additional (some would say obscure) tweaks on CpParameters
required to get an SNK keyfile to import correctly.
Using the code
You can make a new assembly keypair by running sn.exe which is available from the Framework SDK. To make a new keypair run "sn.exe -k keypair.snk". To export just the public key from your keypair, use "sn.exe -p keypair.snk pubkey.pub".
The app I wrote works pretty much as you would expect. You can create new keys, or open an SNK/PUB keypair files by pressing the buttons. If you are using a public key file, the "sign it" button will be disabled. You can also open an XML file with an encoded RSA keypair.
After your keypair is loaded, type some text on the left, hit "sign it", and then hit "verify it". Test that the signature verification really works by twiddling one character in the signature or in the original plaintext, then hitting "verify it" again - it should fail.
The guts of the app lie in RSA1024Util.cs. I hardcoded the class to use 1024 bit encryption because currently sn.exe only generates 1024 bit keyfiles. (As an aside, I wonder why 2048 bits aren't supported for assembly keys? Cryptographers have lately been warning about the eventual fall of 1024 bit keys..)
Here is the function that converts the SNK byte buffer to RSAParameters
. If the buffer is "public length" - 160 bytes - then it will only fill in the public key fields, "exponent" and "modulus". Otherwise it will read the rest of the private key fields.
The byte offsets were discovered by digging around the Win32 CryptoAPI documentation and helpful articles by Dr. Michel I. Gallant, of http://jensign.com . Note that each byte array must be reversed due to a big endian vs. little endian ordering issue.
public const int _magic_priv_idx = 0x08;
public const int _magic_pub_idx = 0x14;
public const int _magic_size = 4;
public static RSAParameters FigureParams(
byte[] keypair)
{
RSAParameters ret = new RSAParameters();
if ((keypair == null) || (keypair.Length < 1))
return ret;
bool pubonly = SnkBufIsPubLength(keypair);
if ((pubonly) && (!CheckRSA1(keypair)))
return ret;
if ((!pubonly) && (!CheckRSA2(keypair)))
return ret;
int magic_idx = pubonly ? _magic_pub_idx : _magic_priv_idx;
// Bitlen is stored here, but note this
// class is only set up for 1024 bit length keys
int bitlen_idx = magic_idx + _magic_size;
int bitlen_size = 4; // DWORD
// Exponent
// In read file, will usually be { 1, 0, 1, 0 } or 65537
int exp_idx = bitlen_idx + bitlen_size;
int exp_size = 4;
//BYTE modulus[rsapubkey.bitlen/8]; == MOD; Size 128
int mod_idx = exp_idx + exp_size;
int mod_size = 128;
//BYTE prime1[rsapubkey.bitlen/16]; == P; Size 64
int p_idx = mod_idx + mod_size;
int p_size = 64;
//BYTE prime2[rsapubkey.bitlen/16]; == Q; Size 64
int q_idx = p_idx + p_size;
int q_size = 64;
//BYTE exponent1[rsapubkey.bitlen/16]; == DP; Size 64
int dp_idx = q_idx + q_size;
int dp_size = 64;
//BYTE exponent2[rsapubkey.bitlen/16]; == DQ; Size 64
int dq_idx = dp_idx + dp_size;
int dq_size = 64;
//BYTE coefficient[rsapubkey.bitlen/16]; == InverseQ; Size 64
int invq_idx = dq_idx + dq_size;
int invq_size = 64;
//BYTE privateExponent[rsapubkey.bitlen/8]; == D; Size 128
int d_idx = invq_idx + invq_size;
int d_size = 128;
// Figure public params
// Must reverse order (little vs. big endian issue)
ret.Exponent = BlockCopy(keypair, exp_idx, exp_size);
Array.Reverse(ret.Exponent);
ret.Modulus = BlockCopy(keypair, mod_idx, mod_size);
Array.Reverse(ret.Modulus);
if (pubonly) return ret;
// Figure private params
// Must reverse order (little vs. big endian issue)
ret.P = BlockCopy(keypair, p_idx, p_size);
Array.Reverse(ret.P);
ret.Q = BlockCopy(keypair, q_idx, q_size);
Array.Reverse(ret.Q);
ret.DP = BlockCopy(keypair, dp_idx, dp_size);
Array.Reverse(ret.DP);
ret.DQ = BlockCopy(keypair, dq_idx, dq_size);
Array.Reverse(ret.DQ);
ret.InverseQ = BlockCopy(keypair, invq_idx, invq_size);
Array.Reverse(ret.InverseQ);
ret.D = BlockCopy(keypair, d_idx, d_size);
Array.Reverse(ret.D);
return ret;
}
There is one other not-well-documented switch that needs to be thrown in order to get the SNK file to be imported correctly. Keys generated by sn.exe have the KeyNumber set to 2, which is "AT_SIGNATURE
". For more information see http://www.jensign.com/JavaScience/dotnet/keyinfo/.
protected void CtorFromSnkBuf(byte[] snkbuf)
{
if ((snkbuf == null) || (snkbuf.Length < 1))
{
CtorFromXml(null);
return;
}
RSAParameters param = FigureParams(snkbuf);
// Must set KeyNumber to AT_SIGNATURE for strong
// name keypair to correctly be imported.
CspParameters cp = new CspParameters();
cp.KeyNumber = 2; // AT_SIGNATURE
_rsa = new RSACryptoServiceProvider(1024, cp);
_rsa.ImportParameters(param);
}
Why did the Framework authors make it inconvenient to work with SNK files generated by sn.exe? I don't know, but in any case there's the code to work around that issue.