Easy, secure communications for Windows.
August 01, 2003
URL:http://www.drdobbs.com/implementing-an-ssltls-enabled-clientser/184401688
In recent years, with the advent of the Internet, secure network communication has become increasingly important and is now present in almost every aspect of the network data exchange. One of the most widely used protocols in that area is Secure Sockets Layer (SSL)[1], which is being gradually replaced by its successor -- Transport Layer Security (TLS)[2]. Even though these protocols were specifically designed for the World Wide Web, their utilization is not limited to just the Web traffic -- virtually any type of data sent through any type of network can be secured. Having said that, there are protection goals that define what the secure data means in each case. SSL and TLS provide confidentiality (the data is kept secret from the third party that is not involved in the data exchange), message integrity (the message received is the message sent), and endpoint authentication (one of the communication endpoints or both are verifiably known). If you are working on a project that requires implementation of secure communication channel and that channel must provide the above-mentioned types of data protection, selecting SSL or TLS gives the advantage of a well-documented and well-tested, broadly accepted, and robust protocol.
Fortunately for Windows developers, Microsoft has provided a version of Generic Security Service Application Program Interface (GSS-API) [10]-- called Security Support Provider Interface (SSPI) [5]. SSPI greatly simplifies the daunting task of digging into SSL internals. Using SSPI slashes the development time threefold since the API effectively shields you from needing to know the details about SSL [3]. All that is left is implementing the underlying transport functionality that supplies the data and the logic to wrap calls to SSPI. And best of all, needed C-libraries and header files are part of the Platform SDK that can be freely downloaded from the Microsoft web site. This article describes how to implement an SSL/TLS client/server system using SSPI. The material in this article requires the reader's familiarity with cryptography, SSL/TLS specifications, and X.509 security certificates.
SSPI is a set of generic functions that can be used to access a specific security provider. Providers are either installed as part of the operating system (NTLM, Kerberos) or added later (Microsoft RSA SChannel provider, Microsoft Exchange provider, etc.) by installing other software. In this article, I will be working with the RSA SChannel provider [5] to implement the SSL/TLS functionality.
SChannel is the Microsoft implementation of the GSS API that wraps the SSL/TLS protocol. In the following paragraphs I'll try to explain in detail how to use SChannel and how it works, but first I'd like to stress a few points about advantages of utilizing SChannel:
On Windows ME/2000/XP platforms, SChannel is installed and configured by default and does not need additional tuning. For Windows 95/98 OSs a few extra setting up steps are required to ensure that the application you are developing will run smoothly. First, W2K Active Directory client must be installed. The client, dsclient.exe, can be found on the Windows 2000 server CD in the Clients\Win9x directory. After the client is installed, a registry entry must be added to the following key HKLM\System\CurrentControlSet\ Control\SecurityProviders\ SecurityProviders. Simply append the string:
, schannel.dllto the existing REG_SZ value. Windows NT machines will require at least Option Pack 4.0.
Provided freely with the article is a project demonstrating implementation of a HTTP server with SSL/TLS capabilities. This server utilizes SSPI wrapper classes -- CSslProvider, CSslCredentials, CSslTransport, and CSspiLib. Listings 1, 2, and 3 (Listing 3 available for download at <www.cuj.com/code>), respectively, show partial definition and implementation files for the first three classes. The design goal here was to simplify access to the functionality provided by SChannel. Implementing the handshake or the data exchange phase of the secure protocol with the SSPI can be tedious because of the number of error codes returned and the interpretation of input/output buffer types used. By utilizing the wrappers, your application eliminates the need to keep track of the data flow inside the secure channel, thus you can concentrate on providing the plain or encrypted data. In contrast to the SSPI functions, the wrapper classes have two types of return codes -- success (S_OK) and failure. This greatly simplifies the decision-making process if any type of error occurs; receiving a failure code should lead to the socket closure, or whatever the transport you are using to receive or send the data.
To deliver the secure context and the encrypted or plain data to the SChannel, I'm providing the CSslTransport class. This class can only be obtained through a call to the CSslProvider's function ObtainTransport, which ensures that the transport object during its construction receives a reference to the CSslCredentials object. The latter in turn provides security credentials during the negotiation phase of the SSL protocol. The sequence of getting the wrappers to do the work required is as follows:
1. Your application calls CSspiLib::Load() to prepare the PSecurityFunctionTable
(see the topic below);
2. Next, the application calls one of the overloaded Init() functions
of CSslProvider to obtain security credentials, used later in the negotiation
phase;
3. Once the credentials are obtained, any number of the CSslTransport
instances can be spawned by calling the ObtainTransport() function.
When the application is done using the transport object, it should call its
Destroy() method (see the CAsyncXferSocket class for details);
4. Shutting down the application will require calling CSslProvider's
Destroy() function to release the credentials obtained earlier;
5. The last step is releasing the hold on the SSPI DLL by calling CSspiLib::Unload().
In the case of the demo HTTP server, a descendant of the MFC's CAsyncSocket class safeguards the transport object. The SSL transport class fires events to control the inbound and outbound data flow. There are three types of events -- Inbound Data Available, Outbound Data Available, and Disconnect. See the CAsyncXferSocket class for implementation details and for an example how this class utilizes the new unified event model provided in Visual C++ v7.0. Because of the aforementioned events, to compile and run the project, you'll need Visual C++ v7.0, which is available as part of the VS.NET suite. If you don't have the new compiler, you'll have to modify the code to provide old-fashion callback notification functions similar to these three events fired by the CSslTransport object. Note also that the wrapper classes are independent of the MFC and can be plugged into any environment.
The first step in taking advantage of the SChannel functionality is loading the SSPI dynamic link library (DLL) and setting up a special table, PSecurityFunctionTable, with valid SSPI function pointers. The DLL's name differs depending on the operating system. Windows NT systems have security.dll, exposing the SSPI functionality; all other Microsoft systems use secur32.dll.
You can download the function table by calling a little nifty procedure, which the SSPI DLL exports as InitSecurityInterface. The export is available in two flavors: ANSI and UNICODE, with the letters A and W at the end, respectively, identifying the flavor. To simplify, you may consider using a SECURITY_ENTRYPOINT string defined in sspi.h, which will map to the correct entry point name in either ANSI or UNICODE environments. The CSspiLib class, provided in the supplemental code accompanying this article, wraps DLL initialization and function calls to SChannel using a set of static functions with the same names and parameters that are defined by the SSPI. This class takes care of loading the correct DLL and initializing the function table.
Before SChannel can assist in creating a secure channel, it must tag us with the Windows-type handle: its credentials. This handle is then passed around to other functions, so SChannel knows who is requesting the service and thus does not get confused in servicing multiple users. The handle will also be associated with any special attributes that the principal initiating a session possesses, an X.509 certificate for instance. (Microsoft prefers to use the term 'principal' to identify the entity on behalf of which calls to SChannel functions are made. I'll be using the same term.) For obtaining the handle SSPI provides the function named AcquireCredentialsHandle(). A lot of references to structures, function, and error codes mentioned in this article can be found in [5] under the SChannel section.
Multiple overrides of the CSslProvider Init() function in the sample code provide shortcuts to the function that acquires SChannel credentials. There, I'm simplifying the call to AcquireCredentialsHandle() with fewer parameters (see Listing 1 for details), which is sufficient enough for most of the cases. If the application you are developing requires a special treatment, you'll need to acquire the credentials yourself and then supply them in one of the Init() overrides.
The original AcquireCredentialsHandle() takes a lot of parameters (remember that this is a generic function, which is used by other types of security providers) but to initialize SChannel credentials only four of the parameters are relevant. Although "the fifth element," ptsExpiry, can optionally be used, SChannel always returns zero in it. I won't be wasting the reader's time describing each parameter except for one: the SCHANNEL_CRED structure. This is a rather important fellow through whom SChannel receives everything it needs to know about the principal, so let's scrutinize it.
A CERT_CONTEXT structure is obtained in the number of ways. In the accompanying sample application I'm extracting the certificate from the system's certificate store using the CSecCertificate wrapper class and its function -- GetCertificate(). If you decide to go the same route, you'll need to open the store with CertOpenSystemStore() and look up the certificate by the principal's name with CertFindCertificateInStore(). By default Windows maintains four stores -- MY, CA (Certification Authority), ROOT, and SPC (Software Publisher Certificates). You can examine them by going to the Internet Explorer properties dialog and clicking on the Content tab and then the Certificates... button. Also by default, when you request a certificate from a Certification Authority (CA) that is based on Microsoft Certificate Server, Windows will add it to one of the system stores. Of course, there are ways to change that behavior. For example, you can create your own certificate store (look at certificate-related functions in [5]) and import the certificates. The store can be an internal file, or a registry blob, or what have you. If the software you are developing is for the in-house utilization only, then the best way to avoid higher costs associated with valid certificates issued by CA Root Authorities such as VeriSign is to install an in-house certificate server and establish your company's own Certification Authority. Certificate servers are beyond the scope of this article but you can easily install them on WinNT/2000/XP server platforms. For WinNT machines they can be downloaded and installed as part of the NT Option Pack, for 2000 and XP they are part of the system installation package. At the reader's disposal I provide a test certificate in the form of a PKCS#12 [6] file, which contains the certificate and the matching private key. To import the certificate and the key, simply double-click on the file and follow the wizard's directions. Be aware that the certificate has been issued and signed by Nebula Technologies CA, which is not part of the root certificate store on your machine. You'll have to suffer with an annoying dialog revealing that the certificate's path cannot be verified, although, this will suffice for testing purposes.
Finding the certificate in the system store will work if you already have a certificate and it was successfully imported into the store. Once the certificate is found your application may try to verify it before putting it to use. A more generic version, CertOpenStore(), can open a store that has been previously serialized into an external file. Or, it can open a PKCS#7 [7] file containing X.509 certificate chain. Another way is to manually create a certificate context from an existing X.509 certificate and add it to the previously opened store, thus obtaining both the certificate and the store context. The functions to look at are CertCreateCertificateContext() and CertAddCertificateContextToStore(). In all the above cases of acquiring the certificate, it's implied that the private key that corresponds to the certificate's public key exists in the security provider repository. Otherwise, your application will not be able to negotiate SSL read/write keys and calls to AcceptSecurityContext() or InitializeSecurityContext() will fail.
To get a sense of how SChannel handles the data, try playing with the supplemental code at the CUJ website. The supplemental code provides a good and verbose diagnostic feature. Good luck.
[2] Dierks, T., Allen, C., "The TLS Protocol Version 1.0," RFC 2246. January 1999.
[3] ITU-T, "OSI networking and system aspects -- Abstract Syntax Notation One (ASN.1)," ITU-T Recommendation X.690, December 1997.
[4] ITU-T, "Directory," ITU-T Recommendation X.509, August 1997.
[5] Microsoft, "MSDN Help." April 2002.
[6] RSA Laboratories, "Password Based Encryption Standard," PKCS #12. June 1999.
[7] RSA Laboratories, "Cryptographic Message Syntax Version 1.5," PKCS #7. November 1993.
[8] Rescorla, E., SSL and TLS: Designing and Building Secure Systems. Addison-Wesley, 2001.
[9] Thomas, S., SSL and TLS Essentials. John Wiley & Sons, Inc., 1999.
[10] Linn, J., "Generic Security Service Application Program Interface (GSS-API)," RFC 1508. September 1993.
[11] Hua W., "Winsock 2: QoS API Fine-Tunes Networked App Throughput and Reliability." MSDN Magazine, Microsoft, April 2001.
Alen Talibov holds MS in electrical engineering from Marine Technical University in St.Petersburg, Russia. He started his career as an acoustical engineer moving on to teaching Computer Science and Operating Systems disciplines to college graduates. Since 1993, information technology became his top priority. After coming to the US in 1995 he's been working as a software engineer for a retail company, financial institutions and startup and manufacturing companies. Right now Alen Talibov works as a software consultant in the metropolitan Boston area. He can be reached at [email protected].
Listing 1: CSslProvider class
... // other code not shown
// definition
class CSslProvider
{
public:
CSslProvider(void);
~CSslProvider(void);
private:
CSslCredentials m_sslCredentials;
CNbtCriticalSection m_critSec;
public:
// Intialization functions that must be
// called prior to calling ObtainTransport()
HRESULT Init(BOOL bAsServer, LPCTSTR strPrincipal,
LPCTSTR strStore, BOOL bMutualAuth);
HRESULT Init(BOOL bAsServer, LPCTSTR strPrincipal,
HCERTSTORE hStore, BOOL bMutualAuth);
HRESULT Init();
void Destroy();
HRESULT Attach(CredHandle hCredentials, BOOL bAsServer,
BOOL bMutualAuth);
HRESULT Detach();
HRESULT ObtainTransport(CSslTransport*& pSslTransport);
... // other code not shown
};
... // other code not shown
// implementation
HRESULT CSslProvider::ObtainTransport(
CSslTransport*& pSslTransport)
{
m_critSec.Lock();
HRESULT hr = S_OK;
pSslTransport = NULL;
// Make sure we obtained the credentials prior to this call
if (m_sslCredentials.HasCredHandle())
{
pSslTransport = new CSslTransport(m_sslCredentials);
if (pSslTransport == NULL)
hr = E_OUTOFMEMORY;
}
else
hr = SEC_E_NO_CREDENTIALS;
m_critSec.Unlock();
if (FAILED(hr))
TRACE1("Failed to obtain transport. Error: 0x%08X.\n", hr);
return hr;
}
Listing 2: CSslCredentials class
... // other code not shown
// definition
class CSslCredentials
{
friend class CSslProvider;
protected:
CSslCredentials();
~CSslCredentials();
public:
HRESULT Obtain(BOOL bAsServer, LPCTSTR strPrincipal,
LPCTSTR strStore, BOOL bMutualAuth = FALSE,
DWORD dwProtoFlags = 0);
HRESULT Obtain(BOOL bAsServer, LPCTSTR strPrincipal,
HCERTSTORE hStore, BOOL bMutualAuth = FALSE,
DWORD dwProtoFlags = 0);
HRESULT Obtain(DWORD dwProtoFlags = 0);
HRESULT Attach(CredHandle hCredentials, BOOL bAsServer,
BOOL bMutualAuth = FALSE, DWORD dwProtoFlags = 0);
HRESULT Detach();
BOOL IsServer() {return m_bServer;}
BOOL MutualAuthRequired() {return m_bMutualAuth;}
DWORD GetProtoFlags() {return m_dwProtoFlags;}
BOOL HasCredHandle()
{
return SecIsValidHandle(&m_hCredentials);
}
PCredHandle GetHandle()
{
return (HasCredHandle() ? &m_hCredentials : NULL);
}
void CleanUp();
private:
HRESULT OpenSysStore(LPCTSTR strStore, HCERTSTORE& hStore);
HRESULT FindCertificate(HCERTSTORE hStore,
LPCTSTR strPrincipal, PCCERT_CONTEXT& certContext);
HRESULT ObtainImpl(PCCERT_CONTEXT certContext,
BOOL bAsServer, BOOL bMutualAuth, DWORD dwProtoFlags);
private:
CredHandle m_hCredentials;
BOOL m_bServer;
BOOL m_bMutualAuth;
DWORD m_dwProtoFlags;
HCERTSTORE m_hStore;
};
... // other code not shown
// implementation
HRESULT CSslCredentials::ObtainImpl(PCCERT_CONTEXT certContext,
BOOL bAsServer,
BOOL bMutualAuth,
DWORD dwProtoFlags)
{
// Build Schannel credentials structure.
SCHANNEL_CRED credSchannel = {0};
credSchannel.dwVersion = SCHANNEL_CRED_VERSION;
credSchannel.grbitEnabledProtocols = dwProtoFlags;
if (certContext != NULL)
{
credSchannel.cCreds = 1;
credSchannel.paCred = &certContext;
}
// Create SSL credentials.
TimeStamp tsExpires;
HRESULT hr = CSspiLib::AcquireCredentialsHandle(
NULL,
UNISP_NAME,
(bAsServer ?
SECPKG_CRED_INBOUND :
SECPKG_CRED_OUTBOUND),
NULL,
&credSchannel,
NULL,
NULL,
&m_hCredentials,
&tsExpires);
// See, if we have succeeded
if (SUCCEEDED(hr))
{
m_bServer = bAsServer;
if (m_bServer)
m_bMutualAuth = bMutualAuth;
m_dwProtoFlags = dwProtoFlags;
}
else
TRACE(_T("Line: %d. Error: 0x%08X\n"), __LINE__, hr);
return hr;
}
... // other code not shown
HRESULT CSslCredentials::OpenSysStore(LPCTSTR strStore,
HCERTSTORE& hStore)
{
CT2CA storeName(strStore);
m_hStore = ::CertOpenSystemStore(0, storeName);
if (m_hStore == NULL)
{
DWORD dwErrCode = ::GetLastError();
return HRESULT_FROM_WIN32(dwErrCode);
}
return S_OK;
}
HRESULT CSslCredentials::FindCertificate(
HCERTSTORE hStore,
LPCTSTR strPrincipal,
PCCERT_CONTEXT& certContext)
{
USES_CONVERSION;
CT2CA hostName(strPrincipal);
ASSERT(m_hStore != NULL);
certContext = ::CertFindCertificateInStore(
m_hStore,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_STR_A,
hostName,
NULL);
if (certContext == NULL)
{
DWORD dwErrCode = ::GetLastError();
return HRESULT_FROM_WIN32(dwErrCode);
}
return S_OK;
}
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.