Secure It

WS-Security and Remoting Channel Sinks Give Message-Level Security to Your SOAP Packets

Neeraj Srivastava

Code download available at:RemotingChannelSinks.exe(119 KB)

This article assumes you're familiar with C# and SOAP

Level of Difficulty123

SUMMARY

As more organizations adopt XML-based Web Services, the need for message-level security has become evident. WS-Security, now supported in the Microsoft .NET Framework, addresses this need. Using the WS-Security framework, developers can implement channel sinks to intercept Remoting messages as they pass through the .NET Remoting infrastructure. The sink can read the message, change it, and pass it along. During this process, the message can be signed for added security. This article explains how to implement a Remoting channel sink that will modify the Remoting message by including a UserName token in the header, then sign the body using the token.

Channel Sink Architecture
WSE Classes
Implementing the Channel Sink
Client Channel Sink Implementation
ProcessMessage and SignMessage
Server Channel Sink Implementation
Extending the Implementation
Conclusion

Over the past few years there has been an explosive growth in the use of XML-based Web Services across the industry. However, some of this expansion has been constrained by a lack of message-level security as a protocol specification. The recently published WS-Security spec is an attempt to remedy this situation. WS-Security is now supported in the Microsoft® .NET Framework through the Web Services Enhancements (WSE) 1.0 for Microsoft .NET classes. WSE provides a framework to secure SOAP messages, and it supports the WS-Routing, DIME, and WS-Attachments specs. With the powerful programming model exposed by WSE, you can work directly with and modify SOAP messages.

Figure 1** Message Generation with WSE **

Channel sinks are a powerful extensibility feature of .NET Remoting that let you intercept a Remoting message as it travels through the Remoting infrastructure. The sink can read the message, modify it, and pass it further down the chain. This article will show you how to implement a Remoting channel sink that will modify the Remoting message by including a UserName token in the header, then signing the body using the token. It makes use of the WSE to do this, so the message is generated according to the WS-Security specification (see Figure 1).

Channel Sink Architecture

Let's look at the architecture of a channel sink by reviewing the various elements involved in a Remoting method call, the different stages at which you can intercept messages, and the things you can do with a message once you've intercepted it.

When you construct a proxy and make a method call, you generate a Message object, which travels through a series of sinks as it makes round-trips to the server. The Message class, which is referenced through the IMessage interface within the sinks, contains a dictionary that holds the data representing the call.

The Remoting framework exposes a number of IMessage implementing classes, which represent different messages passed along either during construction (ConstructionCall, Construction Response) or during a method call (MethodCall, MethodResponse).

There are two important sinks through which the remoted message passes: FormatterSink and TransportSink. The FormatterSink does the job of serializing the message into a stream and creating transport headers. The TransportSink converts the transport headers into protocol-specific headers and transfers the stream over the wire using the chosen protocol. You can put custom sinks both before and after the FormatterSink.

Figure 2** Channel Sink Architecture **

The sinks between the FormatterSink and the TransportSink have to implement either the IClientChannelSink (if it is a client channel sink) or IServerChannelSink interface (if it is a server channel sink). Figure 2 shows the channel sink architecture and Figure 3 lists the interface definition for these interfaces. Later in the article, I will look into how to implement these interfaces.

Public interface IClientChannelSink { IClientChannelSink NextChannelSink {get;} void AsyncProcessRequest(IClientChannelSinkStack sinkStack, IMessage msg, ITransportHeaders headers, Stream stream); void AsyncProcessResponse(IClientResponseChannelSinkStack sinkStack, object state,ITransportHeaders headers, Stream stream); Stream GetRequestStream (IMessage msg, ITransportHeaders headers); void ProcessMessage(IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream); } Public interface IServerChannelSink { IServerChannelSink NextChannelSink {get;} void AsyncProcessResponse(IServerResponseChannelSinkStack sinkStack,object state,IMessage msg, ITransportHeaders headers, Stream stream); Stream GetRequestStream (IServerResponseChannelSinkStack sinkStack,object state,IMessage msg ,ITransportHeaders headers); void ProcessMessage(IServerResponseChannelSinkStack sinkStac, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream,out IMessage responseMsg ,out ITransportHeaders responseHeaders, out Stream responseStream); }

Any sink that's inserted before a FormatterSink can only access the raw message object. Such sinks have to implement IMessageSink. The interface definition of IMessageSink is: Public interface IMessageSink { IMessageSink NextSink {get;} IMessageSink AsyncProcessMessage (IMessage msg, IMessageSink replySink); IMessageSink SyncProcessMessage (IMessage msg); }

