The Difference Between the Strong Name Hash and Hash Evidence

The System.Security.Policy.Hash class allows you to make security decisions based upon the hash of an assembly using the HashMembershipCondition.  That sounds awfully similar to how strong names are calculated ... According to ECMA partition II section, a strong name is computed by using RSA to encrypt the SHA1 hash of the assembly.  Taking those two statements together, one logical jump to make would be that we could take an assembly's private key and use it to sign the value of the Hash.SHA1 property, creating the assembly's signature ourselves.

However, that wouldn't work as expected.  The value generated by Hash.SHA1 is a hash over every bit that makes up the assembly, whereas the hash used to create the strong name signature actually skips over portions of the assembly's PE image.  We can see this from looking up the strong name signature algorithm in the SSCLI source -- the function in question is named ComputeHash(); it begins on line 1964 of sscli\clr\src\dlls\mscorsn\strongname.cpp.

Following ComputeHash() along, after adding the the DOS header to the hash on lines 1978-1979, we move on to the NT headers.  Here we encounter the first section of the assembly that gets skipped.  In the OptionalHeaders, we zero out the checksum as well as the security data directory, which is used for Authenticode signing, before adding their values into the hash.

The reason for this is that we want the Authenticode signature to envelope the strong name signature instead of the other way around.  If we left these entries in place, providing an Authenticode signature for the assembly would change these values, invalidating our hash and thus the strong name signature.

Once the headers are hashed, lines 2004-2028 are responsible for computing the hash of the sections within the PE file.  Again, you'll notice that we don't hash every byte here -- before adding a section to the hash, we need to check to see if the strong name blob falls within that section.  If not we can hash the entire section, however if it does, we hash everything but the blob itself ... effectively removing it from our view of the assembly.  This step is obviously necessary, since once the signature is computed, writing it into the assembly will change the contents of that blob, and if it was part of the hash would immediately render the signature invalid.

Hash.SHA1 doesn't have any of these restrictions since the primary reason to use this class is to enable the HashMembershipCondition in policy.  At policy evaluation time, the assembly has already been signed with a strong name key and certificate (if it was going to be signed at all), so we don't need to skip the Authenticode directory entry.  Additionally, since the value computed by Hash is never written back to the assembly, we don't have to worry about skipping any space for it to be written to.

In summary, Hash.SHA1 is a hash over every byte in the assembly, whereas the SHA1 hash used in the signing process:

  1. Hashes the DOS headers if they exist
  2. Hashes the NT headers, after zeroing out the checksum and Authenticode directory entry
  3. Hashes each section of the PE file, skipping over the location where the signature will eventually be stored


