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 6.2.1.3, 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