![]() |
Show Changes |
![]() |
Edit |
![]() |
|
![]() |
Recent Changes |
![]() |
Subscriptions |
![]() |
Lost and Found |
![]() |
Find References |
![]() |
Rename |
| Search |
History
| 2/24/2005 3:01:56 PM |
![]() |
List all versions |
VS.NET 2005 adds some very nice support for SSPI (WhatIsSSPI), making it almost trivial to establish an authenticated connection over a socket. Kerberizing a socket-based application has never been easier. The essence of this new support lies in a class called NegotiateStream (these details are based on Beta 1 of version 2.0 of the .NET Framework).
Remember, the goal of SSPI is to help add CIA support to a channel (WhatIsCIA), so it makes sense to use a stream based programming model, where the steps are:
Here's what the class looks like:
namespace System.Net.Security {
public abstract class AuthenticatedStream : Stream
{
protected AuthenticatedStream(Stream InnerStream,
bool leaveStreamOpen);
protected Stream InnerStream { get; }
public virtual bool IsAuthenticated { get; }
public virtual bool IsMutuallyAuthenticated { get; }
public virtual bool IsSigned { get; }
public virtual bool IsEncrypted { get; }
public virtual bool IsServer { get; }
}
public class NegotiateStream: AuthenticatedStream
{
public NegotiateStream(Stream innerStream)
public NegotiateStream(Stream innerStream,
bool leaveStreamOpen);
// async Authenticate methods omitted for brevity...
public virtual void ClientAuthenticate()
public virtual void ClientAuthenticate(
NetworkCredential credential,
string targetName);
public virtual void ClientAuthenticate(
NetworkCredential credential,
string targetName,
ProtectionLevel requiredProtectionLevel,
TokenImpersonationLevel allowedImpersonationLevel);
public virtual void ServerAuthenticate();
public virtual void ServerAuthenticate(
NetworkCredential credential,
SecurityLevel requiredProtectionLevel,
TokenImpersonationLevel requiredImpersonationLevel);
public virtual TokenImpersonationLevel
ImpersonationLevel { get; }
public virtual IIdentity RemoteIdentity { get; }
}
}
There are a few functions on the client and server sides that allow you to implement the handshake asynchronously; I've omitted them for brevity, but their use is pretty obvious once you see the synchronous version.
The NegotiateStream class is a lot like a CryptoStream, where you simply tie the new stream onto an existing one. As the client pushes bits into her NegotiateStream, where they're framed, MAC-protected, and encrypted, the ciphertext is pushed through the underlying stream (which is most likely tied to a socket). The server then pulls data from its NegotiateStream, which pulls ciphertext from the underlying stream, decrypts the ciphertext, verifies the MAC, and gives the server the plaintext. The server can also push data back through its stream (assuming the underlying stream supports bidirectional communication), and the client can receive that data in a similar fashion.
The one thing that's different about this stream as opposed to any other is that, if you want CIA protection, the client must first call ClientAuthenticate and the server must call ServerAuthenticate. These functions map down to SSPI's InitializeSecurityContext and AcceptSecurityContext, and basically implement an authenticated key exchange using a protocol called SPNEGO, which stands for "secure, protected negotiation." In the negotiation phase, NegotiateStream prefers Kerberos (WhatIsKerberos), but will accept NTLM as a fallback for down-level systems like Windows NT 4, or any time you are using local as opposed to domain accounts on the client and server.
Once the handshake is complete, the server can obtain the client's token (WhatIsAToken) via the RemoteIdentity property and can impersonate the client via WindowsIdentity.Impersonate (HowToImpersonateAUserGivenHerToken). Given this, the client can protect its identity by specifying an impersonation level during the handshake.
namespace System.Security.Principal {
public enum TokenImpersonationLevel {
None = 0,
Anonymous = 1,
Identification = 2,
Impersonation = 3,
Delegation = 4
}
}
For details on what these different levels mean, please read WhatIsTheCOMImpersonationLevel. All of my recommendations for using the COM impersonation level apply here as well. For now, suffice it to say that this allows the client to choose programmatically whether or not to allow delegation of its credentials to the server.
The client should also specify a service princpal name (SPN) if its wants to use Kerberos. I talked about SPN in WhatIsAServicePrincipalNameSPN.
There's one very important setting that the client and server must agree on, and that's the level of protection for the channel. For example, if the server were to tell its NegotiateStream to encrypt and the client didn't, the client would be sending cleartext data that the server would then try to decrypt, resulting in garbage! Unless you know you'll be running over some secure channel such as IPSEC (WhatIsIPSEC) I implore you to use SecurityLevel.EncryptAndSign, which is the default level if either the client or the server doesn't specify one manually. You might want to allow this to be ratcheted down to SecurityLevel.Sign when debugging in a lab environment: Network packet sniffers used during debugging won't be of much help if you're encrypting everything. In production, however, encrypt those packets!
namespace System.Net {
public enum ProtectionLevel {
None = 0, // only provides "A"
Sign = 1, // provides "IA"
EncryptAndSign = 2 // provides "CIA"
}
}
Here is a simple example that shows how to use NegotiateStream in client code:
void SpeakSecurelyWithServer(NetworkStream s,
string serverHostName) {
// form a service principal name (SPN)
string spn = string.Format("SSPISample/{0}",
serverHostName);
// wrap the raw stream in a secure one
NegotiateStream ns = new NegotiateStream(s);
ns.ClientAuthenticate(CredentialCache.DefaultNetworkCredentials,
spn,
ProtectionLevel.EncryptAndSign,
TokenImpersonationLevel.Impersonation);
// verify we achieved mutual authentication
// (note this will only be true if we’re using domain accounts)
if (!ns.IsMutuallyAuthenticated) {
Console.WriteLine("Warning: we don’t know who the server is!");
}
// now we can chat with the server over our secure channel
using (StreamReader r = new StreamReader(ns))
using (StreamWriter w = new StreamWriter(ns)) {
w.WriteLine("GET_PRODUCT_SHIP_DATE");
w.Flush();
string response = r.ReadLine();
Console.WriteLine(response);
}
}
Note in the model how I formed an SPN based on the target host name. This decouples the client from server configuration, because the client literally requests a Kerberos ticket for SSPISample/MAC3 (let's assume the server is running on a machine called MAC3). To make this work, we must tell Active Directory which server account to map this name onto, and we can do this with the setspn.exe tool as I showed in HowToUseServicePrincipalNames:
setspn -A SSPISample/MAC3 ACME\Bob
setspn.exe effectively tells the KDC that if it receives a request for a ticket for someone named SSPISample/MAC3, it should issue a ticket for ACME\Bob because that's what the server is configured to run as. This helps assure the client that the server is running under the account it's supposed to be running under, but the client doesn't have to know exactly which account that happens to be. If an attacker were spoofing the server, he'd have to know the password for ACME\Bob in order to decrypt the ticket, and authentication would fail (none of this will make much sense if you're not familiar with Kerberos, so be sure to read WhatIsKerberos).
Here's what the server might look like:
void SpeakSecurelyWithClient(NetworkStream s) {
// wrap the raw stream in a secure one
NegotiateStream ns = new NegotiateStream(s);
ns.ServerAuthenticate(
CredentialCache.DefaultNetworkCredentials,
ProtectionLevel.EncryptAndSign,
TokenImpersonationLevel.Identification);
// record who the client is for this call
Thread.CurrentPrincipal =
new WindowsPrincipal(ns.RemoteIdentity);
using (StreamReader r = new StreamReader(ns))
using (StreamWriter w = new StreamWriter(ns)) {
try {
ProcessRequest(r, w);
}
catch (Exception x) {
int recordID = RecordDetailedErrorInSecureServerLog(x);
SendClientAFriendlyButVagueErrorMessage(w, recordID);
}
}
}
[PrincipalPermission(SecurityAction.Demand, Authenticated=true)]
void ProcessRequest(StreamReader r, StreamWriter w) {
string command = r.ReadLine();
switch (command) {
case "GET_PRODUCT_SHIP_DATE":
w.WriteLine("RSN");
break;
// ... and so on
}
}
Note the use of Thread.CurrentPrincipal to track the client's identity (WhatIsThreadDotCurrentPrincipal) and the PrincipalPermissionAttribute used to gate access to the ProcessRequest method so that only authenticated clients are allowed to pass (HowToTrackClientIdentityUsingThreadDotCurrentPrincipal).
Keith's first book-in-a-wiki. If you would like to read the book online or order a physical copy to throw at annoying coworkers, surf to the HomePage. Please note that due to overwhelming wikispam, this particular wiki is no longer editable.
About FlexWiki.
Recent Topics