Understanding Remoting and Data Encryption


Remoting is the act of accessing an object in another process. The object can appear on another machine, on the same machine in a different application, in a different application domain (see the “Relying on the AppDomain for Managed Code” section of Chapter 6 for an explanation of the application domain), or any other process that is different from the current process. This technology is an essential part of certain types of distributed applications. You can create applications that cross machine and domain boundaries using remoting—even if the boundary is the Internet.

The .NET Framework places certain boundaries on remoting. For example, you can’t use it as a replacement for Web services, in many cases, because your application has to know that the component and the corresponding object exist. (It isn’t necessary to know where they exist, just the fact that they do exist is sufficient.) When using a Web service application you can use Discovery of Web Services (DISCO) or Universal Description, Discovery, and Integration (UDDI) to locate a service from an unknown source. In addition, remoting doesn’t necessarily provide the ease of using a Simple Object Access Protocol (SOAP) message to encapsulate data. Yes, you can use SOAP as one of the interoperability options and you can even expose the object through IIS, but you have to perform all of the work required to locate and use the resource manually.

All remoting applications consist of three parts: component with remoting capabilities, host application that acts as a request listener, and a client application that makes remoting requests. This chapter doesn’t provide a full discussion of remoting. You can discover the basics of remoting at http://msdn.microsoft.com/library/en-us/cpguide/html/cpconbuildingbasicnetremotingapplication.asp.

The following sections describe the security requirements for a remoting application. This discussion includes some information about low-level functionality such as deserialization of data. In addition, the example code concentrates on the security features you need to build into the three parts of a remoting application.

Understanding Automatic Deserialization

Whenever the application communicates with the remote components that it uses, it needs to serialize the data (package the data for transmission) at the sender and deserialize it (remove the data from the package) at the recipient. Serialization doesn’t incur much of a security risk because you have full control over the content of the package. However, deserialization does incur a security risk because the cracker could use the short time the process exposes the data to corrupt or change it in some way. In addition, the cracker could change the package in transit so that it contains data elements not found in the original package. Consequently, Microsoft devised a method to reduce the deserialization risk that includes two levels of deserialization.

The default deserialization (called the low deserialization level) only allows the package to contain certain data types and information. By reducing the amount and type of information the package can carry, the .NET Framework can reduce the potential security risk. Generally, you can use the low deserialization level because it includes all standard value and reference types, in addition to some custom object types (explained in detail later in the section). In essence, the .NET Framework makes the problem smaller so that the variables are easier to control.

Unfortunately, you can’t solve every remoting problem using the security settings in place with the low deserialization level. The .NET Framework also provides the full deserialization level, which allows the recipient to access all of the data in the package without filtering it first. The most important addition for developers is the ObjRef object passed as a parameter.

Tip

This section provides an overview of the data serialization and deserialization considerations for remoting. You can find out more about the deserialization levels that the .NET Framework supports at http://msdn.microsoft.com/library/en-us/cpguide/html/cpconautomaticdeserializationinnetremoting.asp.

An ObjRef object is literally a reference to a remote object. From a COM perspective, the ObjRef object is the proxy the server uses to converse with the client. If you pass an object to the remote location as part of a method call, what CLR really passes is an ObjRef object. However, don’t confuse an ObjRef type with other reference types. For example, you can build a custom object, mark it with the [Serializable] attribute (without implementing the ISerializable interface), and still pass it to the remote location using the low deserialization level. (See the example in the “Creating a Permission Assembly” section of Chapter 5 for details on using the [Serializable] attribute.)

You also need the full deserialization level to work with objects that interact with partially trusted code (including unmanaged code). The objects that allow this kind of access have the [assembly: AllowPartiallyTrustedCallersAttribute] attribute. By default, only fully trusted assemblies can access any assembly with a strong name, which includes all of the assemblies in the Global Assembly Cache (GAC). The normal effect of accessing an assembly with a strong name is having a permission attribute attached with the SecurityAction.LinkDemand = FullTrust argument. Using the [assembly: AllowPartiallyTrustedCallersAttribute] attribute lets partially trusted code access the assembly. You can still add the full trust requirement to individual classes, methods, properties, fields, and other elements. All that this element offers is a little calling flexibility. (See the “Working with Methods that Require Full Trust” section of Chapter 15 for additional examples of how the [assembly: AllowPartiallyTrustedCallersAttribute] attribute works with the .NET Framework and CLR).

