This article discusses
how to build assemblies and secure them from being tampered.
Basics
Every assembly built using the .NET compiler has a four-part name
consisting of the following elements:
-
Friendly name
-
Version number
-
Culture setting
-
Public key or
public key token
The first part is a
friendly name Like MyAssembly.
The second part is the version number say 1.0.5.0. These four numbers represent
the major version, minor version, build number, and revision number
respectively.
The assembly can be built with a specific version number by placing the
following code to the top of any of the project source files:
Imports System.Reflection
<Assembly: AssemblyVersion ("1.0.24.0")>
The AssemblyVersion attribute is defined in System.Reflection namespace. If no
version number is specified the compiler assigns a version numnber 0.0.0.0.
The third part is the culture setting indicating that the assembly has been
localized for a particular spoken language.
The fourth and the last part of an assembly name is the public key. This value
is unique and represents a particular company or developer. Two companies should
never use the same public key.
The public key occupies 128 bytes and is stored as a binary format in a key
file. It can also be written using a text based hexadecimal format as shown
below:
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
CD 79 18 40 51 8C D7 25 A5 49 13 BA 4B 4F CF 34
03 4A 7B 6E 17 8E 5C 41 9E 95 EA 78 0B E9 60 5D
9D EE 52 24 25 0A 25 90 E0 7D 4A EC 13 90 21 45
76 BB AE FB F4 B6 08 1D F4 7B 6C B9 A4 EC 62 AF
1F C8 54 B2 92 96 11 B6 B8 9D 41 8C 24 B5 30 10
0B 1C F5 1C CA D3 4A 2F D5 59 16 D5 E0 B5 B9 D6
68 61 18 4D 3B D0 E7 C4 73 6C 6B DF 41 D0 00 4F
A6 E0 9F 98 52 6C 68 FA 1E 86 CA 1D AE CE A5 B7
Because the public key is so large, a smaller 8 byte value known as a public key
token is used in its place. The public key token is usually represented by a
16-character hexadecimal string value that looks like this:
A9 FA E6 01 40 67 F5 F1
There is one to one correspondence between a public key value and a public key
token i.e. for a specific public key token there is exactly one corresponding
public key value.
How to build a strong name assembly?
An assembly is said to have a strong name when it has a public key and
a digital signature. This protects the assembly from being tampered.
The first step to build a assembly with a strong name is get a pair of
public-private key pair. The public key is written into the physical image of
the assembly. The private key is used to generate digital signatures.
The key is generated using the .NET framework strong name utility SN.exe by
specifying -k switch and the target key file from the command line.
SN.exe -k keyfilename.snk
This key file can be used by several different assembly projects.
The next step is to add the following code at the top of one of the source files
in a project:
Imports System.Reflection
<Assembly:AssemblyVersion("1.0.24.0")>
<Assembly:AssemblyKeyFile("..\..\Keyfilename.snk")>
When the project is build an assembly with a strong name is generated. The
compiler records the full 128 byte value into the manifest of the assembly file
it outputs. The compiler uses the private key to generate the digital signature.
Signing of the assembly and prevent tampering
The .NET framework relies on two industry-standard forms of
cryptography know as one way hash functions and asymmetric encryption. The hash
function allows you to create a digital fingerprint from a large digital object
such as a public key or a file image and returns a smaller value known hash
value. The hash value of an assembly serves as its digital finger print for a
particular build
The next step in generating a digital signature involves asymmetric encryption.
While signing an assembly, the private key is used to generate a digital
signature by encrypting the hash value of an assembly file. Later the public key
can be used to decrypt the digital signature and retrieve the hash value of the
assembly file.
Hence, the sequence of compilation involves the following steps:
First the compiler generates a image of the assembly file containing its
manifest, its type information, and its executable code in the form of IL.
However the compiler does not generate the digital signature and leaves a blank
space at the end of the assembly file to act as a placeholder.
Second, the compiler generates the Hash value from the image of the assemble
file that was built during the first phase. Then the compiler encrypts the hash
value with the private key to generate the digital signature and writes it into
the placeholder at the end of the assembly file.
Once the assembly has been distributed, let us discuss how the CLR verifies the
authenticity of the assembly's digital signature. The CLR uses the public key to
decrypt the digital signature to get the hash value of the assembly that was
present during compilation. Next the CLR uses the same Hash function to generate
the hash value form the assembly's physical image.
The CLR compares the two hash values and. If the hash values are equal then the
CLR considers the test a success because it is sure of two things-namely, that
the digital signature was generated by someone in possession of the private key
and the physical image of the assembly file originally signed. If the CLR
determines that the two hash values are not equal it considers the verification
test a failure. In case MyAssembly.dll is in the global assembly cache it is
verified when it was placed in the GAC and not retrieved at run time.
Lets discuss a typical case using MyApp.exe and MyAssembly.dll. Imagine you
compiled MyAssembly.dll with a strong name and, after that, you have compiled
Myapp.exe with a reference to MyAssembly.dll. It is now impossible for someone
without the private key to change the behavior of the application on the
production machine by replacing the real version of the MyAssembly.dll with a
tampered version. Note that the assemble manifest for MyApp.exe contains a
reference with the public key token associated with the same public key compile
into MyLibarary.dll. Therefore you have the guarantee that the CLR meets two
important criteria. The first is the assembly file for MyAssembly.dll contains a
public key value that matches the public key token in the assembly manifest of
MyApp.exe. The second, MyAssembly.dll has been signed by someone in procession
of the private key. When the user runs MyApp.exe and it executes code that makes
the first call to MyAssembly.dll. At this point the CLR will attempt to load
Myassembly.dll that has a strong name, so it runs a verification check. This
verification check allows the CLR to detect if someone without access to the
private key has made changed to the physical image of the assembly file.
Now if some one wants to tamper with MyAssembly.dll on the production machine,
the verification scheme thwarts the bad guy. There fore any changes made to the
physical image of the after it has been signed renders the existing digital
signature invalid. Note that this verification scheme doesn't really prevent
tampering. It only prevents tampering from going undetected. When the CLR
attempts to load a strongly named assembly with a digital signature that doesn't
match the physical layout of the assembly file, it fails to load attempt by
throwing System.IO.FileLoadExecption. This scheme would not be as reliable if
your application depended on a strongly named assemble which in turn , depended
on another assembly that did not have a strong name. The bad guy could replace
the assembly that did not have a string name and the CLR would not be able to
detect this. The CLR enforces this restriction so that you are guaranteed that a
strongly named assembly depends on only other strongly named assemblies.
The following figure shows an assembly manifest for reference. This assemble
manifest is got using the IL dissembler tool of the .NET framework.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:3300:0
}
.assembly MyAssembly
{
.custom instance void
[mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string)
= ( 01 00 10 69 6E 73 69 64 65 43 73 68 61 72 70 2E // ...insideCsharp.
6B 65 79 00 00 ) // key..
// --- The following custom attribute is added
automatically, do not uncomment
-------
// .custom instance void
[mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,
// bool) = ( 01 00 00 01 00 00 )
.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 // .$..RSA1........
CD 99 07 7B 3B 4A 0C 85 5B FC 74 0E 20 CF 11 5F // ...{;J..[.t. .._
1A E6 E7 B1 9A 76 49 8A 91 89 97 B2 FD 14 8F 13 // .....vI.........
5A 81 78 6D F7 83 B1 0E D7 88 A5 AD B3 80 3D F6 // Z.xm..........=.
C8 8C E3 D9 34 53 06 B8 7F CB 09 60 0B 00 4A E5 // ....4S.....`..J.
B0 F9 29 EB FF A2 74 81 35 FC AB 36 29 C0 DE 79 // ..)...t.5..6)..y
28 A2 31 C3 91 F7 37 D3 1C 60 31 7E 61 FE 64 A9 // (.1...7..`1~a.d.
58 B0 E4 85 D2 6E B8 49 49 0E 24 DF B4 02 00 4C // X....n.II.$....L
53 6C 2E 90 49 3F B1 2A 7A 6F 85 EE AC BA C8 B6 ) // Sl..I?.*zo......
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module MyAssembly.exe
// MVID: {9F7C6908-6359-46AA-B016-C81C15458D33}
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000009
// Image base: 0x03b50000