Programming for Secure Connectivity

[Previous] [Next]

The SSPI provides a common set of functions that can be used to authenticate clients and servers as well as ensure data privacy and integrity in your software's communication. The SSPI was designed to create a common interface for multiple SSPs supported by Windows, and to a great extent it does this.

This section discusses how to use the SSPI to initiate a conversation between a client and a server by using the Kerberos and NTLM protocols. The SSPI is designed in such a way that the same code, with only minor modifications, can be used to take advantage of Kerberos and NTLM. Because of the variety of security protocols, some SSPs will cause your SSPI code to vary greatly from the SSPI code for other protocols. SSL is an example of such an SSP. Because of this, I will discuss SSL in a section of its own later in this chapter.

NOTE
The SSPI set of functions is available on Windows 95/98, Windows NT, and Windows 2000. You should use the SSPI in both your server and client software, so it is very helpful that the SSPI functions are available on all platforms. Use of these functions is basically the same from one platform to the next, but there are also some differences in the details.

This book is dedicated to the software development on Windows 2000 and will be covering the SSPI as used on this platform. The concepts discussed in this chapter can be used as a guide for your SSPI code running on other platforms. You should consult Platform SDK docmentation for information on the differences between platforms.

NOTE
If you are going to use the SSPI functions in your code, you should be sure to include the Security.h header file with your source code. Additionally, you must link Secur32.lib with your application.

Credentials, Contexts, and Blobs

The SSPI relies on two data constructs with which you will become intimately familiar. They are credentials and contexts. Credentials are data that allow authentication. This data can be a username and password or signed information using public key infrastructure (PKI). You will see that in any SSPI exchange, both the client and the server will establish the credentials they want to use in the exchange. The credentials themselves exist outside of the SSPI, and you will use the SSPI to retrieve a handle to the credentials.

The SSPI functions will use the credentials that your client and/or server software establishes to create a user context. The client and server software will communicate their contexts to each other, allowing authentication. Server software will also be able to impersonate a user context sent to it by the client. Contexts do not contain credential information, but they contain information derived from credentials and used for a communication session. This information can include session keys, Kerberos tickets, and other security-relevant data.

Your software will maintain handles to credentials and handles to contexts that are managed by the SSPI. Also, your software will communicate context information to its counterpart so that it can build context information on the other side of the conversation. The context that a server sends to its client (or vice versa) comes in the form of a security blob generated by the SSPI. When your software receives a blob from the SSPI, it should communicate it across the wire. When your software receives a blob from the wire, it passes the blob to an SSPI function.

When using the SSPI, you will see that the authentication stage of a conversation becomes little more than a series of sending and receiving blobs, which enables the SSPI to build a user context for each side. Because the responsibility of completing an authentication is so completely shared between the server and the client, the flow of logic can be confusing. But it helps to know the components and functions involved.

First I am going to introduce the SSPI functions used by the server, and then those used by the client, and I'll show how they interact. Then we'll look at the details of calling these functions. This way you start with a big picture.

The Server and the SSPI

A server using the SSPI for secure connectivity is most likely doing so to achieve one or more of these goals:

  • Authentication/session initiation
  • Impersonation
  • Data privacy or encryption
  • Data integrity or message signing

Table 12-2 shows the functions that your server will use, grouped by which task they help to perform.

Table 12-2. SSPI functions for servers

TaskFunctionDescription
Authentication/session initiation AcquireCredentialsHandle Used to retrieve a handle indicating your credentials. One or more contexts of clients can be associated with a credentials handle. Also selects the security protocol that you are using.
AcceptSecurityContext Called repeatedly. Blobs returned from the client function InitializeSecurityContext are passed to AcceptSecurityContext and vice versa, until a context has been completed.
Impersonation ImpersonateSecurityContext Your current thread impersonates a completed context.
Data privacy/encryption EncryptMessage Encrypts data and creates blobs to be communicated.
DecryptMessage Takes blobs created by EncryptMessage and returns decrypted data.
Data integrity and message signing MakeSignature Signs data provided by the application and creates blobs intended for communication.
VerifySignature Takes blobs returned from MakeSignature and checks the signature of the message contained within the blobs.
Cleanup DeleteSecurityContext FreeCredentialsHandle Used to clean up handles when you are finished with them.

The functions in Table 12-2 are the tools your server uses to converse using the SSPI. Much of the time your server will be receiving blobs from these functions that it must communicate across the wire to its client. Other times your server will be waiting for blobs from the client. The flowchart in Figure 12-3 shows a common example of a conversation from the server's point of view.

NOTE
In this diagram, and throughout this chapter, I will be referring to made-up functions called "SendData" and "ReceiveData," which you should view as placeholders for communication functions (such as ReadFile and WriteFile or WSARecv and WSASend). I do this to emphasize the point that the SSPI is transport layer_independent.

click to view at full size.

Figure 12-3. A secure conversation from the server's point of view. The numbered squares call out the points at which the server is communicating with the client. They are numbered so that you can match them up with the points of communication on the client's flowchart, shown in Figure 12-4.

The conversation illustrated in Figure 12-3 shows the building of the user context, how the user context impersonates the client, and the sending and receiving of encrypted messages. Of course, a real conversation is likely to have many more messages and a real server would perform actions based on those messages.

NOTE
Not all servers using the SSPI will encrypt every transaction with their client. Encryption might not be necessary, so the server might sign messages instead. This type of detail—whether encryption is necessary—is your decision as the developer of the service. You don't have to sign or encrypt your data at all. If you did not, you would be using the SSPI only for an initial authentication.

The Client and the SSPI

A client uses the SSPI to achieve the same goals as a server except for impersonation, which a client cannot do. Like the server, the client calls certain functions to achieve these goals. These functions are listed in Table 12-3.

Table 12-3. SSPI functions for clients

TaskFunctionDescription
Authentication/session initiation AcquireCredentialsHandle Used to retrieve a handle indicating your credentials. One or more contexts of clients can be associated with a credentials handle. Also selects the security protocol that you are using.
InitializeSecurityContext Called repeatedly. Blobs returned from this function are sent to the server's function AcceptSecurityContext and vice versa until a context has been completed.
Data Privacy/encryption to be communicated. EncryptMessage Encrypts data and creates blobs.
DecryptMessage Takes blobs created by EncryptMessage and returns decrypted data.
Data integrity and message signing MakeSignature Signs data provided by the application and creates blobs intended for communication.
VerifySignature Takes blobs returned from MakeSignature and checks the signature of the message contained within the blobs.
Cleanup DeleteSecurityContext FreeCredentialsHandle Used to clean up handles when you are finished with them.

Notice that the key difference between the server functions (described in Table 12-2) and the client functions is that the client makes repeated calls to InitializeSecurityContext rather than to AcceptSecurityContext. Figure 12-4 shows the conversation that was illustrated in Figure 12-3, but from the client's point of view.

click to view at full size.

Figure 12-4. Secure conversation from the client's point of view. Notice that the numbered squares in this illustration match those in Figure 12-3.