Comments (7)

  1. Isn’t SHA-1 compromised?

    The findings are that SHA-1 is not collision free and can be broken in 2^69 attempts instead of 2^80. This is about 2000 times faster. With todays computing power and Moores Law, a SHA-1 hash does not last too long. Using a modified DES Cracker, for the small sum of up to $38M, SHA-1 can be broken in 56 hours, with current computing power.

  2. To Sushant:

    Even so Wang, Yin, Yu collision attacks on SHA1 are of the great significance to the cryptography, but public reaction on these findings is exaggerated and mostly is FUD. Collision resistance <> preimage resistance <> second premimage resistance. Assembly signature is foremost concerned second preimage which was not affected by Wang’s & co attack. Collision attack on assembly hash could only apply when using some convoluted techniques like Dan Kaminsky’s trick which is very easy to spot once you are aware of it. Wang&Co didn’t reveal details of their attacks. This particularly means that attack’s memory requirement is unclear. Looking at their note on SHA-1 attack – I see them saying that estimate attack has bounds of 2^69 ops, but mentioning that the real attack could be performed in about 2^70 on modern supercomputers. Last could be thought as indication of memory requirement of their attack, because they increased complexity bounds for the real attack and also they are not mentioning any massive parallel VLSI boards full of FPGA chips (as DES Cracker). FPGA chips could be used only incase if attack is essentially memoryless (ie. has comparatively low memory requirements). Of course, hash collision attacks could be made memoryless by using Floyd’s cycle finding algorithms, but that increases computational complexity of the attack… By the way DES Cracker has had such success also due to the luck of the team during DES Challenge II and DES Challenge III. On both challenges they managed to find the key when they checked less than 20% of total keyspace! Don’t misunderstand me – I know that 2^69 – 2^70 is achievable, but way too expensive for constructing two random collisions. That doesn’t pay off! Just for comparison you can check the effort distributed net is taking to break 72 bits key. Several thousands computers (with quite serious horse power among them) are working together for 813 days and only managed to search about 2^63 key combinations:

    So, to build a massive parallel VLSI board full of FPGA chips to make 2^69, 2^70 operations in one year timeframe would cost at least several millions… and all that just for conveying an attack which has very low probability of success… It would be much chipper to bug the processor or the mother board or use some side channel/tempest, or install rootkit or … whatever… to get guaranteed results.

    Yes, attack will get better in years to come (just as an old NSA’s saying that "attacks always get better; they never get worse"). However when it concerns SHA-1 collision resistance, even improving this attack 32 times still means that would be harder than MD5 has ever supposed to be. If we say that attack techniques will be improved to speed it four-five times then to compensate rest of 32/4 speed increase Moore’s law will require at least 12-16 years. But Moore law would not hold forever (man can’t do anithing against phisical limits like speed of light and size of electron)… Moore’s law already started to shows significant cracks. And meanwhile that attack will be quite expensive regardless of the progress – so, it will be easier to bribe the guy…


  3. AFAIK that seems to be true in Fx 2.0.

    However the Hash evidence results in Fx 1.x does return different results (wrt Fx 2.0) so I guess the algorithm doesn’t process all the bytes in that version ?

  4. Shawn says:

    Hi Sebastien,

    Nope, Hash evidence covers all bytes of the assembly on all versions of the runtime. The reason you’ll see differences is that the assembly will be different when compiled against different runtimes (even if the underlying IL is the same). Things like referenced assembly hashes (for mscorlib, etc), and the version string in the CLR header will have changed. Additionally, the metadata format in v2.0 has changed from v1.x, so you’ll have changes there too.


  5. Hello Shawn,

    I may be doing something wrong (it’s been known to happen from time to time 😉 but there is definitively something different between 1.1 and 2.0 (Dec CTP).

    1. Compile the following source with CSC 7.1;

    2. Execute it;

    3. Compile it again with CSC 8.0;

    4. Execute it;



    using System;

    using System.IO;

    using System.Reflection;

    using System.Security.Cryptography;

    using System.Security.Policy;

    class Program {

    static void Main ()


    Assembly a = Assembly.GetExecutingAssembly ();

    Hash h = new Hash (a);

    Console.WriteLine ("Hash: {0}", BitConverter.ToString (h.SHA1));

    using (FileStream fs = File.OpenRead (a.Location)) {

    SHA1 s = SHA1.Create ();

    Console.WriteLine ("SHA1: {0}", BitConverter.ToString (s.ComputeHash (fs)));





    My results are:

    C:Temp>csc hash.cs

    Microsoft (R) Visual C# .NET Compiler version 7.10.6001.4

    for Microsoft (R) .NET Framework version 1.1.4322

    Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.


    Hash: 8F-D3-8C-8F-94-48-49-5A-77-49-72-C8-B4-4C-81-2E-BE-16-E8-9B

    SHA1: 7C-25-0E-2B-9D-DE-2C-AB-90-72-2E-A0-AC-76-F6-1B-4C-F1-F4-E7

    C:Temp>csc hash.cs

    Microsoft (R) Visual C# 2005 Compiler version 8.00.41115.19

    for Microsoft (R) Windows (R) 2005 Framework version 2.0.41115

    Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.


    Hash: 09-38-EF-73-2E-02-4B-F6-6A-AA-40-72-5D-90-CF-AF-8F-79-59-83

    SHA1: 09-38-EF-73-2E-02-4B-F6-6A-AA-40-72-5D-90-CF-AF-8F-79-59-83

    Now I didn’t expect Hash to return the same value as the assembly is compiled twice. However if fx1.1 is digesting the complete assembly then the first Hash/SHA1 pair should be identical (like it is with 2.0) – shouldn’t it ?

    P.S. The same pattern occurs when loading another assembly (instead of digesting itself).

    BTW thanks for blogging on such interesting subjects!

  6. Several of us at SRT have formed a study group to better learn the .NET framework. Our eventual goal

Skip to main content