Checking For A Valid Strong Name Signature

Recently a question came up from someone who was trying to have a plugin architecture for their application, but wanted to do some checks before loading a plugin.  Specifically, they wanted to ensure that the plugin was signed with a specific public key.  An initial attempt at this code might produce something like this:

/// <summary>
/// Check an assembly to see if it has the given public key token
/// </summary>
/// <remarks>
/// Does not check to make sure the assembly's signature is valid.
/// Loads the assembly in the LoadFrom context.
/// </remarks>
/// <param name='assembly'>Path to the assembly to check</param>
/// <param name='expectedToken'>Token to search for</param>
/// <exception cref='System.ArgumentNullException'>If assembly or expectedToken are null</exception>
/// <returns>true if the assembly was signed with a key that has this token, false otherwise</returns>
public static bool CheckToken(string assembly, byte[] expectedToken)
{
    if(assembly == null)
        throw new ArgumentNullException("assembly");
    if(expectedToken == null)
        throw new ArgumentNullException("expectedToken");

    try
    {
        // Get the public key token of the given assembly
        Assembly asm = Assembly.LoadFrom(assembly);
        byte[] asmToken =  asm.GetName().GetPublicKeyToken();
            
        // Compare it to the given token
        if(asmToken.Length != expectedToken.Length)
            return false;
            
        for(int i = 0; i < asmToken.Length; i++)
            if(asmToken[i] != expectedToken[i])
                return false;

        return true;
    }
    catch(System.IO.FileNotFoundException)
    {
        // couldn't find the assembly
        return false;
    }
    catch(BadImageFormatException)
    {
        // the given file couldn't get through the loader
        return false;
    }
}

The problem here is that this code only gets the public key token, it doesn't check for a valid signature.  Anyone could hack their assembly and modify the reported public key token, but you'd be unable to discover this without checking to see if the assembly passes verification.

There's no managed API to verify that an assembly's strong name is valid, so you must P/Invoke out to StrongNameSignatureVerificationEx (located in mscorsn.dll in v1.0 and 1.1 of the framework, mscorwks.dll in Whidbey ... both versions provide a forwarding stub in mscoree.dll).  The P/Invoke signature to use looks like:

[DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool  pfWasVerified);

The parameters can be little confusing, so let me provide a short explanation.  wszFilePath is, as you would expect, the path to the file that is to be checked.  fForceVerification is a boolean flag that tells StrongNameSignatureVerificationEx if it should set the pfWasVerified output flag.  The return value from the API is true if the strong name is valid, false otherwise.

fForceVerification and pfWasVerified work together to allow you to use the skip verification list.  (More details on this can be found in my post on delay signing).  If you pass false to fForceVerification, StrongNameSignatureVerificationEx will consult the skip verification list, and the return value of the API call will be true if the assembly was on the list.  However, since no verification was actually performed, the pfWasVerified flag will be set to false.  pfWasVerified is only set if fForceVerification is set to false, if you pass true to this parameter, pfWasVerified will be meaningless.

It's a little easier to think of it this way.  StrongNameSignatureVerificationEx will by default consult the skip verification list, and return true if the assembly is on that list.  If you really want to force the issue, pass true into fForceVerification in order to force the API into actually doing the signature check on the input assembly.

Here's some scenarios that might help to clarify:

fForceVerification fWasVerified Return Value
Strongly Named Assembly true true true
Strongly Named Assembly false true true
Strongly Named and Tampered With true false false
Strongly Named and Tampered With false false false
Delay Signed, Skip Verification true false false
Delay Signed, Skip Verification false false true

Using that information, combined with the code from the beginning of the post, it's pretty easy to check the validity of signatures on an assembly (and check that you know who signed them).  For instance, here's some code that checks for assemblies that ship with the .NET Framework:

// check the signature first
bool notForced = false;
bool verified = StrongNameSignatureVerificationEx(assembly, false, ref notForced);
Console.WriteLine("Verified: {0}\nForced: {1}", verified, !notForced);
        
// check to see if it is a Microsoft assembly
byte[] msClrToken = new byte[]    { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 };
byte[] msFxToken = new byte[]    { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a };
bool isMsAsm = CheckToken(assembly, msClrToken) || CheckToken(assembly, msFxToken);
        
if(isMsAsm && verified && notForced)
    Console.WriteLine("Microsoft signed assembly");
else if(isMsAsm && verified && !notForced)
    Console.WriteLine("Microsoft delay signed assembly");
else if(isMsAsm && !verified)
    Console.WriteLine("Microsoft assembly, modified since signing");
else
    Console.WriteLine("Not a Microsoft assembly");