Notice in Figure 12-4 that the flow of logic for the client is similar to that of the server, but the client does call some different functions, and it is the initiator. (The client's first communication is a send rather than a receive.)

The SSPI Rationale

Once you understand that the communication between your client and your server will be managed within a framework, the SSPI is a piece of cake. Writing code flexible enough to loop at seemingly arbitrary times with the goal of building a "context" can be a challenge, so before jumping in, let me present the rationale.

As you know, the SSPI allows you to converse using different security protocols, which are packaged in the form of SSPs. Different protocols have different needs in terms of what must be communicated and when, which is the reason for some of the looping and blob management of the SSPI.

Take NTLM, for example. You saw in Figure 12-1 that NTLM requires three communication legs between the client and the server: a request for authentication, a challenge forward, and a response return. When your SSPI code is authenticating using NTLM, it repeats the InitializeSecurityContext loop twice, requiring your client to communicate with the server three times. These loops correlate directly with the legs of communication seen in Figure 12-1.

If you are authenticating using Kerberos, however, the SSPI will require only one pass through the entire SSPI process. This may come as a surprise, but revisit the Kerberos protocol in Figure 12-2, which shows the client communicating with the server only once. (If mutual authentication is required, the client communicates with the server twice.) The flow of your code through the SSPI sequence will reflect this.

NOTE
The SSPI only exposes communication between the client and the server to your software. Any communication with third parties such as the KDC or the DC is handled behind the scenes in the SSPI.

OK, I have said enough about how this is all going to look. Let's examine the functions used to implement the SSPI code.

Acquiring Credentials

The first step in developing the SSPI code is to establish your own credentials to the SSPI, and to establish which security protocol you wish to use. You call AcquireCredentialsHandle to do this:

 SECURITY_STATUS AcquireCredentialsHandle(    SEC_CHAR*   pszPrincipal,    SEC_CHAR*   pszPackage,    ULONG       lCredentialUse,    PLUID       pvLogonId,    PVOID       pAuthData,    PVOID       pGetKeyFn,    PVOID       pvGetKeyArgument,    PCredHandle phCredential,    PTimeStamp  ptsExpiration); 

Notice that the AcquireCredentialsHandle function has some new data types. The SEC_CHAR data type is the string type used by the SSPI functions, and it boils down to a TCHAR. The SECURITY_STATUS type is an error value returned by all SSPI functions. Table 12-6 shows some of the currently defined errors.

AcquireCredentialsHandle might look daunting because of the number of parameters you must pass, but you will typically pass NULL for most of them. The pszPrincipal parameter is the name of the principal or entity for which you are retrieving a credential handle. You will typically pass NULL for this value, indicating the identity of the current thread (or process) token.

You pass a string indicating the security protocol you wish to use for the pszPackage parameter. The Platform SDK has values that map to the string names representing Kerberos, NTLM, Negotiate, and SSL. Table 12-4 shows these values.

Table 12-4. Security package values that can be passed for AcquireCredentialsHandle's pszPackage parameter

ValueProtocol
MICROSOFT_KERBEROS_NAME Kerberos security support provider
NTLMSP_NAME NTLM security support provider
NEGOSSP_NAME Negotiate security support provider
UNISP_NAME SChannel security support provider (SSL)

You should pass one of the defines in Table 12-4 for the pszPackage parameter.

The lCredentialsUse parameter can be set to one of the values in Table 12-5 to indicate how a particular credentials handle will be used.

Table 12-5. Values that can be passed for AcquireCredentialsHandle's lCredentialUse parameter that indicate how credentials will be used

ValueDescription
SECPKG_CRED_INBOUND Your software will validate an incoming user context. (This is a common setting for a server.)
SECPKG_CRED_OUTBOUND Your software will be authenticated to a remote party. (This is a common setting for a client.)
SECPKG_CRED_BOTH The value indicates both uses for your credentials handle, which is a common setting for a server that supports mutual authentication.

The pvLogonId parameter is used by file-system services that make calls to AcquireCredentialsHandle, and you should always pass NULL.

The pAuthData parameter is used to supply protocol-specific credentials that you wish to use to build a credentials handle. If you pass NULL, a credentials handle is returned for your token's credentials. The NTLM, Kerberos, and Negotiate security providers also allow you to pass, for the pAuthData parameter, a pointer to an instance of the following structure:

 typedef struct {    SEC_CHAR* User;    ULONG     UserLength;    SEC_CHAR* Domain;    ULONG     DomainLength;    SEC_CHAR* Password;    ULONG     PasswordLength;    ULONG     Flags; } SEC_WINNT_AUTH_IDENTITY; 

If you use the _SEC_WINNT_AUTH_IDENTITY structure and pass it as the pAuthData parameter, you should set the structure as follows:

 SEC_WINNT_AUTH_IDENTITY authIdentity = {0}; authIdentity.User = TEXT("UserName"); authIdentity.UserLength = lstrlen(TEXT("UserName")); authIdentity.Domain = TEXT("Domain"); authIdentity.DomainLength = lstrlen(TEXT("Domain")); authIdentity.Password = TEXT("Password"); authIdentity.PasswordLength = lstrlen(TEXT("Password")); authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; 

Of course you should use a real username and domain and real password values. And if you are compiling for ANSI instead of Unicode, you should assign the value SEC_WINNT_AUTH_IDENTITY_ANSI to the Flags member of the structure.

The AcquireCredentialsHandle function allows you to define a callback function that is used to create keys for use with SSPI. Most software will not need this feature and will pass NULL for the pGetKeyFn and pvGetKeyArgument parameters.

You should pass the address of a PCredHandle variable as the phCredential parameter. The AcquireCredentialsHandle function returns the newly created credentials handle in this variable. And finally, you should pass the address of a TimeStamp structure as the ptsExpiration parameter.

NOTE
Several of the SSPI functions return a TimeStamp structure. This structure is interchangeable with the standard FILETIME structure used with functions such as FileTimeToSystemTime.

All SSPI functions should return TimeStamp (or FILETIME) information in local time. See the Platform SDK documentation for more information on the FILETIME structure.

Like all SSPI functions, when AcquireCredentialsHandle succeeds, it returns SEC_E_OK. Table 12-6 lists some relevant SECURITY_STATUS values.

Table 12-6. Relevant SECURITY_STATUS values

Status CodeDescription
SEC_E_OK All is well.
SEC_E_NOT_OWNER The caller of the function is not the owner of the desired credentials.
SEC_E_INVALID_HANDLE An invalid handle was passed to a function.
SEC_E_INVALID_TOKEN An invalid token was passed to a function.
SEC_E_NOT_SUPPORTED A requested feature is not supported by a specified support provider.
SEC_E_QOP_NOT_SUPPORTED A quality-of-protection attribute is not supported by a specified support provider.
SEC_E_NO_IMPERSONATION The provided context does not have an impersonation token.
SEC_E_TARGET_UNKNOWN The target is unknown.
SEC_E_SECPKG_NOT_FOUND An unknown security package was specified.
SEC_E_NO_IMPERSONATION Impersonation is not allowed for a context.
SEC_E_LOGON_DENIED A principal is unable to log on because it does not possess the required credentials.
SEC_E_UNKNOWN_CREDENTIALS The provided credentials were not recognized.
SEC_E_NO_CREDENTIALS Credentials are not available.
SEC_E_MESSAGE_ALTERED A message supplied for verification or decryption has been modified in transit.
SEC_E_OUT_OF_SEQUENCE A message supplied for verification is out of sequence.
SEC_E_NO_AUTHENTICATING_AUTHORITY A KDC or DC was unreachable.
SEC_E_CONTEXT_EXPIRED A context expired and is now invalid.
SEC_E_INCOMPLETE_MESSAGE An incomplete message was supplied.
SEC_I_CONTINUE_NEEDED A context was not completed, and a function must be called again.
SEC_I_COMPLETE_NEEDED The function completed, but you must call CompleteAuthToken.
SEC_I_COMPLETE_AND_CONTINUE You must call CompleteAuthToken and loop and call the function again.
SEC_I_INCOMPLETE_CREDENTIALS The remote party is requesting more complete credentials. This status code can apply when the client's current credentials are anonymous.
SEC_I_RENEGOTIATE The remote party is requesting that credentials be renegotiated.
SEC_E_INSUFFICIENT_MEMORY A supplied buffer is too small.

This code fragment shows a common call to AcquireCredentialsHandle:

 CredHandle hCredentials; TimeStamp tsExpires; ss = AcquireCredentialsHandle( NULL, MICROSOFT_KERBEROS_NAME,    SECPKG_CRED_BOTH, NULL, NULL, NULL,     NULL, &hCredentials, &tsExpires ); ReportSSPIError(L"AcquireCredentialsHandle", ss); if(ss != SEC_E_OK){    // Error } 

If this call succeeds, the hCredentials variable will hold a valid handle for use in a mutually authenticating Kerberos conversation. When you are finished with the handle, pass it to FreeCredentialsHandle:

 SECURITY_STATUS FreeCredentialsHandle(    PCredHandle phCredential); 

Now that you have a handle for your credentials, you are ready to begin the authentication process.

Authentication—The Client's Role

As you already know, the authentication process differs for the client and the server. I will start explaining the authentication process with the client function InitializeSecurityContext, because an authentication typically starts with the client.

 SECURITY_STATUS InitializeSecurityContext(    PCredHandle    phCredential,    PCtxtHandle    phContext,    SEC_CHAR       *pszTargetName,    ULONG          lContextReq,    ULONG          lReserved1,    ULONG          lTargetDataRep,    PSecBufferDesc pInput,    ULONG          lReserved2,    PCtxtHandle    phNewContext,    PSecBufferDesc pOutput,    PULONG         plContextAttr,    PTimeStamp     ptsExpiration); 

The first parameter to the InitializeSecurityContext function is a handle to the credentials you received with AcquireCredentialsHandle. The InitializeSecurityContext function has a fair number of parameters, and it is meant to be called multiple times in a loop. Some of these parameters, however, are not relevant each time you call the function. I will discuss the parameters in terms of the first time you call the function, and then I will point out differences in the successive calls to InitializeSecurityContext.

The phContext structure is NULL in your first call to the function. (In future calls, it will be a pointer to a CtxtHandle variable, which holds the handle of a context "in progress.") The pszTargetName parameter is the username of the server to which you are authenticating. If the server is running in the system account on its host machine, the pszTargetName is the machine name.

The lContextReq parameter is your way of indicating to the SSPI and to the server what you would like to get out of a secure conversation. Table 12-7 lists the flags that you can pass for lContextReq.

Table 12-7. Flags you can pass for InitializeSecurityContext's lContextReq parameter

Flag Description
ISC_REQ_DELEGATE The server is allowed to delegate the client's user context. This delegation enables the server to act as a client to yet another server on behalf of the client.
ISC_REQ_MUTUAL_AUTH If you set this flag, the server must also be able to authenticate itself to the client. Mutual authentication is not supported by the NTLM protocol, but it is supported by Kerberos.
ISC_REQ_REPLAY_DETECT This flag indicates that you want the security package to sign messages making it impossible for a malicious third party to perform a replay attack. This flag implies the ISC_REQ_ INTEGRITY flag.
ISC_REQ_SEQUENCE_DETECT The SSPI will detect out-of-sequence messaging for this session context. This also requires message signing and implies the ISC_REQ_ INTEGRITY flag.
ISC_REQ_CONFIDENTIALITY You will use the context to generate encrypted messages. In Kerberos, you must have mutual authentication before you can have message confidentiality. NTLM does not support mutual authentication, so this restriction is not imposed.
ISC_REQ_USE_SESSION_KEY You wish to generate a new session key with the remote principal.
ISC_REQ_PROMPT_FOR_CREDS If the client is an interactive user, the security package will attempt to prompt the user for the appropriate credentials. This feature is not implemented for all security providers.
ISC_REQ_USE_SUPPLIED_CREDS You are supplying package-specific credentials as an input buffer to the function.
ISC_REQ_ALLOCATE_MEMORY The security package will allocate memory for your out buffers.
ISC_REQ_USE_DCE_STYLE You want a three-leg authentication transaction.
ISC_REQ_DATAGRAM Your communication layer is using data gram_style communication.
ISC_REQ_CONNECTION Your communication layer is using connection-oriented communication.
ISC_REQ_STREAM Your communication layer is using stream-style communication.
ISC_REQ_EXTENDED_ERROR If the context fails, you want to receive extended error information.
ISC_REQ_INTEGRITY Message signing is requested, but not specifically applied by the package for replay detection or message-ordering purposes.

Many of the flags in Table 12-7 will never be used in your SSPI code, but you can see that the flexibility of the package is fairly high. I will discuss some of these flags throughout this section.

The lTargetDataRep parameter indicates what byte-ordering scheme you will be using when communicating across the network. Your options are SECURITY_NATIVE_DREP and SECURITY_NETWORK_DREP. If you have the choice, you should always use SECURITY_NETWORK_DREP, to promote operating system interoperability. The pInput parameter indicates input information that you are passing to InitializeSecurityContext. With this parameter, you pass blobs and other information to the function. On your initial call to InitializeSecurityContext, you will typically pass NULL for your input buffers because you have no blobs to start with. This process is similar to that of the pOutput parameter, which returns to your software blobs that will be communicated to the server. I will discuss input and output buffers in more detail shortly.

You should pass the pointer to a CtxtHandle variable as the phNewContext parameter. The system returns the context handle to you via this parameter. This is the same CtxtHandle that you will pass to InitializeSecurityContext via the phContext field in successive calls to the function.

The plContextAttr parameter returns the attributes of your session as imposed by the security provider. This will be a composite of the attributes that you requested in the lContextReq parameter and the attributes imposed by the provider. You should always check the value returned in this parameter to ensure that your session has the attributes that are important to your software.

The ptsExpiration parameter will return a timestamp that indicates the expiration time of the session represented by the context that you are building.

Successive Calls to InitializeSecurityContext

So far, we have been discussing your client's first call to InitializeSecurityContext. In successive calls to this function, you'll notice some differences in the process:

  • The phContext parameter must point to a variable holding the context handle previously returned by the phNewContext parameter.
  • The SSPI ignores the pszTargetName parameter on successive calls to InitializeSecurityContext.
  • You will pass blobs that you received from the server into InitializeSecurityContext via the pInput parameter.
  • The server's context requirements are returned via the plContextAttr parameter, and they should be checked for a mismatch with your client's needs.

You should make successive calls to InitializeSecurityContext based on the function's return value. If it returns SEC_I_CONTINUE_NEEDED, your client should loop and call InitializeSecurityContext again. When the function returns SEC_E_OK, you have a completed context. Other return values indicate error scenarios.

Input and Output Buffers

Our discussion of InitializeSecurityContext is not finished until we really nail down the topic of input and output buffers. Input and output buffers are found throughout the SSPI functions and show up as the pInput and pOutput parameters.

Input and output buffers provide a way for software to give information, as needed, to a security support provider, in a way that is flexible enough to meet the needs of any supported protocol. This is why they are used with most of the SSPI functions that send or receive data to or from the system.

Let me explain how you use these buffers. You create an array of SecBuffer variables, which you define, and you point them to memory buffers, which you have allocated. You then put the address of the array in an instance of the SecBufferDesc type, which indicates how many buffers are in your array. Here is the definition of the SecBufferDesc structure:

 typedef struct _SecBufferDesc {    ULONG      ulVersion; // Set to SECBUFFER_VERSION    ULONG      cBuffers;     PSecBuffer pBuffers;  } SecBufferDesc; 

Following is the definition of the SecBuffer structure:

 typedef struct _SecBuffer {    ULONG cbBuffer;    ULONG BufferType;    PVOID pvBuffer; } SecBuffer; 

The cbBuffer member of SecBuffer indicates the size of the memory block pointed to by the pvBuffer member. The ulVersion member of SecBufferDesc should always be set to SECBUFFER_VERSION.

NOTE
For output buffers, you can set cbBuffer to 0 and pvBuffer to NULL, and the system will allocate buffers for you to return data. When you are finished with the returned buffers, you pass them to FreeContextBuffer. You can request that the system allocate buffers for you in this way by passing the ISC_REQ_ALLOCATE_MEMORY context requirement to the InitializeSecurityContext function.

The illustration in Figure 12-5 shows the relationship between SecBuffer and SecBufferDesc, your actual memory blocks, and InitializeSecurityContext.

click to view at full size.

Figure 12-5. SSPI input and output buffers

InitializeSecurityContext and Buffers

Trying to understand the way the SSPI deals with buffers can be confusing until you learn about the specifics. Then things clear up quite a bit. As I mentioned before, InitializeSecurityContext uses buffers to input and output security blobs that are intended for communication to the server. On the first call to InitializeSecurityContext, you can pass NULL for the pInput parameter. However, as you receive data from the server, you will be constructing buffers and passing them to the function.

For both input and output, each call to InitializeSecurityContext takes an array of one SecBuffer of type SECBUFFER_TOKEN. This buffer type indicates to the system that this buffer is either an input blob for building a context or an output blob for building a context.

Here is a code fragment that shows how you will construct these buffers for use with InitializeSecurityContext:

 // Set up "Out" buffers SecBuffer secBufferOut[1]; secBufferOut[0].BufferType = SECBUFFER_TOKEN; secBufferOut[0].cbBuffer = cbBlockToSend; secBufferOut[0].pvBuffer = pbBlockToSend; // Set up "Out" buffer descriptor SecBufferDesc secBufDescriptorOut; secBufDescriptorOut.cBuffers = 1; secBufDescriptorOut.pBuffers = secBufferOut; secBufDescriptorOut.ulVersion = SECBUFFER_VERSION; // Set up "In" buffers SecBuffer secBufferIn[1]; secBufferIn[0].BufferType = SECBUFFER_TOKEN; secBufferIn[0].cbBuffer = cbBlockReceived; secBufferIn[0].pvBuffer = pbBlockReceived; // Set up "In" buffer descriptor; SecBufferDesc secBufDescriptorIn; secBufDescriptorIn.cBuffers = 1; secBufDescriptorIn.pBuffers = secBufferIn; secBufDescriptorIn.ulVersion = SECBUFFER_VERSION; 

The pbBlockReceived and pbBlockToSend buffers are allocated before calling InitializeSecurityContext. Alternatively, you could choose to have the function allocate a block for your out buffer.

After calling InitializeSecurityContext, the cbBuffer member of the out buffer contains the size of the blob to be sent to the server. If the size is a nonzero value, you should send to the server as many bytes as indicated by the value from the buffer pointed to by the pvBuffer member. This is the "handshake" for authentication.

The SSPI defines a number of different buffer types. I will describe them as they relate to our discussion. For a list of the currently defined buffer types, see the Platform SDK documentation.

InitializeSecurityContext—Putting It All Together

There is no question about it—the number of details you have to remember when dealing with the SSPI can be daunting. Seeing how it all works in a simple function, however, will begin to make the process clearer.

Here is a sample function that uses the techniques we have discussed to build a completed context:

 BOOL ClientHandshakeAuth(CredHandle* phCredentials,     PULONG plAttributes, CtxtHandle* phContext, PTSTR pszServer){    BOOL fSuccess = FALSE;    __try{       SECURITY_STATUS ss;             // Declare in and out buffers       SecBuffer secBufferOut[1];       SecBufferDesc secBufDescriptorOut;       SecBuffer secBufferIn[1];       SecBufferDesc secBufDescriptorIn;                    // Set up some "loop state" information       BOOL fFirstPass = TRUE;       ss = SEC_I_CONTINUE_NEEDED;       while ( ss == SEC_I_CONTINUE_NEEDED ){          // In blob pointer          PBYTE pbData = NULL;          if(fFirstPass){ // First pass, no in buffers             secBufDescriptorIn.cBuffers = 0;             secBufDescriptorIn.pBuffers = NULL;             secBufDescriptorIn.ulVersion = SECBUFFER_VERSION;          }else{ // Successive passes             // Get size of blob             ULONG lSize;             ULONG lTempSize = sizeof(lSize);             ReceiveData(&lSize, &lTempSize);             // Get blob             pbData = (PBYTE)alloca(lSize);             ReceiveData(pbData, &lSize);                       // Point "In Buffer" to blob             secBufferIn[0].BufferType = SECBUFFER_TOKEN;             secBufferIn[0].cbBuffer = lSize;             secBufferIn[0].pvBuffer = pbData;             // Point "In" BufDesc to in buffer             secBufDescriptorIn.cBuffers = 1;             secBufDescriptorIn.pBuffers = secBufferIn;             secBufDescriptorIn.ulVersion = SECBUFFER_VERSION;          }                    // Set up out buffer (The SSPI will be allocating buffers for us)          secBufferOut[0].BufferType = SECBUFFER_TOKEN;          secBufferOut[0].cbBuffer = 0;          secBufferOut[0].pvBuffer = NULL;          // Point "Out" Bufdesc to out buffer          secBufDescriptorOut.cBuffers = 1;          secBufDescriptorOut.pBuffers = secBufferOut;          secBufDescriptorOut.ulVersion = SECBUFFER_VERSION;                    ss=              InitializeSecurityContext(                phCredentials,                 fFirstPass?NULL:phContext,                 pszServer,                *plAttributes | ISC_REQ_ALLOCATE_MEMORY,                0, SECURITY_NETWORK_DREP,                 &secBufDescriptorIn, 0,                 phContext,                 &secBufDescriptorOut,                 plAttributes, NULL);                     // No longer first pass through the loop          fFirstPass = FALSE;                 // Was a blob output? If so, send it.          if (secBufferOut[0].cbBuffer!=0){             // Server communication!!!             // Send the size of the blob.             SendData(&secBufferOut[0].cbBuffer, sizeof(ULONG));             // Send the blob itself             SendData(secBufferOut[0].pvBuffer,                 secBufferOut[0].cbBuffer);             // Free out buffer             FreeContextBuffer(secBufferOut[0].pvBuffer);          }       }// Loop if ss == SEC_I_CONTINUE_NEEDED;       // Final result       if (ss != SEC_E_OK){          __leave;       }       fSuccess = TRUE;    }__finally{       // Clear the context handle if we fail       if (!fSuccess){          ZeroMemory(phContext, sizeof(*phContext));       }    }    return (fSuccess); } 

The ClientHandshakeAuth function takes a credentials handle returned from AcquireCredentialsHandle, some flags, and a server name, and if successful at building a context, returns a completed context.

Notice that ClientHandshakeAuth function communicates with the server in two spots in the code. The functions I use to communicate are fictional functions named SendData and ReceiveData. They take a buffer and a size, and are placeholders for whatever communication mechanism you choose for your software.

Notice also that when I communicate a blob, I send the blob's size before I send the blob, and when I receive a blob, I read the blob's size before receiving it.

NOTE
Because the SSPI is communication transport-independent, it is necessary for you to create some sort of protocol by which you communicate the blobs to the principal on the other side of the wire.

Pay special attention to the buffer management in this function, and notice how InitializeSecurityContext communicates to our software when a blob is to be sent across the wire and when the function needs to loop again. This is the essence of the client-side responsibility in the authentication handshake with the SSPI.

The following code fragment could be used to get a credentials handle and to call ClientHandshakeAuth to begin authenticating with the Kerberos protocol:

 CredHandle hCredentials; TimeStamp tsExpires; SECURITY_STATUS ss = AcquireCredentialsHandle( NULL,     MICROSOFT_KERBEROS_NAME, SECPKG_CRED_BOTH, NULL, NULL,     NULL, NULL, &hCredentials, &tsExpires ); if(ss != SEC_E_OK){    // Error } ULONG lAttributes =     ISC_REQ_STREAM|ISC_REQ_CONFIDENTIALITY|ISC_REQ_MUTUAL_AUTH; CtxtHandle hContext = {0}; if(!ClientHandshakeAuth(&hCredentials, &lAttributes,     &hContext, TEXT("jclark-piii600"))){    // Error }    // If successful, we have authenticated at this point DeleteSecurityContext(&hContext);    FreeCredentialsHandle(&hCredentials); 

In this code, AcquireCredentialsHandle returns a credentials handle representing the identity of the calling function. Then we call our sample function ClientHandshakeAuth to indicate that we want mutual authentication and encryption capability, and that we will be communicating using a streaming technology. I am now going to talk about the server side of the authentication in the SSPI. Before you move on, you might find it useful to take a moment to review Figures 12-3 and 12-4, which illustrate the client and server sides of an SSPI conversation. Think about these figures in the context of the code fragments we've just examined.

Authentication—The Server's Role

Understanding the client's role in an authentication handshake using the SSPI is helpful in understanding the server's role. In fact, once either side is understood, the other side becomes very approachable. The server function for managing blobs is AcceptSecurityContext:

 SECURITY_STATUS AcceptSecurityContext(    PCredHandle    phCredential,    PCtxtHandle    phContext,    PSecBufferDesc pInput,    ULONG          lContextReq,    ULONG          lTargetDataRep,    PCtxtHandle    phNewContext,    PSecBufferDesc pOutput,    PULONG         pfContextAttr,    PTimeStamp     ptsExpiration); 

Notice that AcceptSecurityContext has the same parameters as InitializeSecurityContext, except that it does not have the two reserved parameters and the parameter indicating the server's name. Naturally the server name is not needed, because it is the server who is calling AcceptSecurityContext.

Like the role of its client counterpart, AcceptSecurityContext's role is to receive and produce blobs communicated with the opposite principal. Both AcceptSecurityContext and InitializeSecurityContext must be allowed to loop until they return SEC_E_OK. There are two noteworthy differences when using AcceptSecurityContext:

  • The first time you call AcceptSecurityContext, you have already received your first blob from your client, so there is always an input buffer used with this function. (This is unlike InitializeSecurityContext, whose initial pass has no input buffer.)
  • AcceptSecurityContext uses the same context requirements as InitializeSecurityContext (as listed in Table 12-7), except that the values for the lContextReq parameter of AcceptSecurityContext have a different prefix. Instead of starting with "ISC_REQ_", which stands for "InitializeSecurityContext Requirement", AcceptSecurityContext's requirements start with "ASC_REQ_". For example, the equivalent of ISC_REQ_CONFIDENTIALITY would be ASC_REQ_CONFIDENTIALITY.

Aside from these two differences, you use AcceptSecurityContext largely the same way you use InitializeSecurityContext. However, the resulting context with AcceptSecurityContext is more capable in that the server can use it to impersonate or otherwise obtain a handle to a token, which we will discuss in a moment. Let's look at a sample function that shows the use of AcceptSecurityContext in server code:

 BOOL ServerHandshakeAuth(CredHandle* phCredentials,     PULONG plAttributes, CtxtHandle *phContext){    BOOL fSuccess = FALSE;    __try{       SECURITY_STATUS ss;       // Declare in and out buffers       SecBuffer secBufferIn[1];       SecBufferDesc secBufDescriptorIn;              SecBuffer secBufferOut[1];       SecBufferDesc secBufDescriptorOut;              // Set up some "loop state" information       BOOL fFirstPass = TRUE;       ss = SEC_I_CONTINUE_NEEDED;       while (ss == SEC_I_CONTINUE_NEEDED){          // Client communication!!!          // Get size of blob.          ULONG lSize;          ULONG lTempSize = sizeof(lSize);          ReceiveData(&lSize, &lTempSize);          // Get blob          PBYTE pbTokenBuf = (PBYTE)alloca(lSize);          ReceiveData(pbTokenBuf, &lSize);             // Point "In Buffer" to blob          secBufferIn[0].BufferType = SECBUFFER_TOKEN;          secBufferIn[0].cbBuffer = lSize;          secBufferIn[0].pvBuffer = pbTokenBuf;          // Point "In" BufDesc to in buffer          secBufDescriptorIn.ulVersion = SECBUFFER_VERSION;          secBufDescriptorIn.cBuffers = 1;          secBufDescriptorIn.pBuffers = secBufferIn;            // Set up out buffer           // (The SSPI will be allocating buffers for us)          secBufferOut[0].BufferType = SECBUFFER_TOKEN;          secBufferOut[0].cbBuffer = 0;          secBufferOut[0].pvBuffer = NULL;          // Point "Out" BufDesc to out buffer            secBufDescriptorOut.ulVersion = SECBUFFER_VERSION;          secBufDescriptorOut.cBuffers = 1;           secBufDescriptorOut.pBuffers = secBufferOut;          // Here is our blob management function          ss =              AcceptSecurityContext(                phCredentials,                 fFirstPass?NULL:phContext,                 &secBufDescriptorIn,                 *plAttributes | ASC_REQ_ALLOCATE_MEMORY,                SECURITY_NETWORK_DREP,                 phContext,                 &secBufDescriptorOut,                 plAttributes, NULL);          // No longer first pass through the loop          fFirstPass = FALSE;          // Was a blob output?  If so, send it.          if (secBufferOut[0].cbBuffer != 0){                           // Client communication !!!             // Send the size of the blob             SendData(&secBufferOut[0].cbBuffer, sizeof(ULONG));             // Send the blob itself             SendData(secBufferOut[0].pvBuffer,                 secBufferOut[0].cbBuffer);                          // Free out buffer             FreeContextBuffer(secBufferOut[0].pvBuffer);          }       }// Loop if ss == SEC_I_CONTINUE_NEEDED;       // Final result       if(ss != SEC_E_OK){          __leave;       }       fSuccess = TRUE;    }__finally{       // Clear the context handle if we fail       if (!fSuccess){          ZeroMemory(phContext, sizeof(*phContext));       }    }       return (fSuccess); } 

Notice that ServerHandshakeAuth is using pseudo-functions named SendData and ReceiveData, which are meant to represent any communication mechanism. Also, ServerHandshakeAuth sends and receives the sizes of blobs so that the other side knows how much data to receive.

Again, pay particular attention to the buffer management code. Notice that the blobs that are received are passed into the function via in buffers. Meanwhile, the existence of an out buffer is checked and then sent across the wire if it exists. Also, this use of AcceptSecurityContext allows the function to allocate memory buffers for output blobs, and these buffers are freed with FreeContextBuffer in the same way they are with InitializeSecurityContext.

The following code fragment could be used to set up a credentials handle for use in calling the ServerHandshakeAuth sample function:

 CredHandle hCredentials; TimeStamp tsExpires; SECURITY_STATUS ss = AcquireCredentialsHandle( NULL,     MICROSOFT_KERBEROS_NAME, SECPKG_CRED_BOTH, NULL, NULL,     NULL, NULL, &hCredentials, &tsExpires ); if(ss != SEC_E_OK){    // Error } ULONG lAttributes = ASC_REQ_STREAM; CtxtHandle hContext = {0}; if(!ServerHandshakeAuth(&hCredentials,     &lAttributes, &hContext)){    // Error } ss = ImpersonateSecurityContext(&hContext); if(ss != SEC_E_OK){    __leave; } DeleteSecurityContext(&hContext); FreeCredentialsHandle(&hCredentials); 

Impersonation and Token Acquisition

Impersonation is an important part of authentication, from the server's point of view. Fortunately, when you have a completed context handle, impersonation is easy. You use ImpersonateSecurityContext and RevertSecurityContext as shown here:

 SECURITY_STATUS ImpersonateSecurityContext(    PCtxtHandle phContext); SECURITY_STATUS RevertSecurityContext(    PCtxtHandle phContext); 

You can also request a token handle from a security context using the QuerySecurityContextToken function:

 SECURITY_STATUS QuerySecurityContextToken(    PCtxtHandle phContext,    HANDLE      *phToken); 

With the useful QuerySecurityContextToken function, you can get a client's token and store it with the connection information, without first impersonating the client.

Impersonation is a great way for a server to manage security and is covered in detail in Chapter 11. The SSPI allows you to impersonate a client over any communication medium. You are no longer limited to pipes or RPC. You can impersonate a client with sockets, IPX/SPX, or a serial connection if it serves your software's needs.

You can find a complete implementation of client and server SSPI programming in the SSPIChat sample application discussed later in this chapter. But for now I would like to talk more about message signing and encryption.

Message Signing and Encryption

Once you have negotiated an authentication between the client and server, the client and server can begin a systematic exchange of messages as expected in client/server communication. To take full advantage of this exchange, you should sign or encrypt the messages that are communicated. Here are guidelines to follow:

  1. Sign messages when privacy is not needed but assuredness of message integrity is needed (which is just about always).
  2. Encrypt messages when privacy is needed. (With encryption, signing is not necessary.)

Signing and encryption use parallel functions in the same way that authentication does, but the mechanisms are the same for client and server. The function pair for creating a signed message is MakeSignature and VerifySignature. The MakeSignature function is defined as follows:

 SECURITY_STATUS MakeSignature(    PCtxtHandle    phContext,    ULONG          lQOP,    PSecBufferDesc pMessage,    ULONG          MessageSeqNo); 

You pass a data buffer and a completed context to MakeSignature, and the function returns a signature that you pass (along with the data buffer) across the wire. The receiving side of the communication takes this information and verifies the data buffer by using the signature. Once again, you should view the data returned by MakeSignature as an opaque blob intended for communication only.

The lQOP parameter is security protocol_specific and allows you to specify the details of the signing algorithm used. You will usually pass zero for this parameter. If you are keeping track of message sequence numbers, you should pass the message's sequence number as the MessageSeqNo parameter. If you pass zero, sequence does not matter to you.

As with the other SSPI functions we have dealt with, data is passed to and received from MakeSignature via data buffers. In the case of MakeSignature, two buffers are passed with a single buffer descriptor, so you must make an array of two SecBuffer variables. You will set one to type SECBUFFER_TOKEN, which will receive the signature for the message. The second buffer is of type SECBUFFER_DATA, which indicates the data itself. Here is a function that signs a data buffer and sends it using the pseudo-communication function SendData:

 BOOL SendSignedMessage(CtxtHandle* phContext, PVOID pvData, ULONG lSize) {    BOOL fSuccess = FALSE;    __try{       SECURITY_STATUS ss;              // Find some important max buffer size information       SecPkgContext_Sizes sizes;       ss = QueryContextAttributes( phContext, SECPKG_ATTR_SIZES, &sizes );       if(ss != SEC_E_OK){          __leave;       }       PVOID pvSignature = alloca(sizes.cbMaxSignature);       SecBuffer secBuffer[2];             // Set up buffer to receive signature       secBuffer[0].BufferType = SECBUFFER_TOKEN;       secBuffer[0].cbBuffer = sizes.cbMaxSignature;       secBuffer[0].pvBuffer = pvSignature;       // Set up buffer to point to message data       secBuffer[1].BufferType = SECBUFFER_DATA;       secBuffer[1].cbBuffer = lSize;       secBuffer[1].pvBuffer = pvData;       // Set up buffer descriptor       SecBufferDesc secBufferDesc;       secBufferDesc.cBuffers = 2;       secBufferDesc.pBuffers = secBuffer;       secBufferDesc.ulVersion = SECBUFFER_VERSION;       // Make signature       ss = MakeSignature( phContext, 0, &secBufferDesc, 0 );       if(ss != SEC_E_OK){          __leave;       }       // Send signature         SendData(&secBuffer[0].cbBuffer, sizeof(ULONG));       SendData(secBuffer[0].pvBuffer, secBuffer[0].cbBuffer);       // Send message       SendData(&secBuffer[1].cbBuffer, sizeof(ULONG));       SendData(secBuffer[1].pvBuffer, secBuffer[1].cbBuffer);       fSuccess = TRUE;    }__finally{    }          return (fSuccess); } 

NOTE
Unlike InitializeSecurityContext and AcceptSecurityContext, MakeSignature does not have separate in and out buffers, and it will not allocate buffers for output. This means that you must supply a buffer large enough for the generated signature. You can find the maximum signature size by using the QueryContextAttributes function with the SECPKG_ATTR_SIZES attribute. The QueryContextAttributes function is defined as follows:

 SECURITY_STATUS QueryContextAttributes(    PCtxtHandle phContext,     ULONG       lAttribute,     PVOID       pBuffer);

In this case you pass an instance of a SecPkgContext_Sizes structure to be populated by the function. Here is the structure's definition:

 typedef struct _SecPkgContext_Sizes {     ULONG cbMaxToken;     ULONG cbMaxSignature;     ULONG cbBlockSize;     ULONG cbSecurityTrailer;  } SecPkgContext_Sizes; 

The SecPkgContext_Sizes structure conveniently contains some information about maximum sizes, including cbMaxSignature, which you will use with MakeSignature, as shown in the preceding code sample.

The other side of the transaction must read the signature and the message data, and pass it to VerifySignature:

 SECURITY_STATUS VerifySignature(    PCtxtHandle    phContext,    PSecBufferDesc pMessage,    ULONG          MessageSeqNo,    PULONG         plQOP); 

The VerifySignature function is similar to MakeSignature in that it takes the same buffers and types. However, VerifySignature assures only that the message has not been modified and does not modify the buffers. Here is a sample function that receives a signature and a message and calls VerifySignature:

 PVOID GetSignedMessage(CtxtHandle* phContext, PULONG plSize) {    PVOID pvMessage = NULL;    __try{       SECURITY_STATUS ss;       ULONG lSigLen;       PVOID pvDataSig;       // Get signature length       ULONG lTempSize = sizeof(lSigLen);       ReceiveData(&lSigLen, &lTempSize);       pvDataSig = alloca(lSigLen);       // Get signature       ReceiveData(pvDataSig, &lSigLen);       ULONG lMsgLen;       PVOID pvDataMsg;       // Get message length       lTempSize = sizeof(lMsgLen);       ReceiveData(&lMsgLen, &lTempSize);       pvDataMsg = alloca(lMsgLen);       // Get message       ReceiveData(pvDataMsg, &lMsgLen);           SecBuffer secBuffer[2];             // Set up signature buffer       secBuffer[0].BufferType = SECBUFFER_TOKEN;       secBuffer[0].cbBuffer = lSigLen;       secBuffer[0].pvBuffer = pvDataSig;       // Set up message buffer       secBuffer[1].BufferType = SECBUFFER_DATA;       secBuffer[1].cbBuffer = lMsgLen;       secBuffer[1].pvBuffer = pvDataMsg;       // Set up buffer descriptor       SecBufferDesc secBufferDesc;       secBufferDesc.cBuffers = 2;       secBufferDesc.pBuffers = secBuffer;       secBufferDesc.ulVersion = SECBUFFER_VERSION;       ULONG lQual=0;       // Verify signature       ss = VerifySignature(phContext, &secBufferDesc, 0, &lQual);       if (ss != SEC_E_OK){          __leave;       }       // Return a buffer that must be freed, containing message       pvMessage = LocalAlloc(LPTR, secBuffer[1].cbBuffer);       if (pvMessage != NULL){          CopyMemory(pvMessage, secBuffer[1].pvBuffer,              secBuffer[1].cbBuffer);       }    }__finally{};    return pvMessage; } 

If the message has been modified in transit, VerifySignature will return SEC_E_MESSAGE_ALTERED or SEC_E_OUT_OF_SEQUENCE.

Encrypting Messages

The process of encrypting and decrypting messages is very similar to that of signing and verifying messages, but the message is encrypted and decrypted in place. This means that the data buffer you pass will be modified by these functions.

Because some encryption algorithms require that encrypted data come in multiples of certain block sizes (such as 8 bytes or 16 bytes), it is necessary to check the block size and include a third buffer as a sort of "encryption overflow" that must also be sent across the wire. The functions we use are EncryptMessage and DecryptMessage. EncryptMessage is defined as follows:

 SECURITY_STATUS EncryptMessage(    PCtxtHandle    phContext,    ULONG          lQOP,    PSecBufferDesc pMessage,    ULONG          MessageSeqNo); 

DecryptMessage is defined like this:

 SECURITY_STATUS DecryptMessage(    PCtxtHandle    phContext,    PSecBufferDesc pMessage,    ULONG          MessageSeqNo,    PULONG         plQOP ); 

Here is a sample function that encrypts a message and sends it across the wire by using SendData:

 BOOL SendEncryptedMessage(CtxtHandle* phContext,     PVOID pvData, ULONG lSize){    BOOL fSuccess = FALSE;    __try{       SECURITY_STATUS ss;       // Get some important size information       SecPkgContext_Sizes sizes;       ss = QueryContextAttributes(phContext, SECPKG_ATTR_SIZES, &sizes);       if(ss != SEC_E_OK){          __leave;       }       // Allocate our buffers       PVOID pvPadding = alloca(sizes.cbBlockSize);       PVOID pvSignature = alloca(sizes.cbSecurityTrailer);       // Best to copy the message buffer, since it is encrypted in place       PVOID pvMessage = alloca(lSize);       CopyMemory(pvMessage, pvData, lSize);       SecBuffer secBuffer[3] = {0};       // Set up the signature buffer       secBuffer[0].BufferType = SECBUFFER_TOKEN;       secBuffer[0].cbBuffer = sizes.cbSecurityTrailer;       secBuffer[0].pvBuffer = pvSignature;        // Set up the message buffer       secBuffer[1].BufferType = SECBUFFER_DATA;       secBuffer[1].cbBuffer = lSize;       secBuffer[1].pvBuffer = pvMessage;       // Set up the padding buffer       secBuffer[2].BufferType = SECBUFFER_PADDING;       secBuffer[2].cbBuffer = sizes.cbBlockSize;       secBuffer[2].pvBuffer = pvPadding;       // Set up buffer descriptor       SecBufferDesc secBufferDesc;       secBufferDesc.cBuffers = 3;       secBufferDesc.pBuffers = secBuffer;       secBufferDesc.ulVersion = SECBUFFER_VERSION;       // Encrypt message       ss = EncryptMessage( phContext, 0, &secBufferDesc, 0 );       if(ss != SEC_E_OK){          __leave;       }       // Send token       SendData(&secBuffer[0].cbBuffer, sizeof(ULONG));       SendData(secBuffer[0].pvBuffer, secBuffer[0].cbBuffer);              // Send message       SendData(&secBuffer[1].cbBuffer, sizeof(ULONG));       SendData(secBuffer[1].pvBuffer, secBuffer[1].cbBuffer);                          // Send padding       SendData(&secBuffer[2].cbBuffer, sizeof(ULONG));       SendData(secBuffer[2].pvBuffer, secBuffer[2].cbBuffer);       fSuccess = TRUE;    }__finally{}    return fSuccess; } 

Notice that this code is very similar to code that you are already familiar with from message signing. The next function, GetEncryptedMessage, is a complementary sample function to decrypt a message and return a buffer:

 PVOID GetEncryptedMessage(CtxtHandle* phContext, PULONG plSize){    PVOID pvMessage = NULL;    __try{       SECURITY_STATUS ss;       ULONG lSigLen;       PVOID pvDataSig;       // Get signature length       ULONG lTempSize = sizeof(lSigLen);       ReceiveData(&lSigLen, &lTempSize);       pvDataSig = alloca(lSigLen);       // Get signature       ReceiveData(pvDataSig, &lSigLen);       ULONG lMsgLen;       PVOID pvDataMsg;       // Get message length       lTempSize = sizeof(lMsgLen);       ReceiveData(&lMsgLen, &lTempSize);       pvDataMsg = alloca(lMsgLen);       // Get message       ReceiveData(pvDataMsg, &lMsgLen);       ULONG lPadLen;       PVOID pvDataPad;       // Get padding length       lTempSize = sizeof(lPadLen);       ReceiveData(&lPadLen, &lTempSize);       pvDataPad = alloca(lPadLen);       // Get padding       ReceiveData(pvDataPad, &lPadLen);                SecBuffer secBuffer[3] = {0};       // Set up signature buffer       secBuffer[0].BufferType = SECBUFFER_TOKEN;       secBuffer[0].cbBuffer = lSigLen;       secBuffer[0].pvBuffer = pvDataSig;       // Set up message buffer       secBuffer[1].BufferType = SECBUFFER_DATA;       secBuffer[1].cbBuffer = lMsgLen;       secBuffer[1].pvBuffer = pvDataMsg;       // Set up padding buffer       secBuffer[2].BufferType = SECBUFFER_PADDING;       secBuffer[2].cbBuffer = lPadLen;       secBuffer[2].pvBuffer = pvDataPad;             // Set up buffer descriptor       SecBufferDesc secBufferDesc;       secBufferDesc.cBuffers = 3;       secBufferDesc.pBuffers = secBuffer;       secBufferDesc.ulVersion = SECBUFFER_VERSION;       ULONG lQual=0;       ss = DecryptMessage( phContext, &secBufferDesc, 0, &lQual );       if (ss != SEC_E_OK){          __leave;       }       // Return a buffer that must be freed, containing message       pvMessage = LocalAlloc(LPTR, secBuffer[1].cbBuffer);       if (pvMessage != NULL){          CopyMemory(pvMessage, secBuffer[1].pvBuffer,              secBuffer[1].cbBuffer);       }    }__finally{}    return (pvMessage); } 

Now that you are familiar with encrypting and signing messages, you have the tools to implement a full session with the SSPI. You know how to negotiate an authentication, and your server can impersonate the client. And you know how to send and receive data in a safe way.

The SSPIChat Sample Application

The SSPIChat sample application ("12 SSPIChat.exe") demonstrates all the SSPI-related technologies we have discussed so far, including client and server authentication negotiation, impersonation, message signing, and encryption. The source code and resource files for the sample application are in the 12-SSPIChat directory on the companion CD. Figure 12-6 shows the user interface for the SSPIChat sample application.

click to view at full size.

Figure 12-6. User interface for the SSPIChat sample application

To use the sample, you should run it more than once, either on the same machine or on different machines connected on a network. The sample application communicates using TCP/IP. You can select which security provider you want to use before initiating conversation. You can also select whether you want mutual authentication, encryption, or delegation.

The delegation feature causes the server to create a second chat window assuming the client's identity. The new window can then be used to act as a client to a third server, which will see it as the original client. (This feature will fail if the server machine has not been trusted for delegation.)

In the spirit of communication layer independence, the communication functionality in this sample application is abstracted to a class known as CTransport, which includes SendData and ReceiveData functions. These two functions are much like the pseudo-functions I have been using in the code fragments in this chapter.

I strongly suggest you become familiar with the sample code for this application before attempting to write SSPI code using SSL (which is covered later in this chapter). The SSPI programming model is complex, and it comes with a handful of gotchas. Comfort with the overall approach will make the SSL model much more palatable.

CryptoAPI

The CryptoAPI, or CAPI, provides a full set of functions for generating keys used to encrypt and decrypt data, as well as for exporting keys and sharing them securely. The CryptoAPI also includes full support for certificates and certificate management. The cryptography and certificate functions provided by the CryptoAPI are defined in WinCrypt.h; your code should include this header. You should also be sure to link with the Crypt32.lib library file.

The CryptoAPI provides such a rich set of features that you could use its functions to implement your own fully secure protocol for communicating over insecure network environments. However, the SSPI already does this for us for free. In fact, the SSPI uses the CryptoAPI under the covers to implement its cryptographic needs. If you are interested in creating your own secure protocol, you can use the CryptoAPI. However, for most projects you should use the known protocols supported by the SSPI.

In two instances it is valid to use the CryptoAPI because the SSPI is not able to do the job for you. They are as follows:

  • Non-session-related cryptography This includes file cryptography or any other encryption of persistent data that is not being communicated in a session-oriented environment.
  • Certificate management When we discuss SSL using the SSPI, you will see how critical certificates are to the protocol. Certificate management is performed by the CryptoAPI set.

Encryption for persistent storage is not a common need, and because it is fully supported by the Encrypted File System (EFS) technology of Windows 2000, I will not cover this particular use of the CryptoAPI here. If you are interested in using the CryptoAPI for encryption of this type, you can find a full description of its features in the Platform SDK documentation.

However, certificate management is an integral part of the SSL security protocol. In fact, before I can move into my discussion of SSL, I have to spend some time discussing certificate management using the CryptoAPI.

Certificate Stores

Windows manages certificates in terms of stores. The CryptoAPI allows you to manage many different kinds of stores, ranging from personal stores for a single user, to stores for machines, to temporary stores that exist only in memory. When you create a store, you can designate the medium on which it persists. Or it does not have to persist at all.

Stores contain certificates, and as you know, certificates are used for authentication, signing, and encryption. When dealing with SSL, however, you are unlikely to need to deal with any store other than the machine store (which you will use to store server certificates), or perhaps a personal store (which would be used to store client certificates). Allow me to describe several scenarios.

  • Scenario 1 You are designing a service that will communicate by using SSL, which is running as the LocalSystem account on a machine. Let's say that the client will authenticate the server, but not vice versa. You would most likely create a certificate (and matching private key) for this service and store them in the machine store for your host system. The service knows which certificate it is by its common name. When the service connects to a client, it uses the CryptoAPI to look up its certificate, and then it uses this certificate to initiate a communication session using SSPI and SSL. In this case, the server knows that it will look up the certificate in the machine store.
  • After authentication, the client has a copy of the certificate, communicated over the wire. Although the client is not sending a certificate back, it must still decrypt the information in the certificate to find out whether it has connected to the server it was looking for. With Windows 2000, SSL automatically verifies the certificate chain against your client's trusted CAs. It also does a comparison between the server name (as requested by the client) and the common name on the server's certificate.

    Your client, however, can choose to use the CryptoAPI functions to open the certificate, extract the common name, and compare it against the known common name of the public certificate that it expected to receive. If the names don't match, the client can choose to close the connection, because the server might have been spoofed by a malicious third party. Although Windows 2000 performs this test for you, earlier versions of Windows will not, so it is a good skill to be familiar with.

    Trust enters into the situation here, because you must be sure that the CA does not issue certificates with the same common name to multiple parties. Policy in this area could vary from one certificate provider to the next.

NOTE
Note that the common name of a certificate is simply a text string. However, remember that certificates are signed with the certificate authority's private key. This means that no information on the certificate has changed when you open it using the CA's known public key.

Browser software uses the common name of a certificate to perform a character-for-character comparison of the certificate and the URL to which the browser is connected. If the names do not match, the browser assumes that security has been breached.

  • Scenario 2 Everything is the same as Scenario 1, except that the server is not running in the LocalSystem account but rather is running in a special domain account created just for the service. In terms of certificates, the only major difference is that you (the server's administrator) would want to store a certificate in the personal store for the server's account. This is because the server's account might not be an administrator of its host machine, and thus would not have access to the machine certificate store. So you must know how to access personal accounts.
  • Scenario 3 Whether or not your server will be accessing machine accounts or personal accounts, a third scenario must be considered. If your server requires a certificate from your client, your client will have to follow a similar certificate-finding approach as your server.
  • When a connection is made, your client will look up a certificate on its machine and use that certificate to initiate an SSL conversation. Since most client software is being run under an interactive user account, your client software will most likely have to know how to look up a certificate from a personal store, by common name, and then send it across the wire.

If you will be writing software to handle any of these scenarios, here are the tasks you must be able to perform with the CryptoAPI:

  1. Open a certificate store.
  2. Look up a certificate by common name.
  3. Decrypt a certificate (communicated to you).
  4. Look up a decrypted certificate's common name (and compare it to a known name).

You can perform all these tasks using the CryptoAPI. However, there is one implied task I have not mentioned that you must be able to do—get a certificate.

Getting a Certificate

Getting a certificate is a complex topic, largely because of the different ways that it can be performed. I will give you some starting points, but you will have to research your options and decide which is best for you. Here are the two main approaches:

  1. Purchase a certificate from a public certificate authority.
  2. Run your own certificate authority using Microsoft Certificate Services.

The first approach involves interacting with a third party such as VeriSign or Thawte (among many others) to get a certificate for your server software. These third parties will store information about your company as well as the uses for your certificate, and they will give you a certificate, signed by them, for a fee.

The advantage of this approach is that known certificate authorities are trusted by most operating systems by default. So the client software (or human user) does not have to go to any extra trouble to recognize your server's certificate.

Some of these public certificate authorities will happily give you a temporary certificate signed by a test authority (also owned by them), with which you can test their services. Some companies offer these temporary certificates for free.

The second approach involves running Microsoft Certificate Services. This approach is great, except that you must make sure your client software trusts the certificate authority that you set up. If your client and server are running in the same enterprise, you will most likely follow this approach. If your client and server run on the Internet, you can still take this approach, but your client software must know how to help the user establish trust with your CA.

Once you have a certificate, the certificate can be managed and moved from one system store to another using the Certificates snap-in for Microsoft Management Console (MMC). This allows you to test different scenarios with a single certificate. For example, you could create a server certificate, drag it into the machine's Personal certificates folder, and then test your server under the machine account. Later you might decide to run your server under a user account, at which time you would use the snap-in to drag the certificate to the Personal certificates folder for that user's account.

Using the CryptoAPI to Open a Certificate Store

Certificate stores are identified by a textual name. The personal store for a machine account or a user account is named "MY". You use CertOpenStore to open a certificate store:

 HCERTSTORE WINAPI CertOpenStore(    PCSTR      pszStoreProvider,    DWORD      dwMsgAndCertEncodingType,    HCRYPTPROV hCryptProv,    DWORD      dwFlags,    const void *pvPara); 

This function is very flexible and will let you open and create stores in many ways. We will focus our discussion on the machine's personal store or a user account's personal store. Both of these stores are known as system stores. To open a system store, you pass CERT_STORE_PROV_SYSTEM as the pszStoreProvider parameter. (Pass CERT_STORE_PROV_SYSTEM_A if you are using ANSI.) If you are interested in dealing with other certificate stores, such as stores kept only in memory, reference the Platform SDK documentation.

You'll always pass X509_ASN_ENCODING | PKCS_7_ASN_ENCODING for the dwMsgAndCertEncodingType parameter. At this time, no other encoding types are supported. You should pass NULL for the hCryptProv parameter to indicate usage of the default cryptographic provider.

The dwFlags parameter is where you indicate which type of store you want to open. Table 12-8 shows selected values for the dwFlags parameter.

Table 12-8. Selected values for CertOpenStore's dwFlags parameter

Flag Description
CERT_SYSTEM_STORE_CURRENT_SERVICE Indicates a store for the account under which the current service is running
CERT_SYSTEM_STORE_CURRENT_USER Indicates a store for the current user account of the calling code
CERT_SYSTEM_STORE_LOCAL_MACHINE Indicates a store for the local machine

The pvPara parameter provides specific information used to find the store. For system stores, you pass a string indicating the name of the store. If you are using the personal store, you would use the text "MY". If you pass a name that is not recognized by the system, the system will create this store for you. This can be a convenient way of logically grouping certificates on a machine.

The CertOpenStore function returns an HCERTSTORE variable, which is a handle to a certificate store. When you are finished using the certificate store you should close this handle using the CertCloseStore function:

 BOOL WINAPI CertCloseStore(    HCERTSTORE hCertStore,    DWORD      dwFlags); 

You will typically pass zero for the dwFlags value.

Finding a Certificate

Certificates contain a fair amount of information, and the CryptoAPI allows you to look certificates up using just about any information stored in the certificate. However, most of the information in certificates is not unique, which creates ambiguities when you're conducting your lookup. Certificates are generally referenced by their common names, so you should look them up by their common names.

The common name of a certificate is an attribute of the certificate. When looking up a certificate in a store, you can use one or more of its attributes as criteria. You do this by creating an array of structures, one per attribute, and then passing them to CertFindCertificateInStore:

 PCCERT_CONTEXT WINAPI CertFindCertificateInStore(    HCERTSTORE     hCertStore,    DWORD          dwCertEncodingType,    DWORD          dwFindFlags,    DWORD          dwFindType,    const void     *pvFindPara,    PCCERT_CONTEXT pPrevCertContext); 

The hCertStore parameter is the handle to an open store containing the certificate. The dwCertEncodingType parameter should always be X509_ASN_ ENCODING | PKCS_7_ASN_ENCODING. You should pass zero for the dwFindFlags parameter for most situations. For exceptions, consult the Platform SDK documentation.

The dwFindType parameter indicates how you want to find a certificate. If you want to find a certificate by one or more attributes of the certificate, you should pass CERT_FIND_SUBJECT_ATTR.

NOTE
You can find a certificate using many other approaches as well. For example, if you are sure that your certificate is the only one in the open store, you can pass CERT_FIND_ANY for the dwFindType parameter. The search options are discussed in the Platform SDK documentation. For this chapter, I am focusing on approaches that are commonly used.

You pass the actual search criteria to the pvFindPara parameter. If you are using the CERT_FIND_SUBJECT_ATTR flag to find a certificate by looking up its attributes, you will pass a pointer to a CERT_RDN structure for this parameter.

CertFindCertificateInStore can be used to enumerate certificates in a store. The pPrevCertContext parameter indicates the last context found. You should pass NULL for the first certificate context, or when you are looking up only one certificate.

If your search is successful, CertFindCertificateInStore will return a pointer to a certificate context structure. When you are finished with the structure, you must free it by using CertFreeCertificateContext:

 BOOL WINAPI CertFreeCertificateContext(    PCCERT_CONTEXT pCertContext); 

The CERT_RDN structure, which you pass as the pvFindPara structure when calling CertFindCertificateInStore, is defined as follows:

 typedef struct _CERT_RDN {    DWORD          cRDNAttr;    PCERT_RDN_ATTR rgRDNAttr; } CERT_RDN; 

The cRDNAttr member indicates the number of attributes you are using to find the certificate. The rgRDNAttr member is a pointer to an array of CERT_RDN_ATTR structures that are filled with attribute information. Following is the definition of CERT_RDN_ATTR:

 typedef struct _CERT_RDN_ATTR {    PSTR                pszObjId;    DWORD               dwValueType;    CERT_RDN_VALUE_BLOB Value; } CERT_RDN_ATTR; 

This structure is designed to be very flexible because it needs to contain the many types of data that can make up an attribute in a certificate. The pszObjId member is the ID of the attribute we are concerned with. For the common name of a certificate, you should use the value szOID_COMMON_NAME. The dwValueType member indicates the data type of this value. For the common name, set this value to CERT_RDN_PRINTABLE_STRING. (Note that you must always use ANSI strings with certificate common names.)

The Value member is a blob structure that contains two members: cbData and pbData. They are the size of the data and a pointer to the data, respectively.

NOTE
Some of the attribute types and their data types are discussed in the Platform SDK documentation. If the structures you want to populate are not described in the documentation, you might have to use another approach to find a certificate. For example, you could open a certificate and look at its RDN values so that you can use similar data for looking up certificates in the future.

The following code sample shows the use of the certificate functions we have discussed so far. It opens the personal store of the local machine and looks up a certificate whose common name is "Jason's Test Certificate".

NOTE
Although "Jason's Test Certificate" is a perfectly legal common name for a certificate, it does not represent common usage. Usually, a certificate is named after the network location of the server. This way client software can compare the certificate's common name to the network location with which it is attempting to initiate a secure session.

 // Open the personal certificate store for the local machine HCERTSTORE hMyCertStore =     CertOpenStore(CERT_STORE_PROV_SYSTEM_A,       X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,       0,       CERT_SYSTEM_STORE_LOCAL_MACHINE,       "MY"); if(hMyCertStore==NULL){    // Error } // Fill in an attribute structure for the certificate common name PSTR pszCommonName = "Jason's Test Certificate"; CERT_RDN_ATTR certRDNAttr[1]; certRDNAttr[0].pszObjId = szOID_COMMON_NAME; certRDNAttr[0].dwValueType = CERT_RDN_PRINTABLE_STRING; certRDNAttr[0].Value.pbData = (PBYTE) pszCommonName; certRDNAttr[0].Value.cbData = lstrlen(pszCommonName); CERT_RDN certRDN = {1, certRDNAttr}; // Find the certificate context PCCERT_CONTEXT pCertContext =     CertFindCertificateInStore(hMyCertStore,        X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,       0,       CERT_FIND_SUBJECT_ATTR,       &certRDN,       NULL); if (pCertContext == NULL){    // Error } 

The code that performs the task of looking up a certificate is fairly short and simple, but you might be daunted by the new concepts behind the code. If a server is authenticating itself to a client and is not concerned with client authentication, the previous code is essentially all the certificate code the server need worry about. The rest of the job will be handled by SSL, which I will discuss shortly.

Client or server code that is authenticating a client certificate, however, must open a certificate context and find information about the certificate. Specifically, the code has to identify the certificate by using its common name.

Reading Certificate Information

Your software will usually read certificate information that it has received from a remote principle using SSL. However, this is not always the case, and your software can read certificate information from a certificate that you received from CertFindCertificateInStore. Either way, the approach is the same, and you will be starting with a PCCERT_CONTEXT structure returned from SSL or the CryptoAPI. Here are the steps the system must take to read attribute information from a certificate:

  1. Decode the "subject" blob of the certificate.
  2. Find the RDN attribute for the attribute you wish to read.
  3. Extract the attribute information.

Although there are sufficient CryptoAPI functions to allow you to take this three-step approach, you will also find that there are higher-level functions for extracting most useful information from a certificate that avoid the hassle. To extract the common name (or any other name information) from a certificate, you use CertGetNameString:

 DWORD WINAPI CertGetNameString(    PCCERT_CONTEXT pCertContext,    DWORD          dwType,    DWORD          dwFlags,    void           *pvTypePara,    PTSTR          pszNameString,    DWORD          cchNameString); 

The pCertContext parameter is a pointer to a certificate context for which you wish to extract a name. The dwType parameter indicates what type of name you wish to return. To retrieve a name that is an attribute of the certificate, you would pass CERT_NAME_ATTR_TYPE. To get the friendly name of the certificate, you would pass CERT_NAME_FRIENDLY_DISPLAY_ID.

The value that you pass to dwType also indicates the purpose of the data that you pass as the pvTypePara parameter. For example, in the case of CERT_NAME_FRIENDLY_DISPLAY_ID, you would pass NULL. In the case of CERT_NAME_ATTR_TYPE, you would pass the string representing the object identifier (OID) of the object you want returned; szOID_COMMON_NAME would be used to get the common name string. For more dwType and pvTypePara usage combinations, see the Platform SDK documentation.

You will typically pass zero for the dwFlags parameter of CertGetNameString; however, if you want information related to the "issuer" of the certificate rather than the certificate subject itself, you can pass CERT_NAME_ISSUER_FLAG for dwFlags.

The pszNameString and cchNameString parameters of CertGetNameString indicate a buffer to retrieve the string name and the size of the buffer, respectively. If you pass NULL and zero for these parameters, CertGetNameString returns the required size of the buffer.

The following code fragment shows how this function is used to retrieve the common name of a certificate context:

 TCHAR szCommonName[1024]; ULONG lBytes =     CertGetNameString(pCertContext,       CERT_NAME_ATTR_TYPE, 0,       szOID_COMMON_NAME,       szCommonName, 1024); if(lBytes != 1){   // If not empty string    // Do something with the string } 

Using the concepts presented in this chapter, you will be able to manage certificates sufficiently for your needs in communicating via SSL. You will also find a complete implementation of the topics discussed in this chapter in the SSLChat sample application, which I cover later in this chapter.



Programming Server-Side Applications for Microsoft Windows 2000
Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Programming)
ISBN: 0735607532
EAN: 2147483647
Year: 2000
Pages: 126

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