Authentication and Key Exchange - Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Chapter 8. Authentication and Key Exchange

At first glance, it may not be clear that authentication and key exchange are two topics that go together. But they do. This chapter is really all about secure connection establishment—everything the client and server need to do before they start talking. Generally, the server will need to authenticate the client; the client will need to make sure the server is the correct machine (not some attacker). Then the two parties will need to come to some agreement on how to communicate securely beyond that, also agreeing on an encryption key (or a set of keys).

Yes, authentication doesn't always happen over an insecure network connection—it is certainly possible to authenticate over a console or some other medium where network attacks pose little to no risk. In the real world, however, it's rare that one can assume a secure channel for authentication.

Nonetheless, many authentication mechanisms need some kind of secure channel, such as an authenticated SSL connection, before they can offer even reasonable security levels.

In this chapter, we'll sort through these technologies for connection establishment. Note that in these recipes we cover only standalone technologies for authentication and key exchange. In Chapter 9, we cover authentication with SSL/TLS, and in Chapter 10, we cover authentication in the context of public key infrastructures (PKI).

8.1. Choosing an Authentication Method

Problem

You need to perform authentication, and you need to choose an appropriate method.

Solution

The correct method depends on your needs. When a server needs to be authenticated, and the client does not, SSL/TLS is a popular solution. When mutual authentication is desirable, there are a whole bevy of options, such as tunneling a traditional protocol over SSL/TLS or using a dedicated protocol. The best dedicated protocols not only perform mutual authentication but also exchange keys that can then be used for encryption.

Discussion

An authentication factor is some thing that contributes to establishing an identity. For example, a password is an authentication factor, as is a driver's license. There are three major categories of authentication factors:

Things you know

This category generally refers to passwords, PIN numbers, or passphrases. However, there are systems that are at least partially based on the answers to personal questions (though such systems are low on the usability scale; they are primarily used to reset forgotten passwords without intervention from customer service people, in order to thwart social engineering attacks).

Things you have

ATM cards are common physical tokens that are often implicitly used for authentication. That is, when you go to an ATM, having the card is one factor in having the ATM accept who you are. Your PIN by itself is not going to allow someone to get money out in your name.

Things you are

This category generally refers to biometrics such as fingerprints or voice analysis. It includes things you have that you are not going to lose. Of course, an attacker could mimic your information in an attempt to impersonate you.

No common authentication factors are foolproof. Passwords tend to be easy to guess. While cryptography can help keep properly used physical tokens from being forged, they can still be lost or stolen. And biometric devices today have a significant false positive rate. In addition, it can be simple to fool biometric devices; see http://www.puttyworld.com/thinputdeffi.html.

In each of these major categories, there are many different technologies. In addition, it is easy to have a multifactor system in which multiple technologies are required to log in (supporting the common security principle of defense in depth). Similarly, you can have "either-or" authentication to improve usability, but that tends to decrease security by opening up new attack vectors.

Clearly, choosing the right technology requires a thorough analysis of requirements for an authentication system. In this chapter, we'll look at several common requirements, then examine common technologies in light of those requirements.

However, let us first point out that it is good to build software in such a way that authentication is implemented as a framework, where the exact requirements can be determined by an operational administrator instead of a programmer. PAM (Pluggable Authentication Modules) lets you do just that, at least on the server side, in a client-server system. SASL (Simple Authentication and Security Layer) is another such technology that tries to push the abstraction that provides plugability off the server and into the network. We find SASL a large mess and therefore do not cover it here. PAM is covered in Recipe 8.12.

There are several common and important requirements for authentication mechanisms. Some of these may be more or less important to you in your particular environment:

Practicality of deployment

This is the reason that password systems are so common even though there are so many problems with them. Biometrics and physical tokens both require physical hardware and cost money. When deploying Internet-enabled software, it is generally highly inconvenient to force users to adopt one of these solutions.

Usability

Usability is a very important consideration. Unfortunately, usability often trades off against good security. Passwords are a good example: more secure mechanism would require public keys to establish identity. Often, the user's private key will be password-protected for defense in depth, but that only protects against local attacks where an attacker might get access to steal the key—a well-designed public key-based protocol should not be vulnerable to password-guessing attacks.

Another common usability-related requirement is that the user should not have to bring any special bits with him to a computer to be able to log in. That is, many people want a user to be able to sit down at an arbitrary computer and be able to authenticate with data in his head (e.g., a password), even if it means weaker security. For others, it is not unreasonable to ask users to carry a public key around.

When passwords are used, there are many different mechanisms to improve security, but most of them decrease usability. You can, for example, expire passwords, but users hate that. Alternatively, you can enforce passwords that seem to have sufficient entropy in them (e.g., by checking against a dictionary of words), but again, users will often get upset with the system. In many cases, adding something like a public key mechanism adds more security and is less burdensome than such hacks turn out to be.

Use across applications

For some people, it is important to manage authentication centrally across a series of applications. In such a situation, authentication should involve a separate server that manages credentials. Kerberos is the popular technology for meeting this requirement, but a privately run public key infrastructure can be used to do the same thing.

Patents

Many people also want to avoid any algorithms that are likely to be covered by patent.

Efficiency

Other people may be concerned about efficiency, particularly on a server that might need to process many connections in a short period of time. In that situation, it could be important to avoid public key cryptography altogether, or to find some other way to minimize the impact on the server, to prevent against denial of service.

Common mechanism

It may also be a requirement to have authentication and key exchange be done by the same mechanism. This can improve ease of development if you pick the right solution.

Economy of expression

An authentication protocol should use a minimal number of messages to do work. Generally, three messages are considered the target to hit, even when authentication and key exchange are combined. This is usually not such a big deal, however. A few extra messages generally will not noticeably impact performance. Protocol designers like to strive to minimize the number of messages, because it makes their work more elegant and less ad hoc. Of course, simplicity should be a considered requirement, but then again, we have seen simple five-message protocols, and ridiculously complex three-message protocols!

Security

Security is an obvious requirement at the highest level, but there are many different security properties you might care about, as we'll describe in the rest of this section.

In terms of the security of your mechanism, you might require a mechanism that effectively provides its own secure channel, resisting sniffing attacks, man-in-the-middle attacks, and so on that might lead to password compromise, or even just the attacker's somehow masquerading as either the client or server without compromising the password. (This could happen, for example, if the attacker manages to get the server password database.)

On the other hand, you might want to require something that does not build its own secure channel. For example, if you are writing something that will be used only on the console, you will already be assuming a trusted path from the user to your code, so why bother building a secure channel? Similarly, you might already be able to establish an authenticated remote connection to a server through something like SSL, in which case you get a secure channel over which you can do a simpler authentication protocol. (Mutual authentication versus one-sided authentication is therefore another potentially interesting requirement.) Of course, that works only if the server really is authenticated, which people often fail to do properly.

Whether or not you have a secure channel, you will probably want to make sure that you avoid capture replay attacks. In addition, you should consider which possible masquerading scenarios worry you. Obviously, it is bad if an arbitrary person can masquerade as either the client or the server just from watching network traffic. What if an attacker manages to break into a server, however? Should the attacker then be able to masquerade as the user to that server? To other servers where the user has the same credentials (e.g., the same password)?

In addition, when a user shares authentication credentials across multiple servers, should he be able to distinguish those servers? Such a requirement can demand significant trade-offs, because to meet it, you will need either a public key infrastructure or some other secure secret that users need to carry around that authenticates each server. If you are willing to assume that the server is not compromised at account creation time but may be compromised at some later point, you can meet the requirement more easily.

We have already mentioned no susceptibility to password guessing attacks as a possible requirement. When that is too strict, there are other requirements we can impose that are actually reasonable:

§ When an attacker steals the authentication database on the server, an offline cracking job should be incredibly difficult—with luck, infeasible, even if the password being attacked is fairly predictable.

§ Guessing attacks should be possible only by attempting to authenticate directly with the server, and the login attempt should not reveal any information about the actual password beyond whether or not the guess was correct.

§ There should not be large windows of vulnerability where the server has the password. That is, the server should need to see the password only at account initialization time, or not at all. It should always be unacceptable for a server to store the actual password.

No doubt there are other interesting requirements for password systems.

For authentication systems that also do key exchange, there are other interesting requirements you should consider:

Recoverability from randomness problems

You might want to require that the system be able to recover if either the client or the server has a bad source of randomness. That is generally done by using a key agreement protocol , where both sides contribute to the key, instead of a key transport protocol , where one side selects the key and sends it to the other.

Forward secrecy

You might want to require that an attacker who manages to break one key exchange should not be able to decrypt old connections, if he happens to capture the data. Achieving this property often involves some tradeoffs.

Let's look at common technologies in light of these requirements.

Traditional UNIX crypt( )

This solution is a single-factor, password-based system. Using it requires a preexisting secure channel (and one that thwarts capture replay attacks). There are big windows of vulnerability because the user's password must be sent to the server every time the user wishes to authenticate. It does not meet any of the desirable security requirements for a password-based system we outlined above (it is susceptible to offline guessing attacks, for example), and the traditional mechanism is not even very strong cryptographically. Using this mechanism on an unencrypted channel would expose the password. Authentication using crypt( ) is covered in Recipe 8.9.

MD5 Modular Crypt Format (a.k.a. md5crypt or MD5-MCF)

This function replaces crypt( ) on many operating systems (the API is the same, but it is not backward-compatible). It makes offline cracking attacks a little harder, and it uses stronger cryptography. There are extensions to the basic modular format that use other algorithms and provide better protection against offline guessing; the OpenBSD project's Blowfish-based authentication mechanism is one. Using this mechanism on an unencrypted channel would expose the password. Authentication using MD5-MCF is covered in Recipe 8.10.

PBKDF2

You can use PBKDF2 (Password-Based Key Derivation Function 2; see Recipe 4.10) as a password storage mechanism. It meets all the same requirements as the Blowfish variant of MD5-MCF discussed in the previous subsection. Authentication using PBKDF2 is covered in Recipe 8.11.

S/KEY and OPIE

S/KEY and OPIE are one-time password systems, meaning that the end user sends a different password over the wire each time. This requires the user and the server to preestablish a secret. As a result, if an attacker somehow gets the secret database (e.g., if he manages to dumpster-dive for an old backup disk), he can masquerade as the client.

In addition, the user will need to keep some kind of physical token, like a sheet of one-time passwords (which will occasionally need to be refreshed) or a calculator to compute correct passwords. To avoid exposing the password if the server database is compromised, the user will also need to reinitialize the server from time to time (and update her calculator).

These mechanisms do not provide their own secure channel. S/KEY, as specified, relies on MD4, which is now known to be cryptographically broken. If it's used on an unencrypted channel, no information about the password is revealed, but an attacker can potentially hijack a connection.

CRAM

CRAM (Challenge-Response Authentication Mechanism) is a password-based protocol that avoids sending the password out over the wire by using a challenge-response protocol, meaning that the two ends each prove to the other that they have the secret, without someone actually sending the secret. Therefore, CRAM (which does not itself provide a secure channel) can be used over an insecure channel. However, it is still subject to a number of password attacks on the server, particularly because the server must store the actual password. Therefore, you should not use CRAM in new systems.

Digest-Auth (RFC 2617)

Digest-Auth is one of the authentication mechanisms specified for HTTP/1.1 and later (the other is quite weak). It does not provide a secure channel, and it provides only moderate protections against attacks on passwords (much of it through an optional nonce that is rarely used).

SRP

All of the mechanisms we've looked at so far have been password-based. None of them create their own secure channel, nor do they provide mutual authentication. SRP (Secure Remote Password) is a password-based mechanism that does all of the above, and it has a host of other benefits:

Client-server authentication

SRP not only allows a server to authenticate clients, but it also allows clients to know that they're talking to the right server—as long as the authentication database isn't stolen.

Protection against information leakage

SRP also prevents all but a minimal amount of information leakage. That is, an attacker can try one password at a time by contacting the server, but that is the only way he can get any information at all about the password's value. Throttling the number of allowed login attempts to a few dozen a day should reasonably thwart most attacks, though it opens up a denial of service risk. You might consider slightly more sophisticated throttling, such as a limit of 12 times a day per IP address. (Of course, even that is not perfect). A far less restrictive method of throttling failed authentication attempts is discussed in Recipe 8.8.

Protection against compromise

SRP protects against most server-compromise attacks (but not a multiserver masquerading attack, which we do not think is worth worrying about anyway). It even prevents an attacker who compromises the server from logging into other machines using information in the database.

Key exchange

Another big benefit is that SRP exchanges a key as a side effect of authentication. SRP uses public key cryptography, which can be a denial-of-service issue.

The big problem with SRP is that patents cover it. As a result, we do not explore SRP in depth. Another potential issue is that this algorithm does not provide forward secrecy, although you could easily introduce forward secrecy on top of it.

Basic public key exchange

There are plenty of strong authentication systems based on public key cryptography. These systems can meet most of the general requirements we've discussed, depending on how they're implemented.

Generally, the public key is protected by a password, but the password-protected key must be transported to any client machine the user might wish to use. This is a major reason why people often implement password-based protocols instead of using public key-based protocols. We discuss a basic protocol using public key cryptography in Recipe 8.16.

SAX

SAX (Symmetric Authenticated eXchange) is a protocol that offers most of the same benefits of SRP, but it is not covered by patents. Unlike SRP, it does not use public key encryption, which means that it minimizes computational overhead. There is a masquerading attack in the case of server compromise, but it effectively requires compromise of two servers and does not buy the attacker any new capabilities, so it is not very interesting in practice.

SAX has two modes of use:

§ You can avoid leaking any information about the password if the user is willing to carry around or memorize a secret provided by the server at account creation time (that secret needs to be entered into any single client only once, though).

§ Otherwise, SAX can be used in an SRP-like manner, where the user need not carry around anything other than the password, but information about the password can be learned, but primarily through guessing attacks. Someone can mount an offline dictionary attack on the server side, but the cost of such an attack can be made prohibitive.

If an attacker somehow gets the secret database (e.g., if he manages to dumpster-dive for an old backup disk), he can masquerade as the client. PAX is a similar protocol that fixes this problem.

PAX

PAX (Public key Authenticated eXchange) is a basic two-way authenticating key exchange using public key encryption that uses passwords to generate the keys. The server needs to know the password once at initialization time, and never again.

