Introduction
In response to a request (thanks, Furty) that I extend my original example to a secure version, I decided to write this as an appendix to my original article - it's not required, but it may add value. If you haven't already done so, I encourage you to read the original article (assuming CodeProject hasn't moved it). I will assume you are comfortable with the concepts presented in that article, and I won't waste any time rehashing things covered there.
Getting down to it, in a nutshell, the problem that this article addresses centers around the security risk of having an open-ended design with published (and easily read) interfaces. If you are building an application that you want John Q. Public to be able to write modules for, do not read further. The techniques presented ensure that the only modules loaded by your shell are modules coded by your company. In my implementation, if the shell tries to load a module (whether by malicious intent or benign experiment) that wasn't compiled and signed by your company, the shell will throw an error and shut down, assuming the worst case: someone's trying to sabotage your application.
Primer: Public Key Cryptography and .NET Code Signing
There are two aspects of public key cryptography that most people consider: encrypting messages and encrypting signatures. There is a ton of information written about both, in books and on the Web, so I'll just gloss over them quickly to make sure you have enough basics handy to understand what I'm doing. If you're a pro with this technology, feel free to skip ahead - not much new follows.
As you know, public key cryptography (AKA asymmetric cryptography) involves a public and private key pair that are created simultaneously using some common algorithm, such as RSA. With message encryption, the sender uses the recipient's public key to encode the message, and the recipient decodes using her private key. Conversely, the sender can use his private key to sign the message, and the recipient can use the sender's public key to verify that the message was signed by him. It's this latter signing feature that interests us and can be applied to sign code.
The specifications for .NET compilers dictate that there will be a slot available to insert a key signature in the Manifest of a given assembly. By signing your code, you utilize this slot to ensure that people who use your components know that, indeed, these components came from you, since the only way to get this particular order of bytes in this particular slot in the Manifest is to compile the component using your private key. Here's what that section of the manifest looks like if I, and only I, sign it.
.publickey = (
00 24 00 00 04 80 00 00 94 00 00 00 06 02 00 00 00 24 00 00 52 53 41 31 00 04 00 00 01 00 01 00 9F 84 32 EE 7E CB A3 AA 31 A2 C2 47 5D CE 96 AA F0 DC AB 01 6F 6A CB E1 16 EB E8 9C 2C 42 BF 07 33 91 63 85 57 CF FB 39 CD 42 F8 84 22 53 1E E2 69 0E 4A 7C 6B 40 7C 89 22 06 5F F3 85 7B E8 84 69 78 C3 59 47 39 F3 2D 9C EE F1 E2 3F 11 44 99 B1 CD F6 18 C3 B4 03 F7 75 14 DD 68 90 6A 45 FB ED 5B FE A8 13 7D 61 C5 4D 23 79 3E E8 41 42 1F 89 02 9E 08 64 BD 6B 13 91 53 78 76 92 AB 73 C7 )
It may not look it (heh, or maybe it does), but it is statistically impossible for anyone else in the world to generate this particular signature due to the incomprehensibly large number of permutations involved in public key cryptography. The one thing a human eye can pick out in the ASCII translation of bytes is the algorithm used to encrypt the signature - "RSA1" - the rest is unreadable.
Solution Architecture
There's really not that much to the solution implementation that you couldn't probably have already guessed. Step 1 is going to be signing the component (the IModule
implementer, if you've been following from the beginning). Step 2 is going to be checking for the signature in the consuming component (the IShell
implementer). After you have your hands on a key pair, that is it! So I'm going to jump into the code details now.
Code Walk-Through
I'll assume that you've downloaded the code samples so you can follow along in as much or as little detail as you want.
Important: You need to know that you are not going to be able to use the code samples 100% as-is, because there is a private key involved, which I'm afraid I cannot share with you. In any places where I refer to the key pair, be sure you are working with your own key pair.
Generating a Key Pair
Creating your own key pair is quite simple with the .NET software provided. Depending on your company, you may already have a key pair, and there may be only couple people with access to it. If it is the case that you work in a company with highly restricted access to your private key(s), you may need to take a special step during development and only truly sign your code at deployment, using "delayed signing." For this example, though, I'm going to assume this is the first key pair you've created at your company, or that the keys are readily available to you.
If you need to create a key pair, start up a Visual Studio .NET Command Prompt. Choose a good location on your computer or on your network in which you're going to safely keep your key pair (for this demo, I just created a directory C:\Keys\). Change to this directory and run the Strong Name utility as follows to generate a key pair: sn -k SnapIns.snk. "snk" is the common extension for a strong name key, but you use whatever extension you like - no file association exists for "snk." Protect this file a you see fit - this is your certificate of authenticity for modules being loaded by your shell.
You must then extract the public key from this key pair. This public key will be distributed with your software so that the shell can validate that each module loaded was built by you (remember, you sign the code with your private key, and your public key is used to verify you signed it). To do this, use the Strong Name utility as follows: sn -p SnapIns.snk SnapIns-public.snk. Both files, when opened in a text editor, look like garbage, but you can trust that the first contains public and private keys, and the other contains only the public key that can be freely distributed. (Actually, if you want to play with the utility, you can extract the binary data to ASCII formats, though there's not much practical application for it.)
Signing an Assembly
This couldn't be much easier, thanks to .NET's attributes. To sign a assembly (component or module), open the project's AssemblyInfo.cs
and scroll to the very bottom. You will see a line that looks like this:
[assembly: AssemblyKeyFile("")]
To have the compiler sign the assembly for you, simply change it like this (of course providing the correct path to your key pair):
[assembly: AssemblyKeyFile(@"C:\Keys\SnapIns.snk")]
Note: The documentation says that you are supposed to use relative paths instead of absolute paths, but I have not had any difficulty using absolute paths, and this is my recommendation to you, considering how hard it is to get a passel of programmers to agree on anything simple like relative directory structure or lunch. Plus, I find it difficult to figure out where ..\..\..\Keys\ actually lives in relation to my binaries. Further, if you're a VB.NET programmer, the relative path is based on where your .vbproj file is, as opposed to where your compiled assembly is! Confused? Use absolute paths.
Warning: Code signing is an all-or-nothing proposition. That is, to sign a consuming assembly that references another assembly, the referenced assembly must also be signed. If you try to add the AssemblyKeyFile
attribute only to YourModule
's AssemblyInfo.cs
, when you build, you will get the following error because the Interfaces
assembly is not signed: "Assembly generation failed -- Referenced assembly 'Interfaces' does not have a strong name." Simply add the AssemblyKeyFile
attribute to the referenced assembly, Interfaces
, and you're in business. Pretty much the only assembly you can get away without signing will be Shell
.
Using Signed Modules
You don't have to do anything special with your signed modules if you don't want to, but since that is the whole point of this dissertation, you will have to make a few changes to the original Shell.csproj. If you were enterprising and followed my recommendation to create a more robust external configuration file than App.config, make sure you adjust these directions as needed. However, as before, to keep this simple and focused, I'm continuing to develop a somewhat klunky solution using App.config.
To load a signed assembly at runtime, I will need four pieces of information, as opposed to the old two that I used earlier. These are: the code base (location), the assembly's contained type (class), the simple name of the assembly, and the assembly version. So I added two more keys to App.config and renamed the "path" one to "asm," since I intend only to provide a assembly's file name, instead of its entire path. Since I'm going to need the full path still, I moved that information into the appSettings
section and called the key "ModulePath
." The reason for this path change is that I now also have another external file - the public key - that I need to locate, and I'm going to keep it with the modules for tidy bookkeeping.
With the new data in App.config, the moduleInfo
structure will need to be extended to hold this data, and of course, it will need to be loaded at runtime. This is simple work and uninteresting so I won't detail it here, and trust you can see what I did in the code sample.
What is interesting are the changes required in the LoadModule()
implementation. The method signature stays the same - ain't broke, didn't fix - but I can no longer use the Activator
object to load my assembly into a runtime object instantiation. Instead, I have to use the static Assembly.Load()
, provide it with the details of the assembly to load on disk and then cast the returned Assembly
instance into an IModule
type. All the code to do this follows:
private IModule LoadModule(ModuleInfo moduleInfo)
{
Debug.Assert(moduleInfo.asm != null);
Debug.Assert(moduleInfo.type != null);
Debug.Assert(moduleInfo.simpleName != null);
Debug.Assert(moduleInfo.version != null);
try
{
AssemblyName asmName = new AssemblyName();
string modulePath = ConfigurationSettings.AppSettings["ModulePath"];
asmName.CodeBase = string.Concat(@"file:///",
Path.Combine(modulePath, moduleInfo.asm));
asmName.CultureInfo = new CultureInfo("");
asmName.Name = moduleInfo.simpleName;
asmName.Version = new Version(moduleInfo.version);
asmName.Flags = AssemblyNameFlags.PublicKey;
FileStream publicKeyStream = File.Open(
Path.Combine(modulePath, "SnapIns-public.snk"), FileMode.Open);
byte[] publicKey = new byte[publicKeyStream.Length];
publicKeyStream.Read(publicKey, 0, (int)publicKeyStream.Length);
asmName.SetPublicKey(publicKey);
publicKeyStream.Close();
Assembly asm = Assembly.Load(asmName);
return (IModule)asm.CreateInstance(moduleInfo.type);
}
catch (System.IO.FileLoadException ex)
{
MessageBox.Show(
string.Format("Attempt to load improperly configured" +
" or unsigned module!\n({0})",
ex.Message), "Module Load Failed",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
The CodeBase
field of AssemblyName
requires a valid protocol. The downside is I can't just provide a path, but need to say I'm using the file protocol; the upside is that this could be HTTP! The Name
property seemed a little useless to me, but if it's not specified, no further checks are done on the assembly, and unsigned modules will actually load. If you're not sure what to put in here, run ILDASM on your assembly and find the bits that look similar to this in the Manifest (from YourModule.dll): .assembly YourModule {...}
. The version is important as well, so if you aren't manually setting the version in AssemblyInfo.cs
, it's time you started, unless you don't mind managing auto-generated version numbers. If any of these pieces of information are incorrect, you will throw a FileLoadException
when you call Assembly.Load()
.
Finally, to check for the signing, you need to have planned to distribute your public key with your shell and modules. (Warning : do not distribute your key pair file!) Again, I suggest keeping all the modules in a single directory and putting the public key file in that same directory - it will keep you more sane. Then, load the key using a FileStream
object, read the file into a byte array, and call the AssemblyName
instance's SetPublicKey()
with this byte array. If the module was signed with the matching private key, Assembly.Load()
will succeed! Otherwise it will fail, throwing a FileLoadException
. Huzzah! Remember, all of these things need to come together for this to work, not just the public key; if your version number is wrong, you'll wind up with the exact same exception thrown as if the module weren't signed.
For my example, I decided to handle the exception in the LoadModule()
method itself, but you could let the exception bubble up to the caller instead, if you prefer (which probably makes more sense for a real implementation). I also decided to check the return value of LoadModule()
for null
- a change from my earlier Debug.Assert
- and shut down the program immediately, assuming the presence of malicious code. That's pretty drastic, and how you want to handle this is ultimately up to you and your company's requirements. You could just quietly ignore the failed attempt; you could warn the user and continue without loading the module; or you could alert the user and have her decide whether to fall back to using Activator.CreateInstanceFrom()
if she doesn't mind ignoring the invalid or missing signature.
Testing
To test, you can try out a number of things. At least be sure to test the following:
- Unsigned module
- Signed module with mismatched public/private key
- Wrong/missing CodeBase
- Wrong/missing Name
- Wrong/missing Version
Summary
Visual Studio makes a lot of the work automatic when it comes to generating keys, signing code and checking for a trusted signature. Simply setting the appropriate assembly attribute to a valid key file will generate a signed (strong named) assembly. To load a signed assembly at runtime requires specifying additional details, including the assembly's name and version. Then to actually perform the signing check, set the public key with the distributed copy's bytes (as well as the assembly's name and version!) before attempting to load from disk, and only properly signed assemblies will load without throwing an exception.
Any questions, comments and brickbats may be Emailed to todd.sprang@cox.net. Good luck and happy coding!
Change History
- 09.09.03, 09.11.03 - First Draft