Typically, FormatterSink is the first channel sink in the chain on the client side. FormatterSink implements both IMessageSink and IClientChannelSink/IServerChannelSink by implementing IClientFormatterSink on the client side and IServerFormatterSink on the server side. The formatter sink on the client side receives the message on IMessageSink, serializes it, and forwards it on to IClientChannelSink. On the server side, it reverses this process, sending the message through IServerChannelSink.

Remoting channels are associated with a default chain of sinks. For example, the HttpChannel is associated by default with SoapFormatterSink, and the TcpChannel with BinaryFormatterSink.

Using a configuration file, you can override the sink chain by providing a reference to custom sink providers. A sink provider implements IClientChannelSinkProvider for creating client-side channel sinks and implements IServerChannelSinkProvider for creating server-side channel sinks: public interface IClientChannelSinkProvider { IClientChannelSinkProvider Next {get; set;} IClientChannelSink CreateSink (IChannelSender channel, string url, object remoteChannelData); } public interface IServerChannelSinkProvider { IServerChannelSinkProvider Next {get; set;} IServerChannelSink CreateSink (IChannelReceiver channel); void GetChannelData(IChannelDataStore channelData); }

The provider chain is initialized when the config file is read and the channel initialized. The order in which the providers are placed in the chain depends on the order in which they are defined in the config file. The CreateSink method of the provider is called when the proxy is created. It should forward the call to the next provider in the provider chain, and then chain the next sink (obtained after its own channel sink). This order sets up the sink chain through which the message passes.

WSE Classes

Now let's take a look at WS-Security and employing user name tokens to sign Remoting messages. The WSE classes provide a very good place to start this process. Here they are, along with a description of each.

Microsoft.Web.Services.SoapWebRequest , derived from System.Net.WebRequest, does the job of passing the SOAP message through a series of transforming filters. This class is related to the WSE infrastructure and looks at the information in SoapContext to transform the message.

Microsoft.Web.Services.SoapContextholds the information that is used to transform the message-security tokens, referral information, and attachments. The infrastructure uses this information to perform different transforms on the incoming and outgoing SOAP messages.

Microsoft.Web.Services.SoapEnvelopeencapsulates a SOAP envelope class and makes it easy to work with SOAP messages.

Microsoft.Web.Services.Security.Securityholds a collection of security tokens (like the user name token) and security elements (like the signature). This class provides the GetXml method, which takes an XML document as input and returns an XML signature that conforms to the WS-Security specification.

Microsoft.Web.Services.Security.Signatureholds a signature based on a security token. You can sign different and multiple portions of a message using this class. In this article, I am only signing the SOAP body.

The sink implementation shows how to use these five classes to write and sign the Remoting messages. The sink uses the user name token, which consists of the user name and password. The user name is passed along with the SOAP message; the password is used to sign the message, and may or may not be passed along as well. In the current implementation of this sink, I decided not to send the password in any form.

Implementing the Channel Sink

Let's take a quick look at using the Remoting sink. In order to use the sink, you have to write two classes: one implements the IUserNameTokenHelper interface and the other implements IPasswordProvider.

IUserNameTokenHelper is declared in the SecuritySink namespace, which is available in the code download for this article. The client sink calls the GetUserName and GetPassword methods of this interface to retrieve the user name and password for signing the SOAP message. In this implementation, I provide a property of client sink provider (see TokenHelper) which refers to an object that implements this interface.

The IPasswordProvider interface is declared in the Microsoft.Web.Services.Security namespace. The WSE framework calls this interface to get the password corresponding to a user name while validating a signature. This password is used to sign the SOAP message by comparing its signature value to the one received in the SOAP message. The class that implements this interface is specified in the WSE section of the app.config file.

In addition, you need to make sure that you position the sink correctly in the provider sink chain within app.config. The sink should be positioned after the Formatter in the client app.config file and before the Formatter in the server app.config file. This is because you need to work with the message after it has been serialized on the client side but before it gets deserialized on the server side. Figure 4 and Figure 5 show the relevant portions from each config file. Figure 4 shows the client config file, in which I reference in the client channel sink provider (in the Provider section). The Provider section also refers to the TokenHelper object as one of the properties of the provider.

Client Config for RemotingServer Config for RemotingServer app.config for WSE

Note that the config files on the server side can be merged into a single file. To do this, you would need to move the Remoting configuration section in the application config file and load it on the server while configuring Remoting. Figure 5 shows the server config file. As you can see, the server channel sink provider is referenced under the provider section. The order in which you specify the formatter and sink providers is the order in which they get called in the sink chain.

