Developer Feature
Article |
Assembling Secure, Stable .NET Applications |
 |
By Scott Mauvais,
MCSD
As I write this, Microsoft� just announced
the release to manufacturing (RTM) of Microsoft Visual Studio�
.NET, and the official launch at VSLive in San Francisco is
just around the corner. So, I thought it would be a good time
to talk about the processes involved in shipping your own .NET
applications. Fortunately, streamlining the deployment of
applications is a major design goal of the Microsoft .NET
Framework, and the process is pretty straightforward. As a
result, this article is a bit shorter than usual�so you can
get back to building and deploying your new .NET applications.
I'll start off by looking at some of
the deployment problems Microsoft sought to solve with the
.NET Framework. Next I'll provide a quick introduction to
assemblies, and then I'll show you how to use them to prevent
version conflicts and ensure the security and integrity of
your application. As with most elegant solutions, you'll find
that the Microsoft .NET Framework provides a very simple (in a
good way) solution to a complex problem.
|
My goal here is to give you the basic information you need
to decide on your deployment strategy so that you can get your
applications in the hands of your end-users right away. No
fuss, no muss.
If you want to learn more about the deployment process,
including the nitty-gritty of how the common language runtime
(CLR) locates and executes your code, you'll want to pick up a
copy of Jeffrey Richter's brand new book Applied
Microsoft .NET Framework Programming from Microsoft
Press�. He has two full chapters devoted to the ins and outs
of deployment. I really like this book for two reasons. First
off, if you have read either of his earlier books (and if you
haven't you should)�Programming
Applications for Microsoft Windows� and Programming
Server-Side Applications for Microsoft Windows 2000�or his
MSDN� (and before that MSJ) columns, you
know how useful they are, and this book is just as good.
Second, in addition to all the low-level info that you are
used to in his books, this new book takes a practical approach
by giving you proscriptive advice for common design and
implementation challenges. To get a feel for what is in the
book and to make sure it is right for you, check out the
book's table
of contents and read Chapter
6: Common Object Operations.
|
The
Complicated Problem |
One of the big advances of Microsoft Windows was the
concept of DLLs, which enable you to share code across
multiple applications�no longer did each app need its own set
of utility functions such as memory management, hardware
interfaces, and the like. Back in the late 1980s, when disk
storage and memory were very expensive, this led to an
immediate and significant cost savings. This approach also
held out the promise of increased quality because you had
fewer people implementing the same functionality. In other
words, everyone could use the Microsoft WinSock components
rather than having to recreate them�and potentially introduce
bugs�themselves.
The problem, of course, came when you had several
applications using the same core set of DLLs, and you updated
one of these applications. Oftentimes the update would replace
one of these shared DLLs. Theoretically, this should not be a
problem because the DLLs were supposed to be backward
compatible. However, we all can tell painful stories that
demonstrate that this was not always the case. This problem is
exacerbated because the OS assumes there will be only one
version of a given component on a system at any one time.
Microsoft Windows 2000 does help address both of these issues
through the use of the Windows File Protection (WPF) and
Private DLLs (sometimes called side-by-side DLLs) features.
The WPF feature prevents the replacement of files shipped with
the operating system. With Private DLLs you can configure the
application to instruct the operating system to check the
application's private directory for dependent DLLs
before looking in the search path.
Contrary to most people's beliefs (or at least their
complaints), the root of the problem is not that a given DLL
did not maintain backward compatibility. This is inevitable
because sometimes you need to break backward compatibility to
fix a bug or add some important new functionality. Rather, the
problem is that there is no way to ensure that your
application will use the same components at runtime that it
did when you built, tested, and installed it. For example, I
may build, test, and ship my application with Microsoft XML
2.6. After a couple of months of running the application
flawlessly, a user decides to download and install the new
version of Microsoft Internet Explorer, which just happens to
update the XML parser to Microsoft XML 3.0. Will my app
continue to work? What about version 3.5? 4.0? Of course, the
answer is that it is very likely that the application will run
because Microsoft XML has been very solid and has not suffered
the regressions that some other components have. But that is
not the point; the point is there is no way of guaranteeing
this. As you will see later, the Microsoft .NET framework
fixes this because you can insure that your application runs
only with the specific version of the components that you
built and tested it with.
A related problem is that cleaning up after an application
is a messy process. When you install a Win32� application, it
is almost as if it is a requirement that the installer
disperse parts of it throughout every nook and cranny of your
system. Besides creating the application's private directory,
it almost always requires new Registry settings in your
system. Beyond that, many applications place files in
%SystemRoot%\System32 and %ProgramFiles%\Common Files and
store user-specific data in locations such as
%USERPROFILE%\Application Data. When it comes time to
uninstall a program, tracking down and removing all its bits
and pieces is often difficult.
Worse yet, because so many components are shared, whenever
you uninstall an application you run the risk that it will
remove a component that some other application is still using.
While this should never, ever happen if the application and
its installer are well behaved, a poorly written setup or
uninstall program can wreak havoc on your shared
components.
The end result of this is two-fold. First, over time, if
you install and then remove many programs, you may start to
experience some stability problems as applications start to
get out of synch with the versions of the components they
expect. Second, this risk of instability causes people to be
reluctant about installing new software, out of fear that it
will either overwrite the DLLs of existing applications, or
they will never be able to purge all of it from their system.
This not only increases the transaction costs associated with
distributing your applications, but also often causes
end-users to miss out on otherwise good applications. Okay,
enough of the problems with the current system, let's take a
look at how the Microsoft .NET Framework addresses these
issues. |
|
The Simple
Solution |
The process for sharing and locating Win32 components is
fairly complicated and has many moving parts (Registry
entries, CLSIDs, AppIDs, ProgIDs, IIDS, Interface Definition
Language, the Service Control Manager, DLLHost.Exe, and so
on�oyou get the idea). When everything is working fine, the
system is pretty elegant. However, when something goes awry it
is often hard to track down the problem and undo any
damage.
As with most systems that break because of their
complexity, the solution is straightforward: simplify the
process. If shared components are causing problems, reduce the
dependence on shared components. If version conflicts are
causing errors, have the runtime enforce versioning. If it's
hard to find all the pieces of an application, place them all
in one place. Well, this is just what the Microsoft .NET
Framework does. Pretty simple, huh?
The key to solving DLL Hell is the .NET assembly.
Assemblies are the most basic unit for deployment, versioning,
and security enforcement. As such, you can look at them as the
building blocks of .NET applications. An assembly is uniquely
identified by four pieces of information: the assembly name,
its version (in the major.minor.build.revision format ),
culture (an RFC 1766-based identifier that specifies language
and locality), and optionally a strong name (a guaranteed
unique "name" generated by signing the assembly and providing
a public key to verify the digital signature).
So what is in an assembly, you may ask? An assembly
contains a manifest that both describes the assembly and
contains a hash of each of the assembly's elements and the
Microsoft Intermediate Language (MSIL) that the common
language runtime executes. While it is an oversimplification,
you can look at a .NET assembly as an improved version of the
DLL (or EXE). The main improvement comes in the form of the
manifest or metadata that is compiled into the assembly.
This metadata makes assemblies self-describing, meaning
that an assembly has compiled into it all the configuration
data the CLR needs to execute it, including the names and
locations of any required external components and the runtime
permissions required. This embedded configuration information
allows for the much-touted XCOPY deployment of .NET
applications because an application does not have any external
dependencies. By implication, this enables zero-impact
installations: if an application does not have any external
dependencies, you can completely remove an application from
your system simply by removing its directory from your
computer. Furthermore, you can install different versions of
the same assembly side-by-side on the same computer�you can
(with a great deal of care) even use different versions in the
same process. For more information on this trick, see About
Isolated Applications and Side-By-side Assemblies in the
platform SDK.
Besides easy setup and removal of applications, this
metadata provides a framework for developers to specify their
application's versioning requirements and security policy.
Better yet, at runtime, CLR will inspect the metadata and
enforce the rules defined by you, the developer. So it is
written (in the metadata); so it is done (by the CLR).
To get a better understanding of how versioning works,
let's take a simplified look at what happens when an
application starts up. For a more detailed description, see
Jeffrey Richter's book Applied
Microsoft .NET Framework Programming. When an application
starts, only the assemblies the application initially calls
must be present. If the application needs additional
assemblies, the CLR can retrieve these on demand from the
code-base specified in the application's configuration file.
The benefit here is that if you have portions of your
application that are rarely used, you can place them in
separate assemblies and reduce the time it takes users to
download and install your application and the disk space it
requires.
If the assembly includes a strong name, when CLR attempts
to locate it the assembly will ensure that it returns the
proper version to the calling application. By default, it will
load only an assembly that exactly matches the version that
the developer used to build the application. You can, of
course, override this behavior with the configuration file and
instruct the CLR to always load the newest version or
potentially some other specific version (for example, an
updated version with a bug fix). |
|
Make Sure
It Is Secure |
It is important to remember that the CLR only guarantees
version compatibility if you are using strong names. While you
may think this is a limitation at first, when you understand
the rationale it makes perfect sense. Without the digital
signature included in the strong name, there would be nothing
to prevent someone from creating their own assembly and giving
it the same name, version, and culture as your original. It
would be counter-productive to rely on the CLR to
��guarantee�� something that it has no way of verifying.
The strong name prevents this sort of forged-assembly
Trojan Horse. Remember that the assembly's manifest contains a
hash of each of the elements that make up the assembly, and
the strong name adds your digital signature to the assembly.
Therefore, when you use strong names, the CLR can guarantee
not only that it loads the exact version you specify but also
that by comparing the digital signature and your public key it
can verify that no one has modified the assembly since you
built your application. A subtle but important corollary is
that the CLR can also ensure that no one forges an updated
version of your component but you. Were someone to
attempt this sort of attack, the CLR would prevent the forged
assembly from loading because the digital signature would be
invalid.
This concludes my overview of assemblies. I started off by
looking at some of the design goals for deploying applications
based on the Microsoft .NET framework. Next I walked you
through the features of .NET assemblies and showed you how
they ease deployment and prevent versioning conflicts. I
concluded with a discussion of how the CLR uses strongly named
assemblies to ensure that you are loading the exact assembly
you specify with the assurance that no one has modified
it. |
|
Microsoft
Press Solutions |
Now that you have an overview of .NET assemblies and have
seen how easy they are to work with, you should be ready to
start deploying your own Microsoft .NET applications. As I
mentioned in my intro, my goal was to quickly give you the
basic information you need to safely and securely deploy your
apps. The best way to ramp up on all the features and best
practices for deploying your applications is to get a copy of
Jeffrey Richter's Applied
Microsoft .NET Framework Programming. Microsoft Press�
provides in-depth documentation for these and all the other
issues related to developing for .NET. For a complete list of
.NET titles from Microsoft Press, see the Inside
Information About Microsoft .NET page.
For a broader overview of all the Microsoft .NET
technologies, you should look at David S. Platt's Introducing
Microsoft .NET. It has some great information on COM
Interop, Windows Forms, and .NET memory management.
One of my favorites is Jake Sturm's book, Developing
XML Solutions, which examines XML-related technologies for
data exchange and shows how XML fits in the Microsoft Windows
architecture.
For a complete list of all the developer titles, see the Developer
Tools section.
For some great links and the latest information on XML Web
Services, see the MSDN Web
Services home page. For more information on Microsoft
Visual Studio .NET, see the Visual
Studio Next Generation Home Page. |
 |
 |
|
Last Updated: Tuesday, March 5, 2002 |
| |
 |
 |
|