This protocol is similar to SAX, but has some minor advantages because it uses public key cryptography. For example, you can back away from using passwords (for example, you might take the key and put the client's private key onto a smart card, obviating the need to type in a password on the client end). Additionally, if an attacker does get the authentication database, he nonetheless cannot masquerade as the client.

PAX can be used in one of two modes:

§ You can get all the advantages of a full public-key based system if the user is willing to carry around or memorize a secret provided by the server at account creation time (that secret needs to be entered into any single client only once, though).

§ Otherwise, PAX can be used in an SRP-like manner, where the user need not carry around anything other than the password; information about the password can be learned, but only through guessing attacks.

As with SRP, you can easily layer forward secrecy on top of PAX (by adding another layer of cryptography; see Recipe 8.21).

Unlike SRP, PAX is not believed to be covered by patents.

Kerberos

Kerberos is a password-based authentication mechanism that requires a central authentication server. It does not use any public key cryptography whatsoever, instead relying on symmetric cryptography for encryption and authentication (typically DES or Triple-DES in CBC mode with MD5 or SHA1 for authentication).

Although Kerberos never transmits passwords in the clear, it does make the assumption that users will not use weak passwords, which is a poor assumption to make, because users will invariably use passwords that they find easy to remember. That typically also makes these passwords easy for an attacker to guess or to discover by way of a dictionary attack.

Kerberos does assume that the environment in which it operates is insecure. It can overcome a compromised system or network; however, if the system on which its central database resides is compromised, the security afforded by Kerberos is seriously compromised.

We cover authentication with Kerberos in Recipe 8.13. Because of the complexity of the SSPI API in Windows, we do not cover Kerberos on Windows in this book. Instead, recipes are available on our web site.

Windows NT LAN Manager (NTLM)

Windows NT LAN Manager is a password-based protocol that avoids sending the password out over the wire by using a challenge-response protocol, meaning that the two ends each prove to the other that they have the secret, without someone actually sending the secret. Therefore, NTLM (which does not itself provide a secure channel) can be used over an insecure channel. However, it is still subject to a number of password attacks on the server, particularly because the server must store the actual password.

Windows uses NTLM for network authentication and for interactive authentication on standalone systems. Beginning with Windows 2000, Kerberos is the preferred network authentication method on Windows, but NTLM can still be used in the absence of a Kerberos infrastructure.

Because of the complexity of the SSPI API in Windows, we do not cover authentication with NTLM in this book. Instead, recipes are available on our web site.

SSL certificate-based checking

Secure Sockets Layer (SSL) and its successor, Transport Layer Security (TLS), use certificates to allow entities to identify entities in a system. Certificates are verified using a PKI where a mutually trusted third party vouches for the identity of a certificate holder. See Recipe 10.1 for an introduction to certificates and PKI.

Certificates are obtained from a trusted third party known as a certification authority (CA), which digitally signs the certificate with its own private key. If the CA is trusted, and its signature on the certificate is valid, the certificate can be trusted. Certificates typically also contain other important pieces of information that must also be verified—for example, validity dates and the name of the entity that will present the certificate.

To be effective, certificates require the mutually trusted third party. One of the primary problems with certificates and PKI is one of revocation. If the private key for a certificate is compromised, how is everyone supposed to know that the certificate should no longer be trusted? CAs periodically publish lists known as certificate revocation lists (CRLs) that identify all of the certificates that have been revoked and should no longer be trusted, but it is the responsibility of the party verifying a certificate to seek out these lists and use them properly. In addition, there is often a significant window of time between when a CA revokes a certificate and when a new CRL is published.

SSL is widely deployed and works sufficiently well for many applications; however, because it is difficult to use properly, it is often deployed insecurely. We discuss certificate verification in Recipe 10.4 through Recipe 10.7.

See Also

§ Thinking Putty article on defeating biometric fingerprint scanners: http://www.puttyworld.com/thinputdeffi.html

§ RFC 1510: The Kerberos Network Authentication Service (V5)

§ RFC 2617: HTTP Authentication: Basic and Digest Access Authentication

§ Recipe 4.10, Recipe 8.8, Recipe 8.9, Recipe 8.10, Recipe 8.11, Recipe 8.12, Recipe 8.13, Recipe 8.16, Recipe 8.21, Recipe 10.1, Recipe 10.4, Recipe 10.5, Recipe 10.6, Recipe 10.7

8.2. Getting User and Group Information on Unix

Problem

You need to discover information about a user or group, and you have a username or user ID or a group name or ID.

Solution

On Unix, user and group names correspond to numeric identifiers. Most system calls require numeric identifiers upon which to operate, but names are typically easier for people to remember. Therefore, most user interactions involve the use of names rather than numbers. The standard C runtime library provides several functions to map between names and numeric identifiers for both groups and users.

Discussion

Declarations for the functions and data types needed to map between names and numeric identifiers for users are in the header file pwd.h . Strictly speaking, mapping functions do not actually exist. Instead, one function provides the ability to look up user information using the user's numeric identifier, and another function provides the ability to look up user information using the user's name.

The function used to look up user information by numeric identifier has the following signature:

#include <sys/types.h>

#include <pwd.h>

struct passwd *getpwuid(uid_t uid);

The function used to look up user information by name has the following signature:

#include <sys/types.h>

#include <pwd.h>

struct passwd *getpwnam(const char *name);

Both functions return a pointer to a structure allocated internally by the runtime library. One side effect of this behavior is that successive calls replace the information from the previous call. Another is that the functions are not thread-safe. If either function fails to find the requested user information, a NULL pointer is returned.

The contents of the passwd structure differ across platforms, but some fields remain the same everywhere. Of particular interest to us in this recipe are the two fields pw_name and pw_uid . These two fields are what enable mapping between names and numeric identifiers. For example, the following two functions will obtain mappings:

#include <sys/types.h>

#include <pwd.h>

#include <string.h>

int spc_user_getname(uid_t uid, char **name) {

struct passwd *pw;

if (!(pw = getpwuid(uid)) ) {

endpwent( );

return -1;

}

*name = strdup(pw->pw_name);

endpwent( );

return 0;

}

int spc_user_getuid(char *name, uid_t *uid) {

struct passwd *pw;

if (!(pw = getpwnam(name))) {

endpwent( );

return -1;

}

*uid = pw->pw_uid;

endpwent( );

return 0;

}

Note that spc_user_getname( ) will dynamically allocate a buffer to return the user's name, which must be freed by the caller. Also notice the use of the function endpwent( ) . This function frees any resources allocated by the lookup functions. Its use is important because failure to free the resources can cause unexpected leaking of memory, file descriptors, socket descriptors, and so on. Exactly what types of resources may be leaked vary depending on the underlying implementation, which may differ not only from platform to platform, but also from installation to installation.

In our example code, we call endpwent( ) after every lookup operation, but this isn't necessary if you need to perform multiple lookups. In fact, if you know you will be performing a large number of lookups, always calling endpwent( ) after each one is wasteful. Any number of lookup operations may be performed safely before eventually calling endpwent( ).

Looking up group information is similar to looking up user information. The header file grp.h contains the declarations for the needed functions and data types. Two functions similar to getpwnam( ) and getpwuid( ) also exist for groups:

#include <sys/types.h>

#include <grp.h>

struct group *getgrgid(gid_t gid);

struct group *getgrnam(const char *name);

These two functions behave as their user counterparts do. Thus, we can use them to perform name-to-numeric-identifier mappings, and vice versa. Just as user information lookups require a call to endpwent( ) to clean up any resources allocated during the lookup, group information lookups require a call to endgrent( ) to do the same.

#include <sys/types.h>

#include <grp.h>

#include <string.h>

int spc_group_getname(gid_t gid, char **name) {

struct group *gr;

if (!(gr = getgruid(gid)) ) {

endgrent( );

return -1;

}

*name = strdup(gr->gr_name);

endgrent( );

return 0;

}

int spc_group_getgid(char *name, gid_t *gid) {

struct group *gr;

if (!(gr = getgrnam(name))) {

endgrent( );

return -1;

}

*gid = gr->gr_gid;

endgrent( );

return 0;

}

Groups may contain more than a single user. Theoretically, groups may contain any number of members, but be aware that some implementations may impose artificial limits on the number of users that may belong to a group.

The group structure that is returned by either getgrnam( ) or getgrgid( ) contains a field called gr_mem that is an array of strings containing the names of all the member users. The last element in the array will always be a NULL pointer. Determining whether a user is a member of a group is a simple matter of iterating over the elements in the array, comparing each one to the name of the user for which to look:

#include <sys/types.h>

#include <grp.h>

#include <string.h>

int spc_group_ismember(char *group_name, char *user_name) {

int i;

struct group *gr;

if (!(gr = getgrnam(group_name))) {

endgrent( );

return 0;

}

for (i = 0; gr->gr_mem[i]; i++)

if (!strcmp(user_name, gr->gr_mem[i])) {

endgrent( );

return 1;

}

endgrent( );

return 0;

}

8.3. Getting User and Group Information on Windows

Problem

You need to discover information about a user or group, and you have a username or user ID or a group name or ID.

Solution

Windows identifies users and groups using security identifiers (SIDs), which are unique, variably sized values assigned by an authority such as the local machine or a Windows NT server domain. Functions and data structures typically represent users and groups using SIDs, rather than using names.

The Win32 API provides numerous functions for manipulating SIDs, but of particular interest to us in this recipe are the functions LookupAccountName( ) and LookupAccountSid( ), which are used to map between names and SIDs.

Discussion

The Win32 API function LookupAccountName( ) is used to find the SID that corresponds to a name. You can use it to obtain information about a name on either the local system or a remote system. While it might seem that mapping a name to a SID is a simple operation, LookupAccountName( )actually requires a large number of arguments to allow it to complete its work.

LookupAccountName( ) has the following signature:

BOOL LookupAccountName(LPCTSTR lpSystemName, LPCTSTR lpAccountName, PSID Sid,

LPDWORD cbSid, LPTSTR ReferencedDomainName,

LPDWORD cbReferencedDomainName, PSID_NAME_USE peUse);

This function has the following arguments:

lpSystemName

String representing the name of the remote system on which to look up the name. If you specify this argument as NULL, the lookup will be done on the local system.

lpAccountName

String representing the name of the user or group to look up. This argument may not be specified as NULL.

Sid

Buffer into which the SID will be written. Initially, you may specify this argument as NULL to determine how large a buffer is required to hold the SID.

cbSid

Pointer to an integer that both specifies the size of the buffer to receive the SID, and receives the size of the buffer required for the SID.

ReferencedDomainName

Buffer into which the domain name where the user or group name was found is to be written. Initially, you may specify this argument as NULL to determine how large a buffer is required to hold the domain name.

cbReferencedDomainName

Pointer to an integer that both specifies the size of the buffer to receive the domain name, and receives the size of the buffer required for the domain name.

peUse

Pointer to an enumeration that receives the type of SID to which the looked-up name corresponds. The most commonly returned values are SidTypeUser (1) and SidTypeGroup (2).

The following function, SpcLookupName( ) , is essentially a wrapper around LookupAccountName( ). It handles the nuances of performing user and group name lookup, including allocating the necessary buffers and error conditions. If the name is successfully found, the return will be a pointer to a dynamically allocated SID structure, which you must later free using LocalFree( ). If the name could not be found, NULL will be returned, and GetLastError( ) will return ERROR_NONE_MAPPED. If any other kind of error occurs, SpcLookupName( ) will return NULL, and GetLastError( ) will return the relevant error code.

#include <windows.h>

PSID SpcLookupName(LPCTSTR lpszSystemName, LPCTSTR lpszAccountName) {

PSID Sid;

DWORD cbReferencedDomainName, cbSid;

LPTSTR ReferencedDomainName;

SID_NAME_USE eUse;

cbReferencedDomainName = cbSid = 0;

if (LookupAccountName(lpszSystemName, lpszAccountName, 0, &cbSid,

0, &cbReferencedDomainName, &eUse)) {

SetLastError(ERROR_NONE_MAPPED);

return 0;

}

if (GetLastError( ) != ERROR_INSUFFICIENT_BUFFER) return 0;

if (!(Sid = (PSID)LocalAlloc(LMEM_FIXED, cbSid))) return 0;

ReferencedDomainName = (LPTSTR)LocalAlloc(LMEM_FIXED, cbReferencedDomainName);

if (!ReferencedDomainName) {

LocalFree(Sid);

return 0;

}

if (!LookupAccountName(lpszSystemName, lpszAccountName, Sid, &cbSid,

ReferencedDomainName, &cbReferencedDomainName, &eUse)) {

LocalFree(ReferencedDomainName);

LocalFree(Sid);

return 0;

}

LocalFree(ReferencedDomainName);

return Sid;

}

The Win32 API function LookupAccountSid( ) is used to find the name that corresponds to a SID. You can use it to obtain information about a SID on either the local system or a remote system. While it might seem that mapping a SID to a name is a simple operation, LookupAccountSid( )actually requires a large number of arguments to allow it to complete its work.

LookupAccountSid( ) has the following signature:

BOOL LookupAccountSid(LPCTSTR lpSystemName, PSID Sid,LPTSTR Name, LPDWORD cbName,

LPTSTR ReferencedDomainName, LPDWORD cbReferencedDomainName,

PSID_NAME_USE peUse);

This function has the following arguments:

lpSystemName

String representing the name of the remote system on which to look up the SID. If you specify this argument as NULL, the lookup will be done on the local system.

Sid

Buffer containing the SID to look up. This argument may not be specified as NULL.

Name

Buffer into which the name will be written. Initially, you may specify this argument as NULL to determine how large a buffer is required to hold the name.

cbName

Pointer to an integer that both specifies the size of the buffer to receive the name, and receives the size of the buffer required for the name.

ReferencedDomainName

Buffer into which the domain name where the SID was found is to be written. Initially, you may specify this argument as NULL to determine how large a buffer is required to hold the domain name.

cbReferencedDomainName

Pointer to an integer that both specifies the size of the buffer to receive the domain name, and receives the size of the buffer required for the domain name.

peUse

Pointer to an enumeration that receives the type of SID to which the looked-up SID corresponds. The most commonly returned values are SidTypeUser (1) and SidTypeGroup (2).

The following function, SpcLookupSid( ) , is essentially a wrapper around LookupAccountSid( ). It handles the nuances of performing SID lookup, including allocating the necessary buffers and error conditions. If the SID is successfully found, the return will be a pointer to a dynamically allocated buffer containing the user or group name, which you must later free using LocalFree( ). If the SID could not be found, NULL will be returned, and GetLastError( ) will return ERROR_NONE_MAPPED. If any other kind of error occurs, SpcLookupSid( ) will return NULL, and GetLastError( ) will return the relevant error code.

#include <windows.h>

LPTSTR SpcLookupSid(LPCTSTR lpszSystemName, PSID Sid) {

DWORD cbName, cbReferencedDomainName;

LPTSTR lpszName, ReferencedDomainName;

SID_NAME_USE eUse;

cbName = cbReferencedDomainName = 0;

if (LookupAccountSid(lpszSystemName, Sid, 0, &cbName,

0, &cbReferencedDomainName, &eUse)) {

SetLastError(ERROR_NONE_MAPPED);

return 0;

}

if (GetLastError( ) != ERROR_INSUFFICIENT_BUFFER) return 0;

if (!(lpszName = (LPTSTR)LocalAlloc(LMEM_FIXED, cbName))) return 0;

ReferencedDomainName = (LPTSTR)LocalAlloc(LMEM_FIXED, cbReferencedDomainName);

if (!ReferencedDomainName) {

LocalFree(lpszName);

return 0;

}

if (!LookupAccountSid(lpszSystemName, Sid, lpszName, &cbName,

ReferencedDomainName, &cbReferencedDomainName, &eUse)) {

LocalFree(ReferencedDomainName);

LocalFree(lpszName);

return 0;

}

LocalFree(ReferencedDomainName);

return lpszName;

}

8.4. Restricting Access Based on Hostname or IP Address

Problem

You want to restrict access to the network based on hostname or IP address.

Solution

First, get the IP address of the remote connection, and verify that the address has a hostname associated with it. To ensure that the hostname is not being spoofed (i.e., the address reverses to one hostname, but the hostname does not map to that IP address), look up the hostname and compare the resulting IP address with the IP address of the connection; if the IP addresses do not match, the hostname is likely being spoofed.

Next, compare the IP address and/or hostname with a set of rules that determine whether to grant the remote connection access.

Discussion

WARNING

Restricting access based on the remote connection's IP address or hostname is risky at best. The hostname and/or IP address could be spoofed, or the remote system could be compromised with an attacker in control. Address-based access control is no substitute for strong authentication methods.

The first step in restricting access from the network based on hostname or IP address is to ensure that the remote connection is not engaging in a DNS spoofing attack. No foolproof method exists for guaranteeing that the address is not being spoofed, though the code presented here can provide a reasonable assurance for most cases. In particular, if the DNS server for the domain that an IP address reverse-maps to has been compromised, there is no way to know.

The first code listing that we present implements a worker function, check_spoofdns( ) , which performs a set of DNS lookups and compares the results. The first lookup retrieves the hostname to which an IP address maps. An IP address does not necessarily have to reverse-map to a hostname, so if this first lookup yields no mapping, it is generally safe to assume that no spoofing is taking place.

If the IP address does map to a hostname, a lookup is performed on that hostname to retrieve the IP address or addresses to which it maps. The hostname should exist, but if it does not, the connection should be considered suspect. Although it is possible that something funny is going on with the remote connection, the lack of a name-to- address mapping could be innocent.

Each of the addresses returned by the hostname lookup is compared against the IP address of the remote connection. If the IP address of the remote connection is not matched, the likelihood of a spoofing attack is high, though still not guaranteed. If the IP address of the remote connection is matched, the code assumes that no spoofing attack is taking place.

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netdb.h>

#include <errno.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <ctype.h>

#define SPC_ERROR_NOREVERSE 1 /* IP address does not map to a hostname */

#define SPC_ERROR_NOHOSTNAME 2 /* Reversed hostname does not exist */

#define SPC_ERROR_BADHOSTNAME 3 /* IP addresses do not match */

#define SPC_ERROR_HOSTDENIED 4 /* TCP/SPC Wrappers denied host access */

static int check_spoofdns(int sockfd, struct sockaddr_in *addr, char **name) {

int addrlen, i;

char *hostname;

struct hostent *he;

*name = 0;

for (;;) {

addrlen = sizeof(struct sockaddr_in);

if (getpeername(sockfd, (struct sockaddr *)addr, &addrlen) != -1) break;

if (errno != EINTR && errno != EAGAIN) return -1;

}

for (;;) {

he = gethostbyaddr((char *)&addr->sin_addr, sizeof(addr->sin_addr), AF_INET);

if (he) break;

if (h_errno = = HOST_NOT_FOUND) {

endhostent( );

return SPC_ERROR_NOREVERSE;

}

if (h_errno != TRY_AGAIN) {

endhostent( );

return -1;

}

}

hostname = strdup(he->h_name);

for (;;) {

if ((he = gethostbyname(hostname)) != 0) break;

if (h_errno = = HOST_NOT_FOUND) {

endhostent( );

free(hostname);

return SPC_ERROR_NOHOSTNAME;

}

if (h_errno != TRY_AGAIN) {

endhostent( );

free(hostname);

return -1;

}

}

/* Check all IP addresses returned for the hostname. If one matches, return

* 0 to indicate that the address is not likely being spoofed.

*/

for (i = 0; he->h_addr_list[i]; i++)

if (*(in_addr_t *)he->h_addr_list[i] = = addr->sin_addr.s_addr) {

*name = hostname;

endhostent( );

return 0;

}

/* No matches. Spoofing very likely */

free(hostname);

endhostent( );

return SPC_ERROR_BADHOSTNAME;

}

The next code listing contains several worker functions as well as the function spc_host_init( ) , which requires a single argument that is the name of a file from which access restriction information is to be read. The access restriction information is read from the file and stored in an in-memory list, which is then used by spc_host_check( ) (we'll describe that function shortly).

Access restriction information read by spc_host_init( ) is required to be in a very specific format. Whitespace is mostly ignored, and lines beginning with a hash mark (#) or a semicolon (;) are considered comments and ignored. Any other line in the file must begin with either "allow:" or "deny:" to indicate the type of rule.

Following the rule type is a whitespace-separated list of addresses that are to be either allowed or denied access. Addresses may be hostnames or IP addresses. IP addresses may be specified as an address and mask or simply as an address. In the former case, the address may contain up to four parts, where each part must be expressed in decimal (ranging from 0 to 255), and a period (.) must be used to separate them. A forward slash (/) separates the address from the mask, and the mask is expressed as the number of bits to set. Table 8-1 lists example representations that are accepted as valid.

Table 8-1. Example address representations accepted by spc_host_init( )

Representation

Meaning

www.oreilly.com

The host to which the reverse-and-forward maps www.oreilly.com will be matched.

12.109.142.4

Only the specific address 12.109.142.4 will be matched.

10/24

Any address starting with 10 will be matched.

192.168/16

Any address starting with 192.168 will be matched.

If any errors are encountered when parsing the access restriction data file, a message containing the name of the file and the line number is printed. Parsing of the file then continues on the next line. Fatal errors (e.g., out of memory) are also noted in a similar fashion, but parsing terminates immediately and any data successfully parsed so far is thrown away.

When spc_host_init( ) completes successfully (even if parse errors are encountered), it will return 1; otherwise, it will return 0.

#define SPC_HOST_ALLOW 1

#define SPC_HOST_DENY 0

typedef struct {

int action;

char *name;

in_addr_t addr;

in_addr_t mask;

} spc_hostrule_t;

static int spc_host_rulecount;

static spc_hostrule_t *spc_host_rules;

static int add_rule(spc_hostrule_t *rule) {

spc_hostrule_t *tmp;

if (!(spc_host_rulecount % 256)) {

if (!(tmp = (spc_hostrule_t *)realloc(spc_host_rules,

sizeof(spc_host_rulecount) * (spc_host_rulecount + 256))))

return 0;

spc_host_rules = tmp;

}

spc_host_rules[spc_host_rulecount++] = *rule;

return 1;

}

static void free_rules(void) {

int i;

if (spc_host_rules) {

for (i = 0; i < spc_host_rulecount; i++)

if (spc_host_rules[i].name) free(spc_host_rules[i].name);

free(spc_host_rules);

spc_host_rulecount = 0;

spc_host_rules = 0;

}

}

static in_addr_t parse_addr(char *str) {

int shift = 24;

char *tmp;

in_addr_t addr = 0;

for (tmp = str; *tmp; tmp++) {

if (*tmp = = '.') {

*tmp = 0;

addr |= (atoi(str) << shift);

str = tmp + 1;

if ((shift -= 8) < 0) return INADDR_NONE;

} else if (!isdigit(*tmp)) return INADDR_NONE;

}

addr |= (atoi(str) << shift);

return htonl(addr);

}

static in_addr_t make_mask(int bits) {

in_addr_t mask;

bits = (bits < 0 ? 0 : (bits > 32 ? 32 : bits));

for (mask = 0; bits--; mask |= (1 << (31 - bits)));

return htonl(mask);

}

int spc_host_init(const char *filename) {

int lineno = 0;

char *buf, *p, *slash, *tmp;

FILE *f;

size_t bufsz, len = 0;

spc_hostrule_t rule;

if (!(f = fopen(filename, "r"))) return 0;

if (!(buf = (char *)malloc(256))) {

fclose(f);

return 0;

}

while (fgets(buf + len, bufsz - len, f) != 0) {

len += strlen(buf + len);

if (buf[len - 1] != '\n') {

if (!(buf = (char *)realloc((tmp = buf), bufsz += 256))) {

fprintf(stderr, "%s line %d: out of memory\n", filename, ++lineno);

free(tmp);

fclose(f);

free_rules( );

return 0;

}

continue;

}

buf[--len] = 0;

lineno++;

for (tmp = buf; *tmp && isspace(*tmp); tmp++) len--;

while (len && isspace(tmp[len - 1])) len--;

tmp[len] = 0;

len = 0;

if (!tmp[0] || tmp[0] = = '#' || tmp[0] = = ';') continue;

memset(&rule, 0, sizeof(rule));

if (strncasecmp(tmp, "allow:", 6) && strncasecmp(tmp, "deny:", 5)) {

fprintf(stderr, "%s line %d: parse error; continuing anyway.\n",

filename, lineno);

continue;

}

if (!strncasecmp(tmp, "deny:", 5)) {

rule.action = SPC_HOST_DENY;

tmp += 5;

} else {

rule.action = SPC_HOST_ALLOW;

tmp += 6;

}

while (*tmp && isspace(*tmp)) tmp++;

if (!*tmp) {

fprintf(stderr, "%s line %d: parse error; continuing anyway.\n",

filename, lineno);

continue;

}

for (p = tmp; *p; tmp = p) {

while (*p && !isspace(*p)) p++;

if (*p) *p++ = 0;

if ((slash = strchr(tmp, '/')) != 0) {

*slash++ = 0;

rule.name = 0;

rule.addr = parse_addr(tmp);

rule.mask = make_mask(atoi(slash));

} else {

if (inet_addr(tmp) = = INADDR_NONE) rule.name = strdup(tmp);

else {

rule.name = 0;

rule.addr = inet_addr(tmp);

rule.mask = 0xFFFFFFFF;

}

}

if (!add_rule(&rule)) {

fprintf(stderr, "%s line %d: out of memory\n", filename, lineno);

free(buf);

fclose(f);

free_rules( );

return 0;

}

}

}

free(buf);

fclose(f);

return 1;

}

Finally, the function spc_host_check( ) performs access restriction checks. If the remote connection should be allowed, the return will be 0. If some kind of error unrelated to access restriction occurs (e.g., out of memory, bad socket descriptor, etc.), the return will be -1. Otherwise, one of the following error constants may be returned:

SPC_ERROR_NOREVERSE

Indicates that the IP address of the remote connection has no reverse mapping. If strict checking is not being done, this error code will not be returned.

SPC_ERROR_NOHOSTNAME

Indicates that the IP address of the remote connection reverse-maps to a hostname that does not map to any IP address. This condition does not necessarily indicate that a DNS spoofing attack is taking place; however, we do recommend that you treat it as such.

SPC_ERROR_BADHOSTNAME

Indicates that the likelihood of a DNS spoofing attack is high. The IP address of the remote connection does not match any of the IP addresses that its hostname maps to.

SPC_ERROR_HOSTDENIED

Indicates that no DNS spoofing attack is believed to be taking place, but the access restriction rules have matched the remote address with a deny rule.

The function spc_host_check( ) has the following signature:

int spc_host_check(int sockfd, int strict, int action);

This function has the following arguments:

sockfd

Socket descriptor for the remote connection. This argument is used solely to obtain the IP address of the remote connection.

strict

Boolean value indicating whether strict DNS spoofing checks are to be done. If this argument is specified as 0, IP addresses that do not have a reverse mapping will be allowed; otherwise, SPC_ERROR_NOREVERSE will be returned for such connections.

action

Default action to take if the remote IP address does not match any of the defined access restriction rules. It may be specified as either SPC_HOST_ALLOW or SPC_HOST_DENY. Any other value will be treated as equivalent to SPC_HOST_DENY.

You may use spc_host_check( ) without using spc_host_init( ), in which case it will essentially only perform DNS spoofing checks. If you do not use spc_host_init( ), spc_host_check( ) will have an empty rule set, and it will always use the default action if the remote connection passes the DNS spoofing checks.

int spc_host_check(int sockfd, int strict, int action) {

int i, rc;

char *hostname;

struct sockaddr_in addr;

if ((rc = check_spoofdns(sockfd, &addr, &hostname)) = = -1) return -1;

if (rc && (rc != SPC_ERROR_NOREVERSE || strict)) return rc;

for (i = 0; i < spc_host_rulecount; i++) {

if (spc_host_rules[i].name) {

if (hostname && !strcasecmp(hostname, spc_host_rules[i].name)) {

free(hostname);

return (spc_host_rules[i].action = = SPC_HOST_ALLOW);

}

} else {

if ((addr.sin_addr.s_addr & spc_host_rules[i].mask) = =

spc_host_rules[i].addr) {

free(hostname);

return (spc_host_rules[i].action = = SPC_HOST_ALLOW);

}

}

}

if (hostname) free(hostname);

return (action = = SPC_HOST_ALLOW);

}

8.5. Generating Random Passwords and Passphrases

Problem

You would like to avoid problems with easy-to-guess passwords by randomly generating passwords that are difficult to guess.

Solution

For passwords, choose random characters from an acceptable set of characters using spc_rand_range( ) (see Recipe 11.11). For passphrases, choose random words from a predefined list of acceptable words.

Discussion

In many situations, it may be desirable to present a user with a pregenerated password. For example, if the user is not present at the time of account creation, you will want to generate a reasonably secure password for the account and deliver the password to the user via some secure mechanism such as in person or over the phone.

Randomly generated passwords are also useful when you want to enforce safe password requirements. If the user cannot supply an adequately secure password after a certain number of attempts, it may be best to present her with a randomly generated password to use, which will most likely pass all of the requirements tests.

The primary disadvantage of randomly generated passwords is that they are usually difficult to memorize (and type), which often results in users writing them down. In many cases, however, this is a reasonable trade-off.

The basic strategy for generating a random password is to define a character set that contains all of the characters that are valid for the type of password you are generating, then choose random members of that set until enough characters have been chosen to meet the length requirements.

The string spc_password_characters defines the character set from which random password characters are chosen. The function spc_generate_password( ) requires a buffer and the size of the buffer as arguments. The buffer is filled with randomly chosen password characters and is properly NULL-terminated. As written, the function will always succeed, and it will return a pointer to the buffer filled with the randomly generated password.

#include <string.h>

static char *spc_password_characters = "abcdefghijklmnopqrstuvwxyz0123456789"

"ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*( )"

"-=_+;[ ]{ }\\|,./<>?;";

char *spc_generate_password(char *buf, size_t bufsz) {

size_t choices, i;

choices = strlen(spc_password_characters) - 1;

for (i = 0; i < bufsz - 1; i++) /* leave room for NULL terminator */

buf[i] = spc_password_characters[spc_rand_range(0, choices)];

buf[bufsz - 1] = 0;

return buf;

}

Although there is no conceptual difference between a password and a passphrase, each has different connotations to users:

Password

Typically one word, short or medium in length (usually under 10 characters, and rarely longer than 15).

Passphrases

Usually short sentences, or a number of unrelated words grouped together with no coherent meaning.

While a passphrase can be a long string of random characters and a password can be multiple words, the typical passphrase is a sentence that the user picks, usually because it is related to something that is easily remembered. Even though their length and freeform nature make passphrases much harder to run something such as the Crack program on, they are still subject to guessing.

For example, if you are trying to guess someone's passphrase, and you know that person's favorite song, trying some lyrics from that song may prove to be a very good strategy for discovering what the passphrase is. It is important to choose a passphrase carefully. It should be something easy to remember, but it should not be something that someone who knows a little bit about you will be able to guess quickly.

As with passwords, there are times when a randomly generated passphrase is needed. The strategy for randomly generating a passphrase is not altogether different from randomly generating a password. Instead of using single characters, whole words are used, separated by spaces.

The function spc_generate_passphrase( ) uses a data file to obtain the list of words from which to choose. The words in the file should be ordered one per line, and they should not be related in any way. In addition, the selection of words should be sufficiently large that a brute-force attack on generated passphrases is not feasible. Most Unix systems have a file, /usr/share/dict/words, that contains a large number of words from the English dictionary.

This implementation of spc_generate_passphrase( ) keeps the word data file open and builds an in-memory list of the offsets into the file for the beginning of each word. The function keeps offsets instead of the whole words as a memory-saving measure, although with a large enough list of words, the amount of memory required for this list is not insignificant. To choose a word, the function chooses an index into the list of offsets, moves the file pointer to the proper offset, and reads the word. Word lengths can be determined by computing the difference between the next offset and the selected one.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define SPC_WORDLIST_FILE "/usr/share/dict/words"

static FILE *spc_wordlist_file;

static size_t *spc_wordlist_offsets;

static size_t spc_wordlisthortest;

static unsigned int spc_wordlist_count;

static int load_wordlist(void) {

char buf[80];

FILE *f;

size_t *offsets, shortest, *tmp;

unsigned int count;

if (!(f = fopen(SPC_WORDLIST_FILE, "r"))) return 0;

if (!(offsets = (size_t *)malloc(sizeof(size_t) * 1024))) {

fclose(f);

return 0;

}

count = 0;

shortest = ~0;

offsets[0] = 0;

while (fgets(buf, sizeof(buf), f))

if (buf[strlen(buf) - 1] = = '\n') {

if (!((count + 1) % 1024)) {

if (!(offsets = (size_t *)realloc((tmp = offsets),

sizeof(size_t) * (count + 1025)))) {

fclose(f);

free(tmp);

return 0;

}

}

offsets[++count] = ftell(f);

if (offsets[count] - offsets[count - 1] < shortest)

shortest = offsets[count] - offsets[count - 1];

}

if (!feof(f)) {

fclose(f);

free(offsets);

return 0;

}

if (ftell(f) - offsets[count - 1] < shortest)

shortest = ftell(f) - offsets[count - 1];

spc_wordlist_file = f;

spc_wordlist_offsets = offsets;

spc_wordlist_count = count;

spc_wordlisthortest = shortest - 1; /* shortest includes NULL terminator */

return 1;

}

static int get_wordlist_word(unsigned int num, char *buf, size_t bufsz) {

size_t end, length;

if (num >= spc_wordlist_count) return -1;

if (num = = spc_wordlist_count - 1) {

fseek(spc_wordlist_file, 0, SEEK_END);

end = ftell(spc_wordlist_file);

} else end = spc_wordlist_offsets[num + 1];

length = end - spc_wordlist_offsets[num]; /* includes NULL terminator */

if (length > bufsz) return 0;

if (fseek(spc_wordlist_file, spc_wordlist_offsets[num], SEEK_SET) = = -1)

return -1;

fread(buf, length, 1, spc_wordlist_file);

buf[length - 1] = 0;

return 1;

}

char *spc_generate_passphrase(char *buf, size_t bufsz) {

int attempts = 0, rc;

char *outp;

size_t left, len;

unsigned int idx;

if (!spc_wordlist_file && !load_wordlist( )) return 0;

outp = buf;

left = bufsz - 1;

while (left > spc_wordlisthortest) {

idx = spc_rand_range(0, spc_wordlist_count - 1);

rc = get_wordlist_word(idx, outp, left + 1);

if (rc = = -1) return 0;

else if (!rc && ++attempts < 10) continue;

else if (!rc) break;

len = strlen(outp) + 1;

*(outp + len - 1) = ' ';

outp += len;

left -= len;

}

*(outp - 1) = 0;

return buf;

}

When spc_generate_passphrase( ) is called, it opens the data file containing the words to choose from and leaves it open. In addition, depending on the size of the file, it may allocate a sizable amount of memory that remains allocated. When you're done generating passphrases, you should callspc_generate_cleanup( ) to close the data file and free the memory allocated by spc_generate_passphrase( ).

void spc_generate_cleanup(void) {

if (spc_wordlist_file) fclose(spc_wordlist_file);

if (spc_wordlist_offsets) free(spc_wordlist_offsets);

spc_wordlist_file = 0;

spc_wordlist_offsets = 0;

spc_wordlist_count = 0;

spc_wordlisthortest = 0;

}

See Also

Recipe 11.11

8.6. Testing the Strength of Passwords

Problem

You want to ensure that passwords are not easily guessable or crackable.

Solution

Use CrackLib, which is available from http://www.crypticide.org/users/alecm/.

Discussion

When users are allowed to choose their own passwords, a large number of people will inevitably choose passwords that are relatively simple, making them either easy to guess or easy to crack. Secure passwords are often difficult for people to remember, so they tend to choose passwords that are easy to remember, but not very secure. Some of the more common choices are simple words, dates, names, or some variation of these things.

Recognizing this tendency, Alec Muffett developed a program named Crack that takes an encrypted password from the system password file and attempts to guess—or crack—the password. It works by trying words found in a dictionary, combinations of the user's login name and real name, and simple patterns and combinations of words.

CrackLib is the core functionality of Crack, extracted into a library for the intended purpose of including it in password-setting and -changing programs to prevent users from choosing insecure passwords. It exports a simple API, consisting of a single function, FascistCheck( ) , which has the following signature:

char *FascistCheck(char *pw, char *dictpath);

This function has the following arguments:

pw

Buffer containing the password that the user is attempting to use.

dictpath

Buffer containing the name of a file that contains a list of dictionary words for CrackLib to use in its checks.

The dictionary file used by CrackLib is a binary data file (actually, several of them) that is normally built as part of building CrackLib itself. A small utility built as part of CrackLib (but not normally installed) reads in a text file containing a list of words one per line, and builds the binary dictionary files that can be used by CrackLib.

If the FascistCheck( ) function is unable to match the password against the words in the dictionary and its other tests, it will return NULL to indicate that the password is secure and may be used safely. Otherwise, an error message (rather than an error code) is returned; it is suitable for display to the user as a reason why the password could not be accepted.

CrackLib is intended to be used on Unix systems. It relies on certain Unix-specific functions to obtain information about users. In addition, it requires a list of words (a dictionary). Porting CrackLib to Windows should not be too difficult, but we are not aware of any efforts to do so.

See Also

CrackLib by Alec Muffett: http://www.crypticide.org/users/alecm/

8.7. Prompting for a Password

Problem

You need to prompt an interactive user for a password.

Solution

On Unix systems, you can use the standard C runtime function getpass( ) if you can accept limiting passwords to _PASSWORD_LEN, which is typically defined to be 128 characters. If you want to read longer passwords, you can use the function described in the following Section 8.7.3.

On Windows, you can use the standard EDIT control with ES_PASSWORD specified as a style flag to mask the characters typed by a user.

Discussion

In the following subsections we'll look at several different approaches to prompting for passwords.

Prompting for a password on Unix using getpass( ) or readpassphrase( )

The standard C runtime function getpass( ) is the most portable way to obtain a password from a user interactively. Unfortunately, it does have several limitations that you may find unacceptable. The first is that only up to _PASSWORD_LEN (typically 128) characters may be entered; any characters after that are simply discarded. The second is that the password is stored in a statically defined buffer, so it is not thread-safe, but ordinarily this is not much of a problem because there is fundamentally no way to read from the terminal in a thread-safe manner anyway.

The getpass( ) function has the following signature:

#include <sys/types.h>

#include <unistd.h>

char *getpass(const char *prompt);

The text passed as the function's only argument is displayed on the terminal, terminal echo is disabled, and input is gathered in a buffer internal to the function until the user presses Enter. The return value from the function is a pointer to the internal buffer, which will be at most _PASSWORD_LEN + 1 bytes in size, with the additional byte left to hold the NULL terminator.

FreeBSD and OpenBSD both support an alternative function, readpassphrase( ) , that provides the underlying implementation for getpass( ). It is more flexible than getpass( ), allowing the caller to preallocate a buffer to hold a password or passphrase of any size. In addition, it also supports a variety of control flags that control its behavior.

The readpassphrase( ) function has the following signature:

#include <sys/types.h>

#include <readpassphrase.h>

char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags);

This function has the following arguments:

prompt

String that will be displayed to the user before accepting input.

buf

Buffer into which the input read from the interactive user will be placed.

bufsiz

Size of the buffer (in bytes) into which input read from the interactive user is placed. Up to one less byte than the size specified may be read. Any additional input is silently discarded.

flags

Set of flags that may be logically OR'd together to control the behavior of the function.

A number of flags are defined as macros in the readpassphrase.h header file. While some of the flags are mutually exclusive, some of them may be logically combined together:

RPP_ECHO_OFF

Disables echoing of the user's input on the terminal. If neither this flag nor RPP_ECHO_ON is specified, this is the default. The two flags are mutually exclusive, but if both are specified, echoing will be enabled.

RPP_ECHO_ON

Enables echoing of the user's input on the terminal.

RPP_REQUIRE_TTY

If there is no controlling tty, and this flag is specified, readpassphrase( ) will return an error; otherwise, the prompt will be written to stderr, and input will be read from stdin. When input is read from stdin, it's often not possible to disable echoing.

RPP_FORCELOWER

Causes all input from the user to be automatically converted to lowercase. This flag is mutually exclusive with RPP_FORCEUPPER; however, if both flags are specified, RPP_FORCEUPPER will take precedence.

RPP_FORCEUPPER

Causes all input from the user to be automatically converted to uppercase.

RPP_SEVENBIT

Indicates that the high bit will be stripped from all user input.

For both getpass( ) and readpassphrase( ), a pointer to the input buffer will be returned if the function completes successfully; otherwise, a NULL pointer will be returned, and the error that occurred will be stored in the global errno variable.

WARNING

Both getpass( ) and readpassphrase( ) can return an error with errno set to EINTR, which means that the input from the user was interrupted by a signal. If such a condition occurs, all input from the user up to the point when the signal was delivered will be stored in the buffer, but in the case of getpass( ), there will be no way to retrieve that data.

Once getpass( ) or readpassphrase( ) return successfully, you should perform as quickly as possible whatever operation you need to perform with the password that was obtained. Then clear the contents of the returned buffer so that the cleartext password or passphrase will not be left visible in memory to a potential attacker.

Prompting for a password on Unix without getpass( ) or readpassphrase( )

The function presented in this subsection, spc_read_password( ) , requires two arguments. The first is a prompt to be displayed to the user, and the second is the FILE object that points to the input source. If the input source is specified as NULL, spc_read_password( ) will use _PATH_TTY, which is usually defined to be /dev/tty.

The function reads as much data from the input source as memory is available to hold. It allocates an internal buffer, which grows incrementally as it is filled. If the function is successful, the return value will be a pointer to this buffer; otherwise, it will be a NULL pointer.

Note that we use the unbuffered I/O API for reading data from the input source. The unbuffered read is necessary to avoid potential odd side effects in the I/O. We cannot use the stream API because there is no way to save and restore the size of the stream buffer. That is, we cannot know whether the stream was previously buffered.

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <termios.h>

#include <signal.h>

#include <paths.h>

#define BUF_STEP 1024 /* Allocate this much space for the password, and if it gets

* this long, reallocate twice the space.

* Rinse, lather, repeat.

*/

static unsigned char *read_password(int termfd) {

unsigned char ch, *ret, *tmp;

unsigned long ctr = 0;

if (!(ret = (unsigned char *)malloc(BUF_STEP + 1))) return 0;

for (;;) {

switch (read(termfd, &ch, 1)) {

case 1:

if (ch != '\n') break;

/* FALL THROUGH */

case 0:

ret[ctr] = 0;

return ret;

default:

free(ret);

return 0;

}

ret[ctr] = ch;

if (ctr && !(ctr & BUF_STEP)) {

if (!(tmp = (unsigned char *)realloc(ret, ctr + BUF_STEP + 1))) {

free(ret);

return 0;

}

ret = tmp;

}

ctr++;

}

}

unsigned char *spc_read_password(unsigned char *prompt, FILE *term) {

int close = 0, termfd;

sigset_t saved_signals, set_signals;

unsigned char *retval;

struct termios saved_term, set_term;

if (!term) {

if (!(term = fopen(_PATH_TTY, "r+"))) return 0;

close = 1;

}

termfd = fileno(term);

fprintf(term, "%s", prompt);

fflush(term);

/* Defer interruption when echo is turned off */

sigemptyset(&set_signals);

sigaddset(&set_signals, SIGINT);

sigaddset(&set_signals, SIGTSTP);

sigprocmask(SIG_BLOCK, &set_signals, &saved_signals);

/*Save the current state and set the terminal to not echo */

tcgetattr(termfd, &saved_term);

set_term = saved_term;

set_term.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL);

tcsetattr(termfd, TCSAFLUSH, &set_term);

retval = read_password(termfd);

fprintf(term, "\n");

tcsetattr(termfd, TCSAFLUSH, &saved_term);

sigprocmask(SIG_SETMASK, &saved_signals, 0);

if (close) fclose(term);

return retval;

}

Prompting for a password on Windows

On Windows, prompting for a password is as simple as setting the ES_PASSWORD style flag for an EDIT control. When this flag is set, Windows will not display the characters typed by the user. Instead, the password character will be displayed for each character that is typed. By default, thepassword character is an asterisk (*), but you can change it by sending the control an EM_SETPASSWORDCHAR message with wParam set to the character to display.

Unfortunately, there is no way to prevent Windows from displaying something as the user types. The closest that can be achieved is to set the password character to a space, which will make it difficult for an onlooker to determine how many characters have been typed.

To safely retrieve the password stored in the EDIT control's internal buffer, the control should first be queried to determine how many characters it holds. Allocate a buffer to hold the data and query the data from the control. The control will make a copy of the data but leave the original internal buffer unchanged.

To be safe, it's a good idea to set the contents of the buffer to clear the password from internal memory used by the EDIT control. Simply setting the control's internal buffer to an empty string is not sufficient. Instead, set a string that is the length of the string retrieved, then set an empty string if you wish. For example:

#include <windows.h>

BOOL IsPasswordValid(HWND hwndPassword) {

BOOL bValid = FALSE;

DWORD dwTextLength;

LPTSTR lpText;

if (!(dwTextLength = (DWORD)SendMessage(hwndPassword, WM_GETTEXTLENGTH, 0, 0)))

return FALSE;

lpText = (LPTSTR)LocalAlloc(LMEM_FIXED, (dwTextLength + 1) * sizeof(TCHAR));

if (!lpText) return FALSE;

SendMessage(hwndPassword, WM_GETTEXT, dwTextLength + 1, (LPARAM)lpText);

/* Do something to validate the password */

while (dwTextLength--) *(lpText + dwTextLength) = ' ';

SendMessage(hwndPassword, WM_SETTEXT, 0, (LPARAM)lpText);

LocalFree(lpText);

return bValid;

}

WARNING

Other processes running on the same machine can access the contents of your edit control. Unfortunately, the best mitigation strategy, at this time, is to get rid of the edit control as soon as possible.

8.8. Throttling Failed Authentication Attempts

Problem

You want to prevent an attacker from making too many attempts at guessing a password through normal interactive means.

Solution

It's best to use a protocol where such attacks don't leak any information about a password, such as a public key-based mechanism.

Delay program execution after a failed authentication attempt. For each additional failure, increase the delay before allowing the user to make another attempt to authenticate.

Discussion

Throttling failed authentication attempts is a balance between allowing legitimate users who simply mistype a password or passphrase to have a quick retry and delaying attackers who are trying to brute-force passwords or passphrases.

Our recommended strategy has three variables that control how it delays repeated authentication attempts:

Maximum number of attempts

If this limit is reached, the authentication should be considered a complete failure, resulting in a disconnection of the network connection or shutting down of the program that requires authentication. A reasonable limit on the maximum number of allowed authentication attempts is three, or perhaps five at most.

Maximum number of failed attempts allowed before enabling throttling

In general, it is reasonable to allow one or two failed attempts before instituting delays, depending on the maximum number of allowed authentication failures.

Number of seconds to delay between successive authentication attempts

For each successive failure, the delay increases exponentially. For example, if the base number of seconds to delay is set to two, the first delay will be two seconds, the second delay will be four seconds, the third delay will be eight seconds, and so on. A reasonable starting delay is generally one or two seconds, but depending on the settings you choose for the first two variables, you may want to increase the starting delay. In particular, if you allow a large number of attempts, it is probably a good idea to increase the delay.

The best way to institute a delay depends entirely upon the architecture of your program. If authentication is being performed over a network in a single-threaded server that is multiplexing connections with select( ) or poll( ), the best option may be to compute the future time at which the next authentication attempt will be accepted, and ignore any input until that time arrives.

When authenticating a user interactively on a terminal on Unix, the best solution is likely to be to use the sleep( ) function. On Windows, there is no strict equivalent. The Win32 API functions Sleep( ) and SleepEx( ) will both return immediately—regardless of the specified wait time—if there are no other threads of equal priority waiting to run.

WARNING

Some of these techniques can increase the risk of denial-of-service attacks.

In a GUI environment, any authentication dialog presented to the user will have a button labeled "OK" or some equivalent. When a delay must be made, disable the button for the duration of the delay, then enable it. On Windows, this is easily accomplished using timers.

The following function, spc_throttle( ) , computes the number of seconds to delay based on the three variables we've described and the number of failed authentication attempts. It has four arguments:

attempts

Pointer to an integer used to count the number of failed attempts. Initially, the value of the integer to which it points should be zero, and each call to spc_throttle( ) will increment it by one.

max_attempts

Maximum number of attempts to allow. When this number of attempts has been made, the return from spc_throttle( ) will be -1 to indicate a complete failure to authenticate.

allowed_fails

Number of attempts allowed before enabling throttling.

delay

Base delay in seconds.

If the maximum number of attempts has been reached, the return value from spc_throttle( ) will be -1. If there is to be no delay, the return value will be 0; otherwise, the return value will be the number of seconds to delay before allowing another authentication attempt.

int spc_throttle(int *attempts, int max_attempts, int allowed_fails, int delay) {

int exp;

(*attempts)++;

if (*attempts > max_attempts) return -1;

if (*attempts <= allowed_fails) return 0;

for (exp = *attempts - allowed_fails - 1; exp; exp--)

delay *= 2;

return delay;

}

8.9. Performing Password-Based Authentication with crypt( )

Problem

You need to use the standard Unix crypt( ) function for password-based authentication.

Solution

The standard Unix crypt( ) function typically uses a weak one-way algorithm to perform its encryption, which is usually also slow and insecure. You should, therefore, use crypt( ) only for compatibility reasons.

Despite this limitation, you might want to use crypt( ) for compatibility purposes. If so, to encrypt a password, choose a random salt and call crypt( ) with the plaintext password and the chosen salt. To verify a password encrypted with crypt( ), encrypt the plaintext password using the already encrypted password as the salt, then compare the result with the already encrypted password. If they match, the password is correct.

Discussion

TIP

What we are doing here isn't really encrypting a password. Actually, we are creating a password validator. We use the term encryption because it is in common use and is a more concise way to explain the process.

The crypt( ) function is normally found in use only on older Unix systems that still exclusively use the /etc/passwd file for storing user information. Modern Unix systems typically use stronger algorithms and alternate storage methods for user information, such as the Lightweight Directory Access Protocol (LDAP), Kerberos (see Recipe 8.13), NIS, or some other type of directory service.

The traditional implementation of crypt( ) uses DES (see Recipe 5.2 for a discussion of symmetric ciphers, including DES) to perform its encryption. DES is a symmetric cipher, which essentially means that if you have the key used to encrypt, you can decrypt the encrypted data. To make the function one-way, crypt( ) encrypts the key with itself.[1]

The DES algorithm requires a salt, which crypt( ) limits to 12 bits. It also prepends the salt to the resulting ciphertext, which is base64-encoded. DES is a weak block cipher to start, and the crypt( ) function traditionally limits passwords to a single block, which serves to further weaken its capabilities because the block size is 64 bits, or 8 bytes.

Because DES is a weak cipher and crypt( ) limits the plaintext to a single DES block, we strongly recommend against using crypt( ) in new authentication systems. You should use it only if you have a need to maintain compatibility with an older system that uses it.

Encrypting a password with crypt( ) is a simple operation, but programmers often get it wrong. The most common mistake is to use the plaintext password as the salt, but recall that crypt( ) stores the salt as the first two bytes of its result. Because passwords are limited to eight bytes, using the plaintext password as the salt reveals at least a quarter of the password and makes dictionary attacks easier.

The crypt( ) function has the following signature:

char *crypt(const char *key, const char *salt);

This function has the following arguments:

key

Password to encrypt.

salt

Buffer containing the salt to use. Remember that crypt( ) will use only 12 bits for the salt, so it will use only the first two bytes of this buffer; passing in a larger salt will have no effect. For maximum compatibility, the salt should contain only alphanumeric characters, a period, or a forward slash.

The following function, spc_crypt_encrypt( ) , will generate a suitable random salt and return the result from calling crypt( ) with the password and generated salt. The crypt( ) function returns a pointer to a statically allocated buffer, so you should not call crypt( ) more than once without using the results from earlier calls because the data returned from earlier calls will be overwritten.

#include <string.h>

#include <unistd.h>

char *spc_crypt_encrypt(const char *password) {

char salt[3];

static char *choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

"0123456789./";

salt[0] = choices[spc_rand_range(0, strlen(choices) - 1)];

salt[1] = choices[spc_rand_range(0, strlen(choices) - 1)];

salt[2] = 0;

return crypt(password, salt);

}

Verifying a password encrypted with crypt( ) involves encrypting the plaintext password to be verified and comparing it with the already encrypted password, which would normally be obtained from the passwd structure returned by getpwnam( ) or getpwuid( ). (See Recipe 8.2.)

Recall that crypt( ) stores the salt as the first two bytes of its result. For purposes of verification, you will not want to generate a random salt. Instead, you should use the already encrypted password as the salt.

You can use the following function, spc_crypt_verify( ) , to verify a password; however, we're really only providing an example of how crypt( ) should be called to verify a password. It does little more than call crypt( ) and compare its result with the encrypted password.

#include <string.h>

#include <unistd.h>

int spc_crypt_verify(const char *plain_password, const char *cipher_password) {

return !strcmp(cipher_password, crypt(plain_password, cipher_password));

}

See Also

Recipe 5.2, Recipe 8.2, Recipe 8.13


[1] Some older versions encrypt a string of zeros instead.

8.10. Performing Password-Based Authentication with MD5-MCF

Problem

You want to use MD5 as a method for encrypting passwords.

Solution

Many modern systems support the use of MD5 for encrypting passwords. An encoding known as Modular Crypt Format (MCF) is used to allow the use of the traditional crypt( ) function to handle the old DES encryption as well as MD5 and any number of other possible algorithms.

On systems that support MCF through crypt( ),[2] you can simply use crypt( ) as discussed in Recipe 8.9 with some modification to the required salt. Otherwise, you can use the implementation in this recipe.

Discussion

TIP

What we are doing here isn't really encrypting a password. Actually, we are creating a password validator. We use the term encryption because it is in common use and is a more concise way to explain the process.

MCF is a 7-bit encoding that allows for encoding multiple fields into a single string. A dollar sign delimits each field, with the first field indicating the algorithm to use by way of a predefined number. At present, only two well-known algorithms are defined: 1 indicates MD5 and 2 indicatesBlowfish. The contents of the first field also dictate how many fields should follow and the type of data each one contains. The first character in an MCF string is always a dollar sign, which technically leaves the 0th field empty.

For encoding MD5 in MCF, the first field must contain a 1, and two additional fields must follow: the first is the salt, and the second is the MD5 checksum that is calculated from a sequence of MD5 operations based on a nonintuitive process that depends on the value of the salt and the password. The intent behind this process was to slow down brute-force attacks; however, we feel that the algorithm is needlessly complex, and there are other, better ways to achieve the same goals.

WARNING

As with the traditional DES-based crypt( ), we do not recommend that you use MD5-MCF in new authentication systems. You should use it only when you must maintain compatibility with existing systems. We recommend that you consider using something like PBKDF2 instead. (See Recipe 8.11.)

The function spc_md5_encrypt( ) implements a crypt( )-like function that uses the MD5-MCF method that we've described. If it is successful (the only error that should ever occur is an out-of-memory error), it will return a dynamically allocated buffer that contains the encrypted password in MCF.

In this recipe, we present two versions of spc_md5_encrypt( ) in their entirety. The first uses OpenSSL and standard C runtime functions; the second uses the native Win32 API and CryptoAPI.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <openssl/md5.h>

static char *crypt64_encode(const unsigned char *buf) {

int i;

char *out, *ptr;

unsigned long l;

static char *crypt64_set = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

"abcdefghijklmnopqrstuvwxyz";

if (!(out = ptr = (char *)malloc(23))) return 0;

#define CRYPT64_ENCODE(x, y, z) \

for (i = 0, l = (buf[(x)] << 16) | (buf[(y)] << 8) | buf[(z)]; i++ < 4; \

l >>= 6) *ptr++ = crypt64_set[l & 0x3F]

CRYPT64_ENCODE(0, 6, 12); CRYPT64_ENCODE(1, 7, 13);

CRYPT64_ENCODE(2, 8, 14); CRYPT64_ENCODE(3, 9, 15);

CRYPT64_ENCODE(4, 10, 5);

for (i = 0, l = buf[11]; i++ < 2; l >>= 6) *ptr++ = crypt64_set[l & 0x3F];

*ptr = 0;

#undef CRYPT64_ENCODE

return out;

}

static void compute_hash(unsigned char *hash, const char *key,

const char *salt, size_t salt_length) {

int i, length;

size_t key_length;

MD5_CTX ctx, ctx1;

key_length = strlen(key);

MD5_Init(&ctx);

MD5_Update(&ctx, key, key_length);

MD5_Update(&ctx, salt, salt_length);

MD5_Init(&ctx1);

MD5_Update(&ctx1, key, key_length);

MD5_Update(&ctx1, salt, salt_length);

MD5_Update(&ctx1, key, key_length);

MD5_Final(hash, &ctx1);

for (length = key_length; length > 0; length -= 16)

MD5_Update(&ctx, hash, (length > 16 ? 16 : length));

memset(hash, 0, 16);

for (i = key_length; i; i >>= 1)

if (i & 1) MD5_Update(&ctx, hash, 1);

else MD5_Update(&ctx, key, 1);

MD5_Final(hash, &ctx);

for (i = 0; i < 1000; i++) {

MD5_Init(&ctx);

if (i & 1) MD5_Update(&ctx, key, key_length);

else MD5_Update(&ctx, hash, 16);

if (i % 3) MD5_Update(&ctx, salt, salt_length);

if (i % 7) MD5_Update(&ctx, key, key_length);

if (i & 1) MD5_Update(&ctx, hash, 16);

else MD5_Update(&ctx, key, key_length);

MD5_Final(hash, &ctx);

}

}

char *spc_md5_encrypt(const char *key, const char *salt) {

char *base64_out, *base64_salt, *result, *salt_end, *tmp_string;

size_t result_length, salt_length;

unsigned char out[16], raw_salt[16];

base64_out = base64_salt = result = 0;

if (!salt) {

salt_length = 8;

spc_rand(raw_salt, sizeof(raw_salt));

if (!(base64_salt = crypt64_encode(raw_salt))) goto done;

if (!(tmp_string = (char *)realloc(base64_salt, salt_length + 1)))

goto done;

base64_salt = tmp_string;

} else {

if (strncmp(salt, "$1$", 3) != 0) goto done;

if (!(salt_end = strchr(salt + 3, '$'))) goto done;

salt_length = salt_end - (salt + 3);

if (salt_length > 8) salt_length = 8; /* maximum salt is 8 bytes */

if (!(base64_salt = (char *)malloc(salt_length + 1))) goto done;

memcpy(base64_salt, salt + 3, salt_length);

}

base64_salt[salt_length] = 0;

compute_hash(out, key, base64_salt, salt_length);

if (!(base64_out = crypt64_encode(out))) goto done;

result_length = strlen(base64_out) + strlen(base64_salt) + 5;

if (!(result = (char *)malloc(result_length + 1))) goto done;

sprintf(result, "$1$%s$%s", base64_salt, base64_out);

done:

/* cleanup */

if (base64_salt) free(base64_salt);

if (base64_out) free(base64_out);

return result;

}

We have named the Windows version of spc_md5_encrypt( ) as SpcMD5Encrypt( ) to adhere to conventional Windows naming conventions. In addition, the implementation uses only Win32 API and CryptoAPI functions, rather than relying on the standard C runtime for string and memory handling.

#include <windows.h>

#include <wincrypt.h>

static LPSTR Crypt64Encode(BYTE *pBuffer) {

int i;

DWORD dwTemp;

LPSTR lpszOut, lpszPtr;

static LPSTR lpszCrypt64Set = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

"abcdefghijklmnopqrstuvwyxz";

if (!(lpszOut = lpszPtr = (char *)LocalAlloc(LMEM_FIXED, 23))) return 0;

#define CRYPT64_ENCODE(x, y, z) \

for (i = 0, dwTemp = (pBuffer[(x)] << 16) | (pBuffer[(y)] << 8) | \

pBuffer[(z)]; i++ < 4; dwTemp >>= 6) \

*lpszPtr++ = lpszCrypt64Set[dwTemp & 0x3F]

CRYPT64_ENCODE(0, 6, 12); CRYPT64_ENCODE(1, 7, 13);

CRYPT64_ENCODE(2, 8, 14); CRYPT64_ENCODE(3, 9, 15);

CRYPT64_ENCODE(4, 10, 5);

for (i = 0, dwTemp = pBuffer[11]; i++ < 2; dwTemp >>= 6)

*lpszPtr++ = lpszCrypt64Set[dwTemp & 0x3F];

*lpszPtr = 0;

#undef CRYPT64_ENCODE

return lpszOut;

}

static BOOL ComputeHash(BYTE *pbHash, LPCSTR lpszKey, LPCSTR lpszSalt,

DWORD dwSaltLength) {

int i, length;

DWORD cbHash, dwKeyLength;

HCRYPTHASH hHash, hHash1;

HCRYPTPROV hProvider;

dwKeyLength = lstrlenA(lpszKey);

if (!CryptAcquireContext(&hProvider, 0, MS_DEF_PROV, 0, CRYPT_VERIFYCONTEXT))

return FALSE;

if (!CryptCreateHash(hProvider, CALG_MD5, 0, 0, &hHash)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

CryptHashData(hHash, (BYTE *)lpszKey, dwKeyLength, 0);

CryptHashData(hHash, (BYTE *)lpszSalt, dwSaltLength, 0);

if (!CryptCreateHash(hProvider, CALG_MD5, 0, 0, &hHash1)) {

CryptDestroyHash(hHash);

CryptReleaseContext(hProvider, 0);

return FALSE;

}

CryptHashData(hHash1, lpszKey, dwKeyLength, 0);

CryptHashData(hHash1, lpszSalt, dwSaltLength, 0);

CryptHashData(hHash1, lpszKey, dwKeyLength, 0);

cbHash = 16; CryptGetHashParam(hHash1, HP_HASHVAL, pbHash, &cbHash, 0);

CryptDestroyHash(hHash1);

for (length = dwKeyLength; length > 0; length -= 16)

CryptHashData(hHash, pbHash, (length > 16 ? 16 : length), 0);

SecureZeroMemory(pbHash, 16);

for (i = dwKeyLength; i; i >>= 1)

if (i & 1) CryptHashData(hHash, pbHash, 1, 0);

else CryptHashData(hHash, lpszKey, 1, 0);

cbHash = 16; CryptGetHashParam(hHash, HP_HASHVAL, pbHash, &cbHash, 0);

CryptDestroyHash(hHash);

for (i = 0; i < 1000; i++) {

if (!CryptCreateHash(hProvider, CALG_MD5, 0, 0, &hHash)) {

CryptReleaseContext(hProvider, 0);

return FALSE;

}

if (i & 1) CryptHashData(hHash, lpszKey, dwKeyLength, 0);

else CryptHashData(hHash, pbHash, 16, 0);

if (i % 3) CryptHashData(hHash, lpszSalt, dwSaltLength, 0);

if (i & 7) CryptHashData(hHash, lpszKey, dwKeyLength, 0);

if (i & 1) CryptHashData(hHash, pbHash, 16, 0);

else CryptHashData(hHash, lpszKey, dwKeyLength, 0);

cbHash = 16; CryptGetHashParam(hHash, HP_HASHVAL, pbHash, &cbHash, 0);

CryptDestroyHash(hHash);

}

CryptReleaseContext(hProvider, 0);

return TRUE;

}

LPSTR SpcMD5Encrypt(LPCSTR lpszKey, LPCSTR lpszSalt) {

BYTE pbHash[16], pbRawSalt[8];

DWORD dwResultLength, dwSaltLength;

LPSTR lpszBase64Out, lpszBase64Salt, lpszResult, lpszTemp;

LPCSTR lpszSaltEnd;

lpszBase64Out = lpszBase64Salt = lpszResult = 0;

if (!lpszSalt) {

spc_rand(pbRawSalt, (dwSaltLength = sizeof(pbRawSalt)));

if (!(lpszBase64Salt = Crypt64Encode(pbRawSalt))) goto done;

if (!(lpszTemp = (LPSTR)LocalReAlloc(lpszBase64Salt, dwSaltLength + 1, 0)))

goto done;

lpszBase64Salt = lpszTemp;

} else {

if (lpszSalt[0] != '$' || lpszSalt[1] != '1' || lpszSalt[2] != '$') goto done;

for (lpszSaltEnd = lpszSalt + 3; *lpszSaltEnd != '$'; lpszSaltEnd++)

if (!*lpszSaltEnd) goto done;

dwSaltLength = (lpszSaltEnd - (lpszSalt + 3));

if (dwSaltLength > 8) dwSaltLength = 8; /* maximum salt is 8 bytes */

if (!(lpszBase64Salt = (LPSTR)LocalAlloc(LMEM_FIXED,dwSaltLength + 1)))

goto done;

CopyMemory(lpszBase64Salt, lpszSalt + 3, dwSaltLength);

}

lpszBase64Salt[dwSaltLength] = 0;

if (!ComputeHash(pbHash, lpszKey, lpszBase64Salt, dwSaltLength)) goto done;

if (!(lpszBase64Out = Crypt64Encode(pbHash))) goto done;

dwResultLength = lstrlenA(lpszBase64Out) + lstrlenA(lpszBase64Salt) + 5;

if (!(lpszResult = (LPSTR)LocalAlloc(LMEM_FIXED, dwResultLength + 1)))

goto done;

wsprintfA(lpszResult, "$1$%s$%s", lpszBase64Salt, lpszBase64Out);

done:

/* cleanup */

if (lpszBase64Salt) LocalFree(lpszBase64Salt);

if (lpszBase64Out) LocalFree(lpszBase64Out);

return lpszResult;

}

Verifying a password encrypted using MD5-MCF works the same way as verifying a password encrypted with crypt( ): encrypt the plaintext password with the already encrypted password as the salt, and compare the result with the already encrypted password. If they match, the password is correct.

For the sake of both consistency and convenience, you can use the function spc_md5_verify( ) to verify a password encrypted using MD5-MCF.

int spc_md5_verify(const char *plain_password, const char *crypt_password) {

int match = 0;

char *md5_result;

if ((md5_result = spc_md5_encrypt(plain_password, crypt_password)) != 0) {

match = !strcmp(md5_result, crypt_password);

free(md5_result);

}

return match;

}

See Also

Recipe 8.9, Recipe 8.11


[2] FreeBSD, Linux, and OpenBSD support MCF via crypt( ). Darwin, NetBSD, and Solaris do not. Windows also does not because it does not support crypt( ) at all.

8.11. Performing Password-Based Authentication with PBKDF2

Problem

You want to use a stronger encryption method than crypt( ) and MD5-MCF (see Recipe 8.9 and Recipe 8.10).

Solution

Use the PBKDF2 method of converting passwords to symmetric keys. See Recipe 4.10 for a more detailed discussion of PBKDF2.

Discussion

TIP

What we are doing here isn't really encrypting a password. Actually, we are creating a password validator. We use the term encryption because it is in common use and is a more concise way to explain the process.

The PBKDF2 algorithm provides a way to convert an arbitrary-sized password or passphrase into an arbitrary-sized key. This method fits perfectly with the need to store passwords in a way that does not allow recovery of the actual password. The PBKDF2 algorithm requires two extra pieces of information besides the password: an iteration count and a salt. The iteration count specifies how many times to run the underlying operation; this is a way to slow down the algorithm to thwart brute-force attacks. The salt provides the same function as the salt in MD5 or DES-based crypt( ) implementations.

Storing a password using this method is simple; store the result of the PBKDF2 operation, along with the iteration count and the salt. When verification of a password is required, retrieve the stored values and run the PBKDF2 using the supplied password, saved iteration count, and salt. Compare the output of this operation with the stored result, and if the two are equal, the password is correct; otherwise, the passwords do not match.

The function spc_pbkdf2_encrypt( ) implements a crypt( )-like function that uses the PBKDF2 method that we've described, and it assumes the implementation found in Recipe 4.10. If it is successful (the only error that should ever occur is an out-of-memory error), it will return a dynamically allocated buffer that contains the encrypted password in MCF, which encodes the salt and encrypted password in base64 as well as includes the iteration count.

MCF delimits the information it encodes with dollar signs. The first field is a digit that identifies the algorithm represented, which also dictates what the other fields contain. As of this writing, only two algorithms are defined for MCF: 1 indicates MD5 (see Recipe 8.9), and 2 indicates Blowfish. We have chosen to use 10 for PBKDF2 so that it is unlikely that it will conflict with anything else.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <limits.h>

#include <errno.h>

char *spc_pbkdf2_encrypt(const char *key, const char *salt) {

int error;

char *base64_out, *base64_salt, *result, *salt_end, *tmp_string;

size_t length, result_length, salt_length;

unsigned int iterations, tmp_uint;

unsigned char out[16], *raw_salt;

unsigned long tmp_ulong;

raw_salt = 0;

base64_out = base64_salt = result = 0;

if (!salt) {

if (!(raw_salt = (unsigned char *)malloc((salt_length = 8)))) return 0;

spc_rand(raw_salt, salt_length);

if (!(base64_salt = spc_base64_encode(raw_salt, salt_length, 0))) {

free(raw_salt);

return 0;

}

iterations = 10000;

} else {

if (strncmp(salt, "$10$", 4) != 0) return 0;

if (!(salt_end = strchr(salt + 4, '$'))) return 0;

if (!(base64_salt = (char *)malloc(salt_end - (salt + 4) + 1))) return 0;

memcpy(base64_salt, salt + 4, salt_end - (salt + 4));

base64_salt[salt_end - (salt + 4)] = 0;

tmp_ulong = strtoul(salt_end + 1, &tmp_string, 10);

if ((tmp_ulong = = ULONG_MAX && errno = = ERANGE) || tmp_ulong > UINT_MAX ||

!tmp_string || *tmp_string != '$') {

free(base64_salt);

return 0;

}

iterations = (unsigned int)tmp_ulong;

raw_salt = spc_base64_decode(base64_salt, &salt_length, 1, &error);

if (!raw_salt || error) {

free(base64_salt);

return 0;

}

}

spc_pbkdf2((char *)key, strlen(key), raw_salt, salt_length, iterations,

out, sizeof(out));

if (!(base64_out = spc_base64_encode(out, sizeof(out), 0))) goto done;

for (tmp_uint = iterations, length = 1; tmp_uint; length++) tmp_uint /= 10;

result_length = strlen(base64_out) + strlen(base64_salt) + length + 6;

if (!(result = (char *)malloc(result_length + 1))) goto done;

sprintf(result, "$10$%s$%u$%s", base64_salt, iterations, base64_out);

done:

/* cleanup */

if (raw_salt) free(raw_salt);

if (base64_salt) free(base64_salt);

if (base64_out) free(base64_out);

return result;

}

Verifying a password encrypted using PBKDF2 works the same way as verifying a password encrypted with crypt( ): encrypt the plaintext password with the already encrypted password as the salt, and compare the result with the already encrypted password. If they match, the password is correct.

For the sake of both consistency and convenience, you can use the following function, spc_pbkdf2_verify( ) , to verify a password encrypted using PBKDF2.

int spc_pbkdf2_verify(const char *plain_password, const char *crypt_password) {

int match = 0;

char *pbkdf2_result;

if ((pbkdf2_result = spc_pbkdf2_encrypt(plain_password, crypt_password)) != 0) {

match = !strcmp(pbkdf2_result, crypt_password);

free(pbkdf2_result);

}

return match;

}

See Also

Recipe 4.10, Recipe 8.9, Recipe 8.10

8.12. Authenticating with PAM

Problem

You need to perform authentication in your application, but you do not want to tie your application to any specific authentication system. Instead, you want to allow the system administrator to configure an authentication system that is appropriate for the environment in which the application will run.

Solution

Use Pluggable Authentication Modules (PAM), which provides an API that is independent of the underlying authentication system. PAM allows the system administrator to configure the authentication system or systems to use, and it supports a wide variety of existing systems, such as traditional Unix password-based authentication, Kerberos, Radius, and many others.

Discussion

TIP

We do not discuss building your own PAM modules in this book, but there is a recipe on that topic on the book's web site.

Most modern Unix systems provide support for PAM and even use it for system-wide authentication (for example, for interactive user login for shell access). Many popular and widely deployed services that use authentication are also capable of using PAM.

Every application that makes use of PAM uses a service name, such as "login" or "ftpd". PAM uses the service name along with a configuration file (often /etc/pam.conf) or files (one for each service, named after the service, and usually located in /etc/pam.d). PAM uses configuration information gleaned from the appropriate configuration file to determine which modules to use, how to treat successes and failures, and other miscellaneous information.

Modules are implemented as shared libraries that are dynamically loaded into your application as required. Each module is expected to export several standard functions in order to interact with the PAM infrastructure. Implementation of PAM modules is outside the scope of this book, but our web site contains more information on this topic.

PAM and its modules handle the drudgery of obtaining passwords from users if required, exchanging keys, or doing whatever must be done to authenticate. All that you need to do in your code is make the proper sequence of calls with the necessary information to PAM, and the details of authentication are handled for you, allowing you to concentrate on the rest of your application.

Unfortunately, the PAM API is somewhat clumsy, and the steps necessary for performing basic authentication with PAM are not necessarily as straightforward as they could be. The functions presented in this recipe, spc_pam_login( ) and spc_pam_logout( ), work together to perform the necessary steps properly.

To use PAM in your own code, you will need to include the header files security/pam_appl.h and security/pam_misc.h in your program, and link against the PAM library, usually by specifying -lpam on the linker command line.

To authenticate a user, call spc_pam_login( ) , which has the following signature:

pam_handle_t *spc_pam_login(const char *service, const char *user, int **rc);

This function has the following arguments:

service

Name of the service to use. PAM uses the service name to find the appropriate module configuration information in its configuration file or files. You will typically want to use a service name that does not conflict with anything else, though if you are writing an FTP server, for example, you will want to use "ftpd" as the service.

user

Name of the user to authenticate.

rc

Pointer to an integer that will receive the PAM error code if an error occurs.

If the user is authenticated successfully, spc_pam_login( ) will return a non-NULL pointer to a pam_handle_t context object. Otherwise, it will return NULL, and you should consult the rc argument for the error code.

#include <security/pam_appl.h>

#include <security/pam_misc.h>

static struct pam_conv spc_pam_conv = { misc_conv, 0 };

pam_handle_t *spc_pam_login(const char *service, const char *user, int *rc) {

pam_handle_t *hndl;

if (!service || !user || !rc) {

if (rc) *rc = PAM_ABORT;

return 0;

}

if ((*rc = pam_start(service, user, &spc_pam_conv, &hndl)) != PAM_SUCCESS) {

pam_end(hndl, *rc);

return 0;

}

if ((*rc = pam_authenticate(hndl, PAM_DISALLOW_NULL_AUTHTOK)) != PAM_SUCCESS) {

pam_end(hndl, *rc);

return 0;

}

*rc = pam_acct_mgmt(hndl, 0);

if (*rc = = PAM_NEW_AUTHTOK_REQD) {

pam_chauthtok(hndl, PAM_CHANGE_EXPIRED_AUTHTOK);

*rc = pam_acct_mgmt(hndl, 0);

}

if (*rc != PAM_SUCCESS) {

pam_end(hndl, *rc);

return 0;

}

if ((*rc = pam_setcred(hndl, PAM_ESTABLISH_CRED)) != PAM_SUCCESS) {

pam_end(hndl, *rc);

return 0;

}

if ((*rc = pam_open_session(hndl, 0)) != PAM_SUCCESS) {

pam_end(hndl, *rc);

return 0;

}

/* no need to set *rc to PAM_SUCCESS; we wouldn't be here if it weren't */

return hndl;

}

After the authentication is successful, you should maintain the pam_handle_t object returned by spc_pam_login( ) until the user logs out from your application, at which point you should call spc_pam_logout( ) to allow PAM to perform anything it needs to do to log the user out.

void spc_pam_logout(pam_handle_t *hndl) {

if (!hndl) return;

pam_close_session(hndl, 0);

pam_end(hndl, PAM_SUCCESS);

}

See Also

§ "Pluggable Authentication Modules" by A. G. Morgan: http://www.kernel.org/pub/linux/libs/pam/pre/doc/current-draft.txt

§ OpenPAM home page: http://openpam.sourceforge.net

§ Linux PAM home page: http://www.kernel.org/pub/linux/libs/pam/

§ Solaris PAM home page: http://wwws.sun.com/software/solaris/pam/

8.13. Authenticating with Kerberos

Problem

You need to authenticate using Kerberos.

Solution

If the client and the server are operating within the same Kerberos realm (or in separate realms, but cross-realm authentication is possible), you can use the user's credentials to authenticate from the client with the server. Both the client and the server must support this authentication method.

The code presented in this recipe assumes you are using either the Heimdal or the MIT Kerberos implementation. It further assumes you are using Version 5, which we consider reasonable because Version 4 has been obsolete for so many years. We do not cover the Windows interface to Kerberos in this book because of the significant difference in the API compared to Heimdal and MIT implementations, as well as the complexity of the SSPI API that is required on Windows. We do, however, present an equivalent recipe for Windows on the book's web site.

Discussion

First, we define a structure primarily for convenience. After a successful authentication, several pieces of information are passed back from the Kerberos API. We store each of these pieces of information in a single structure rather than adding several additional arguments to our authentication functions.

#include <krb5.h>

typedef struct {

krb5_context ctx;

krb5_auth_context auth_ctx;

krb5_ticket *ticket;

} spc_krb5bundle_t;

On the client side, only the ctx and auth_ctx fields will be initialized. On the server side, all three fields will be initialized. Before passing an spc_krb5bundle_t object to either spc_krb5_client( ) or spc_krb5_server( ), you must ensure that auth_ctx and ticket are initialized to NULL. If the ctxfield is not NULL, it should be a valid krb5_context object, which will be used instead of creating a new one.

Both the client and the server must be able to handle using Kerberos authentication. The code required for each side of the connection is very similar. On the client side, spc_krb5_client( ) will attempt to authenticate with the server. The code assumes that the user has already obtained a ticket-granting ticket from the appropriate Key Distribution Center (KDC), and that a credentials cache exists.

The function spc_krb5_client( ) has the following signature:

krb5_error_code spc_krb5_client(int sockfd, spc_krb5bundle_t *bundle,

char *service, char *host, char *version);

This function has the following arguments:

sockfd

Socket descriptor over which the authentication should be performed. The connection to the server should already be established, and the socket should be in blocking mode.

bundle

spc_krb5bundle_t object that will be loaded with information if the authentication with the server is successful. Before calling spc_krb5_client( ), you should be sure to zero the contents of this structure. If the structure contains a pointer to a Kerberos context object, spc_krb5_client( ) will use it instead of creating a new one.

service

Name component of the server's principal. It is combined with the server's hostname or instance to build the principal for the server. The server's principal will be of the form service/host@REALM. The realm is assumed to be the user's default realm.

host

Hostname of the server. It is used as the instance component of the server's principal.

version

Version string that is sent to the server. This string is generally used to indicate a version of the protocol that the client and server will speak to each other. It does not have anything to do with the Kerberos protocol or the version of Kerberos in use. The string may be anything you want, but both the client and server must agree on the same string for authentication to succeed.

If authentication is successful, the return value from spc_krb5_client( ) will be 0, and the relevant fields in the spc_krb5bundle_t object will be filled in. The client may then proceed to use other Kerberos API functions to exchange encrypted and authenticated information with the server. Of particular interest is that a key suitable for use with a symmetric cipher is now available. (See Recipe 9.6 for an example of how to use the key effectively.)

If any kind of error occurs while attempting to authenticate with the server, the return value from the following spc_krb5_client( ) function will be the error code returned by the Kerberos API function that failed. Complete lists of error codes are available in the Heimdal and MIT Kerberos header files.

krb5_error_code spc_krb5_client(int sockfd, spc_krb5bundle_t *bundle,

char *service, char *host, char *version) {

int free_context = 0;

krb5_principal server = 0;

krb5_error_code rc;

if (!bundle->ctx) {

if ((rc = krb5_init_context(&(bundle->ctx))) != 0) goto error;

free_context = 1;

}

if ((rc = krb5_sname_to_principal(bundle->ctx, host, service,

KRB5_NT_SRV_HST, &server)) != 0) goto error;

rc = krb5_sendauth(bundle->ctx, &(bundle->auth_ctx), &sockfd, version,

0, server, AP_OPTS_MUTUAL_REQUIRED, 0, 0, 0, 0, 0, 0);

if (!rc) {

krb5_free_principal(bundle->ctx, server);

return 0;

}

error:

if (server) krb5_free_principal(bundle->ctx, server);

if (bundle->ctx && free_context) {

krb5_free_context(bundle->ctx);

bundle->ctx = 0;

}

return rc;

}

The code for the server side of the connection is similar to the client side, although it is somewhat simplified because most of the information in the exchange comes from the client. The function spc_krb5_server( ) , listed later in this section, performs the server-side part of the authentication. It ultimately calls krb5_recvauth( ) , which waits for the client to initiate an authenticate request.

The function spc_krb5_server( ) has the following signature:

krb5_error_code spc_krb5_server(int sockfd, spc_krb5bundle_t *bundle,

char *service, char *version);

This function has the following arguments:

sockfd

Socket descriptor over which the authentication should be performed. The connection to the client should already be established, and the socket should be in blocking mode.

bundle

spc_krb5bundle_t object that will be loaded with information if the authentication with the server is successful. Before calling spc_krb5_server( ), you should be sure to zero the contents of this structure. If the structure contains a pointer to a Kerberos context object, spc_krb5_server( ) will use it instead of creating a new one.

service

Name component of the server's principal. It is combined with the server's hostname or instance to build the principal for the server. The server's principal will be of the form service/hostname@REALM.

On the client side, an additional argument is required to specify the hostname of the server, but on the server side, the hostname of the machine on which the program is running will be used.

version

Version string that is generally used to indicate a version of the protocol that the client and server will speak to each other. It does not have anything to do with the Kerberos protocol or the version of Kerberos in use. The string may be anything you want, but both the client and server must agree on the same string for authentication to succeed.

If authentication is successful, the return value from spc_krb5_server( ) will be 0, and the relevant fields in the spc_krb5bundle_t object will be filled in. If any kind of error occurs while attempting to authenticate with the server, the return value from spc_krb5_server( ) will be the error code returned by the Kerberos API function that failed.

krb5_error_code spc_krb5_server(int sockfd, spc_krb5bundle_t *bundle,

char *service, char *version) {

int free_context = 0;

krb5_principal server = 0;

krb5_error_code rc;

if (!bundle->ctx) {

if ((rc = krb5_init_context(&(bundle->ctx))) != 0) goto error;

free_context = 1;

}

if ((rc = krb5_sname_to_principal(bundle->ctx, 0, service,

KRB5_NT_SRV_HST, &server)) != 0) goto error;

rc = krb5_recvauth(bundle->ctx, &(bundle->auth_ctx), &sockfd, version,

server, 0, 0, &(bundle->ticket));

if (!rc) {

krb5_free_principal(bundle->ctx, server);

return 0;

}

error:

if (server) krb5_free_principal(bundle->ctx, server);

if (bundle->ctx && free_context) {

krb5_free_context(bundle->ctx);

bundle->ctx = 0;

}

return rc;

}

When a successful authentication is completed, an spc_krb5bundle_t object is filled with information resulting from the authentication. This information should eventually be cleaned up, of course. You may safely keep the information around as long as you need it, or you may clean it up at any time. If, once the authentication is complete, you don't need to retain any of the resulting information for further communication, you may even clean it up immediately.

Call the function spc_krb5_cleanup( ) when you no longer need any of the information contained in an spc_krb5bundle_t object. It will free all of the allocated resources in the proper order.

void spc_krb5_cleanup(spc_krb5bundle_t *bundle) {

if (bundle->ticket) {

krb5_free_ticket(bundle->ctx, bundle->ticket);

bundle->ticket = 0;

}

if (bundle->auth_ctx) {

krb5_auth_con_free(bundle->ctx, bundle->auth_ctx);

bundle->auth_ctx = 0;

}

if (bundle->ctx) {

krb5_free_context(bundle->ctx);

bundle->ctx = 0;

}

}

See Also

Recipe 9.6

8.14. Authenticating with HTTP Cookies

Problem

You are developing a CGI application for the Web and need to store data on the client's machine using a cookie, but you want to prevent the client from viewing the data or modifying it without your application being able to detect the change.

Solution

Web cookies are implemented by setting a value in the MIME headers sent to the client in a server response. If the client accepts the cookie, it will present the cookie back to the server every time the specified conditions are met. The cookie is stored on the client's computer, typically in a plaintext file that can be modified with any editor. Many browsers even provide an interface for viewing and editing cookies that have been stored.

A single MIME header is a header name followed by a colon, a space, and the header value. The format of the header value depends on the header name. Here, we're concerned with only two headers: the Set-Cookie header, which can be sent to the client when presenting a web page, and theCookie header, which the client presents to the server when the user browses to a site which stores a cookie.

To ensure the integrity of the data that we store on the client's computer with our cookie, we should encrypt and MAC the data. The server does encoding when setting a cookie, then decrypts and validates whenever the cookie comes back. The server does not share its keys with any other entity—it alone uses them to ensure that the data has not been read or modified since it originally left the server.

Discussion

When encrypting and MAC'ing the data stored in a cookie, we encounter a problem: we can use only a limited character set in cookie headers, yet the output of our cryptographic algorithms is always binary. To solve this problem, we encode the binary data into the base64 character set. The base64 character set uses the uppercase letters, the lowercase letters, the numbers, and a few pieces of punctuation to represent data. Out of necessity, the length of data grows considerably when base64-encoded. We can use the spc_base64_encode( ) function from Recipe 4.5 for base64 encoding to suit our purposes.

The first thing that the server must do is call spc_cookie_init( ) , which will initialize a context object that we'll use for both encoding and decoding cookie data. To simplify the encryption and MAC'ing process, as well as reduce the complexity of sending and processing received cookies, we'll use CWC mode from Recipe 5.10.

Initialization requires a key to use for encrypting and MAC'ing the data in cookies. The implementation of CWC described in Recipe 5.10 can use keys that are 128, 192, or 256 bits in size. Before calling spc_cookie_init( ), you should create a key using spc_rand( ), as defined in Recipe 11.2. If the cookies you are sending to the client are persistent, you should store the key on the server so that the same key is always used, rather than generating a new one every time the server starts up. You can either hardcode the key into your program or store it in a file somewhere that is inaccessible through the web server so that you are sure it cannot be compromised.

#include <stdlib.h>

#include <string.h>

#include <cwc.h>

static cwc_t spc_cookie_cwc;

static unsigned char spc_cookie_nonce[11];

int spc_cookie_init(unsigned char *key, size_t keylen) {

memset(spc_cookie_nonce, 0, sizeof(spc_cookie_nonce));

return cwc_init(&spc_cookie_cwc, key, keylen * 8);

}

To encrypt and MAC the data to send in a cookie, use the following spc_cookie_encode( ) function, which requires two arguments:

cookie

Data to be encrypted and MAC'd. spc_cookie_encode( ) expects the data to be a C-style string, which means that it should not contain binary data and should be NULL terminated.

nonce

11-byte buffer that contains the nonce to use (see Recipe 4.9 for a discussion of nonces). If you specify this argument as NULL, a default buffer that contains all NULL bytes will be used for the nonce.

The problem with using a nonce with cookies is that the same nonce must be used for decrypting and verifying the integrity of the data received from the client. To be able to do this, you need a second plaintext cookie that allows you to recover the nonce before decrypting and verifying the encrypted cookie data. Typically, this would be the user's name, and the server would maintain a list of nonces that it has encoded for each logged-in user.

WARNING

If you do not use a nonce, your system will be susceptible to capture replay attacks. It is worth expending the effort to use a nonce.

The return from spc_cookie_encode( ) will be a dynamically allocated buffer that contains the base64-encoded ciphertext and MAC of the data passed into it. You are responsible for freeing the memory by calling free( ).

char *spc_cookie_encode(char *cookie, unsigned char *nonce) {

size_t cookielen;

unsigned char *out;

cookielen = strlen(cookie);

if (!(out = (unsigned char *)malloc(cookielen + 16))) return 0;

if (!nonce) nonce = spc_cookie_nonce;

cwc_encrypt_message(&spc_cookie_cwc, 0, 0, cookie, cookielen, nonce, out);

cookie = spc_base64_encode(out, cookielen + 16, 0);

free(out);

return cookie;

}

When the cookies are received by the server from the client, you can pass the encrypted and MAC'd data to spc_cookie_decode( ) , which will decrypt the data and verify its integrity. If there is any error, spc_cookie_decode( ) will return NULL; otherwise, it will return the decrypted data in a dynamically allocated buffer that you are responsible for freeing with free( ).

char *spc_cookie_decode(char *data, unsigned char *nonce) {

int error;

char *out;

size_t cookielen;

unsigned char *cookie;

if (!(cookie = spc_base64_decode(data, &cookielen, 1, &error))) return 0;

if (!(out = (char *)malloc(cookielen - 16 + 1))) {

free(cookie);

return 0;

}

if (!nonce) nonce = spc_cookie_nonce;

error = !cwc_decrypt_message(&spc_cookie_cwc, 0, 0, cookie, cookielen,

nonce, out);

free(cookie);

if (error) {

free(out);

return 0;

}

out[cookielen - 16] = 0;

return out;

}

See Also

Recipe 4.5, Recipe 4.6, Recipe 4.9, Recipe 5.10, Recipe 11.2

8.15. Performing Password-Based Authentication and Key Exchange

Problem

You want to establish a secure channel without using public key cryptography at all. You want to avoid tunneling a traditional authentication protocol over a protocol like SSL, instead preferring to build your own secure channel with a good protocol.

Solution

SAX (Symmetric Authenticated eXchange) is a protocol for creating a secure channel that does not use public key cryptography.

PAX (Public key Authenticated eXchange) is similar to SAX, but it uses public key cryptography to prevent against client spoofing if the attacker manages to get the server-side authentication database. The public key cryptography also makes PAX a bit slower.

Discussion

The SAX and PAX protocols both perform authentication and key exchange. The protocols are generic, so they work in any environment. However, in this recipe we'll show you how to use SAX and PAX in the context of the Authenticated eXchange (AX) library, available fromhttp://www.zork.org/ax/. This library implements SAX and PAX over TCP/IP using a single API.

Let's take a look at how these protocols are supposed to work from the user's point of view. The server needs to have authentication information associated with the user. The account setup must be done over a preexisting secure channel. Perhaps the user sits down at a console, or the system administrator might do the setup on behalf of the user while they are talking over the phone.

Account setup requires the user's password for that server. The password is used to compute some secret information stored on the server; then the actual password is thrown away.

At account creation time, the server picks a salt value that is used to thwart a number of attacks. The server can choose to do one of two things with this salt:

§ Tell it to the user, and have the user type it in the first time she logs in from any new machine (the machine can then cache the salt value for subsequent connections). This solution prevents attackers from learning anything significant by guessing a password, because the attacker has to guess the salt as well. The salt effectively becomes part of the password.

§ Let the salt be public, in which case the attacker can try out passwords by attempting to authenticate with the server.

The server

The first thing the server needs to be able to do is create accounts for users. User credential information is stored in objects of type AX_CRED. To compute credentials, use the following function:

void AX_compute_credentials(char *user, size_t ulen, char *pass, size_t plen,

size_t ic, size_t pksz, size_t minkl, size_t maxkl,

size_t public_salt, size_t saltlen, AX_CRED *out);

This function has the following arguments:

user

Arbitrary binary string representing the unique login ID of the user.

ulen

Length of the username.

pass

The password, an arbitrary binary string.

plen

Length of the password in bytes.

ic

Iteration count to be used in the internal secret derivation function. See Recipe 4.10 for recommendations on setting this value (AX uses the derivation function from that recipe).

pksz

Determines whether PAX credentials or SAX credentials should be computed. If you are using PAX, the value specifies the length of the modulus of the public key in bits, which must be 1,024, 2,048, 4,096, or 8,192. If you are using SAX, set this value to 0.

minkl

Minimum key length we will allow the client to request when doing an exchange, in bytes. We recommend 16 bytes (128 bits).

maxkl

Maximum key length we will allow the client to request when doing an exchange, in bytes. Often, the protocol you use will only want a single fixed-size key (and not give the client the option to choose), in which case, this should be the same value as minkl.

public_salt

If this is nonzero, the server will give out the user's salt value when requested. Otherwise, the server should print out the salt at account creation time and have the user enter it on first login from a new client machine.

salt_len

Length of the salt that will be used. The salt value is not actually entirely random. Three bytes of the salt are used to encode the iteration count and the public key size. The rest of it is random. We recommend that, if the salt is public, you use 16-byte salts. If the salt is kept private, you will not want to make them too large, because you will have to convert them into a printable format that the user has to carry around and enter. The minimum size AX allows is 11 bytes, which base64-encodes to 15 characters.

out

Pointer to a container into which credentials will be placed. You are expected to allocate this object.

AX provides an API for serializing and deserializing credential objects:

char *AX_CRED_serialize(AX_CRED *c, size_t *outlen);

AX_CRED *AX_CRED_deserialize(char *buf, size_t buflen);

These two functions each allocate their result with malloc( ) and return 0 on error.

In addition, if the salt value is to stay private, you will need to retrieve it so that you can encode it and show it to the user. AX provides the following function for doing that:

char *AX_get_salt(AX_CRED *creds, size_t *saltlen);

The result is allocated by malloc( ). The size of the salt is placed into the memory pointed to by the second argument.

Now that we can set up account information and store credentials in a database, we can look at how to actually set up a server to handle connections. The high-level AX API does most of the work for you. There's an actual server abstraction, which is of type AX_SRV.

You do need to define at least one callback, two if you want to log errors. In the first callback, you must return a credential object for the associated user. The callback should be a pointer to a function with the following signature:

AX_CRED *AX_get_credentials_callback(AX_SRV *s, char *user, size_t ulen,

char *extra, size_t elen);

This function has the following arguments:

s

Pointer to the server object. If you have multiple servers in a single program, you can use this pointer to determine which server produced the request.

user

Username given to the server.

ulen

Length of the username.

extra

Additional application-specific information the client passed to the server. You can use this for whatever purpose you want. For example, you could use this field to encode the server name the client thinks it's connecting to, in order to implement virtual servers.

elen

Length of the application-specific data.

If the user does not exist, you must return 0 from this callback.

The other callback allows you to log errors when a key exchange fails. You do not have to define this callback. If you do define it, the signature is the same as in the previous callback, except that it takes an extra parameter of type size_t that encodes the error, and it does not return anything. As of this writing, there are only two error conditions that might get reported:

AX_SOCK_ERR

Indicates that a generic socket error occurred. You can use your platform's standard API to retrieve more specific information.

AX_CAUTH_ERR

Indicates that the server was unable to authenticate the client.

The first error can represent a large number of failures. In most cases, the connection will close unexpectedly, which can indicate many things, including loss of connectivity or even the client's failing to authenticate the server.

To initialize a server, we use the following function:

AX_SRV *AX_srv_listen(char *if, unsigned short port, size_t protocol,

AX_get_creds_cb cf, AX_exchange_status_cb sf);

This function has the following arguments:

if

String indicating the interface on which to bind. If you want to bind on all interfaces a machine has, use "0.0.0.0".

port

Port on which to bind.

protocol

Indication of which protocol you're using. As of this writing, the only valid values are SAX_PROTOCOL_v1 and PAX_PROTOCOL_v1.

cf

callback for retrieving credentials discussed above.

sf

Callback for error reporting discussed above. Set this to NULL if you don't need it.

This function returns a pointer to an object of type AX_SRV. If there's an error, an exception is thrown using the XXL exception-handling API (discussed in Recipe 13.1). All possible exceptions are standard POSIX error codes that would indicate some sort of failure when calling the underlying socket API.

To close down the server and deallocate associated memory, pass the object to AX_srv_close( ).

Once we have a server object, we need to wait for a connection to come in. Once a connection comes in, we can tell the server to perform a key exchange with that connection. To wait for a connection to come in, use the following function (which will always block):

AX_CLIENT *AX_srv_accept(AX_SRV *s);

This function returns a pointer to an AX_CLIENT object when there is a connection. Again, if there's an error, an exception gets thrown, indicating an error caught by the underlying socket API.

At this point, you should launch a new thread or process to deal with the connection, to prevent an attacker from launching a denial of service by stalling the key exchange.

Once we have received a client object, we can perform a key exchange with the following function:

int AX_srv_exchange(AX_CLIENT *c, char *key, size_t *kl, char *uname, size_t *ul,

char *x, size_t *xl);

This function has the following arguments:

c

Pointer to the client object returned by AX_srv_accept( ). This object will be deallocated automatically during the call.

key

Agreed-upon key.

kl

Pointer into which the length of the agreed-upon key in bytes is placed.

uname

Pointer to memory allocated by malloc( ) that stores the username of the entity on the other side. You are responsible for freeing this memory with free( ).

ul

Pointer into which the length of the username in bytes is placed.

x

Pointer to dynamically allocated memory representing application-specific data. The memory is allocated with malloc( ), and you are responsible for deallocating this memory as well.

xl

Pointer into which the length of the application-specific data is placed.

On success, AX_srv_exchange( ) will return a connected socket descriptor in blocking mode that you can then use to talk to the client. On failure, an XXL exception will be raised. The value of the exception will be either AX_CAUTH_ERR if we believe the client refused our credentials orAX_SAUTH_ERR if we refused the client's credentials. In both cases, it is possible that an attacker's tampering with the data stream caused the error. On the other hand, it could be that the two parties could not agree on the protocol version or key size.

With a valid socket descriptor in hand, you can now use the exchanged key to set up a secure channel, as discussed in Recipe 9.12. When you are finished communicating, you may simply close the socket descriptor.

Note that whether or not the exchange with the client succeeds, AX_srv_exchange( ) will free the AC_CLIENT object passed into it. If the exchange fails, the socket descriptor will be closed, and the client will have to reconnect in order to attempt another exchange.

The client

The client side is a bit less work. We first connect to the server with the following function:

AX *AX_connect(char *addr, unsigned short port, char *uname, size_t ulen,

char *extra, size_t elen, size_t protocol);

This function has the following arguments:

addr

IP address (or DNS name) of the server as a NULL-terminated string.

port

Port to which we should connect on the remote machine.

uname

Username.

ulen

Length of the username in bytes.

extra

Application-specific data discussed above.

elen

Length of the application-specific data in bytes.

protocol

Indication of the protocol you're using to connect. As of this writing, the only valid values are SAX_PROTOCOL_v1 and PAX_PROTOCOL_v1.

This call will throw an XXL exception if there's a socket error. Otherwise, it will return an object dynamically allocated with malloc( ) that contains the key exchange state.

If the user is expected to know the salt (i.e., if the server will not send it over the network), you must enter it at this time, with the following function:

void AX_set_salt(AX *p, char *salt, size_t saltlen);

AX_set_salt( ) expects the binary encoding that the server-side API produced. It is your responsibility to make sure the user can enter this value. Note that this function copies a reference to the salt and does not copy the actual value, so do not modify the memory associated with your salt until the AX context is deallocated (which happens as a side effect of the key exchange process; see the following discussion).

Note that, the first time you make the user type in the salt on a particular client machine, you should save the salt to disk. We strongly recommend encrypting the salt with the user's supplied password, using an authenticated encryption mode and the key derivation function from Recipe 4.10.

Once the client knows the salt, it can initiate key exchange using the following function:

int AX_exchange(AX *p, char *pw, size_t pwlen, size_t keylen, char *key);

This function has the following arguments:

p

Pointer to the context object that represents the connection to the server.

pw

Password, treated as a binary string (i.e., not NULL-terminated).

pwlen

Length of the associated password in bytes.

keylen

Key length the client desires in the exchange. The server must be prepared to serve up keys of this length; otherwise, the exchange will fail.

key

Buffer into which the key will be placed if authentication and exchange are successful.

On success, AX_exchange( ) will return a connected socket descriptor in blocking mode that you can then use to talk to the server. On failure, an XXL exception will be raised. The value of the exception will be either AX_CAUTH_ERR if we believe the server refused our credentials or AX_SAUTH_ERRif we refused the server's credentials. In both cases, it is possible that an attacker's tampering with the data stream caused the error. On the other hand, it could be that the two parties could not agree on the protocol version or key size.

With a valid socket descriptor in hand, you can now use the exchanged key to set up a secure channel, as discussed in Recipe 9.12. When you are finished communicating, you may simply close the socket descriptor.

Whether or not the connection succeeds, AX_exchange( ) automatically deallocates the AX object passed into it. If the exchange does fail, the connection to the server will need to be reestablished by calling AX_connect( ) a second time.

See Also

§ AX home page: http://www.zork.org/ax/

§ Recipe 4.10, Recipe 9.12, Recipe 13.1

8.16. Performing Authenticated Key Exchange Using RSA

Problem

Two parties in a network communication want to communicate using symmetric encryption. At least one party has the RSA public key of the other, which was either transferred in a secure manner or will be validated by a trusted third party.

You want to do authentication and key exchange without any of the information leakage generally associated with password-based protocols.

Solution

Depending on your authentication requirements, you can do one-way authenticating key transport, two-way authenticating key transport, or two-way authenticating key agreement.

Discussion

WARNING

Instead of using this recipe to build your own key establishment protocols, it is much better to use a preexisting network protocol such as SSL/TLS (see Recipe 9.1 and Recipe 9.2) or to use PAX (Recipe 8.15) alongside the secure channel code from Recipe 9.12.

With key transport, one entity in a system chooses a key and sends it to the entity with which it wishes to communicate, generally by encrypting it with the RSA public key of that entity.

In such a scenario, the sender has to have some way to ensure that it really does have the public key of the entity with which it wants to communicate. It can do this either by using a trusted third party (see Chapter 10) or by arranging to transport the public key in a secure manner, such as on a CD-R.

If the recipient can send a message back to the sender using the session key, and that message decrypts correctly, the sender can be sure that an entity possessing the correct private key has received the session key. That is, the sender has authenticated the receiver, as long as the receiver's public key is actually correct.

Such a protocol can be modified so that both parties can authenticate each other. In such a scheme, the sender generates a secret key, then securely signs and encrypts the key.

WARNING

It is generally insecure to sign the unencrypted value and encrypt that, particularly in a public key-based system. In such a system, it is not even a good idea to sign encrypted values. There are several possible solutions to this issue, discussed in detail in Recipe 7.14. For now, we are assuming that you will be using one of the techniques in that recipe.

Assuming that the recipient has some way to receive and validate the sender's public key, the recipient can now validate the sender as well.

The major limitation of key transport is that the machine initiating a connection may have a weak source of entropy, leading to an insecure connection. Instead, you could build a key agreement protocol, where each party sends the other a significant chunk of entropy and derives a shared secret from the information. For example, you might use the following protocol:

1. The client picks a random 128-bit secret.

2. The client uses a secure technique to sign the secret and encrypt it with the server's public key. (See Recipe 7.14 for how to do this securely.)

3. The client sends the signed, encrypted key to the server.

4. The server decrypts the client's secret.

5. The server checks the client's signature, and fails if the client isn't authenticated. (The server must already have a valid public key for the client.)

6. The server picks a random 128-bit secret.

7. The server uses a secure technique to sign the secret and encrypt it with the client's public key (again, see Recipe 7.14).

8. The server sends its signed, encrypted secret to the client.

9. The client decrypts the server's secret.

10.The client checks the server's signature, and fails if the server isn't authenticated. (The client must already have a valid public key for the server.)

11.The client and the server compute a master secret by concatenating the client secret and the server secret, then hashing that with SHA1, truncating the result to 128 bits.

12.Both the client and the server generate derived keys for encryption and MAC'ing, as necessary.

13.The client and the server communicate using their new agreed-upon keys.

Incorporating either key transport or key exchange into a protocol that involves algorithm negotiation is more complex. In particular, after keys are finally agreed upon, the client must MAC all the messages received, then send that MAC to the server. The server must reconstruct the messages the client received and validate the MAC. The server must then MAC the messages it received (including the client's MAC), and the client must validate that MAC.

This MAC'ing is necessary to ensure that an attacker doesn't maliciously modify negotiation messages before full encryption starts. For example, consider a protocol where the server tells the client which encryption algorithms it supports, and the client chooses one from the list that it also supports. An attacker might intercept the server's list and instead send only the subset of algorithms the attacker knows how to break, forcing the client to select an insecure algorithm. Without the MAC'ing, neither side would detect the modification of the server's message.

WARNING

The client's public key is a weak point. If it gets stolen, other people can impersonate the user. You should generally use PKCS #5 to derive a key from a password (as shown in Recipe 4.10), then encrypt the public key (e.g., using AES in CWC mode, as discussed in Recipe 5.10).

The SSL/TLS protocol handles all of the above concerns for you. It provides either one-way or two-way authenticating key exchange. (Note that in one-way, the server does not authenticate the client using public key cryptography, if at all.) It is usually much better to use that protocol than to create your own, particularly if you're not going to hardcode a single set of algorithms.

If you do not want to use a PKI, but would still like an easy off-the-shelf construction, combine PAX (Recipe 8.15) with the secure channel from Recipe 9.12.

See Also

Recipe 4.10, Recipe 5.10, Recipe 7.14, Recipe 8.15, Recipe 9.1, Recipe 9.2, Recipe 9.12

8.17. Using Basic Diffie-Hellman Key Agreement

Problem

You want a client and a server to agree on a shared secret such as an encryption key, and you need or want to use the Diffie-Hellman key exchange protocol.

Solution

Your cryptographic library should have an implementation of Diffie-Hellman. If it does not, be aware that Diffie-Hellman is easy to implement on top of any arbitrary precision math library. You will need to choose parameters in advance, as we describe in the following Section 8.17.3.

Once you have a shared Diffie-Hellman secret, use a key derivation function to derive an actual secret for use in other cryptographic operations. (See Recipe 4.11.)

Discussion

Diffie-Hellman is a very simple way for two entities to agree on a key without an eavesdropper's being able to determine the key. However, room remains for a man-in-the-middle attack. Instead of determining the shared key, the attacker puts himself in the middle, performing key agreement with the client as if he were the server, and performing key agreement with the server as if he were the client. That is, when you're doing basic Diffie-Hellman, you don't know who you're exchanging keys with; you just know that no one else has calculated the agreed-upon key by snooping the network. (See Recipe 7.1 for more information about such attacks.)

WARNING

To solve the man-in-the-middle problem, you generally need to introduce some sort of public key authentication mechanism. With Diffie-Hellman, it is common to use DSA (see Recipe 7.15 and Recipe 8.18).

Basic Diffie-Hellman key agreement is detailed in PKCS (Public Key Cryptography Standard) #3.[3] It's a much simpler standard than the RSA standard, in part because there is no authentication mechanism to discuss.

The first thing to do with Diffie-Hellman is to come up with a Diffie-Hellman modulus n that is shared by all entities in your system. This parameter should be a large prime number, at least 1,024 bits in length (see the considerations in Recipe 8.17). The prime can be generated using Recipe 7.5, with the additional stipulation that you should throw away any value where (n - 1)/2 is not also prime.

WARNING

Some people like to use a fixed modulus shared across all users. We don't recommend that approach, but if you insist on using it, be sure to read RFCs 2631 and 2785.

Diffie-Hellman requires another parameter g, the "generator," which is a value that we'll be exponentiating. For ease of computation, use either 2 or 5.[4] Note that not every {prime, generator} pair will work, and you will need to test the generator to make sure that it has the mathematical properties that Diffie-Hellman requires.

OpenSSL expects that 2 or 5 will be used as a generator. To select a prime for the modulus, you can use the function DH_generate_parameters( ) , which has the following signature:

DH *DH_generate_parameters(int prime_len, int g,

void (*callback)(int, int, void *), void *cb_arg);

This function has the following arguments:

prime_len

Size in bits of the prime number for the modulus (n) to be generated.

g

Generator you want to use. It should be either 2 or 5.

callback

Pointer to a callback function that is passed directly to BN_generate_prime( ), as discussed in Recipe 7.4. It may be specified as NULL, in which case no progress will be reported.

cb_arg

Application-specific argument that is passed directly to the callback function, if one is specified.

The result will be a new DH object containing the generated modulus (n) and generator (g) parameters. When you're done with the DH object, free it with the function DH_free( ).

Once parameters are generated, you need to check to make sure the prime and the generator will work together properly. In OpenSSL, you can do this with DH_check( ) :

int *DH_check(DH *ctx, int *err);

This function has the following arguments:

ctx

Pointer to the Diffie-Hellman context object to check.

err

Pointer to an integer to which is written an indication of any error that occurs.

This function returns 1 even if the parameters are bad. The 0 return value indicates that the generator is not 2 or 5, as OpenSSL is not capable of checking parameter sets that include other generators. Any error is always passed through the err parameter. The errors are as follows:

H_CHECK_P_NOT_SAFE_PRIME

DH_NOT_SUITABLE_GENERATOR

DH_UNABLE_TO_CHECK_GENERATOR

The first two errors can occur at the same time, in which case the value pointed to by err will be the logical OR of both constants.

Once both sides have the same parameters, they can send each other a message; each then computes the shared secret. If the client initiates the connection, the client chooses a random value x, where x is less than n. The client computes A = gx mod n, then sends A to the server. The server chooses a random value y, where y is less than n. The server computes B = g y mod n, then sends B to the client.

The server calculates the shared secret by computing k = Ay mod n. The client calculates the same secret by computing Bx mod n.

Generating the message to send with OpenSSL is done with a call to the function DH_generate_key( ) :

int DH_generate_key(DH *ctx);

The function returns 1 on success. The value to send to the other party is stored in ctx->pub_key.

Once one side receives the public value from the other, it can generate the shared secret with the function DH_compute_key( ) :

int DH_compute_key(unsigned char *secret, BIGNUM *pub_value, DH *dh);

This function has the following arguments:

secret

Buffer into which the resulting secret will be written, which must be large enough to hold the secret. The size of the secret can be determined with a call to DH_size(dh).

pub_value

Public value received from the other party.

dh

DH object containing the parameters and public key.

Once both sides have agreed on a secret, it generally needs to be turned into some sort of fixed-size key, or a set of fixed-size keys. A reasonable way is to represent the secret in binary and cryptographically hash the binary value, truncating if necessary. Often, you'll want to generate a set of keys, such as an encryption key and a MAC key. (See Recipe 4.11 for a complete discussion of key derivation.)

WARNING

Key exchange with Diffie-Hellman isn't secure unless you have some secure way of authenticating the other end. Generally, you should digitally sign messages in this protocol with DSA or RSA, and be sure that both sides securely authenticate the signature—for example, through a public key infrastructure.

Once a key or keys are established, the two parties try to communicate. If both sides are using message integrity checks, they'll quickly know whether or not the exchange was successful (if it's not, nothing will validate on decryption).

If you don't want to use an existing API, here's an example of generating a random secret and computing the value to send to the other party (we use the OpenSSL arbitrary precision math library):

#include <openssl/bn.h>

typedef struct {

BIGNUM *n;

BIGNUM *g; /* use a BIGNUM even though g is usually small. */

BIGNUM *private_value;

BIGNUM *public_value;

} DH_CTX;

/* This function assumes that all BIGNUMs are already allocated, and that n and g

* have already been chosen and properly initialized. After this function

* completes successfully, use BN_bn2bin( ) on ctx->public_value to get a binary

* representation you can send over a network. See Recipe 7.4 for more info on

* BN<->binary conversions.

*/

int DH_generate_keys(DH_CTX *ctx) {

BN_CTX *tmp_ctx;

if (!(tmp_ctx = BN_CTX_new( ))) return 0;

if (!BN_rand_range(ctx->private_value, ctx->n)) {

BN_CTX_free(tmp_ctx);

return 0;

}

if (!BN_mod_exp(ctx->public_value, ctx->g, ctx->private_value, ctx->n, tmp_ctx)) {

BN_CTX_free(tmp_ctx);

return 0;

}

BN_CTX_free(tmp_ctx);

return 1;

}

When one side receives the Diffie-Hellman message from the other, it can compute the shared secret from the DH_CTX object and the message as follows:

BIGNUM *DH_compute_secret(DH_CTX *ctx, BIGNUM *received) {

BIGNUM *secret;

BN_CTX *tmp_ctx;

if (!(secret = BN_new( ))) return 0;

if (!(tmp_ctx = BN_CTX_new( ))) {

BN_free(secret);

return 0;

}

if (!BN_mod_exp(secret, received, ctx->private_value, ctx->n, tmp_ctx)) {

BN_CTX_free(tmp_ctx);

BN_free(secret);

return 0;

}

BN_CTX_free(tmp_ctx);

return secret;

}

You can turn the shared secret into a key by converting the BIGNUM object returned by DH_compute_secret( ) to binary (see Recipe 7.4) and then hashing it with SHA1, as discussed above.

Traditional Diffie-Hellman is sometimes called ephemeral Diffie-Hellman , because the algorithm can be seen as generating key pairs for one-time use. There are variants of Diffie-Hellman that always use the same values for each client. There are some hidden "gotchas" when doing that, so we don't particularly recommend it. However, if you wish to explore it, see RFC 2631 and RFC 2785 for more information.

See Also

§ RFC 2631: Diffie-Hellman Key Agreement Method

§ RFC 2785: Methods for Avoiding the "Small-Subgroup" Attacks on the Diffie-Hellman Key Agreement Method for S/MIME

§ Recipe 4.11, Recipe 7.1, Recipe 7.4, Recipe 7.5, Recipe 7.15, Recipe 8.17, Recipe 8.18


[3] See http://www.rsasecurity.com/rsalabs/pkcs/pkcs-3/.

[4] It's possible (but not recommended) to use a nonprime value for n, in which case you need to compute a suitable value for g. See the Applied Cryptography for an algorithm.

8.18. Using Diffie-Hellman and DSA Together

Problem

You want to use Diffie-Hellman for key exchange, and you need some secure way to authenticate the key agreement to protect against a man-in-the-middle attack.

Solution

Use the station-to-station protocol for two-way authentication. A simple modification provides one-way authentication. For example, the server may not care to authenticate the client using public key cryptography.

Discussion

WARNING

Remember, authentication requires a trusted third party or a secure channel for exchange of public DSA keys. If you'd prefer a password-based protocol that can achieve all the same properties you would get from Diffie-Hellman and DSA, see the discussion of PAX in Recipe 8.15.

Given a client initiating a connection with a server, the station-to-station protocol is as follows:

1. The client generates a random Diffie-Hellman secret x and the corresponding public value A.

2. The client sends A to the server.

3. The server generates a random Diffie-Hellman secret y and the corresponding public value B.

4. The server computes the Diffie-Hellman shared secret.

5. The server signs a string consisting of the public values A and B with the server's private DSA key.

6. The server sends B and the signature to the client.

7. The client computes the shared secret.

8. The client validates the signature, failing if it isn't valid.

9. The client signs A concatenated with B using its private DSA key, and it encrypts the result using the shared secret (the secret can be postprocessed first, as long as both sides do the same processing).

10.The client sends the encrypted signature to the server.

11.The server decrypts the signature and validates it.

The station-to-station protocol works only if your Diffie-Hellman keys are always one-time values. If you need a protocol that doesn't expose the private values of each party, use Recipe 8.16. That basic protocol can be adapted from RSA to Diffie-Hellman with DSA if you so desire.

Unless you allow for anonymous connection establishment, the client needs to identify itself as part of this protocol. The client can send its public key (or a digital certificate containing the public key) at Step 2. The server should already have a record of the client based on its public key, or else it should fail. Alternatively, you can drop the client validation steps (9-11) and use a traditional login mechanism after the encrypted link is established.

WARNING

In many circumstances, the client won't have the server's public key in advance. In such a case, the server will often send a copy of its public key (or a digital certificate containing the public key) at Step 6. In this case, the client can't assume that the public signing key is valid; there's nothing to distinguish it from an attacker's public key! Therefore, the key needs to be validated using a trusted third party before the client trusts that the party on the other end is really the intended server. (We discuss this problem in Recipe 7.1 and Recipe 10.1.)

See Also

Recipe 7.1, Recipe 8.15, Recipe 8.16, Recipe 10.1

8.19. Minimizing the Window of Vulnerability When Authenticating Without a PKI

Problem

You have an application (typically a client) that is likely to receive from a server identifying information such as a certificate or key that may not necessarily be able to be automatically verified—for example, because there is no PKI.

Without a way to absolutely defend against man-in-the-middle attacks in an automated fashion, you want to do the best that you can, either by having the user manually do certificate validation or by limiting the window of vulnerability to the first connection.

Solution

Either provide the user with trusted certificate information over a secure channel and allow him to enter that information, or prompt the user the first time you see a certificate, and remember it for subsequent connections.

These solutions push the burden of authentication off onto the user.

Discussion

It is common for small organizations to host some kind of a server that is SSL-enabled without a certificate that has been issued by a third-party CA such as VeriSign. Most often, such an organization issues its own certificate using its own CA. A prime example would be an SSL-enabled POP3 or SMTP server. Unfortunately, when this is the case, your software needs to have some way of allowing the client to indicate that the certificate presented by the server is acceptable.

There are two basic ways to do this:

§ Provide the user with some way to add the CA's certificate to a list of trusted certificates. This is certainly a good idea, and any program that verifies certificates should support this capability.

§ Prompt the user, asking if the certificate is acceptable. If the user answers yes, the certificate should be remembered, and the user is never prompted again. This approach could conceivably be something of an automated way of performing the first solution. In this way, the user need not go looking for the certificate and add it manually. It is not necessarily the most secure of solutions, but for many applications, the risk is acceptable.

Prompting the user works for other things besides certificates. Public keys are a good example of another type of identifying information that works well; in fact, public keys are employed by many SSH clients. When connecting to an SSH server for the first time, many SSH clients present the user with the fingerprint of the server's key and ask whether to terminate the connection, remember the key for future connections, or allow it for use only this one time. Often, the key is associated with the server's IP address, so if the key is remembered and the same server ever presents a different key, the user is notified that the key has changed, and that there is some possibility that the server has been compromised.

Be aware that the security provided by this recipe is not as strong as that provided by using a PKI (described in Chapter 10). There still exists the possibility that an attacker might mount a man-in-the-middle attack, particularly if the client has never connected to the server before and has no record of the server's credentials. Even if the client has the server's credentials, and they do not match, the client may opt to continue anyway, thinking that perhaps the server has regenerated its certificate or public key. The most common scenario, though, is that the user will not understand the warnings presented and the implications of proceeding when a change in server credentials is detected.

All of the work required for this recipe is on the client side. First, some kind of store is required to remember the information that is being presented by the server. Typically, this would be some kind of file on disk. For this recipe, we are going to concentrate on certificates and keys.

For certificates, we will store the entire certificate in Privacy Enhanced Mail (PEM) format (see Recipe 7.17). We will put one certificate in one file, and name that file in such a manner that OpenSSL can use it in a directory lookup. This entails computing the hash of the certificate's subject name and using it for the filename. You will generally want to provide a verify callback function in an spc_x509store_t object (see Recipe 10.5) that will ask the user whether to accept the certificate if OpenSSL has failed to verify it. The user could be presented with an option to reject the certificate, accept it this once, or accept and remember it. In the latter case, we'll save the certificate in an spc_x509store_t object in the directory identified in the call to spc_x509store_setcapath( ) .

#include <stdio.h>

#include <string.h>

#include <unistd.h>

#include <openssl/ssl.h>

#include <openssl/x509.h>

char *spc_cert_filename(char *path, X509 *cert) {

int length;

char *filename;

length = strlen(path) + 11;

if (!(filename = (char *)malloc(length + 1))) return 0;

snprintf(filename, length + 1, "%s/%08lx.0", path, X509_subject_name_hash(cert));

return filename;

}

int spc_remember_cert(char *path, X509 *cert) {

int result;

char *filename;

FILE *fp;

if (!(filename = spc_cert_filename(path, cert))) return 0;

if (!(fp = fopen(filename, "w"))) {

free(filename);

return 0;

}

result = PEM_write_X509(fp, cert);

fclose(fp);

if (!result) remove(filename);

free(filename);

return result;

}

int spc_verifyandmaybesave_callback(int ok, X509_STORE_CTX *store) {

int err;

SSL *ssl_ptr;

char answer[80], name[256];

X509 *cert;

SSL_CTX *ctx;

spc_x509store_t *spc_store;

if (ok) return ok;

cert = X509_STORE_CTX_get_current_cert(store);

printf("An error has occurred with the following certificate:\n");

X509_NAME_oneline(X509_get_issuer_name(cert), name, sizeof(name));

printf(" Issuer Name: %s\n", name);

X509_NAME_oneline(X509_get_subject_name(cert), name, sizeof(name));

printf(" Subject Name: %s\n", name);

err = X509_STORE_CTX_get_error(store);

printf(" Error Reason: %s\n", X509_verify_cert_error_string(err));

for (;;) {

printf("Do you want to [r]eject this certificate, [a]ccept and remember it, "

"or allow\nits use for only this [o]ne time? ");

if (!fgets(answer, sizeof(answer), stdin)) continue;

if (answer[0] = = 'r' || answer[0] = = 'R') return 0;

if (answer[0] = = 'o' || answer[0] = = 'O') return 1;

if (answer[0] = = 'a' || answer[0] = = 'A') break;

}

ssl_ptr = (SSL *)X509_STORE_CTX_get_app_data(store);

ctx = SSL_get_SSL_CTX(ssl_ptr);

spc_store = (spc_x509store_t *)SSL_CTX_get_app_data(ctx);

if (!spc_store->capath || !spc_remember_cert(spc_store->capath, cert))

printf("Error remembering certificate! It will be accepted this one time "

"only.\n");

return 1;

}

For keys, we will store the base64-encoded key in a flat file, much as OpenSSH does. We will also associate the IP address of the server that presented the key so that we can determine when the server's key has changed and warn the user. When we receive a key that we'd like to check to see whether we already know about it, we can call spc_lookup_key( ) with the filename of the key store, the IP number we received the key from, and the key we've just received. If we do not know anything about the key or if some kind of error occurs, 0 is returned. If we know about the key, and everything matches—that is, the IP numbers and the keys are the same—1 is returned. If we have a key stored for the IP number and it does not match the key we have just received, -1 is returned.

WARNING

If you have multiple servers running on the same system, you need to make sure that they each keep separate caches so that the keys and IP numbers do not collide.

#include <ctype.h>

#include <stdio.h>

#include <string.h>

#include <openssl/evp.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <arpa/inet.h>

static int get_keydata(EVP_PKEY *key, char **keydata) {

BIO *b64 = 0, *bio = 0;

int keytype, length;

char *dummy;

*keydata = 0;

keytype = EVP_PKEY_type(key->type);

if (!(length = i2d_PublicKey(key, 0))) goto error_exit;

if (!(dummy = *keydata = (char *)malloc(length))) goto error_exit;

i2d_PublicKey(key, (unsigned char **)&dummy);

if (!(bio = BIO_new(BIO_s_mem( )))) goto error_exit;

if (!(b64 = BIO_new(BIO_f_base64( )))) goto error_exit;

BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

if (!(bio = BIO_push(b64, bio))) goto error_exit;

b64 = 0;

BIO_write(bio, *keydata, length);

free(*keydata); *keydata = 0;

if (!(length = BIO_get_mem_data(bio, &dummy))) goto error_exit;

if (!(*keydata = (char *)malloc(length + 1))) goto error_exit;

memcpy(*keydata, dummy, length);

(*keydata)[length - 1] = '\0';

return keytype;

error_exit:

if (b64) BIO_free_all(b64);

if (bio) BIO_free_all(bio);

if (*keydata) free(*keydata);

*keydata = 0;

return EVP_PKEY_NONE;

}

static int parse_line(char *line, char **ipnum, int *keytype, char **keydata) {

char *end, *p, *tmp;

/* we expect leading and trailing whitespace to be stripped already */

for (p = line; *p && !isspace(*p); p++);

if (!*p) return 0;

*ipnum = line;

for (*p++ = '\0'; *p && isspace(*p); p++);

for (tmp = p; *p && !isspace(*p); p++);

*keytype = (int)strtol(tmp, &end, 0);

if (*end && !isspace(*end)) return 0;

for (p = end; *p && isspace(*p); p++);

for (tmp = p; *p && !isspace(*p); p++);

if (*p) return 0;

*keydata = tmp;

return 1;

}

int spc_lookup_key(char *filename, char *ipnum, EVP_PKEY *key) {

int bufsize = 0, length, keytype, lineno = 0, result = 0, store_keytype;

char *buffer = 0, *keydata, *line, *store_ipnum, *store_keydata, tmp[1024];

FILE *fp = 0;

keytype = get_keydata(key, &keydata);

if (keytype = = EVP_PKEY_NONE || !keydata) goto end;

if (!(fp = fopen(filename, "r"))) goto end;

while (fgets(tmp, sizeof(tmp), fp)) {

length = strlen(tmp);

buffer = (char *)realloc(buffer, bufsize + length + 1);

memcpy(buffer + bufsize, tmp, length + 1);

bufsize += length;

if (buffer[bufsize - 1] != '\n') continue;

while (bufsize && (buffer[bufsize - 1] = = '\r' || buffer[bufsize - 1] = = '\n'))

bufsize--;

buffer[bufsize] = '\0';

bufsize = 0;

lineno++;

for (line = buffer; isspace(*line); line++);

for (length = strlen(line); length && isspace(line[length - 1]); length--);

line[length - 1] = '\0';

/* blank lines and lines beginning with # or ; are ignored */

if (!length || line[0] = = '#' || line[0] = = ';') continue;

if (!parse_line(line, &store_ipnum, &store_keytype, &store_keydata)) {

fprintf(stderr, "%s:%d: parse error\n", filename, lineno);

continue;

}

if (inet_addr(store_ipnum) != inet_addr(ipnum)) continue;

if (store_keytype != keytype || strcasecmp(store_keydata, keydata))

result = -1;

else result = 1;

break;

}

end:

if (buffer) free(buffer);

if (keydata) free(keydata);

if (fp) fclose(fp);

return result;

}

If spc_lookup_key( ) returns 0, indicating that we do not know anything about the key, the user should be prompted in much the same way we did for certificates. If the user elects to remember the key, the spc_remember_key( ) function will add the key information to the key store so that the next time spc_lookup_key( ) is called, it will be found.

int spc_remember_key(char *filename, char *ipnum, EVP_PKEY *key) {

int keytype, result = 0;

char *keydata;

FILE *fp = 0;

keytype = get_keydata(key, &keydata);

if (keytype = = EVP_PKEY_NONE || !keydata) goto end;

if (!(fp = fopen(filename, "a"))) goto end;

fprintf(fp, "%s %d %s\n", ipnum, keytype, keydata);

result = 1;

end:

if (keydata) free(keydata);

if (fp) fclose(fp);

return result;

}

int spc_accept_key(char *filename, char *ipnum, EVP_PKEY *key) {

int result;

char answer[80];

result = spc_lookup_key(filename, ipnum, key);

if (result = = 1) return 1;

if (result = = -1) {

for (;;) {

printf("FATAL ERROR! A different key has been received from the server "

"%s\nthan we have on record. Do you wish to continue? ", ipnum);

if (!fgets(answer, sizeof(answer), stdin)) continue;

if (answer[0] = = 'Y' || answer[0] = = 'y') return 1;

if (answer[0] = = 'N' || answer[0] = = 'n') return 0;

}

}

for (;;) {

printf("WARNING! The server %s has presented has presented a key for which "

"we have no\nprior knowledge. Do you want to [r]eject the key, "

"[a]ccept and remember it,\nor allow its use for only this [o]ne "

"time? ", ipnum);

if (!fgets(answer, sizeof(answer), stdin)) continue;

if (answer[0] = = 'r' || answer[0] = = 'R') return 0;

if (answer[0] = = 'o' || answer[0] = = 'O') return 1;

if (answer[0] = = 'a' || answer[0] = = 'A') break;

}

if (!spc_remember_key(filename, ipnum, key))

printf("Error remembering the key! It will be accepted this one time only "

"instead.\n");

return 1;

}

See Also

Recipe 7.17, Recipe 10.5

8.20. Providing Forward Secrecy in a Symmetric System

Problem

When using a series of (session) keys generated from a master secret, as described in the previous recipe, we want to limit the scope of a key compromise. That is, if a derived key is stolen, or even if the master key is stolen, we would like to ensure that no data encrypted by previous session keys can be read by attackers as a result of the compromise. If our system has such a property, it is said to have perfect forward secrecy .

Solution

Use a separate base secret for each entity in the system. For any given client, derive a new key called K1 from the base secret key, as described in Recipe 4.11. Then, after you're sure that communicating parties have correctly agreed upon a key, derive another key from K1 in the exact same manner, calling it K2. Erase the base secret (on both the client and the server), replacing it with K1. Use K2 as the session key.

Discussion

In Recipe 4.11, we commented on how knowledge of a properly created derived key would give no information about any parent keys. We can take advantage of that fact to ensure that previous sessions are not affected if throwing away the base secret somehow compromises the current key, so that old session keys cannot be regenerated. The security depends on the cryptographically strong one-way property of the hash function used to generate the derived keys.

TIP

Remember that when deriving keys, every key derivation needs to include some kind of unique value that is never repeated (see Recipe 4.11 for a detailed discussion).

See Also

Recipe 4.11

8.21. Ensuring Forward Secrecy in a Public Key System

Problem

In a system using public key cryptography, you want to ensure that a compromise of one of the entities in your system won't compromise old communications that took place with different session keys (symmetric keys).

Solution

When using RSA, generate new public keys for each key agreement, ensuring that the new key belongs to the right entity by checking the digital signature using a long-term public key. Alternatively, use Diffie-Hellman, being sure to generate new random numbers each time. Throw away all of the temporary material once key exchange is complete.

Discussion

WARNING

When discarding key material, be sure to zero it from memory, and use a secure deletion technique if the key may have been swapped to disk (See Recipe 13.2).

Suppose that you have a client and a server that communicate frequently, and they establish connections using a set of fixed RSA keys. Suppose that an attacker has been recording all data between the client and the server since the beginning of time. All of the key exchange messages and data encrypted with symmetric keys have been captured.

Now, suppose that the attacker eventually manages to break into the client and the server, stealing all the private keys in the system. Certainly, future communications are insecure, but what about communications before the break-in? In this scenario, the attacker would be able to decrypt all of the data ever sent by either party because all of the old messages used in key exchange can be decrypted with all of the public keys in the system.

The easiest way to fix this problem is to use static (long-term) key pairs for establishing identity (i.e., digital signatures), but use randomly generated, one-time-use key pairs for performing key exchange. This procedure is called ephemeral keying (and in the context of keying Diffie-Hellman it's called ephemeral Diffie-Hellman, which we discussed in Recipe 8.17). It doesn't have a negative impact on security because you can still establish identities by checking signatures that are generated by the static signing key. The upside is that as long as you throw away the temporary key pairs after use, the attacker won't be able to decrypt old key exchange messages, and thus all data for connections that completed before the compromise will be secure from the attacker.

TIP

The only reason not to use ephemeral keying with RSA is that key generation can be expensive.

The standard way of using Diffie-Hellman key exchange provides forward secrecy. With that protocol, the client and server both pick secret random numbers for each connection, and they send a public value derived from their secrets. The public values, intended for one-time use, are akin to public keys. Indeed, it is possible to reuse secrets in Diffie-Hellman, thus creating a permanent key pair. However, there is significant risk if this is done naïvely (see Recipe 8.17).

When using RSA, if you're doing one-way key transport, the client need not have a public key. Here's a protocol:

1. The client contacts the server, requesting a one-time public key.

2. The server generates a new RSA key pair and signs the public key with its long-term key. The server then sends the public key and the signature. If necessary, the server also sends the client its certificate for its long-term key.

3. The client validates the server's certificate, if appropriate.

4. The client checks the server's signature on the one-time public key to make sure it is valid.

5. The client chooses a random secret (the session key) and encrypts it using the one-time public key.

6. The encrypted secret is sent to the server.

7. The parties attempt to communicate using the session key.

8. The server securely erases the one-time private key.

9. When communication is complete, both parties securely erase the session key.

In two-way authentication, both parties generate one-time keys and sign them with their long-term private key.

See Also

Recipe 8.17, Recipe 13.2

8.22. Confirming Requests via Email

Problem

You want to allow users to confirm a request via email while preventing third parties from spoofing or falsifying confirmations.

Solution

Generate a random identifier, associate it with the email address to be confirmed, and save it for verification later. Send an email that contains the random identifier, along with instructions for responding to confirm receipt and approval. If a response is received, compare the identifier in the response with the saved identifier for the email address from which the response was received. If the identifiers don't match, ignore the response and do nothing; otherwise, the confirmation was successful.

Discussion

The most common use for confirmation requests is to ensure that an email address actually belongs to the person requesting membership on some kind of mass mailing list (whether it's a mailing list, newsletter, or some other type of mass mailing). Joining a mass mailing list typically involves either sending mail to an automated recipient or filling out a form on a web page.

The problem with this approach is that it is trivial for someone to register someone else's email address with a mailing list. For example, suppose that Alice wants to annoy Bob. If mailing lists accepted email addresses without any kind of confirmation, Alice could register Bob's email address with as many mailing lists as she could find. Suddenly, Bob would begin receiving large amounts of email from mailing lists with which he did not register. In extreme cases, this could lead to denial of service because Bob's mailbox could fill up with unwanted email, or if Bob has a slow network connection, it could take an unreasonable amount of time for him to download his email.

The solution to this problem is to confirm with Bob that he really made the requests for membership with the mailing lists. When a request for membership is sent for a mailing list, the mailing list software can send an email to the address for which membership was requested. This email will ask the recipient to respond with a confirmation that membership is truly desired.

The simplest form of such a confirmation request is to require the recipient to reply with an email containing some nonunique content, such as the word "subscribe" or something similar. This method is easiest for the mailing list software to deal with because it does not have to keep any information about what requests have been made or confirmed. It simply needs to respond to confirmation responses by adding the sender's email address to the mailing list roster.

Unfortunately, this is not an acceptable solution either, because Alice might know what response needs to be sent back to the confirmation request in order for the mailing list software to add Bob to its roster. If Alice knows what needs to be sent, she can easily forge a response email, making it appear to the mailing list software as if it came from Bob's email address.

Sending a confirmation request that requires an affirmative acknowledgement is a step in the right direction, but as we have just described it, it is not enough. Instead of requiring a nonunique acknowledgment, the confirmation request should contain a unique identifier that is generated at the time that the request for membership is made. To confirm the request, the recipient must send back a response that also contains the same unique identifier.

Because a unique identifier is used, it is not possible for Alice to know what she would need to send back to the mailing list software to get Bob's email address on the roster, unless she somehow had access to Bob's email. That would allow her to see the confirmation request and the unique identifier that it contains. Unfortunately, this is a much more difficult problem to solve, and it is one that cannot be easily solved in software, so we will not give it any further consideration.

To implement such a scheme, the mailing list software must maintain some state information. In particular, upon receipt of a request for membership, the software needs to generate the unique identifier to include in the confirmation requests, and it must store that identifier along with the email address for which membership has been requested. In addition, it is a good idea to maintain some kind of a timestamp so that confirmation requests will eventually expire. Expiring confirmation requests significantly reduces the likelihood that Alice can guess the unique identifier; more importantly, it also helps to reduce the amount of information that must be remembered to be able to confirm requests.

We define two functions in this recipe that provide the basic implementation for the confirmation request scheme we have just described. The first, spc_confirmation_create( ) , creates a new confirmation request by generating a unique identifier and storing it with the email address for which confirmation is to be requested. It stores the confirmation request information in an in-memory list of pending confirmations, implemented simply as a dynamically allocated array. For use in a production environment, a hash table or binary tree might be a better solution for an in-memory data structure. Alternatively, the information could be stored in a database.

The function spc_confirmation_create( ) (SpcConfirmationCreate() on Windows) will return 0 if some kind of error occurs. Possible errors include memory allocation failures and attempts to add an address to the list of pending confirmations that already exists in the list. If the operation is successful, the return value will be 1. Two arguments are required by spc_confirmation_create( ):

address

Email address that is to be confirmed.

id

Pointer to a buffer that will be allocated by spc_confirmation_create( ). If the function returns successfully, the buffer will contain the unique identifier to send as part of the confirmation request email. It is the responsibility of the caller to free the buffer using free( ) on Unix or LocalFree( ) on Windows.

You may adjust the SPC_CONFIRMATION_EXPIRE macro from the default presented here. It controls how long pending confirmation requests will be honored and is specified in seconds.

Note that the code we are presenting here does not send or receive email at all. Programmatically sending and receiving email is outside the scope of this book.

#include <stdlib.h>

#include <string.h>

#include <time.h>

/* Confirmation receipts must be received within one hour (3600 seconds) */

#define SPC_CONFIRMATION_EXPIRE 3600

typedef struct {

char *address;

char *id;

time_t expire;

} spc_confirmation_t;

static unsigned long confirmation_count, confirmation_size;

static spc_confirmation_t *confirmations;

static int new_confirmation(const char *address, const char *id) {

unsigned long i;

spc_confirmation_t *tmp;

/* first make sure that the address isn't already in the list */

for (i = 0; i < confirmation_count; i++)

if (!strcmp(confirmations[i].address, address)) return 0;

if (confirmation_count = = confirmation_size) {

tmp = (spc_confirmation_t *)realloc(confirmations,

sizeof(spc_confirmation_t) * (confirmation_size + 1));

if (!tmp) return 0;

confirmations = tmp;

confirmation_size++;

}

confirmations[confirmation_count].address = strdup(address);

confirmations[confirmation_count].id = strdup(id);

confirmations[confirmation_count].expire = time(0) + SPC_CONFIRMATION_EXPIRE;

if (!confirmations[confirmation_count].address ||

!confirmations[confirmation_count].id) {

if (confirmations[confirmation_count].address)

free(confirmations[confirmation_count].address);

if (confirmations[confirmation_count].id)

free(confirmations[confirmation_count].id);

return 0;

}

confirmation_count++;

return 1;

}

int spc_confirmation_create(const char *address, char **id) {

unsigned char buf[16];

if (!spc_rand(buf, sizeof(buf))) return 0;

if (!(*id = (char *)spc_base64_encode(buf, sizeof(buf), 0))) return 0;

if (!new_confirmation(address, *id)) {

free(*id);

return 0;

}

return 1;

}

Upon receipt of a response to a confirmation request, the address from which it was sent and the unique identified contained within it should be passed as arguments to spc_confirmation_receive( ) (SpcConfirmationReceive() on Windows). If the address and unique identifier are in the list of pending requests, the return from this function will be 1; otherwise, it will be 0. Before the list is checked, expired entries will automatically be removed.

int spc_confirmation_receive(const char *address, const char *id) {

time_t now;

unsigned long i;

/* Before we check the pending list of confirmations, prune the list to

* remove expired entries.

*/

now = time(0);

for (i = 0; i < confirmation_count; i++) {

if (confirmations[i].expire <= now) {

free(confirmations[i].address);

free(confirmations[i].id);

if (confirmation_count > 1 && i < confirmation_count - 1)

confirmations[i] = confirmations[confirmation_count - 1];

i--;

confirmation_count--;

}

}

for (i = 0; i < confirmation_count; i++) {

if (!strcmp(confirmations[i].address, address)) {

if (strcmp(confirmations[i].id, id) != 0) return 0;

free(confirmations[i].address);

free(confirmations[i].id);

if (confirmation_count > 1 && i < confirmation_count - 1)

confirmations[i] = confirmations[confirmation_count - 1];

confirmation_count--;

return 1;

}

}

return 0;

}

The Windows versions of spc_confirmation_create( ) and spc_confirmation_receive( ) are named SpcConfirmationCreate( ) and SpcConfirmationReceive( ), respectively. The arguments and return values for each are the same; however, there are enough subtle differences in the underlying implementation that we present an entirely separate code listing for Windows instead of using the preprocessor to have a single version.

#include <windows.h>

/* Confirmation receipts must be received within one hour (3600 seconds) */

#define SPC_CONFIRMATION_EXPIRE 3600

typedef struct {

LPTSTR lpszAddress;

LPSTR lpszID;

LARGE_INTEGER liExpire;

} SPC_CONFIRMATION;

static DWORD dwConfirmationCount, dwConfirmationSize;

static SPC_CONFIRMATION *pConfirmations;

static BOOL NewConfirmation(LPCTSTR lpszAddress, LPCSTR lpszID) {

DWORD dwIndex;

LARGE_INTEGER liExpire;

SPC_CONFIRMATION *pTemp;

/* first make sure that the address isn't already in the list */

for (dwIndex = 0; dwIndex < dwConfirmationCount; dwIndex++) {

if (CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,

pConfirmations[dwIndex].lpszAddress, -1,

lpszAddress, -1) = = CSTR_EQUAL) return FALSE;

}

if (dwConfirmationCount = = dwConfirmationSize) {

if (!pConfirmations)

pTemp = (SPC_CONFIRMATION *)LocalAlloc(LMEM_FIXED, sizeof(SPC_CONFIRMATION));

else

pTemp = (SPC_CONFIRMATION *)LocalReAlloc(pConfirmations,

sizeof(SPC_CONFIRMATION) * (dwConfirmationSize + 1), 0);

if (!pTemp) return FALSE;

pConfirmations = pTemp;

dwConfirmationSize++;

}

pConfirmations[dwConfirmationCount].lpszAddress = (LPTSTR)LocalAlloc(

LMEM_FIXED, sizeof(TCHAR) * (lstrlen(lpszAddress) + 1));

if (!pConfirmations[dwConfirmationCount].lpszAddress) return FALSE;

lstrcpy(pConfirmations[dwConfirmationCount].lpszAddress, lpszAddress);

pConfirmations[dwConfirmationCount].lpszID = (LPSTR)LocalAlloc(LMEM_FIXED,

lstrlenA(lpszID) + 1);

if (!pConfirmations[dwConfirmationCount].lpszID) {

LocalFree(pConfirmations[dwConfirmationCount].lpszAddress);

return FALSE;

}

lstrcpyA(pConfirmations[dwConfirmationCount].lpszID, lpszID);

/* File Times are 100-nanosecond intervals since January 1, 1601 */

GetSystemTimeAsFileTime((LPFILETIME)&liExpire);

liExpire.QuadPart += (SPC_CONFIRMATION_EXPIRE * (__int64)10000000);

pConfirmations[dwConfirmationCount].liExpire = liExpire;

dwConfirmationCount++;

return TRUE;

}

BOOL SpcConfirmationCreate(LPCTSTR lpszAddress, LPSTR *lpszID) {

BYTE pbBuffer[16];

if (!spc_rand(pbBuffer, sizeof(pbBuffer))) return FALSE;

if (!(*lpszID = (LPSTR)spc_base64_encode(pbBuffer, sizeof(pbBuffer), 0)))

return FALSE;

if (!NewConfirmation(lpszAddress, *lpszID)) {

LocalFree(*lpszID);

return FALSE;

}

return TRUE;

}

BOOL SpcConfirmationReceive(LPCTSTR lpszAddress, LPCSTR lpszID) {

DWORD dwIndex;

LARGE_INTEGER liNow;

/* Before we check the pending list of confirmations, prune the list to

* remove expired entries.

*/

GetSystemTimeAsFileTime((LPFILETIME)&liNow);

for (dwIndex = 0; dwIndex < dwConfirmationCount; dwIndex++) {

if (pConfirmations[dwIndex].liExpire.QuadPart <= liNow.QuadPart) {

LocalFree(pConfirmations[dwIndex].lpszAddress);

LocalFree(pConfirmations[dwIndex].lpszID);

if (dwConfirmationCount > 1 && dwIndex < dwConfirmationCount - 1)

pConfirmations[dwIndex] = pConfirmations[dwConfirmationCount - 1];

dwIndex--;

dwConfirmationCount--;

}

}

for (dwIndex = 0; dwIndex < dwConfirmationCount; dwIndex++) {

if (CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,

pConfirmations[dwIndex].lpszAddress, -1,

lpszAddress, -1) = = CSTR_EQUAL) {

if (lstrcmpA(pConfirmations[dwIndex].lpszID, lpszID) != 0) return FALSE;

LocalFree(pConfirmations[dwIndex].lpszAddress);

LocalFree(pConfirmations[dwIndex].lpszID);

if (dwConfirmationCount > 1 && dwIndex < dwConfirmationCount - 1)

pConfirmations[dwIndex] = pConfirmations[dwConfirmationCount - 1];

dwConfirmationCount--;

return TRUE;

}

}

return FALSE;

}

See Also

Recipe 11.2