Client Channel Sink Implementation

When implementing a Remoting sink, the client channel sink provider is a good place to start as it will create the client channel sink on demand. The client channel sink provider implements the IClientChannelSinkProvider interface. The provider also has to implement a constructor that takes two parameters: a dictionary object and a collection of SinkProviderData. The dictionary object holds the properties of the sink provider as specified in the config file. The collection holds further configuration information that's specified in the config file under the element. The string corresponding to the user name token is passed in through the properties collection, and the provider passes this value to the sink.

The method of most interest on the sink provider is called CreateSink, which (as its name suggests) creates the sink and returns a reference to it. Since you're creating a chain of sinks in a specific order, you must take the sink reference you obtain here and pass it to the CreateSink method of the next provider in the chain. Your sink then stores the returned reference to the next chained sink and calls upon it when it needs to pass messages downstream for processing.

Here is the code for the implementation of this method: public IClientChannelSink CreateSink(IChannelSender channel, String url, Object remoteChannelData) { IClientChannelSink nextSink = null; if (_next != null) { nextSink = _next.CreateSink(channel, url, remoteChannelData); if (nextSink == null) return null; } return new SecurityClientChannelSink (nextSink,_tokenHelperString); } The ClientChannelSink, which is derived from BaseChannelSinkWithProperties, is the base class for sinks that implement named properties. (This sink has to implement the named properties Keys and Indexer.) The object must also implement the IClientChannelSink interface. Implementing IClientChannelSink means that you have to implement a ProcessMessage method for supporting synchronous processing as well as AsyncProcessRequest and AsyncProcessResponse procedures for supporting asynchronous processing. The core of the processing has been delegated to a private method, SignMessage, which does the core job of signing the SOAP message.

Before delving into the implementation of ProcessMessage, let's look into the implementation of the client channel sink's GetRequestStream method. GetRequestStream is supposed to return a stream on which the previous sink in the chain can write. If you return null, it implies that the previous sink in the chain should create its own stream to start the chain and pass it to the next sink. If you are in any way going to change or seek the stream in your implementation of the channel sink, it is recommended that you return null in your GetRequestStream implementation.

ProcessMessage and SignMessage

The ProcessMessage function handles the processing for a message before passing it on to the next sink. This preprocessing is done by delegating to the private method SignMessage. No post-processing of the response is performed.

SignMessage creates a SoapEnvelope object in WSE based on the stream that is passed in. It also creates a UsernameToken helper object from the string read from the config file and passed in by the sink provider.

This method gets the user name and password from this object: string helperObj = _tokenHelperString; string[] strarry = helperObj.Split(new Char[]{','}); ObjectHandle obj = Activator.CreateInstance(strarry[1],strarry[0]); IUserNameTokenHelper tokHelper = obj.Unwrap() as IUserNameTokenHelper; string usr = tokHelper.GetUserName(); string pass =tokHelper.GetPassword(); A UserNameToken is created based on this user name and password. You then create a security object and add the UserNameToken to its collection of security tokens. Next, create the signature object and initialize it with the UsernameToken that was just created. This signature object is added to the security elements collection of the security object.

The SOAP envelope is then signed by passing it to the GetXml method of the security object. This call returns a WS-Security Header element, which is appended to the SOAP envelope's header. Finally, the new SOAP message is saved to the outgoing stream. The code in Figure 6 shows this implementation.

//Stream returnStream; //load the soap envelope SoapEnvelope reqEnv = new SoapEnvelope(); reqEnv.Load(requestStream); //setup the username token UsernameToken tok = new UsernameToken(tokHelper.GetUserName(),tokHelper.GetPassword()); tok.PasswordOption = PasswordOption.SendNone; //create the security object Security sec = new Security(actor); sec.Tokens.Add(tok); if(reqEnv.Header == null) reqEnv.CreateHeader(); //signature Signature sig = new Signature(tok); sec.Elements.Add(sig); //sign reqEnv.Header.AppendChild(sec.GetXml(reqEnv)); //save if(inStream == null) { inStream = new MemoryStream(); reqEnv.Save(inStream); inStream.Position=0; } else { reqEnv.Save(inStream); }

The async implementation of this process differs little. In AsyncProcessRequest, you push the current sink on the sink stack so that it will be called during the async processing of the response. You don't have to do any post-processing on a message; instead, you simply delegate the call to the next sink.

Server Channel Sink Implementation