Understanding Remoting and Code Access Security

One of the biggest security problems with remoting is that it disables code access security. CLR can’t access the call stack in another process or on another machine. When your code uses remoting to call on an object in another process, CLR essentially ignores any required permissions. The code can’t assert or deny a permission if CLR can’t check the call stack.

The consequence of this problem with code access security is that any component that derives from the MarshalByRefObject class must take responsibility for security to ensure the managed application environment remains secure. In other words, the remoting component must provide some type of security for three major concerns: caller authentication, data encryption, and wire integrity. When a component doesn’t implement security that answers these three requirements, it isn’t safe to use in the Internet environment.

Fortunately, you don’t have to program every aspect of these three security needs. Using SSL with an HTTP channel (see the “Using HttpChannel Security” section) takes care of the wire security. Likewise, you can rely on IIS to authenticate the caller. (You can also take care of the authentication using the techniques shown in Chapter 9.)

Data encryption is the remaining requirement and the reason I chose the name for this section of the chapter. You have two choices for meeting the data encryption requirement: internal data encryption or using a channel sink. A channel sink is a special class that intercepts a message and performs some type of work with it, such as formatting the data or getting it ready for transport to the remote system. Remoting normally relies on multiple channel sinks for every communication and channel sinks normally come in pairs. Whatever you do to the message as it leaves the client, you have to undo as it arrives at the server.

Note

Data encryption, especially in distributed applications, stirs controversy because there are so many issues to consider. On the one hand, you have human rights groups and others who use cryptography for good uses that save human lives. On the other hand, you have people with less than honorable goals who use cryptography to hide their activities from the authorities. The bottom line for a developer is that you need to consider the laws in effect at the time you place an application into production to ensure your use of cryptography complies with the laws of every country (or even state) that receives the application. For a recent news story that highlights the issue, see the InfoWorld article at http://www.infoworld.com/article/03/05/21/HNpdapgp_1.html.

The first method, internal data encryption, is the easiest and provides enough security when combined with user authentication and wire security to meet most needs. A major advantage of this technique is that the component remains self-contained so you don’t have to perform any odd setups on either client or server. The disadvantage of this technique is that it only encrypts the data, not the entire message.

The second method, using a channel sink, requires that you write a special class that intercepts the message, performs the required security task, and then sends it to the next channel sink. Channel sinks derive from the IClientChannelSink or IServerChannelSink classes. You must register the channel sink with CLR and provide linkage to the next channel sink in line. Consequently, the major disadvantage of this technique is that it’s complex to implement.

Creating a Remoting Component

The remoting component in this example is the same MathFunctions class used for other examples in the book. This version differs in a number of ways, however, to accommodate remoting and encryption requirements. Although the basic task of performing a match function is the same, the example has to consider security requirements as mentioned in the “Understanding Remoting and Code Access Security” section. It also derives from the MarshalByRefObject class.

Tip

The natural way to approach the problem of adding encryption to an application consisting of a server and a client is to add the code to the individual elements, and then test it for functionality. The easier method is to create the encryption and decryption as separate routines and use a single test application to test all of the routines at one time. The biggest advantage to this technique is that you can debug all of the crucial routines on a single machine and then move them to your client and server modules for further testing. You can find an example of such an application in the \Chapter 10\C#\EncryptionDemo and \Chapter 10\VB\EncryptionDemo folders of the source code located on the Sybex Web site. This chapter doesn’t contain this example because the code is similar to the actual application shown here. One interesting feature of the original test is that I used a MemoryStream, rather than a Byte[] array to transfer data between endpoints. You can’t transfer a MemoryStream across boundaries, so the Byte[] array is the obvious answer. This test therefore points out some of the limitations of using a local test as well as showing the benefits.

Listing 10.4 contains the source code you need for this example. This listing isn’t complete—it includes the code for just the add math operation. You’ll find the complete source for this example in the \Chapter 10\C#\RemotingClient and \Chapter 10\VB\RemotingClient folders of the source code located on the Sybex Web site.

