This is the second in a series of posts looking at the detail of how message exchanges occur over a WCF channel using the named pipe binding.
In the first post of the series, we introduced the Framing Protocol, saw how it included an upgrade phase whereby the client and server negotiate a security context within which message exchange over the named pipe will occur, and demonstrated how the .NET Framework class System.Net.Security.NegotiateStream could be used to implement the protocol exchanges involved in such an upgrade. This time we will look into exactly what is going on in this upgrade process, and how it can be controlled.
Continuing to use my spoof service to listen on the pipe and spy on the data stream from the client, as described previously, this is what we see if we read the contents of the Upgrade Record instead of using the .NET NegotiateStream to process it:
The first five bytes we read from the pipe initiate Microsoft’s .NET Negotiate Stream protocol. The .NET Negotiate Stream protocol specifies how the client and server exchange security tokens to negotiate a security context. Underlying this stream protocol is the Security Support Provider Interface (SSPI), which is Microsoft’s implementation of the public-domain Generic Security Services API, designed to abstract security providers away from application code. The essential pattern is that security tokens are generated by each side’s security provider and passed to the other party’s security provider, the exchange of tokens continuing until both security providers agree that a valid security context has been established, or one or other decides that a suitable context cannot be negotiated.
These bytes can be interpreted as follows:
|0x16 ||Indicates that this is a .NET Negotiate Stream Handshake Message of type HandshakeInProgress |
|Specify that version 1.0 of the .NET Negotiate Stream protocol is being used |
|0x00, 0x37 ||Specify that the AuthPayload field of the message, which follows, is 55 (0x37) bytes long |
The second read from the pipe retrieved the 55 bytes of the AuthPayload field, which I have partly redacted to remove the domain and workstation names in my environment. These bytes constitute the first security token passed from the client to the server as part of the SSPI handshake.To generate this, the client-side WCF channel stack has called the SSPI functions AcquireCredentialsHandle, specifying that the Negotiate security provider should be used, and then InitializeSecurityContext, to obtain the first security token to be passed to the server. The Negotiate security provider has decided that the actual security package to be used for authentication is the NTLM provider (it will always do this when the target server is the same machine), and it is an NTLM token which we see being passed.
The NTLM authentication protocol is documented on MSDN, and from this we can see how the token works. The first eight bytes identify it as an NTLM token. The following 4 bytes specify that the token is an NTLM Negotiate message. The next 4 bytes (0xb7, 0xb2, 0x18, 0xe2)) comprise a NTLM NEGOTIATE structure, which is a bit field specifying the options which the client is proposing as characteristics of the security context. Translating these in terms of the Win32 symbolic constants for the option flags, shows the following:
|0xb7 ||10110111 || |
NTLMSSP_NEGOTIATE_LM_KEY, NTLMSSP_NEGOTIATE_SEAL, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_REQUEST_TARGET, NTLMSSP_NEGOTIATE_OEM, NTLMSSP_NEGOTIATE_UNICODE
|0xb2 ||10110010 || |
NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED, NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED, NTLMSSP_NEGOTIATE_NTLM
|0x18 ||00011000 ||NTLMSSP_NEGOTIATE_IDENTIFY, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY |
|0xe2 ||11100010 || |
NTLMSSP_NEGOTIATE_56, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_128, NTLMSSP_NEGOTIATE_VERSION
The flags which directly correspond to configuration of the WCF client’s NetNamedPipe binding are highlighted:
- NTLMSSP_NEGOTIATE_IDENTIFY: Specifies that the client is willing to allow the server to identify him, but is not prepared to allow the server to impersonate him. This corresponds to the enumerated value System.Security.Principal.TokenImpersonationLevel.Identify. If this flag were not present, the client would be allowing the equivalent of System.Security.Principal.TokenImpersonationLevel.Impersonation
- NTLMSSP_NEGOTIATE_SEAL: Requires that the messages to be exchanged in the proposed security context will all be encrypted, and…
- NTLMSSP_NEGOTIATE_SIGN: Requires that the messages to be exchanged in the proposed security context will contain a cryptographically calculated signature to assure message integrity. These two flags correspond to the enumeration values in System.Net.Security.ProtectionLevel: if they are both absent, ProtectionLevel.None; if both present, ProtectionLevel.EncryptAndSign; if just NTLMSSP_NEGOTIATE_SIGN, then ProtectionLevel.Sign
As I said, these flags represent the proposals the client side is making as to what characteristics the negotiated security context should have. When the token is received at the server end (i.e. in the stream upgrade provider of the Service’s channel stack), it is passed into the SSPI function AcceptSecurityContext, which generates the token which the server will pass back to the client. In my test harness, the token returned through the pipe by the Service looks like this, again redacted:
We can see this has the same signature at the start, identifying it as an NTLM token. In fact it is the server Challenge token, containing a nonce and a series of attribute properties identifying the server. The four bytes highlighted are the server’s version of the NEGOTIATE structure, which differ somewhat from the value sent by the client. Translating them gives:
|0x35 ||00110101 || |
NTLMSSP_NEGOTIATE_SEAL, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_REQUEST_TARGET, NTLMSSP_NEGOTIATE_UNICODE
|0xc2 ||11000010 || |
|0x99 ||10011001 || |
NTLMSSP_NEGOTIATE_TARGET_INFO, NTLMSSP_NEGOTIATE_IDENTIFY, NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY, NTLMSSP_TARGET_TYPE_DOMAIN
|0xe2 ||11100010 || |
NTLMSSP_NEGOTIATE_56, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_128, NTLMSSP_NEGOTIATE_VERSION
The key flags we highlighted before show that the server has agreed the client’s proposal that messages should be both encrypted and signed, and that the security context should only allow the client to be identified, not impersonated. The flags highlighted in green have been added by the server, to confirm that it is including information about itself (the “target” of the client’s request) within the properties of this token, and that this information includes a Domain name, not just a server name.
The client-side stream upgrade provider reads this token from the pipe and passes it into another call to the SSPI function InitializeSecurityContext. This returns a third token, the NTLM Authenticate token, which the client passes back through the pipe to the server. This contains the hash data which enables the server to validate the client’s identity and discover the keys to be used for signing and encryption. When the token is passed by the server-side stream upgrade provider back into the SSPI AcceptSecurityContext function, provided everything is in order the handshake will be complete and the security context for further message exchange established.
There is some misinformation on MSDN which suggests that TokenImpersonationLevel.Identification is the only impersonation level supported by the NetNamedPipe binding. This is not the case: on the client side, the NTLMSSP_NEGOTIATE_IDENTIFY flag in the NTLM handshake can be controlled via the ClientCredentials instance associated with the client channel stack. Using the default instance, TokenImpersonationLevel.Identification is specified, so the flag is set. But if the ClientCredentials are configured explicitly for TokenImpersonationLevel.Impersonation, this flag is removed from the NTLM handshake message.
1: ChannelFactory<IMyService> myChannelFactory = new ChannelFactory<IMyService>(myBinding, myEndpoint);
3: = System.Security.Principal.TokenImpersonationLevel.Impersonation;
On the service side, the service bindings will not complain about the absence of the NTLMSSP_NEGOTIATE_IDENTIFY flag, with the result that the logon token generated when the security context is established is a valid impersonation token which the service may be able to use to impersonate the client identity. This surfaces in the service implementation as the property ServiceSecurityContext.WindowsIdentity.ImpersonationLevel being set to “Impersonation” instead of “Identification”. Note that the OperationBehavior ImpersonationOption property has no effect on the WindowsIdentity impersonation level: it is only relevant to Message security.
Whether this truly allows the service to impersonate the client depends on whether the service’s process possesses the SeImpersonatePrivilege security privilege. If it does, then resources to which the client’s account has access can be accessed by the service even though the service process’s own account does not have access:
1: using (WindowsImpersonationContext impCtx
2: = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Impersonate())
4: // Access resources using the client's identity
5: // E.g. read from client User's Documents folder
Note that there is no way to achieve TokenImpersonationLevel.Delegation in the ServiceSecurityContext: if you set TokenImpersonationLevel.Delegation on the client channel’s ClientCredentials, nothing breaks, but the effect on the server side is identical: the ServiceSecurityContext will have TokenImpersonationLevel.Impersonation.
Edit 19-Nov-2010: A final note on Impersonation. Because the client and service are on the same machine, when using the NetNamedPipeBinding Impersonation does not mean exactly what the WCF documentation suggests that it means i.e. local resource access only. In fact the service can also access remote resources such as network file shares using the client’s identity, but cannot delegate it (so a remote service invoked from the local service’s implementation while it was impersonating the client could not in turn use the client credentials to impersonate the client). Impersonation really means that the client identity is valid for resource access up to one network hop away from where the client logon was initially obtained. The same concept exists in the unmanaged world, using DCOM: the concept hasn’t changed, it’s just that the WCF documentation isn’t quite as clear as the Win32 and COM documentation.
The ProtectionLevel associated with Transport security of the NetNamedPipeBinding can be set in the WCF sections of the configuration file, or directly via the binding in code. For example, the following code would result in the NTLMSSP_NEGOTIATE_SIGN flag being set, but the NTLMSSP_NEGOTIATE_SEAL flag being clear, when a client-side channel using this binding initiated the NTLM handshake:
2: NetNamedPipeBinding stdBinding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport);
3: stdBinding.Security.Transport.ProtectionLevel = System.Net.Security.ProtectionLevel.Sign;
If the ProtectionLevel configured on the server side binding requires greater security than the settings required by the client, the handshake fails and an exception is thrown when the channel is opened:
System.ServiceModel.Security.SecurityNegotiationException: A remote side security requirement was not fulfilled during authentication. Try increasing the ProtectionLevel and/or ImpersonationLevel.
If however, the client settings are “stronger” than the server settings, the server automatically adjusts to accept the security proposed by the client. For example, if the client bindings specify ProtectionLevel.Sign and the service bindings specify ProtectionLevel.None, the NTLM handshake will succeed in establishing a security context in which all messages exchanged carry a signature, but are not encrypted.
That concludes my deep dive into the Transport security aspects of the NetNamedPipeBinding, but before I leave the topic of security in this binding, I want to take a step back and review how we ought to be using it in practice. That’s for my next post.