The implementation of the ServerChannelSinkProvider is similar to that of the client provider. You pass in the reference to the channel object, which is required by the sink implementation. Server-side sinks don't currently support asynchronous processing at the request stage; however, the response processing can be asynchronous, allowing you to implement only the ProcessMessage and AsyncProcessResponse methods. The server response is not being processed in the current implementation of the channel sink, so the response is let through the sink chain. Keeping this in mind, the implementation of the GetResponseStream method just delegates to the next sink in the sink stack for processing. The processing of the AsyncProcessResponse method is similarly trivial, as it also delegates to the next sink in the sink stack.

In the ProcessMessage method, you must first ensure that the method has not already been deserialized. You then create a SOAP envelope that is based on the stream passed in and extract the WS-Security headers from the SOAP message. When you create a security object based on the XML in each of the headers, the signature is then validated.

The WSE processing includes a call to an object that implements IPasswordProvider; this object retrieves the password corresponding to the user name that appears in the header. The particular object that implements IPasswordProvider is user specified in the WSE config file, under ... . For more information, take a look at the config files in the sample code for this article. You must also make sure that you delete the Security headers from the SOAP message before passing it down the sink chain, as shown in Figure 7.

SoapEnvelope reqEnv = new SoapEnvelope(); reqEnv.Load(requestStream); //handle multiple security elements also XmlNodeList nodelist; IEnumerator nodelistEnum; nodelist = reqEnv.Header.GetElementsByTagName(@"Security", @"https://schemas.xmlsoap.org/ws/2002/07/secext"); nodelistEnum = nodelist.GetEnumerator(); ArrayList nodestodelete = new ArrayList(); try { while(nodelistEnum.MoveNext()) { XmlElement elem = nodelistEnum.Current as XmlElement; //Create the security object Security secserver = new Security(elem); //can't delete the node as the enumeration list will change so //store the node nodestodelete.Add(elem); } //delete the security nodes from the header IEnumerator nodestodeleteEnum = nodestodelete.GetEnumerator(); while(nodestodeleteEnum.MoveNext()) { reqEnv.Header.RemoveChild((XmlElement)nodestodeleteEnum.Current); } MemoryStream strm = new MemoryStream(); reqEnv.Save(strm); strm.Position=0; processing = _nextSink.ProcessMessage(sinkStack, null, requestHeaders, strm, out responseMsg, out responseHeaders, out responseStream); }

Exception handling is very important for server-side channel sinks; an exception thrown here propagates back to the transport sink, which will handle it and terminate the connection. You must handle the exception and return an appropriate SOAP fault message along with appropriate HTTP error codes. The transport header keys for modifying the HTTP response codes are __HttpStatusCode and __HttpReasonPhrase.

In the exception handler, you create a SOAP fault message based on the exception and serialize it to the response stream, setting the appropriate HTTP headers to indicate a server-side error. If you're on a TCP channel, only the SOAP fault message is passed back and no headers are set.

Extending the Implementation

One of the possible extensions to this sink is to implement the equivalent of impersonation. Once the signature is validated on the server, you have a valid security object on the server side and you can get the UserName token from this security object. The sender's user name can be obtained from the UserName token with the following code snippet: SecurityTokenCollection col = reqCtx.Security.Tokens; IEnumerator e = col.GetEnumerator(); e.MoveNext(); // we expect only one UsernameToken tokn = (UsernameToken) e.Current; string username = tokn.Username; GenericIdentity idn = new GenericIdentity (username); GenericPrincipal p = new GenericPrincipal (idn, null); System.Threading.Thread.CurrentPrincipal = p; The previous code snippet can be extended to implement role-based security on the server. On the client side of the equation, you can use the thread principal to get the user's identity. Another possible implementation would be to write a Remoting sink for encrypting SOAP messages, again using WSE.

Conclusion

The implementation of client and server Remoting sinks is made very easy thanks to the new WSE classes which are compatible with the .NET Framework. The classes make signing and encrypting SOAP messages quite simple, and the architecture of the sink chain exposes an easy path to extend your message processing.

For related articles see:
Programming with Web Services Enhancements 1.0 for Microsoft .NET
Remoting Example: Channel Sink Provider
Advanced .NET Remoting by Ingo Rammer (APress, 2002)

Neeraj Srivastava works in the Solutions Integration Engineering group at Microsoft Product Support Services. He has been a developer for about eight years, working primarily with Microsoft technologies. Currently, he specializes in middleware technologies such as COM+ Remoting, Web Services, and ASP.NET.