Listing 10.4 Simple Remoting Component Example

start example
public class MathFunctions : MarshalByRefObject {    private RSACryptoServiceProvider RSACrypto;  // The key pair.    public MathFunctions()    {       String         KeyPath;    // The location of the key.       FileStream     KeyFile;    // Key disk storage.       Byte[]         KeyConv;    // Converted key data.       StringBuilder  KeyString;  // Key data as a String.       Int32          Counter;    // Loop counter.       // Create the key path string.       KeyPath = "SpecialKey";       // Detect the presence of a key pair file.       if (!File.Exists(KeyPath))       {          throw(new FileLoadException("Cannot Find Application Key"));       }       else       {          // Open the key file for reading.          KeyFile = File.Open(KeyPath, FileMode.Open);          KeyConv = new Byte[KeyFile.Length];          KeyFile.Read(KeyConv, 0, (Int32)KeyFile.Length);          KeyFile.Close();          // Convert the key file.          KeyString = new StringBuilder(KeyConv.Length);          for (Counter = 0; Counter < KeyConv.Length; Counter++)             KeyString.Append(Convert.ToChar(KeyConv[Counter]));          // Create the key.          RSACrypto = new RSACryptoServiceProvider(2048);          RSACrypto.FromXmlString(KeyString.ToString());       }    }    public Byte[] DoAdd(Byte[] Input)    {       Int32    Value1;  // The first input value.       Int32    Value2;  // The second input value.       Int32    Result;  // Result of the computation.       Byte[]   DataOut; // The output information.       // Decrypt the data.       DecryptInput(Input, out Value1, out Value2);       // Perform the computation.       Result = Value1 + Value2;       // Encrypt the result as a stream.       EncryptOutput(Result, out DataOut);       // Return the result.       return DataOut;    }    private void DecryptInput(Byte[] Input,                              out Int32 Value1, out Int32 Value2)    {       Byte[]   Decrypted;     // Decrypted byte array.       Int32    Counter;       // Loop counter variable.       Int32    CurCounter;    // Loop counter position bookmark.       Char[]   ConvChar;      // Converted data in array form.       String   ConvString;    // Individual value in string form.       // Decrypt the input data.       Decrypted = RSACrypto.Decrypt(Input, false);       // Retrieve the first input value.       ConvChar = new Char[Decrypted[0]];       for (Counter = 0; Counter < Decrypted[0]; Counter++)          ConvChar[Counter] = Convert.ToChar(Decrypted[Counter + 1]);       ConvString = new String(ConvChar);       Value1 = Int32.Parse(ConvString);       // Retrieve the second input value.       CurCounter = Counter + 1;       ConvChar = new Char[Decrypted[CurCounter]];       CurCounter++;       for (Counter = 0;            Counter < Decrypted.Length - CurCounter; Counter++)          ConvChar[Counter] =             Convert.ToChar(Decrypted[Counter + CurCounter]);       ConvString = new String(ConvChar);       Value2 = Int32.Parse(ConvString);       return;    }    private void EncryptOutput(Int32 Input, out Byte[] Output)    {       Char[]   InputData;  // Input data in Char array form.       Int32    Counter;    // Loop counter variable.       Byte[]   ConvData;   // Unencrypted output data.       // Convert the input data.       InputData = Input.ToString().ToCharArray();       // Convert the data to a Byte array.       ConvData = new Byte[InputData.Length];       for (Counter = 0; Counter < InputData.Length; Counter++)          ConvData[Counter] = Convert.ToByte(InputData[Counter]);       // Encrypt the data.       Output = RSACrypto.Encrypt(ConvData, false);       return;    } }
end example

The program begins by creating an RSACryptoServiceProvider object. The first task is to locate the key for the encryption. The client and server use either the public or the private key as needed. If the code doesn’t find the key file, it throws a FileLoadException exception. When the code does find the key, it converts it to a string and then uses it to initialize the RSACryptoServiceProvider object, RSACrypto.

The DoAdd() method takes on a different look for this example. Notice that it passes data in and out as a Byte[] array. You might wonder why the example doesn’t use some type of stream. The fact is that using a stream causes a security error. The ObjRef object discussed in the “Understanding Automatic Deserialization” section won’t allow the use of a data stream, in most cases, because the data stream could contain information that you don’t want the other party to see. A data stream is a security breach waiting to happen, but a Byte[] array contains just the information. The process the DoAdd() method uses is to decrypt the data, perform the required operation, and then encrypt it immediately. Clearing the local variables as a last step would tend to reduce the risk of someone gaining access to local memory and finding the data (this technique doesn’t remove the possibility of memory snooping completely).

The DecryptInput() method accepts the Byte[] array as input. This Byte[] array could contain any value and some reference types. However, the example uses a structured string. The string can contain any number of values of any length (within the confines of the .NET architecture). The DecryptInput() method begins by decrypting the Byte[] array. The first entry in the decrypted Byte[] array is the length of the first string element. The code uses this information to retrieve the first input value. Notice that the code first converts the Byte[] array to a Char[] array, which acts as input for creating a string. The code parses the string into an Int32 value. The code retrieves the second value using the same technique. As before, the first Byte[] array entry contains the length of the string, followed by the string itself.

The EncryptOutput() method begins by converting the Int32 input value into a Char[] array. The next step is to create a Byte[] array that the RSACrypto.Encrypt() method can encrypt. The decrypted array is relatively small, so the example doesn’t have to worry about the size of the input data. The output in this case is always a 256-byte array.

Creating a Remoting Host Application

The host application, or listener, is the easiest part of the application to build. In fact, it’s quite possible that you’ll build just one of these applications and use it for every need. The host application relies on a configuration file to provide information about the component. Consequently, if you want to change how the host application works, you modify the configuration file, not the host application.

Note

This chapter doesn’t even begin to explore the configuration options that remoting provides. The configuration file discussed in this section is actually quite simple and you can use a variety of other options including SOAP interoperability. However, you have to build all of these features into the configuration file manually using the remoting settings schema. You can find an outline of this schema complete with element explanations at http://msdn.microsoft.com/library/en-us/cpgenref/html/gnconremotingsettingsschema.asp.

Listing 10.5 shows the remoting host source code. You’ll find this example in the \Chapter 10\C#\RemotingHost and \Chapter 10\C#\RemotingHost folders of the source code located on the Sybex Web site.

Listing 10.5 Simple Remoting Host Example

start example
static void Main(string[] args) {    RemotingConfiguration.Configure("RemotingHost.EXE.CONFIG");    Console.WriteLine("Listening for requests. Press Enter to exit...");    Console.ReadLine(); }
end example

The remoting host configures the remoting functionality for your application by calling the RemotingConfiguration.Configure() method. Notice the name of the configuration file. Files with other names will load, but CLR tends to ignore their content, which means the host won’t actually listen for component requests. The remoting host continues to listen until you press Enter at the command line. The moment the remoting host application ends, the remoting functionality also ends and CLR closes the channel. Listing 10.6 shows the configuration file used for the remoting host.

Listing 10.6 Typical Remoting Host Configuration File

start example
<?xml version="1.0" encoding="utf-8" ?> <configuration>    <system.runtime.remoting>       <application>          <service>             <wellknown                displayName="RemotingApplication"                mode="Singleton"                type="RemotingComponent.MathFunctions,                      RemotingComponent"                objectUri="RemotingComponent.DLL"             />          </service>          <channels>             <channel ref="http" port="8989"/>          </channels>       </application>    </system.runtime.remoting> </configuration>
end example

The configuration file begins with the usual declaration for XML files. The .NET environment doesn’t appear to require this declaration, but adding it lets you view the configuration file in any of a number of XML utilities. Every remoting configuration file will contain the <system.runtime.remoting>. You normally add the <application> as well. The host always uses the <service> tag, while the client always uses the <client> tag.

When you use the <service> tag, you need to describe the service. The .NET Framework provides a number of methods to perform this task. The example specifically relies on direct access to the RemotingComponent.DLL. This DLL could contain any number of classes, but the example configuration file only accesses the RemotingComponent.MathFunctions class. Note that this is the namespace followed by the class name. The second parameter is the name of the assembly, which could be different from the namespace, so you need to exercise caution. I always include a displayName attribute value. The .NET Framework doesn’t use this value, but it does appear in the .NET Framework Configuration tool, so adding this value can help you locate the component as needed. (See the “Using the .NET Framework Configuration Tool” section of Chapter 4 for details on using this tool.)

The <channel> element is especially important when it comes to security. You can choose either ref=”http” or ref=”tcp” in the current .NET configuration. Other protocols could become available in the future. However, it’s essential that you choose ref=”http”from a security perspective for now because this setting affords you channel level security given specific setup conditions. See the “Using HttpChannel Security” section for details. In addition to choosing the HTTP channel, make sure you choose a unique port. Unlike some technologies, remoting only relies on a single port, but CLR doesn’t tie it to the standard HTTP port 80 used for Web requests. Using a single port means that you can monitor a specific port for incoming requests. However, this technique also means you have to open yet another hole in your firewall security. Overall, the superior monitoring capability does provide enough advantages that remoting could be superior to using other data communication techniques as long as you actually monitor the port.

Creating a Remoting Client Application

This section discusses the remoting client application. When you run a remoting client, it redirects requests from the local environment to the remote object. Like the host application, you can either define the redirection programmatically or use a configuration file, with the configuration file providing the more flexible option. The point is that the client tells the remoting system to use the remote object, rather than the local object.

You still need some type of object reference when you build the application. The reference doesn’t have to contain the full implementation of the object—all it really needs are the declarations so that Visual Studio .NET features such as IntelliSense work as anticipated. The declaration file must have the same name as the full featured file though, so it often helps to create a Declaration folder beneath the folder containing the actual DLL. Using the declaration file is important because it also contains remoting references the code needs to make remoting work properly.

Once you have everything set up, you need to generate a file that contains the declaration using the SoapSuds (or similar) utility. Open a command prompt in the folder that holds the component DLL, type SoapSuds -ia:RemotingComponent -oa:Declaration\ RemotingComponent.DLL, and press Enter. SoapSuds will generate a declaration file based on the assembly you provide as input using the –ia switch (make sure you exclude the assembly file extension). The –oa switch defines the output assembly name. The resulting DLL is significantly smaller than the original DLL. When you create the client, use this declaration assembly as the reference for the application. You can find the declaration component for this example in the \Chapter 10\RemotingComponentDeclaration folder of the source code located on the Sybex Web site.

Listing 10.7 shows the code required to build a remoting client. Unlike other clients you might have built in the past, this one encrypts the data before placing it on the wire and decrypts it on return. The listing isn’t complete—it doesn’t include the initialization code for the cryptographic service provider. This code appears in the form constructor and is precisely the same as the code that appears in Listing 10.4 for the component. You’ll find the complete source code for this example in the \Chapter 10\C#\RemotingClient and \Chapter 10\VB\RemotingClient folders of the source code located on the Sybex Web site.

Listing 10.7 Simple Remoting Client Example

start example
private RSACryptoServiceProvider RSACrypto;  // The key pair. private void btnTest_Click(object sender, System.EventArgs e) {    MathFunctions  MyObject;      // The target object.    Byte[]         DataTransmit;  // The data tranmission stream.    Int32          Result;        // Computation result.    // Create the object.    MyObject = new MathFunctions();    // Encrypt the input.    EncryptRequest(1, 2, out DataTransmit);    // Perform the calculation.    DataTransmit = MyObject.DoAdd(DataTransmit);    // Decrypt the result.    DecryptResult(DataTransmit, out Result);    // Display the result of an addition.    MessageBox.Show(Result.ToString(), "Results of Addition",                    MessageBoxButtons.OK, MessageBoxIcon.Information); } private void EncryptRequest(Int32 Value1,                             Int32 Value2, out Byte[] Data) {    Char[]   SValue1;    // Holds the first input value.    Char[]   SValue2;    // Holds the second input value.    Int32    Counter;    // Loop counter variable.    Int32    CurCounter; // Loop counter position bookmark.    Byte[]   ConvData;   // Unencrypted output data.    // Convert the input values to Char arrays.    SValue1 = Convert.ToString(Value1).ToCharArray();    SValue2 = Convert.ToString(Value2).ToCharArray();    // Set the size of the conversion array.    ConvData = new Byte[SValue1.Length + SValue2.Length + 2];    // Convert the first input value to a byte array.    ConvData[0] = Convert.ToByte(SValue1.Length);    for (Counter = 0; Counter < SValue1.Length; Counter++)       ConvData[Counter + 1] = Convert.ToByte(SValue1[Counter]);    // Convert the second input value to a byte array.    Counter++;    ConvData[Counter] = Convert.ToByte(SValue2.Length);    CurCounter = Counter + 1;    for (Counter = CurCounter; Counter < SValue2.Length + CurCounter;         Counter++)       ConvData[Counter] =          Convert.ToByte(SValue2[Counter - CurCounter]);    // Encrypt the byte array.    Data = RSACrypto.Encrypt(ConvData, false);    return; } private void DecryptResult(Byte[] Input, out Int32 Result) {    Byte[]   Decrypted;     // Decrypted byte array.    Int32    Counter;       // Loop counter variable.    Char[]   ConvChar;      // Converted data in array form.    String   ConvString;    // Individual value in string form.    // Decrypt the input data.    Decrypted = RSACrypto.Decrypt(Input, false);    // Convert the input value to a string.    ConvChar = new Char[Decrypted.Length];    for (Counter = 0; Counter < Decrypted.Length; Counter++)       ConvChar[Counter] = Convert.ToChar(Decrypted[Counter]);    ConvString = new String(ConvChar);    // Define the result.    Result = Int32.Parse(ConvString);    return; }
end example

The example program begins in the form constructor where it creates the RSACrypto object. The code also loads a configuration file similar to the one shown for the remoting host application in Listing 10.6. The configuration file performs the same task—it redirects requests for the RemotingComponent class to the remote machine. The user’s interaction begins with the btnTest_Click() method. This method begins by instantiating a new copy of the MathFunctions class. However, because of the redirection provided by the RemotingConfiguration.Configure() method, the client doesn’t actually use the local copy of the component that it used during design—it calls on the remote component instead.

The code then encrypts the input values for the remote object using the EncryptRequest() method. The object is actually executing on the remote machine—not the local machine, so encrypting the data that will go over the wire is important. The code calls the MyObject.DoAdd() method as usual. However, it calls the method using the encrypted data and receives an encrypted response. The code calls the DecryptResult() method to decrypt the result. Finally, the code displays the result using a message box.

The EncryptRequest() method accepts the two Int32 values as input and returns an encrypted Byte[] array. The code converts the two inputs to Char[] array values. It obtains the length of the first Char[] array and uses that value as the first entry into the Byte[] array, ConvData. The code then places the first Char[] array value into ConvData. The code adds the second Byte[] array using the same technique so that it follows the first in ConvData. What you have now is a Byte[] array that contains both sets of values. The final step is to encrypt ConvData and place the result in Data.

The DecryptResult() method begins by decrypting Input, which is a Byte[] array. Because there’s only one value in the array, conversion is relatively simple. The converted data appears in a Char[] array, which acts as input for a String, and is finally parsed into an Int32 value.

Like the remoting host, the client requires a configuration file to know where to find the remote object. The fact that you’re using a configuration file means that you can change the location of the remote object without recompiling the application. Listing 10.8 shows the configuration file for the client.

Listing 10.8 Typical Remoting Host Configuration File

start example
<?xml version="1.0" encoding="utf-8" ?> <configuration>    <system.runtime.remoting>       <application>          <client>             <wellknown                type="RemotingComponent.MathFunctions,                     RemotingComponent"                url="http://WinServer:8989/RemotingComponent.DLL"             />          </client>       </application>    </system.runtime.remoting> </configuration>
end example

As you can see, the client configuration file contains many of the same elements as the remoting host configuration file. In this case, the configuration file contains the <client> tag. The <wellknown> tag contains attributes that describe the namespace, class name, and assembly name of the component. It also includes the location of the component. Note that you can use LocalHost if you want to test on a local machine, rather than using a remote machine as I did. You’ll need to change the name of the server to match your server (unless you also named your remote server WinServer).

When you click Test the first time, it will appear that nothing happened. After a lengthy delay, you’ll finally see the result. While writing this example, I finally started listening for the hard drive on the server—it takes that long. However, click Test a second time and you’ll notice the response is quite speedy (almost instantaneous). Nothing has changed except the component is loaded in memory. An unbelievable number of tasks must take place before the component can perform encrypted data transfer.

Of course, it’s easy to assume that you’re not engaging in a remote conversation between two machines. At the listener prompt, press Enter. The listener will stop at this point and unload itself from memory. Click Test again and you’ll see the error message shown in Figure 10.6. This is your proof that the remoting is working as advertised. No listener means no component access.

click to expand
Figure 10.6: Test the reality of using remoting by turning the listener off and clicking Test on the client.

Using HttpChannel Security

In the “Using the AuthenticationManager Class” section of Chapter 9, you learned about the technique used to authenticate a user on IIS (see Listing 9.1). The example in that section relied on the PreAuthenticate() method to check the authentication of a caller by using a Web request and one or more credentials in a cache. You also know from the example in this section of this chapter that you can pass requests to a remoting listener directly or through IIS using any of a number of protocols such as SOAP. The HttpChannel class provides access to the low-level communication that occurs when you use remoting through an IIS connection.

The HttpChannel class provides access to the transport protocol used to transfer the information from one point to another. One of the HttpChannel class properties is credentials, which works precisely like the credentials discussed in Listing 9.1. In fact, the HttpChannel object passes the credentials to the AuthenticationManager object, which passes them to the server.

The main reason you would want to use the HttpChannel class and its associated properties is control. Adding the credentials at this level, rather than using the AuthenticationManager directly, is a matter of convenience. Using this class lets you control every aspect of the channel and therefore the communication session. Control requires additional code, testing time, and programming effort. However, by exercising control over the channel, you also gain opportunities to check and validate security.

For this technique to work, you must also configure IIS to provide SSL support. See the “Adding SSL Support to a Server” section of this chapter for details on performing this task. The URL you provide in the configuration files must use HTTPS, rather than the standard HTTP transport. In addition, you must set up the application’s virtual directory to provide some type of authentication: Basic, Digest, Cookie, or Windows Integrated.

You can always verify the security status of a connection before you use it. First, gain access to the HttpRequest object using the HttpContext.Request property. Second, once you have access to an HttpRequest object, use the IsSecureConnection() method to determine the current connection security status.

Note that the TcpChannel class doesn’t provide the same low-level support as the HttpChannel class. The main reason is that the TcpChannel class bypasses IIS, which means you don’t have access to the SSL support the IIS provides. When you create a remoting application that transfers information over the Internet from a client to a Web server, you should always use the HTTP channel. See Listing 10.6 and 10.8 for the application configuration settings that you need to make.

SSL is the method that most Web sites use to secure communication because it’s standardized, relatively easy to implement, and safe. One option that you don’t see too often is the exchange of credentials between client and server. The server normally presents credentials, but the application doesn’t expect the client to provide any. The problem is that many clients don’t have credential to provide, making a two-way credential exchange difficult on public Web sites.

Fortunately, private Web sites can use this solution to keep the exchange of text data to a minimum. You must set the server up to accept the client certificate (see the “Adding SSL Support to a Server” section for details). When you want to use the certificate as part of an authentication strategy in place of the one shown in Listings 10.1 through 10.3, you must also create a certificate to Windows account mapping. The “Creating an SSL Application” section shows how to access the certificate information.

Adding SSL Support to a Server

Once you lock down your Web server, you’ll find that many of the programs that used to work don’t any longer. In general, the changes force you to use a secure connection strategy such as SSL to ensure a cracker can’t monitor your communications. Unfortunately, Microsoft can’t ship IIS with SSL support installed because the server requires a certificate. You can obtain a certificate from a third party source or you can generate one of your own using the procedure in the “Getting Your Own Certificate” section of Chapter 7. After you add a certificate to your system, you can follow the steps below to add SSL support.

Note

You might need to perform this procedure at the server console, rather than use a remote connection. In some cases, a remote connection won’t allow access to the server certificate, which is a requirement for installing SSL support. When this occurs, the Server Certificate button on the Directory Security tab of the target Web site’s Properties dialog box is disabled.

