Share via


This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

MSDN Magazine

Security Briefs
Explore the Security Support Provider Interface Using the SSPI Workbench Utility
Keith Brown
Code for this article:Security0800.exe (135KB)
T

his month I'd like to talk about an interface that hasn't gotten nearly enough press, the Security Support Provider Interface (SSPI). I have a utility, the SSPI Workbench, to help you learn about SSPI and explore the various authentication protocols that Microsoft® Windows® 2000 supports. Let's dive right in, as there is a lot to talk about.

Patterns in Secure Network Communication

      Secure network communication generally follows a common pattern, where two parties, often strangers to one another, introduce themselves electronically over the network. For illustration, I'll call these two strangers Alice and Bob. Just like in real life, this introduction of Alice and Bob generally requires the participation of a trusted third party. This third party is known as an authorityâ€"I'll call him Trent. (The names I use here are culled from Bruce Schneier's inspiring book, Applied Cryptography.) After the introductory handshake, Alice and Bob receive an indication of who the other party purports to be, and if the handshake is successful, Alice and Bob develop trust in each other's identity.
      Once Alice trusts Bob's identity, she may want to send him data over the network. Let's say Alice and Bob are communicating over a TCP/IP connection, and that they are on separate subnets from one another so there are routers in between. It's entirely possible for a bad guy (Mallory) to have hijacked a router between Alice and Bob, so that after Alice and Bob introduce themselves Mallory can start sending packets to Bob pretending to be Alice or vice versa. Mallory can also see all the packets traveling between Alice and Bob through the compromised router, and can modify these packets at will.
      To defend against this sort of attack, during the introductory handshake Alice and Bob both agree on a secret encryption key, known as a session key. This key can be used to protect their communication and establish a secure channel over an otherwise public network. When Alice sends data to Bob, she can encrypt the entire contents of the payload, including a message sequence number. When Bob decrypts the payload and verifies that the resulting plaintext isn't gibberish and that the expected sequence number is present, Bob trusts that the following three things are true: Mallory couldn't have forged the message, Mallory couldn't have decrypted the contents of the message to read it, and Mallory couldn't be replaying a message he recorded earlier. These three assertions presuppose that Mallory doesn't know the session key.
      Encryption is expensive in terms of CPU cycles, so if Alice doesn't care about Mallory reading the contents of her message to Bob, but instead wants to protect against forgery and replays, she can protect the message using what is known as a Message Authentication Code (MAC).

Message Authentication Code

      Conceptually, a MAC is quite simple. Imagine that Alice calculated a checksum of her message and appended that checksum to the end of the message before sending the message to Bob. When Bob receives the message, he can verify the checksum. But what does this prove? It proves nothing. Mallory could have easily made changes to the message and simply recalculated and replaced the checksum. In fact, depending on the algorithm used for the checksum, Mallory might have even been able to modify the message without changing the checksum. So there are two fundamental flaws in this scheme.
      To fix the problem I just mentioned, Alice and Bob simply agree to use a sophisticated algorithm and a reasonably long checksum to guarantee that it's computationally infeasible to find another message with the same checksum. Cryptographic digest algorithms such as MD5 and SHA exist for this purpose. To fix the first problem I mentioned, Alice can use her session key to encrypt the checksum. This is much less expensive than encrypting the entire payload, and it effectively authenticates the message (thus the term "Message Authentication Code"). When Bob receives the message and the MAC, he simply performs his own checksum, encrypts it with the session key, and compares this MAC to the one Alice sent. If all goes well, Bob trusts that Mallory didn't forge or replay the message. Bob trusts that this message is truly an authentic message from Alice, the party to which he was introduced earlier.

Network Authentication Protocols

      Introducing two strangers over a public network and distributing a secret session key that those two strangers can use to secure their communication channel is a tricky business, and there are many protocols for doing it. For Windows the mainstream protocols used include Kerberos, Secure Sockets Layer (SSL), Windows NT® Lan Manager challenge-response protocol (NTLM), and the Secure Protected Negotiation protocol (SPNEGO) that allows Alice and Bob to negotiate the strongest authentication protocol that they both support while guarding against downgrade attacks from Mallory.
      Each of these protocols requires Alice and Bob to send certain bits of information known as tokens to one another. In Kerberos, Alice sends Bob a ticket plus an authenticator, and Bob may or may not respond with his own authenticator, depending on whether mutual authentication is required. SSL requires a four-way handshake, and typically provides mutual or server-only authentication via certificates. NTLM authenticates Alice to Bob using a simple three-leg challenge-response mechanism.
Figure 1 Kerberos versus NTLM
Figure 1Kerberos versus NTLM

      Trying to support more than one of these protocols would be extremely challenging, especially considering that these protocols often require communication with Trent, the trusted authority who helps strangers introduce themselves to one another electronically. Figure 1 shows the difference in the authentication handshake between Kerberos and NTLM, as an example. These differences are even more striking when Alice and Bob are in different domains (thus they have different authorities).

SSPI to the Rescue

      SSPI is the Microsoft version of the Generic Security Service API (GSSAPI) standard that is documented in RFC 1508 and 1509 (https://www.ietf.org/rfc.html). The idea behind both of these interfaces is that network authentication between two parties (such as Alice and Bob) generally follows a common pattern.
      Alice and Bob need to exchange one or more bits of information in an ordered sequence, and since Alice and Bob may be communicating using one of a number of network protocols (HTTP, RPC, SMB, IIOP, DCOM, Java RMI, and so on), it's wise to not hardcode a single authentication handshake into the network protocol itself. It's better to simply provide a mechanism to piggyback authentication tokens on top of whatever connection establishment handshake the network protocol already requires.
      For instance, in DCE RPC (and thus in DCOM, which is ultimately derived from DCE RPC), there is already a handshake used to synchronize the protocol machines on the client and server. DCE RPC simply provides space in this handshake for opaque, arbitrarily sized authentication tokens. A large part of the job that SSPI and GSSAPI perform is the generation and evaluation of these tokens.
      When Kerberos is in use and Alice makes her first RPC or DCOM call to Bob, the Kerberos Security Support Provider (SSP) on Alice's machine will be asked to generate a token to send to the server. The token contains a ticket and an authenticator proving Alice's ownership of the ticket. The server-side RPC runtime on Bob's machine takes this opaque token and simply hands it off to the server-side Kerberos SSP, which decrypts the ticket and authenticator, discovers the client's identity, and generates a token that will prove Bob's identity to Alice, assuming Alice requested mutual authentication. Bob's RPC runtime sends this back to Alice's RPC runtime, which hands the token to the client-side SSP, which will return an error if Bob's token doesn't check out.
      Ignoring the details of the Kerberos authentication handshake, the idea is that the client and server can now mix and match network transports and authentication protocols, which makes the system much more flexible. There are, however, a few exceptions to this rule. For instance, SSL authentication can only be performed over a connection-oriented transport. But even with these limitations, SSPI and GSSAPI are very important technologies.
      As a WinInet, RPC, or COM programmer, you are shielded from the details of the SSPI interaction. But understanding how it works will help you understand and often debug problems in your application. Also, if you're programming raw sockets and you want to secure your communication, it's imperative to understand SSPI.
      I've produced a rather sophisticated interactive workbench that you can use to experiment and learn about SSPI and the various authentication protocols that it abstracts. You can download the source from the link at the top of this article. The sample code focuses on conventional authentication protocols such as Kerberos and NTLM and ignores SSL because it is so utterly different. I plan to write an article in the future that covers this, so stay tuned.

Selecting a Provider

      The first thing to realize is that the client and server need to agree on a protocol. If the client and server want to use Kerberos, they both need to use the Kerberos SSP. If they each want to use NTLM, they both need to use the NTLM SSP. If the client and server want to negotiate a protocol using SPNEGO, they both need to use the SPNEGO SSP. Providers are chosen by nameâ€"for instance, the names Kerberos, NTLM, and Negotiate are valid names of SSPs. SSPI provides a function called EnumerateSecurityPackages so you can see which providers are supported on the local machine and what their various capabilities include.
      Once you've decided on an SSP, you're ready to begin making calls to SSPI.

Selecting Credentials

      The first thing the client and server need to do is choose a set of network credentials with which to perform the authentication handshake. The SSPI function used to select credentials is AcquireCredentialsHandle:

#include <sspi.h>
SECURITY_STATUS AcquireCredentialsHandle(
  SEC_CHAR *pszPrincipal,   // generally unused
  SEC_CHAR *pszPackage,     // name of package
  ULONG fCredentialUse,     // inbound/outbound
  PLUID pvLogonID,          // logon identifier
  PVOID pAuthData,          // SSP-specific data
  PVOID pGetKeyFn,          // generally unused
  PVOID pvGetKeyArgument,   // generally unused
  PCredHandle phCredential, // [out] handle
  PTimeStamp ptsExpiry      // [out] lifetime
);

The critical input parameters are pszPackage, which represents the name of the SSP you want to use, and pAuthData, which points to an SSP-specific data structure. The result of this call is an SSPI credential handle that abstracts the credentials you've chosen.
      The optional pAuthData parameter allows you to specify extra information about the credentials. On Windows 2000, this typically points to a SEC_WINNT_AUTH_IDENTITY_EX structure:


typedef struct _SEC_WINNT_AUTH_IDENTITY_EX {
    unsigned long Version; // version of struct
    unsigned long Length;  // size of struct
    LPTSTR        User;
    unsigned long UserLength;
    LPTSTR        Domain;
    unsigned long DomainLength;
    LPTSTR        Password;
    unsigned long PasswordLength;
    unsigned long Flags;
    LPTSTR        PackageList; // for SPNEGO
    unsigned long PackageListLength;
} SEC_WINNT_AUTH_IDENTITY_EX;

      If you've chosen the Negotiate package, you should specify a comma-delimited list of the real SSP names that you'd like to negotiate with your peer. You can also choose an arbitrary authority, principal, and password if you're using a conventional password-based authentication scheme like Kerberos or NTLM.
      If your process is running in the TCB, such as a service running in the SYSTEM logon session, you can choose to use the default credentials of any logon session on the machine by passing the logon session LUID via the optional parameter pvLogonID. So if Alice is logged in interactively, you can borrow her network credentials if you know her logon session LUID. This is pretty easy to discover by calling GetTokenInformation and asking for TokenStatistics; the logon session LUID is returned in the AuthenticationID field of the resulting data structure. This should be a clear reminder to be careful about which machines you log into using your domain credentials.
      The fCredentialUse parameter indicates how the credentials are to be used. Clients should specify SECPKG_CRED_OUTBOUND, and servers should specify SECPKG_CRED_INBOUND.

Figure 2 Selecting Credentials
Figure 2Selecting Credentials

      Finally, when you're finished using a credential handle, call FreeCredentialsHandle to release it. Figure 2 shows the user interface for selecting credentials in the workbench.

Selecting Context Requirements

      The client and server need to decide what sort of expectations they have for the authentication handshake before they perform it. For instance, the client might want to require mutual authentication, or to delegate her credentials to the server (Kerberos supports this). The client and server also should indicate their desire for confidentiality and integrity protection. The constants for the attributes I've listed are as follows:


#define ISC_REQ_DELEGATE    0x00000001
#define ISC_REQ_MUTUAL_AUTH 0x00000002
#define ISC_REQ_INTEGRITY   0x00010000

      For Kerberos, the client will also need to determine the server principal name she thinks she'll be talking to. (Her SSP will need to obtain a ticket for the server principal, so this is not optional, even if she doesn't require mutual authentication.) Once the client makes these decisions, she's ready to initiate a handshake with the server.

The Handshake

      To generate the first authentication token to send to the server, the client must call InitializeSecurityContext. This is a generic function that is designed to process incoming tokens from the server and generate tokens to send back to the server, so it actually takes a reference to two tokens: the incoming token that was read from the server and the outgoing token being produced by the SSP to be sent to the server on the next leg of the handshake.
      On the first leg, the incoming token reference is NULL, as you'd expect. The result of each call to InitializeSecurityContext is a context handle, which will ultimately hold a reference to the session key discovered during the handshake. This handle will later be used to encrypt or sign messages.


SECURITY_STATUS InitializeSecurityContext(
  PCredHandle phCredential,  // client cred handle
  PCtxtHandle phContext,     // cur ctx handle
  SEC_CHAR *pszTargetName,   // server principal
  ULONG fContextReq,         // required attrs
  ULONG Reserved1,           // must be zero
  ULONG TargetDataRep,       // byte ordering
  PSecBufferDesc pInput,     // incoming token
  ULONG Reserved2,           // must be zero
  PCtxtHandle phNewContext,  // new ctx handle
  PSecBufferDesc pOutput,    // outgoing token
  PULONG pfContextAttr,      // resulting attrs
  PTimeStamp ptsExpiry       // ctx lifetime
);

      Note that the first parameter is the credential handle the client selected earlier. The second parameter, phContext, will be NULL the first time the client calls this function, but for each subsequent leg the client will feed back the context handle generated from the previous call to this function (this is produced via the phNewContext parameter). The pszTargetName and fContextReq parameter are the server principal name and the requested context requirements discussed earlier. The pInput and pOutput parameters point to buffer descriptors that ultimately reference the tokens being read and generated by the SSP.

Figure 3 Context Reqs
Figure 3Context Reqs

      As I mentioned earlier, pInput will be NULL on the client's first call to this function. pOutput must reference a buffer for the SSP to write a token into, and since this is client-managed memory, the client needs to determine how much memory to allocate for the token. The client determines the maximum token size for a given SSP by calling an SSPI function named QuerySecurityPackageInfo. Figure 3 shows the interface provided by the workbench for specifying context requirements and making calls to InitializeSecurityContext.
      At this point, the client can transmit the token to the server. Figure 4 shows the workbench interface for transmitting and receiving messages. (The workbench uses raw TCP and defaults to using the local host address so both client and server can be run on the same machine.) When the server receives the token, he calls AcceptSecurityContext to pass the token to his SSP.


SECURITY_STATUS AcceptSecurityContext(
  PCredHandle phCredential,  // server cred handle
  PCtxtHandle phContext,     // cur ctx handle
  PSecBufferDesc pInput,     // incoming token
  ULONG fContextReq,         // required attrs
  ULONG TargetDataRep,       // byte ordering
  PCtxtHandle phNewContext,  // new ctx handle
  PSecBufferDesc pOutput,    // outgoing token
  PULONG pfContextAttr,      // resulting attrs
  PTimeStamp ptsTimeStamp    // ctx lifetime
);
Figure 4 Transmit/Receive
Figure 4Transmit/Receive

      As with InitializeSecurityContext, the first parameter is the credential handle (for the server this time), and phContext will be NULL the first time the server calls this function. Later calls to this function will feed back the previous context handle produced via phNewContext. The fContextReq parameter is the server's requirements for the context (for example, confidentiality and integrity), and the pInput and pOutput parameters represent the incoming token from the client and the new token produced to send back to the client.
      Given this, it's clear how the client and server produce and consume the tokens used in the authentication handshake, but it's not clear how they know how many legs of the handshake are needed. If you refer back to Figure 1, you'll notice that NTLM requires a three-leg handshake. Kerberos requires a two-leg handshake, but only if the client requests mutual authentication. If the client doesn't care about this, Kerberos only requires one leg. The answer is in the return values from these functions, which will generally take on one of two success codes (or an error code if something failed) if you're using any of the built-in SSPs:


SEC_E_CONTINUE_NEEDED
SEC_E_OK

      The first status code indicates that another call to the function is required, and the second indicates that no further calls to the function are required. Figure 5 shows how this works for Kerberos both with and without mutual authentication. If Alice receives SEC_E_CONTINUE_NEEDED, this indicates that her SSP expects at least one more token from Bob to complete the handshake. The same goes for Bob. When Alice receives SEC_E_OK, her SSP is indicating that it doesn't need any more tokens from Bob, but Alice needs to carefully check the buffer descriptor referenced by pOutput to see if her SSP wrote a token that needs to be sent to Bob. The same is true for Bob.

Figure 5 The Kerberos SSPI Handshake
Figure 5The Kerberos SSPI Handshake

      The workbench sample is basically a state machine that tracks the state of a single SSPI association from either the client's or the server's perspective. (The workbench asks upon startup whether you want to act as a server or client.) For instance, when it comes time to call InitializeSecurityContext, the client's Initialize button will be enabled (see Figure 3). When you press this button, the application calls InitializeSecurityContext and displays the resulting token in a simple hexadecimal viewer (see Figure 6). Once you transmit this token, the viewer is cleared, and if your SSP expects to receive a token from its peer, the Receive button will be enabled (see Figure 4). The workbench user interface will carefully lead you through all the steps necessary to establish a secure connection.

Figure 6 Viewing an Outgoing Token
Figure 6Viewing an Outgoing Token

      As shown in Figure 5, it's clear that SSPI only produces tokens to be shuttled back and forth between Alice and Bob. But with Kerberos, Alice must go to Trent to get a ticket for Bob before she'll have anything to send to him in the first place. When does this happen? The answer is that the SSP does this automatically whenever necessary. When Alice uses SSPI, she's only concerned with communication with her peer (Bob). It turns out that in Alice's first call to InitializeSecurityContext, the Kerberos SSP will simply look at the server principal name specified via the pszTargetName parameter, and look up a cached ticket for Bob from the credential cache specified via phCredential. If an appropriate ticket is not found, the SSP will synchronously retrieve a ticket from Trent and then add it to the cache during this first call to InitializeSecurityContext. Alice doesn't have to worry at all about this detail.
      If you're using the TktView component I presented in Security Briefs in the May 2000 issue, you can watch these tickets being added to your credential cache when you make your first call to InitializeSecurityContext for a server principal you have not authenticated with recently (don't forget to refresh the view). If you completely flush your ticket cache first, you'll notice that InitializeSecurityContext does two things: it gets a TGT so you can talk to the ticket granting service, and then it gets a ticket for the server. If you had never looked at your ticket cache, you never would have known this magic was happening, which shows how good of an abstraction SSPI can be.
      Similarly, on the final call to AcceptSecurityContext using the NTLM SSP, Bob's SSP will quietly perform pass-through authentication to communicate the challenge and response to Trent for verification. You can see this if you're running a network trace utility such as NETMON.EXE.

