Strong naming has always been a complicated matter. Sometimes it is even impossible when you get unsigned assemblies from a third party software vendor and you have to build strong named assemblies to install them into the GAC. When you decide to strong name your project it is a all or nothing decision. If your target is strong named then all references must also be strong named. If you do not have the source code and a project for your third party dll you will not be able to sign your project. I have created
Signer which is hosted at CodePlex that allows you to add strong names to binaries within minutes. You can even create a strong named build of your whole project without any changes to your build settings. Below is a picture which shows Signer in action:
How does it work?
Signer does basically a full round trip by decompiling the assembly into IL code make the necessary modifications and compile it back to a valid assembly. The required modifications include
- Update of all references
- Change/Removal of InternalsVisibleToAttribute
- Update of custom attributes with a type parameter
- A little fix to work around an ILDASM problem
Reference Update in IL
This is the easy part. A reference does consist of the assembly name, public key token and it's version. Signer has only to insert the public key token where none is present to patch the IL file.
.assembly extern Microsoft.Practices.EnterpriseLibrary.Common
{
.publickeytoken = (BE CA 05 5E 5B 7D 2D C8 ) // Inserted by Signer if not present
.ver 2:0:0:0
}
InternalsVisibleTo Attribute
With .NET 2.0 the
InternalsVisibleTo attribute was introduced that allows to grant other assemblies access to classes and methods marked with the internal keyword. To prevent in a strong name scenario that an unsigned assembly does get access to internals directly (you can use reflection instead but this comes with a perf cost) the CLR does check at runtime if all InternalsVisibleTo attribute declarations grant access to strong named assemblies only. If not a runtime exception is thrown and the process terminates.
.custom instance void [mscorlib]System.Runtime.CompilerServices.InternalsVisibleToAttribute::.ctor(string) =
( 01 00 48 4D 69 63 72 6F 73 6F 66 74 2E 50 72 61 // ..HMicrosoft.Pra
63 74 69 63 65 73 2E 45 6E 74 65 72 70 72 69 73 // ctices.Enterpris
65 4C 69 62 72 61 72 79 2E 4C 6F 67 67 69 6E 67 // eLibrary.Logging
2E 43 6F 6E 66 69 67 75 72 61 74 69 6F 6E 2E 44 // .Configuration.D
65 73 69 67 6E 2E 54 65 73 74 73 00 00 ) // esign.Tests.."You have to insert the public key which is over 300 bytes long into your project which is quite tedious if you want to do it manually. At the moment Signer makes life easy and simply removes the attribute to prevent run time errors but if needed the change is trivial.
Custom AttributesThis was by far the most tricky part. When you declare custom attributes with a type parameter the full qualified type name including assembly and public key token is inserted into the IL code. Normally you get this as a binary blob but with the /CAVERBAL option of ILDASM which was introduced with .NET 2.0 things get much easier.
.custom instance void [System]System.ComponentModel.EditorAttribute::.ctor(class [mscorlib]System.Type,
class [mscorlib]System.Type)
= {
type(class
'Microsoft.Practices.EnterpriseLibrary.Configuration.Design.ReferenceEditor,
Microsoft.Practices.EnterpriseLibrary.Configuration.Design, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=beca055e5b7d2dc8')
type(class
'System.Drawing.Design.UITypeEditor, System.Drawing, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a')
}
Signer does parse the attributes and does set the PublicKeyToken where needed.
ILDASM ProblemsAll would work wonderful if, well if the nice /CAVERBAL option would produce running code. But I got many ConfiguratonErrors exceptions which told me that the default value was of the wrong type. Arghhh. After digging a little deeper I found out what was going on. Suppose you have a simple configuration class for settings inside the App.Config file:
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
namespace NoRoundTrip
{
enum MyValues
{
V1
}
class Check : ConfigurationSection
{
const string PropName = "Name";
// DefaultValue is a property of type object
[ConfigurationProperty(PropName,DefaultValue=MyValues.V1)]
public MyValues Value
{
get { return (MyValues) this[PropName]; }
}
}
class Program
{
static void Main(string[] args)
{
Check c = new Check();
MyValues defValue = c.Value; // get default value
}
}
}
The program will work without problem when used as is. But after decompiling with the /CAVERBAL option and recompiling you will get a nasty runtime exception:
Unhandled Exception: System.Configuration.ConfigurationErrorsException:
The default value for the property 'Name' has different type than the one of the property itself.
Here is the innocent looking IL code
.custom instance void
[System.Configuration]System.Configuration.ConfigurationPropertyAttribute::.ctor(string)
= {
string('Name')
property object 'DefaultValue' = object( int32(0) )
}
The ConfigurationProperty DefaultValue does not preserve type identity for enums but does simply declare them as its native integer value which will lead to this exception during run time because there is an extra check code in the BCL code. The generated IL code is valid for the CLR but not for the BCL. Signer does get around this issue by decompiling every assembly once with the parser friendly /CAVERBAL option and the second time without. It will then merge the binary blobs for the ConfigurationProperty Attributes into the other IL file to get a fully functional assembly. Once this obstacle was solved all worked perfectly.
Signer Usage
If you start Signer without any options you will get a pretty self explanatory help to do signing. The only thing to remeber is that the .NET Framework tool sn is needed to generate a strong name key pair by simply executing sn -k c:\key.snk and you are ready to use Signer out of the box with your project.
Signer (C) by Alois Kraus 2007 (akraus1@gmx.de) version 1.0.0.0
Signer -k <KeyFile> -outdir <Output Dir> -a <Assembly1> <Assembly2> ... [-decomp "Options" -comp "Options" -debug]
Signer does sign all assemblies and updates their references if the assembly is not already signed
This way you can convert a whole bunch of assemblies which have never seen a key file into strong named assemblies without recompiling!
-k Key File name of key container file which is created usually by the sign tool (sn -k)
-outdir Output directory where the strong signed assemblies are copied to
-decomp Additional ildasm options e.g. "/STATS"
-comp Additional ilasm options e.g. "/X64"
-a Can be a list of assembly names or a wildcard search pattern e.g. *.dll
-debug Do not delete temporary IL files in %TEMP% flder after completion
Example: Signer -k c:\key.snk -outdir .\build -a *.dll *.exe Copy the signed assemblies into the newly created directory build.
Example: Signer -k c:\key.snk -outdir . -a *.dll *.exe Sign and overwrite the orignal binaries in current directory.
Error: No Key file specifed (use -k KeyFile ).
Error: No Output directory specifed (use -outdir Dir ).
Error: No input assemblies specified (use -a Assembly ).
Conclusions
It was fun to create the tool while learning quite a lot about the assembly structure, strong names and IL. As you can see strong names are not really a protection for your software as they can be easily removed and your assemblies resigned without big problems. The only real advantage strong names offer is that you can prove that an assembly which has your public key on it was created by you as long as your secret key is kept safe.
From this point other extensions to Signer are quite easy to add. E.g. removal of strong names, make internals public or adding your own InternalsVisibleTo attribute to other assemblies. The world is full of possibilities ;-).