  1. Open the Internet Information Services Console.

  2. Right-click the target Web site entry in the hierarchy and choose Properties from the context menu. Select the Directory Security tab and you’ll see a Web site Properties dialog box similar to the one shown in Figure 10.7.

    click to expand
    Figure 10.7: Make sure you can add SSL from the current location by looking at the Server Certificate button.

  3. Click Server Certificate. You’ll see the Welcome to the Web Server Certificate Wizard.

  4. Click Next. You’ll see the Server Certificate dialog box.

  5. Select the Assign an Existing Certificate option. Click Next. You’ll see an Available Certificates dialog box.

  6. Select one of the available certificates. Generally, you’ll see more than one certificate. To choose the correct certificate, look in the Intended Purpose column for a certificate with the Client Authentication feature, such as the one shown in Figure 10.8. Click Next. You’ll see a Certificate Summary dialog box.

    click to expand
    Figure 10.8: Select a client authentication certificate.

  7. Click Next. You’ll see a Completion dialog box.

  8. Click Finish. The wizard will end and you’ll return to the Directory Security tab of the Web site’s Properties dialog box.

  9. Click Edit in the Secure Communications section. You’ll see a Secure Communications dialog box shown in Figure 10.9. Notice that this dialog box lets you set up features such as client certificate mapping, which associates client certificates with specific Windows accounts.