Verifying Context Attributes

      After the handshake is complete, the pfContextAttr parameter to both InitializeSecurityContext and AcceptSecurityContext will indicate the resulting attributes of the context to the client and server, respectively. This answers questions such as whether mutual authentication was established, whether integrity and confidentiality can be supported, and so on. If the client requested mutual authentication, she should verify that this was achieved by checking for the ISC_RET_MUTUAL_AUTH flag. The workbench displays these flags (interpreted in English) in the dropdown list shown at the bottom of Figure 3.

Impersonating the Client's Security Context

      Bob's final call to AcceptSecurityContext will establish a network logon session for Alice on Bob's machine. The token associated with this logon session will hold all of the authorization attributes for Alice that make sense on Bob's machine. Universal and global groups were injected by the client's authority, domain local groups were injected by the server's authority, aliases were injected by the server's machine, and, finally, privileges were injected from the server's machine. By calling the SSPI function ImpersonateSecurityContext (or the more modern QuerySecurityContextToken), the server can obtain this token to perform access checks to validate the client's request. Note that both of these functions require as input a server-side SSPI context handle previously established via AcceptSecurityContext.


SECURITY_STATUS ImpersonateSecurityContext(
  PCtxtHandle phContext  // preestablished
);

SECURITY_STATUS QuerySecurityContextToken(
  PCtxtHandle phContext, // preestablished context
  HANDLE *phToken        // put the token here
);

      The server-side workbench provides an option to impersonate the client once the handshake is complete. When you push the Impersonate button, the workbench calls ImpersonateSecurityContext, and calls into a helper COM component called the token dumper, producing an HTML-formatted report that shows the contents of the token.
      In a future column I'll return to complete this discussion, focusing on how Alice and Bob can use the SSPI context handles they've established to sign and encrypt messages to one another. The workbench is quite functional at this point. I've only tested it with NTLM, Kerberos, and SPNEGO (with NTLM and Kerberos in the package list). If you want to play with the other SSPs, you're on your own at this point. One fun thing to do is use the MSNâ„¢ provider in a client workbench and ask it to prompt you for credentials. Please see the readme.htm file with the workbench for instructions on its use, and notes on any known bugs that I've not yet worked out. Have fun!

Keith Brown works at DevelopMentor researching, writing, teaching, and promoting an awareness of security among programmers. Keith authored Programming Windows Security (Addison-Wesley, 2000). He co-authored Essential COM, also published by Addison-Wesley.

From the August 2000 issue of MSDN Magazine.