    click to expand
    Figure 10.9: Choose the client authentication option.

  10. Select the Accept Client Certificates option. Click OK. This step ensures the server can determine the identity of clients accessing the Web site.

Creating an SSL Application

Listing 10.9 shows how to obtain information from a certificate. In general, you’ll use certificates to authenticate the caller and to learn more about the caller. You’ll find this example in the \Chapter 10\C#\SSLCredential and \Chapter 10\VB\SSLCredential folders of the source code located on the Sybex Web site.

Listing 10.9 Obtaining Client Certificate Information

start example
private void Page_Load(object sender, System.EventArgs e) {    StringBuilder   ConvChar;   // Conversion for public key.    Int32    Counter;    // Loop counter.    // Determine whether the certificate is present.    if (Request.ClientCertificate.IsPresent)    {       // Convert the public key.       ConvChar = new StringBuilder();       for (Counter = 0;            Counter < Request.ClientCertificate.PublicKey.Length;            Counter++)          ConvChar.Append(             Request.ClientCertificate.PublicKey[Counter].ToString("X2") +             " ");       // Get each of the statistics.       txtIssuer.Text =          Request.ClientCertificate.Issuer.ToString();       cbValid.Checked =          Request.ClientCertificate.IsValid;       txtKeySize.Text =          Request.ClientCertificate.KeySize.ToString();       txtPublicKey.Text =          ConvChar.ToString();       txtSerialNumber.Text =          Request.ClientCertificate.SerialNumber.ToString();       txtServerIssuer.Text =          Request.ClientCertificate.ServerIssuer.ToString();       txtServerSubject.Text =          Request.ClientCertificate.ServerSubject.ToString();       txtSubject.Text =          Request.ClientCertificate.Subject.ToString();       txtValidFrom.Text =          Request.ClientCertificate.ValidFrom.ToLongDateString();       txtValidTo.Text =          Request.ClientCertificate.ValidUntil.ToLongDateString();    }    else       // There is no certificate.       txtIssuer.Text = "No Certificate Supplied"; }
end example

All of the values for a certificate appear within the Request.ClientCertificate property. However, this property is null if the client doesn’t provide a certificate, so you need to make this check first. The Request.ClientCertificate.IsPresent property always defines the presence or absence of a certificate.

Once it knows the client has supplied a certificate, the code begins by converting the Request.ClientCertificate.PublicKey property to a string. The code then displays the various values on screen using the property values shown in the listing. The example shows a few of the properties—the certificate provides a wealth of additional information. Figure 10.10 shows typical output from this application.

click to expand
Figure 10.10: Typical certificate information you can use to validate a Web site caller.




.Net Development Security Solutions
.NET Development Security Solutions
ISBN: 0782142664
EAN: 2147483647
Year: 2003
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net