Chapter 10. Advanced Programming Topics

We have explained quite a bit about using the OpenSSL library. Often, tasks such as certificate management are most easily accomplished with the command-line tool. For other tasks, such as SSL communications, we must flex our knowledge of the API. By this point it should be clear, though we have not explicitly stated it, that the command-line utilities all use various parts of the OpenSSL API, some of which we have not yet discussed in any detail.

In this chapter, we tackle some of the more advanced topics of programming with OpenSSL, including the programmatic interfaces to some features we’ve discussed only when using the command-line tool. In addition, we’ll cover the interface for reading program variables at runtime. Using the details in this chapter, we will investigate how OpenSSL provides for a variety of other tasks, such as creating S/MIME secure email, importing certificates into common web browsers, and hooking into certificates to access public key components for more primitive cryptographic functions.

Object Stacks

OpenSSL has a large number of macros for dealing with stacks of typed objects. The API can perform only a small number of operations on a stack; however, there are a large number of macros to ensure type safety for objects on the stacks. For instance, if we had a stack of X509 objects and a generic push method for adding an object to the stack, nothing would prevent us from accidentally pushing a non-X509 object onto the stack. To alleviate this problem, OpenSSL provides type-specific macros on top of the generic operations. When manipulating stacks, the type-specific macros should always be used instead of the generic routines. Since the actual operations for a single type have the same behavior as the operations for any type, we will look at them generically. See Example 10-1.

Example 10-1. Stack manipulation functions in generic form
STACK_OF(TYPE) * sk_TYPE_new_null(void);
void sk_TYPE_free(STACK_OF(TYPE) *st);
void sk_TYPE_pop_free(STACK_OF(TYPE) *st, void (*free_func)(TYPE *));
void sk_TYPE_zero(STACK_OF(TYPE) *st);
STACK_OF(TYPE) * sk_TYPE_dup(STACK_OF(TYPE) *st);

int sk_TYPE_push(STACK_OF(TYPE) *st, TYPE *val);
TYPE * sk_TYPE_pop(STACK_OF(TYPE) *st);
int sk_TYPE_unshift(STACK_OF(TYPE) *st, TYPE *val);
TYPE * sk_TYPE_shift(STACK_OF(TYPE) *st);
int sk_TYPE_num(STACK_OF(TYPE) *st);
TYPE * sk_TYPE_value(STACK_OF(TYPE) *st, int i);
TYPE * sk_TYPE_set(STACK_OF(TYPE) *st, int i, TYPE *val);
TYPE * sk_TYPE_delete(STACK_OF(TYPE) *st, int i);
TYPE * sk_TYPE_delete_ptr(STACK_OF(TYPE) *st, TYPE *ptr);
int sk_TYPE_insert(STACK_OF(TYPE) *st, TYPE *val, int i);

Example 10-1 shows the prototypes for some of the generic stack functions in OpenSSL. The type of a stack is implemented through the macro STACK_OF. Stacks are opaque types; an application should not check or set members directly. By replacing the TYPE placeholder used by the sample declarations in Example 10-1 with a specific object type, we get a good look at the prototypes of the actual stack manipulation calls for that type.

Tip

The “functions” shown in Example 10-1, like many members of the OpenSSL API, are implemented via macros. Thus, it should be obvious that function pointer manipulation is not possible. In general, applications should use the API directly since the underlying implementation is subject to change in future versions.

The operations on the stack should be self-explanatory; we’ll cover them briefly. The sk_TYPE_new_null function simply creates an empty stack, while the sk_TYPE_free function frees a stack; the latter frees only the stack, not the objects contained in the stack. To free a stack and all its members, the sk_TYPE_pop_free function should be used; we must pass in a function pointer to the free method for this to work correctly. The last of the general manipulation functions are sk_TYPE_zero to empty a stack and sk_TYPE_dup to copy a stack object.

There are also the general functions sk_TYPE_push and sk_TYPE_pop that we would expect in a stack implementation. Two that are less likely are sk_TYPE_unshift and sk_TYPE_shift ; the former pushes an element to the bottom of the stack, and the latter pops the bottom item off the stack. The functions that add elements (sk_TYPE_push and sk_TYPE_unshift) return the total number of elements in the stack, and the other two functions return the item that was removed from the stack.

The last group of macros is a set of nontypical functions for a stack implementation; they add functionality that is more commonly found in lists. The first, sk_TYPE_num , returns the total number of items in the stack. To retrieve an item by index number (if the bottom item is zero) without changing the stack, we should use sk_TYPE_value . In order to set an item in the stack explicitly, the function sk_TYPE_set will replace the ith item with the item passed to the call. The delete functions delete a single item from the stack. The item to delete can be selected by index or pointer value; the stack is shifted to fill the space left by the removed item. Finally, sk_TYPE_insert adds the indicated item to the ith position in the stack and moves all the items above it, including the ith item before the call, up one position.

As we’ll see when we move into further topics of this chapter, proper stack handling is critical for setting up some data structures. While we’ll probably put only a subset of this simple interface to use, we now have the tools to do more complex tasks.

Configuration Files

We learned how to create a CA by first creating a configuration file to hold our parameters in Chapter 3. The command-line tool used this file to perform as we had configured, such as obeying choices for algorithms, providing default values for fields in the subject name, etc. The public API has a suite of functions for processing and accessing values of configuration files. The files themselves simply organize and map keys to values. In general, the keys are strings, and the values can be either integers or strings, although all values are stored internally as strings.

The goal of the configuration file interface is to make the format of the file opaque to the code that processes it. This is done through NCONF objects. When such objects are created, a CONF_METHOD structure is specified that aggregates the routines to perform the low-level file parsing operations. OpenSSL most commonly uses the function NCONF_default to get the CONF_METHOD object. This method reads files of the format we described in Chapter 2. Because of the flexibility afforded by specifying the underlying CONF_METHOD, the NCONF interface may be extended in future versions of OpenSSL to include support for reading configuration files of new formats, such as XML.

There are only a few functions to this simple interface, and we’ll explore them by looking at an example. Example 10-2 presents a small sample configuration file.

Example 10-2. A sample configuration file (testconf.cnf)
# The config file
GlobalVar = foo
GlobalNum = 12
 
[Params]
SectionName = mySection
 
[mySection]
myVar = bar
myNum = 7

Example 10-3 provides a test program to read the sample configuration file.

Example 10-3. Code to interact with the configuration file
#include <stdio.h>
#include <stdlib.h>
#include <openssl/conf.h>
 
void handle_error(const char *file, int lineno, const char *msg)
{
    fprintf(stderr, "** %s:%i %s\n", file, lineno, msg);
    ERR_print_errors_fp(stderr);
    exit(-1);
}
#define int_error(msg)  handle_error(__FILE__, __LINE_  _, msg)
 
#define GLOB_VAR "GlobalVar"
#define GLOB_NUM "GlobalNum"
#define PARAMS   "Params"
#define SEC_NAME "SectionName"
 
#define CONFFILE "testconf.cnf"
 
int main(int argc, char *argv[])
{
    int                  i;
    long                 i_val, err = 0;
    char                 *key, *s_val;
    STACK_OF(CONF_VALUE) *sec;
    CONF_VALUE           *item;
    CONF                 *conf;
 
    conf = NCONF_new(NCONF_default(  ));
    if (!NCONF_load(conf, CONFFILE, &err))
    {
        if (err == 0)
            int_error("Error opening configuration file");
        else
        {
            fprintf(stderr, "Error in %s on line %li\n", CONFFILE, err);
            int_error("Errors parsing configuration file");
        }
    }
    if (!(s_val = NCONF_get_string(conf, NULL, GLOB_VAR)))
    {
        fprintf(stderr, "Error finding \"%s\" in [%s]\n", GLOB_VAR, NULL);
        int_error("Error finding string");
    }
    printf("Sec: %s, Key: %s, Val: %s\n", NULL, GLOB_VAR, s_val);
#if (OPENSSL_VERSION_NUMBER > 0x00907000L)
    if (!(err = NCONF_get_number_e(conf, NULL, GLOB_NUM, &i_val)))
    {
        fprintf(stderr, "Error finding \"%s\" in [%s]\n", GLOB_NUM, NULL);
        int_error("Error finding number");
    }
#else
    if (!(s_val = NCONF_get_string(conf, NULL, GLOB_NUM)))
    {
        fprintf(stderr, "Error finding \"%s\" in [%s]\n", GLOB_VAR, NULL);
        int_error("Error finding number");
    }
    i_val = atoi(s_val);
#endif
    printf("Sec: %s, Key: %s, Val: %i\n", NULL, GLOB_VAR, i_val);
    if (!(key = NCONF_get_string(conf, PARAMS, SEC_NAME)))
    {
        fprintf(stderr, "Error finding \"%s\" in [%s]\n", SEC_NAME, PARAMS);
        int_error("Error finding string");
    }
    printf("Sec: %s, Key: %s, Val: %s\n", PARAMS, SEC_NAME, key);
    if (!(sec = NCONF_get_section(conf, key)))
    {
        fprintf(stderr, "Error finding [%s]\n", key);
        int_error("Error finding string");
    }
    for (i = 0;  i < sk_CONF_VALUE_num(sec);  i++)
    {
        item = sk_CONF_VALUE_value(sec, i);
        printf("Sec: %s, Key: %s, Val: %s\n",
               item->section, item->name, item->value);
    }
 
    NCONF_free(conf);
    return 0;
}

In the example program, a new CONF object is created using the default method, and the file is loaded by the call to NCONF_load. There are also other functions for loading the file from open FILE or BIO objects: NCONF_load_fp and NCONF_load_bio . Three different functions are used to probe the configuration file for values. The first one that we use is NCONF_get_string , which returns the string of the value corresponding to the section and key values passed to it. If either the section or the key is undefined, it returns NULL. Our sample program uses a few preprocessor defines as shortcuts to the section and key strings:

char *NCONF_get_string(const CONF *conf, const char *section,
                       const char *key);
int NCONF_get_number_e(const CONF *conf, const char *section,
                       const char *key, long *result);
STACK_OF(CONF_VALUE) *NCONF_get_section(const CONF *conf,
                                        const char *section);

One interesting point made in the example is the preprocessor conditionals around usage of the function NCONF_get_number_e . Versions of OpenSSL prior to 0.9.7 have a function, NCONF_get_number , that takes three arguments, the same as NCONF_get_string , except that NCONF_get_number returns an integer value instead of a string. This function should be avoided since it does not allow for error checking. The better way to read integer values from a configuration file is to get the value as a string, check for an error condition by checking for a NULL return value, and then convert the string to an integer ourselves. Of course, if the NCONF_get_number_e function is available, it can be used safely. This function writes the retrieved value to the last argument and returns nonzero on success.

Tip

The function NCONF_get_number may be reimplemented in 0.9.7 simply as a macro for NCONF_get_number_e, requiring calling applications to be changed to account for the modified interface. Because of possible changes, it is safest to use NCONF_get_number_e.

The last NCONF function used in the example is NCONF_get_section . This function returns a stack of all the configuration parameters in the specified section. Our sample program iterates over the stack and prints all the pairs that it contains. This code actually manually accesses members of the CONF_VALUE structure; the declaration is provided in Example 10-4.

Example 10-4. The declaration of CONF_VALUE
typedef struct
{
    char *section;
    char *name;
    char *value;
} CONF_VALUE;

In general, the NCONF interface can provide a simple, readily available infrastructure for customized, application-specific configuration files. This interface is useful for other reasons, as well. For instance, we can build a custom CA application that shares configuration files with the command-line tool.

X.509

In the previous chapters, we discussed certificates in detail. We know how to perform all the common operations using the command-line tool, but we haven’t discussed how to do this programmatically. Knowing this isn’t strictly necessary, since applications aside from those implementing features similar to a typical CA will not need to do this programmatically. However, it can be useful in those cases.

We will cover several aspects of programmatically dealing with the X.509 family of operations. To do this most logically, we’ll first look at the proper way to generate a certificate request. We’ll also look at several common operations on the request itself. Once we know how to make a request, we’ll look into the functions used to create a full-fledged certificate from a certificate request. Finally, we’ll discuss how to verify a certificate chain. We’ve already discussed this process for SSL connections, but here we’ll focus on verification when dealing with just X509 objects.

Generating Requests

Recall that an X.509 certificate is a public key packaged with information about the certificate owner and issuer. Thus, to make a request, we must generate a public and private key on which we’ll base our certificate. We’ll start our discussion assuming generation of the key pair has already been completed (see Chapter 8 for details).

An X.509 certificate request is represented by an X509_REQ object in OpenSSL. As we learned in Chapter 3, a certificate request’s main component is the public half of the key pair. It also contains a subjectName field and additional X.509 attributes. In reality, the attributes are optional parameters for the request, but the subject name should always be present. As we’ll see, creating a certificate is not a very difficult task.

Subject name

Before looking at an example, we need a little more background information on subject name manipulation with the API. The object type X509_NAME represents a certificate name. Specifically, a certificate request has only a subject name, while full certificates contain a subject name and an issuer name. The purpose of the name field is to fully identify an entity, whether it is a server, person, corporation, etc. To this end, a name field is composed of several entries for country name, organization name, and common name, just to name a few. Again, we can think of the fields in a name as key/value pairs; the key is the name of the field, and the value is its content.

In theory, there can be arbitrary fields in a name, but in practice, a few standard ones are expected. In OpenSSL, fields are internally identified through an integer value known as the NID. All of this information rapidly becomes relevant when it comes time to build the subject name of our request.

As we’ve already said, a certificate name is represented by an X509_NAME object. This object is essentially a collection of X509_NAME_ENTRY objects. Each X509_NAME_ENTRY object represents a single field and the corresponding value. Thus, our application needs to generate a X509_NAME_ENTRY object for each of the fields we’ll put in the name of the certificate request.

The process is simple. First, we look up the NID of the field we need to create. Using the NID, we create the X509_NAME_ENTRY object and add our data. The entry is added to the X509_NAME, and we repeat the process until all the desired fields are entered. After the name is fully assembled, we can add it to an X509_REQ object.

OpenSSL provides many functions for manipulating X509_NAME and X509_NAME_ENTRY objects that enable us to perform the subject name assembly using many different methods. For instance, the function call X509_NAME_add_entry_by_txt automatically looks up the NID, creates the entry, and adds it to the X509_NAME. In the example below, we elected to show the explicit implementation instead of demonstrating the kinds of operations that are available.

X.509 Version 3 extensions

In previous chapters, we discussed the basics of X.509 Version 3 certificate extensions. In particular, in Chapter 5, we learned that the subjectAltName extension is very useful for SSL. This extension contains a field named dNSName that will hold the FQDN for the entity possessing the certificate. As far as certificate requests are concerned, we should add in the extension before sending the request to the CA for certification. Doing this programmatically is straightforward.

The X509_EXTENSION type object represents a single extension to an X509 object. The process of adding extensions to a request requires us to put all the requested extensions into a STACK_OF(X509_EXTENSION) object. After the stack is created, we can add the stack to the request, and our task is complete.

Putting it all together

Now we know how to create a certificate request: we have to create an X509_REQ object, add a subject name and public key to it, add all desired extensions, and sign the request with the private key. To represent the public and private key components, we should use the generic type EVP_PKEY and its corresponding functions. The slightly confusing part to this process is signing the request.

With OpenSSL, message digest algorithms are represented by EVP_MD objects. We need to specify a different EVP_MD object based on whether we’re signing with an RSA or DSA key, but the public key algorithm isn’t known when we do the signing, since we must use the abstract EVP_PKEY interface. We can probe the internals of an EVP_PKEY object to find out the underlying algorithm, but doing so isn’t very clean since it requires directly accessing members of the structure. Unfortunately, this is the only solution to the problem. The example below shows how the EVP_PKEY_type function can be used in conjunction with a member of the type EVP_PKEY to perform this task.

We’re ready to create a request now that we know the theory behind it. The code appears in Example 10-5.

Example 10-5. A program to generate a certificate request
#include <stdio.h>
#include <stdlib.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include <openssl/pem.h>
 
void handle_error(const char *file, int lineno, const char *msg)
{
    fprintf(stderr, "** %s:%i %s\n", file, lineno, msg);
    ERR_print_errors_fp(stderr);
    exit(-1);
}
#define int_error(msg)  handle_error(__FILE__, __LINE_  _, msg)
 
#define PKEY_FILE   "privkey.pem"
#define REQ_FILE    "newreq.pem"
#define ENTRY_COUNT 6
 
struct entry
{
    char *key;
    char *value;
};
 
struct entry entries[ENTRY_COUNT] = 
{
    { "countryName",            "US"              },
    { "stateOrProvinceName",    "VA"              },
    { "localityName",           "Fairfax"         },
    { "organizationName",       "Zork.org"        },
    { "organizationalUnitName", "Server Division" },
    { "commonName",             "Server 36, Engineering"  },
};
 
int main(int argc, char *argv[])
{
    int       i;
    X509_REQ  *req;
    X509_NAME *subj;
    EVP_PKEY  *pkey;
    EVP_MD    *digest;
    FILE      *fp;
 
    OpenSSL_add_all_algorithms(  );
    ERR_load_crypto_strings(  );
    seed_prng(  );
 
    /* first read in the private key */
    if (!(fp = fopen(PKEY_FILE, "r")))
        int_error("Error reading private key file");
    if (!(pkey = PEM_read_PrivateKey(fp, NULL, NULL, "secret")))
        int_error("Error reading private key in file");
    fclose(fp);
 
    /* create a new request and add the key to it */
    if (!(req = X509_REQ_new(  )))
        int_error("Failed to create X509_REQ object");
    X509_REQ_set_pubkey(req, pkey); 
 
    /* assign the subject name */
    if (!(subj = X509_NAME_new(  )))
        int_error("Failed to create X509_NAME object");
 
    for (i = 0;  i < ENTRY_COUNT;  i++)
    {
        int             nid;
        X509_NAME_ENTRY *ent;
    
        if ((nid = OBJ_txt2nid(entries[i].key)) == NID_undef)
        {
            fprintf(stderr, "Error finding NID for %s\n", entries[i].key);
            int_error("Error on lookup");
        }
        if (!(ent = X509_NAME_ENTRY_create_by_NID(NULL, nid, MBSTRING_ASC,
                                                 entries[i].value, -1)))
            int_error("Error creating Name entry from NID");
        if (X509_NAME_add_entry(subj, ent, -1, 0) != 1)
            int_error("Error adding entry to Name");
    } 
    if (X509_REQ_set_subject_name(req, subj) != 1)
        int_error("Error adding subject to request");
    /* add an extension for the FQDN we wish to have */
    {
        X509_EXTENSION           *ext;
        STACK_OF(X509_EXTENSION) *extlist;
        char                     *name = "subjectAltName";
        char                     *value = "DNS:splat.zork.org";
 
        extlist = sk_X509_EXTENSION_new_null(  );
 
        if (!(ext = X509V3_EXT_conf(NULL, NULL, name, value)))
            int_error("Error creating subjectAltName extension");
 
        sk_X509_EXTENSION_push(extlist, ext);
 
        if (!X509_REQ_add_extensions(req, extlist))
            int_error("Error adding subjectAltName to the request");
        sk_X509_EXTENSION_pop_free(extlist, X509_EXTENSION_free);
    }
 
    /* pick the correct digest and sign the request */
    if (EVP_PKEY_type(pkey->type) == EVP_PKEY_DSA)
        digest = EVP_dss1(  );
    else if (EVP_PKEY_type(pkey->type) == EVP_PKEY_RSA)
        digest = EVP_sha1(  );
    else
        int_error("Error checking public key for a valid digest");
    if (!(X509_REQ_sign(req, pkey, digest)))
        int_error("Error signing request");
 
    /* write the completed request */
    if (!(fp = fopen(REQ_FILE, "w")))
        int_error("Error writing to request file");
    if (PEM_write_X509_REQ(fp, req) != 1)
        int_error("Error while writing request");
    fclose(fp);
 
    EVP_PKEY_free(pkey);
    X509_REQ_free(req);
    return 0;
}

Using the appropriate PEM call, we read in our private key. Recall that a public key is a subset of the information in a private key, so we need not read in anything more than the private key. Using the function X509_REQ_set_pubkey , we add the public key portion of the private key to the request:

int OBJ_txt2nid(const char *field);
X509_NAME_ENTRY *X509_NAME_ENTRY_create_by_NID(X509_NAME_ENTRY **ne, int nid,
                                               int type, unsigned char *value,
                                               int len);
int X509_NAME_add_entry(X509_NAME *name, X509_NAME_ENTRY *ne,
                        int loc, int set);

Using a loop, we read from our global array containing the fields and values, and add to our subject name. The function OBJ_txt2nid performs a lookup of the built-in field definitions. This function returns the integer NID value. After obtaining the NID, we use the X509_NAME_ENTRY_create_by_NID function to create the X509_NAME_ENTRY object properly. The third argument to this function must specify the type of character encoding; common specifications are MBSTRING_ASC for ASCII and MBSTRING_UTF8 for UTF8 encoding. The last argument is the length of the value of the field we are setting. By passing in a -1 for this argument, the data is interpreted as a C-style, NULL-terminated string. The length of the data is determined by searching the data for a NULL terminator. The last call used in the loop is X509_NAME_add_entry . This call adds the entry to the subject name. The third argument specifies the position at which we want to place the data. In essence, the X509_NAME is a stack of X509_NAME_ENTRY objects. Thus, there is an ordering to the fields in a name. Specifying -1 for this argument adds the new field after any other fields already in the X509_NAME. Alternatively, we could have passed in the return from X509_NAME_entry_count , but using -1 is better because it ensures that the field is added to the end of the list. The last argument to X509_NAME_add_entry specifies the operation to be performed on the item already in the location indicated by the third argument. For instance, if the X509_NAME object contained three fields, and we made a call to this function specifying 1 for the third argument and 0 for the last argument, the field in the middle would be replaced by the new data. Using -1 for the last argument will cause the new data to be appended to the previous data, while using 1 would cause it to be prepended.

After the subject name is fully built, we add it to the certificate request. Then we build and add our extension for the subjectAltName. This can be easily done through the X509V3_EXT_conf function:

X509_EXTENSION *X509V3_EXT_conf(LHASH *conf, X509V3_CTX *ctx, 
                               char *name, char *value);
int X509_REQ_add_extensions(X509_REQ *req, STACK_OF(X509_EXTENSION) *exts);

The first two parameters to X509_EXT_conf aren’t important for creating the simple extension we need. This function returns NULL on error and the built object otherwise. We revisit this function in more detail when we discuss creating certificates below. After the X509_EXTENSION object is created, it is added to the stack. The stack is then added to the request through the function X509_REQ_add_extensions. This function will return 1 on success.

Next, we perform the check for the key type to determine the correct EVP_MD object to pass to X509_REQ_sign. This check involves using EVP_PKEY_type to translate the member of the EVP_PKEY object to something we can test. If the key is RSA, we use EVP_sha1; if it is DSA, we use EVP_dss1. In either case, SHA1 is the algorithm used in the signing process.

Using what we’ve learned about parsing configuration files, this example could easily be extended to read the field names and values from an OpenSSL configuration file instead of the hardcoded information that we provided in the example. Doing this would allow the program to interact with the same configuration files as the command-line tools.

Making Certificates

We already know that creating a certificate requires a certificate request, a CA certificate, and a CA private key to match the CA certificate. However, we haven’t discussed much beyond passing these elements into the command-line tool to produce a finished certificate. Programmatically, this process requires the programmer to perform several steps. We can break down the process into four essential steps.

  1. Verify the certificate request and check its contents (subjectName and subjectAltName in this example) to decide if we wish to certify the data with our CA’s certificate.

  2. Create a new certificate and set all the necessary fields, such as public key, subject and issuer names, expiration date, etc.

  3. Add applicable extensions to the certificate, including the requested subjectAltName.

  4. Sign the certificate with the CA’s private key.

Step 1 is perhaps the most important. To verify the request, we first check that the enclosed signature is valid; this helps to ensure the request was not modified after it was submitted for signing. More importantly, we need to determine if we actually want to certify the data in the request. Recall that signing a request into a certificate means that the CA has verified the identity of the requestor. This leaves the obligation of determining identity up to the application and, ultimately, the user of the application. It isn’t a good idea to automatically sign any certificate presented. For instance, an attacker could create a request with the identity information for another user, submit it through the “normal” channels, and automatically be granted unauthorized privileges. For this reason, the application must prompt the user with all the information in the request and then ask whether it appears to be correct because the user is in the unique position to make that determination. For instance, we must somehow verify that the presenter of the request does indeed possess the FQDN in the dNSName of the request. Finally, the information in a request should not be altered in any way. In other words, if the CA does not authorize any part of the request, the whole request should be refused rather than changing the unacceptable portions.

In the second step, we create the certificate and assign all of its properties. These are generally some CA standard parameters for a new certificate. For example, the CA should determine the default certificate version to use and the default expiration time. In addition to the standard settings, we need to assign a subject name and an issuer name to the newly created certificate. The subject name for the certificate should be taken directly from the subject name of the certificate request, which we determined was valid in the previous step. For the issuer name, the CA certificate’s subject name should be used. Lastly, the public key from the request must be added into the certificate.

One very important step in programmatically creating a certificate is the addition of certificate extensions; in step 3, we handle this process. As we discussed in Chapter 3, the use of X.509v3 certificates is nearly ubiquitous. As such, we need to add the relevant v3 fields. An important part of doing this correctly is knowing which extensions we actually want to give the certificate. For instance, we must determine if we wish to grant the new certificate the ability to act as a CA. In the example below, we simply use the default extensions for the OpenSSL command-line application and save the subjectAltName, which has been added.

The last step of creating a certificate is signing it with the CA private key. This process will prevent changes to any of the data we’ve placed in the certificate at this point; thus, it should be done last. A sample program that performs all of the actions described in this section appears in Example 10-6.

Example 10-6. Creating a certificate from a request and CA credentials
#include <stdio.h>
#include <stdlib.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include <openssl/pem.h>
 
void handle_error(const char *file, int lineno, const char *msg)
{
    fprintf(stderr, "** %s:%i %s\n", file, lineno, msg);
    ERR_print_errors_fp(stderr);
    exit(-1);
}
#define int_error(msg)  handle_error(__FILE__, __LINE_  _, msg)
 
/* these are defintions to make the example simpler */
#define CA_FILE          "CA.pem"
#define CA_KEY           "CAkey.pem"
#define REQ_FILE         "newreq.pem"
#define CERT_FILE        "newcert.pem"
#define DAYS_TILL_EXPIRE 365
#define EXPIRE_SECS (60*60*24*DAYS_TILL_EXPIRE)
 
#define EXT_COUNT 5
 
struct entry
{
    char *key;
    char *value;
};
 
struct entry ext_ent[EXT_COUNT] =
{
    { "basicConstraints",   "CA:FALSE" },
    { "nsComment",          "\"OpenSSL Generated Certificate\"" },
    { "subjectKeyIdentifier", "hash" },
    { "authorityKeyIdentifier", "keyid,issuer:always" },
    { "keyUsage",             "nonrepudiation,digitalSignature,keyEncipherment" }
};
 
int main(int argc, char *argv[])
{
    int                      i, subjAltName_pos;
    long                     serial = 1;
    EVP_PKEY                 *pkey, *CApkey;
    const EVP_MD             *digest;
    X509                     *cert, *CAcert;
    X509_REQ                 *req;
    X509_NAME                *name;
    X509V3_CTX               ctx;
    X509_EXTENSION           *subjAltName;
    STACK_OF(X509_EXTENSION) *req_exts;
    FILE                     *fp;
    BIO                      *out;
 
    OpenSSL_add_all_algorithms(  );
    ERR_load_crypto_strings(  );
    seed_prng(  );
 
    /* open stdout */
    if (!(out = BIO_new_fp(stdout, BIO_NOCLOSE)))
        int_error("Error creating stdout BIO");
 
    /* read in the request */
    if (!(fp = fopen(REQ_FILE, "r")))
        int_error("Error reading request file");
    if (!(req = PEM_read_X509_REQ(fp, NULL, NULL, NULL)))
        int_error("Error reading request in file");
    fclose(fp);
 
    /* verify signature on the request */
    if (!(pkey = X509_REQ_get_pubkey(req)))
        int_error("Error getting public key from request");
    if (X509_REQ_verify(req, pkey)) != 1)
        int_error("Error verifying signature on certificate");
 
    /* read in the CA certificate */
    if (!(fp = fopen(CA_FILE, "r")))
        int_error("Error reading CA certificate file");
    if (!(CAcert = PEM_read_X509(fp, NULL, NULL, NULL)))
        int_error("Error reading CA certificate in file");
    fclose(fp);
 
    /* read in the CA private key */
    if (!(fp = fopen(CA_KEY, "r")))
        int_error("Error reading CA private key file");
    if (!(CApkey = PEM_read_PrivateKey(fp, NULL, NULL, "password")))
        int_error("Error reading CA private key in file");
    fclose(fp);
 
    /* print out the subject name and subject alt name extension */
    if (!(name = X509_REQ_get_subject_name(req)))
        int_error("Error getting subject name from request");
    X509_NAME_print(out, name, 0);
    fputc('\n', stdout);
    if (!(req_exts = X509_REQ_get_extensions(req)))
        int_error("Error getting the request's extensions");
    subjAltName_pos = X509v3_get_ext_by_NID(req_exts,
                                          OBJ_sn2nid("subjectAltName"), -1);
    subjAltName = X509v3_get_ext(req_exts, subjAltName_pos);
    X509V3_EXT_print(out, subjAltName, 0, 0);
    fputc('\n', stdout);
 
    /* WE SHOULD NOW ASK WHETHER TO CONTINUE OR NOT */
 
    /* create new certificate */
    if (!(cert = X509_new(  )))
        int_error("Error creating X509 object");
 
    /* set version number for the certificate (X509v3) and the serial number */
    if (X509_set_version(cert, 2L) != 1)
        int_error("Error settin certificate version");
    ASN1_INTEGER_set(X509_get_serialNumber(cert), serial++);
 
    /* set issuer and subject name of the cert from the req and the CA */
    if (!(name = X509_REQ_get_subject_name(req)))
        int_error("Error getting subject name from request");
    if (X509_set_subject_name(cert, name) != 1)
        int_error("Error setting subject name of certificate");
    if (!(name = X509_get_subject_name(CAcert)))
        int_error("Error getting subject name from CA certificate");
    if (X509_set_issuer_name(cert, name) != 1)
        int_error("Error setting issuer name of certificate");
 
    /* set public key in the certificate */
    if (X509_set_pubkey(cert, pkey)) != 1)
        int_error("Error setting public key of the certificate");
 
    /* set duration for the certificate */
    if (!(X509_gmtime_adj(X509_get_notBefore(cert), 0))) 
        int_error("Error setting beginning time of the certificate");
    if (!(X509_gmtime_adj(X509_get_notAfter(cert), EXPIRE_SECS)))
        int_error("Error setting ending time of the certificate");
 
    /* add x509v3 extensions as specified */
    X509V3_set_ctx(&ctx, CAcert, cert, NULL, NULL, 0);
    for (i = 0;  i < EXT_COUNT;  i++)
    {
        X509_EXTENSION *ext;
 
        if (!(ext = X509V3_EXT_conf(NULL, &ctx,
                                   ext_ent[i].key, ext_ent[i].value)))
        {
            fprintf(stderr, "Error on \"%s = %s\"\n",
                    ext_ent[i].key, ext_ent[i].value);
            int_error("Error creating X509 extension object");
        }
        if (!X509_add_ext(cert, ext, -1))
        {
            fprintf(stderr, "Error on \"%s = %s\"\n",
                    ext_ent[i].key, ext_ent[i].value);
            int_error("Error adding X509 extension to certificate");
        }
        X509_EXTENSION_free(ext);
    }
 
    /* add the subjectAltName in the request to the cert */
    if (!X509_add_ext(cert, subjAltName, -1))
        int_error("Error adding subjectAltName to certificate");
 
    /* sign the certificate with the CA private key */
    if (EVP_PKEY_type(CApkey->type) == EVP_PKEY_DSA)
        digest = EVP_dss1(  );
    else if (EVP_PKEY_type(CApkey->type) == EVP_PKEY_RSA)
        digest = EVP_sha1(  );
    else
        int_error("Error checking CA private key for a valid digest");
    if (!(X509_sign(cert, CApkey, digest)))
        int_error("Error signing certificate");
 
    /* write the completed certificate */
    if (!(fp = fopen(CERT_FILE, "w")))
        int_error("Error writing to certificate file");
    if (PEM_write_X509(fp, cert) != 1)
        int_error("Error while writing certificate");
    fclose(fp);
  
    return 0;
}

At the beginning of this program, we read the request and verify its signature using X509_REQ_verify . Like in Example 10-5, we use the shortcut for specifying the password to the deserialization routine for the private key; real-world implementations normally prompt the user for this in an application-specific way.

Once we’ve read in all the data for creating the certificate, we print the important data from the certificate request, namely the subjectName and the subjectAltName. The subjectName is retrieved using the function X509_REQ_get_subject_name and printed with X509_NAME_print . The last parameter of the print function controls whether the short name of the field or the OID is printed: 0 for the short name, 1 for the OID.

Extracting the subjectAltName extension is more complex than getting the subjectName. First, we must extract the stack of all the extensions in the request. With this stack, we use X509v3_get_ext_by_NID to get the integer indicating the position of the subjectAltName field. We can use this integer with the function X509v3_get_ext to get the actual X509_EXTENSION object. This object is then printed with X509V3_EXT_print. The third parameter to this call is identical to the last parameter of the X509_NAME_print call. The last parameter is the number of spaces to be indented before printing the data. After printing this crucial information, the application should query the CA administrator to determine if this data is correct and not a masquerading attempt.

After this process, we create an empty certificate and set the version of the new certificate. Since the version numbers start at 1, and the internal representation of versions starts at 0, using 2 in the call to X509_set_version really indicates Version 3 of X.509. Certificates must also bear a serial number assigned from the CA at the time of signing. To do this, we use a simple ASN.1 family function call.[1]

When signing certificates in applications, it is the application’s responsibility to track serial numbers and assign them uniquely. Setting the subject and issuer names is performed as described above. After setting the names, we use X509_set_pubkey to assign the certificate the same public key we obtained from the request. Setting the certificate expiration time is performed by setting the notBefore and notAfter attributes. We use the X509_gmtime_adj function to set the starting time retrieved by calling X509_get_notBefore to 0. This sets the start time to the current time and date. By setting the notAfter parameter in a similar fashion, we specify the number of seconds for the lifetime of the certificate.

We use a previously unseen function, X509V3_set_ctx , to prepare a context for creating the extensions to the certificate:

void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
                    X509_REQ *req, X509_CRL *crl, int flags);

This versatile function is called with several NULLs since we don’t need to add extensions to any X509_REQ or X509_CRL objects. Additionally, we don’t need any flags, so a 0 is used for the last argument. This X509V3_CTX object is used by the X509V3_EXT_conf to allow for some of the more complex extensions we wish to add. For example, the subjectKeyIdentifier extension is computed as a hash of part of the data in the certificate, and the X509V3_CTX object provides the routine access to this certificate (we added it to the context earlier).

We then loop through the array of extension data to create and add extensions to the new certificate. Unlike the process of adding extensions to a certificate request, we don’t need to add the extensions to a stack before adding them to an X509 object. As we mentioned earlier, the extensions we add are the defaults for the OpenSSL command-line utility. After the loop is completed, we add the subjectAltName extension that we extracted from the request previously.

The last task we perform before writing our new certificate is to sign it. This is critical, since an unsigned certificate is essentially useless. The function X509_sign performs this for us. As in Example 10-5, we must perform a check to determine the type of the private key so that we can decide on a correct EVP_MD object to use as the hash algorithm.

We now have a complete, valid certificate. This is a big step. Using what we’ve learned, we are armed with enough knowledge to implement a minimal CA. Before leaving the topic of programming with X.509, however, one important topic remains to be discussed: certificate verification.

X.509 Certificate Checking

In Chapter 5, we discussed SSL server certificate verification extensively. Here, we’ll discuss the layer just below SSL that performs certificate verification. Specifically, we’ll discuss how OpenSSL’s SSL functionality verifies a certificate against CRLs and other certificates in the certificate hierarchy. To do this, we’ll require functions from the X.509 package. The SSL protocol implementation handles much of what we’re about to discuss here for us; even so, some setup work is required on our part, particularly if we wish to include CRLs in the verification process, which we almost certainly do.

Knowing how to programmatically perform the verification of a certificate chain gives us valuable insight that we might not ordinarily have into what is actually involved in properly verifying a certificate. It also provides us with the information necessary to verify certificates on our own when we’re not using the SSL protocol. Before delving into the verification process, however, it’s helpful to understand the purpose of some of the objects that are involved.

In general, a certificate can be validated only against a collection of other certificate material, i.e., CA certificates and CRLs. OpenSSL uses the object type X509_STORE to represent a collection of certificates and certificate revocation lists to serve this purpose. Additionally, OpenSSL uses the type X509_STORE_CTX to hold the data used during an actual verification. This distinction is important; our implementations will look somewhat incongruous with respect to the context-object relationship we’ve seen with other OpenSSL packages. For certificate verification, we will create an X509_STORE first and populate it with all the available certificate and revocation list information. When it’s time to verify a peer certificate, we will use the store to create an X509_STORE_CTX to perform the actual verification.

Along with the certificate stores and the associated contexts, the X509_LOOKUP_METHOD object is also important. Objects of this type represent a general method of finding certificates or CRLs. For instance, the X509_LOOKUP_file function returns a method to find certificate-related objects within a single file, and the X509_LOOKUP_hash_dir function returns a method to find objects within a properly set up OpenSSL CA directory. X509_LOOKUP_METHOD objects are important for creating X509_LOOKUP objects. These objects aggregate the collection of certificates accessible through the underlying method. For instance, if we have a certificate directory, we can create an X509_LOOKUP from an X509_STORE and the return value of X509_LOOKUP_hash_dir; this X509_LOOKUP object can then be assigned to a directory, and our X509_STORE will have access to all of the certificates and CRLs that the lookup aggregates.

To review: an X509_STORE holds X509_LOOKUP objects built on X509_LOOKUP_METHODs. This is how the store gains access to certificate and CRL data. The store can then be used to create an X509_STORE_CTX to perform a verification operation.

Knowing the relationships between some of these objects, we can begin to see the general form of what our code will have to do to verify a peer certificate. However, a few other important subtleties about correctly verifying a certificate have not yet been discussed. These can be made clear by analyzing Example 10-7, which demonstrates the whole process of validating a peer certificate.

Example 10-7. Verifying a client certificate
#include <stdio.h>
#include <stdlib.h>
#include <openssl/x509_vfy.h>
#include <openssl/err.h>
#include <openssl/pem.h>
 
void handle_error(const char *file, int lineno, const char *msg)
{
    fprintf(stderr, "** %s:%i %s\n", file, lineno, msg);
    ERR_print_errors_fp(stderr);
    exit(-1);
}
#define int_error(msg)  handle_error(__FILE__, __LINE_  _, msg)
 
/* these are defintions to make the example simpler */
#define CA_FILE     "CAfile.pem"
#define CA_DIR      "/etc/ssl"
#define CRL_FILE    "CRLfile.pem"
#define CLIENT_CERT "cert.pem"
 
int verify_callback(int ok, X509_STORE_CTX *stor)
{
    if(!ok)
        fprintf(stderr, "Error: %s\n",
                X509_verify_cert_error_string(stor->error));
    return ok;
}
 
int main(int argc, char *argv[])
{
    X509           *cert;
    X509_STORE     *store;
    X509_LOOKUP    *lookup;
    X509_STORE_CTX *verify_ctx;
    FILE           *fp;
 
    OpenSSL_add_all_algorithms(  );
    ERR_load_crypto_strings(  );
    seed_prng(  );
 
    /* first read the client certificate */
    if (!(fp = fopen(CLIENT_CERT, "r")))
        int_error("Error reading client certificate file");
    if (!(cert = PEM_read_X509(fp, NULL, NULL, NULL)))
        int_error("Error reading client certificate in file");
    fclose(fp);
 
    /* create the cert store and set the verify callback */
    if (!(store = X509_STORE_new(  )))
        int_error("Error creating X509_STORE_CTX object");
    X509_STORE_set_verify_cb_func(store, verify_callback);
 
    /* load the CA certificates and CRLs */
    if (X509_STORE_load_locations(store, CA_FILE, CA_DIR) != 1)
        int_error("Error loading the CA file or directory"); 
    if (X509_STORE_set_default_paths(store) != 1)
        int_error("Error loading the system-wide CA certificates");
    if (!(lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file(  ))))
        int_error("Error creating X509_LOOKUP object");
    if (X509_load_crl_file(lookup, CRL_FILE, X509_FILETYPE_PEM) != 1)
        int_error("Error reading the CRL file");
 
    /* enabling verification against CRLs is not possible 
       in prior versions */
#if (OPENSSL_VERSION_NUMBER > 0x00907000L)
    /* set the flags of the store so that CRLs are consulted */
    X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK |
                                X509_V_FLAG_CRL_CHECK_ALL);
#endif
   
    /* create a verification context and initialize it */
    if (!(verify_ctx = X509_STORE_CTX_new(  )))
        int_error("Error creating X509_STORE_CTX object");
    /* X509_STORE_CTX_init did not return an error condition
       in prior versions */
#if (OPENSSL_VERSION_NUMBER > 0x00907000L)
    if (X509_STORE_CTX_init(verify_ctx, store, cert, NULL) != 1)
        int_error("Error initializing verification context");
#else
    X509_STORE_CTX_init(verify_ctx, store, cert, NULL);
#endif
 
    /* verify the certificate */
    if (X509_verify_cert(verify_ctx) != 1)
        int_error("Error verifying the certificate");
    else
        printf("Certificate verified correctly!\n");
 
    return 0;
}

After reading in the peer certificate, we create our certificate store as expected. We also assign a verification callback function. The form and purpose of this callback is identical to the verification callback for SSL connections we saw in Chapter 5.

The following two function calls should also look familiar; we’ve already examined their mirrored functions for SSL_CTX objects. They behave just like the SSL-specific versions. To load the CRL file, however, we use the method described at the beginning of this section. The function X509_STORE_add_lookup will create the lookup object we need when we pass it the correct lookup method, given by X509_LOOKUP_file . After we’ve created the lookup (it’s already added to the store), we need only assign the lookup the file from which to read. This is done by the call to X509_load_crl_file . In fact, the call to X509_STORE_load_locations could have been removed and done with lookups instead. For instance, the conditional clause using the function could be replaced by the following:

if (!(lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file(  ))))
    fprintf(stderr, "Error creating X509_LOOKUP object\n");
if (X509_LOOKUP_load_file(lookup, CA_FILE, X509_FILETYPE_PEM) != 1)
    fprintf(stderr, "Error reading the CA file\n");
if (!(lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir(  ))))
    fprintf(stderr, "Error creating X509_LOOKUP object\n");
if (X509_LOOKUP_add_dir(lookup, CA_DIR, X509_FILETYPE_PEM) != 1)
    fprintf(stderr, "Error reading the CRL file\n");

This code snippet simply follows the paradigm laid out above; we create a lookup and then set the lookup in an appropriate location. Using this expanded code can be useful in applications in which we want to do a more specific type of loading of the store, such as an application that has several CA files, each of which may contain more than one certificate.

Setting the flags for the certificate store is very important. By setting flags in the store, they are automatically copied to the store contexts created from it. Thus, setting the flag X509_V_FLAG_CRL_CHECK instructs the contexts to check client certificates for possible revocation. This flag will cause only the last item, the identity certificate itself, to be checked; the chain is not checked for possible revocation. To check the entire chain, we must also specify X509_V_FLAG_CRL_CHECK_ALL. As noted in the code, this capability is not available in versions of OpenSSL prior to Version 0.9.7.

After setting the flags, our store is adequately set up, and we are ready to begin the rather simple process of verifying the actual certificate. We create an X509_STORE_CTX, initialize it, and then call the verify function to determine the result. Looking at the initialization function in more detail is helpful, however.

int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store,
                        X509 *x509, STACK_OF(X509) *chain);

The last argument to this function optionally allows us to pass in the complete peer certificate chain for verification. This is often necessary, since the verifying party may not have a complete list of certificates that includes the identity certificate. This problem arises most commonly when CAs sign other CAs, as in the example SSL applications in Chapter 5. By passing the entire peer chain, we can attempt to verify the whole chain and have fewer errors because valid issuer certificates could not be found. Of course, in some applications—namely those that want only directly signed, authorized clients—this is inappropriate and should be left as NULL. In versions prior to 0.9.7, this function does not return an integer error code.

At the end of the main function, we can check the return value of X509_verify_cert and determine if the verification succeeded. As we would expect, our callback is used during this function call (it was passed on from the store to the store context).

PKCS#7 and S/MIME

PKCS#7 defines a standard format for data that has had cryptography applied to it. Like most standards, using this format will guarantee a level of interoperability with both existing and future applications. The standard itself is based on other PKCS standards for performing cryptographic operations. It is important to note that PKCS#7 specifies only a data format, not the choice of any specific algorithms.

Perhaps the most important trait of PKCS#7 is that it is the basis for Secure Multipurpose Internet Mail Extensions (S/MIME). S/MIME is a specification for sending secure email. Built on top of PKCS#7 and the former MIME standard, S/MIME allows us to email messages that can assure secrecy, integrity, authentication, and non-repudiation.

Using S/MIME, we can sign, verify, encrypt, and decrypt messages. This is very useful when developing mail applications, but it can also be used by programs that need to transmit data over a text-based medium, such as an instant-messaging implementation. As we’ll see, programming with OpenSSL’s PKCS#7 and S/MIME packages requires us to use much of our knowledge of the other packages.

A common misconception is that PKCS#7 and S/MIME are one in the same. In fact, they are not. S/MIME merely defines a specific encoding for PKCS#7 data. Using the S/MIME standard as implemented by OpenSSL, we can create applications that securely interact with other S/MIME-compliant applications, since the data encoding is standardized. It is also important to note here that OpenSSL’s support for PKCS#7 and S/MIME is limited. Only S/MIMEv2 and PKCS#7 v1.5 are supported.

Signing and Verifying

The concept of signing and verifying is familiar by this point. Conceptually, to sign a message, we will need the sender’s private key and the message to sign; the verification operation will require the sender’s public key and the signed message. With S/MIME, the implementation is rather simple.

The signing process is opaque to the calling application. We simply provide all of the information with one function call to PKCS7_sign , and we get back a PKCS7 object. From there, we can use SMIME_write_PKCS7 to output the S/MIME armored message. Likewise, with verification, we obtain a PKCS7 object using SMIME_read_PKCS7 and perform the verification by calling the PKCS_verify function.

While the calls to the actual PKCS#7 and S/MIME family of functions are simple, we must perform some nontrivial setup for all of the arguments to these functions. In Example 10-8 below, our implementation focuses on that critical setup. Before we get ahead of ourselves, we should first look at these four functions in more detail.

PKCS7 *PKCS7_sign(X509 *signcert, EVP_PKEY *pkey, STACK_OF(X509) *certs, BIO *data, int flags);

The first argument to PKCS7_sign is the certificate with which we’ll sign the message. The second argument is the corresponding private key to the certificate. The third argument allows us to add other certificates to the S/MIME message. This is most useful when we have a long certificate chain and we wish to aid the receiving party’s verification process. The fourth argument accepts a BIO object from which the message will be read. The last argument, flags, allows us to set properties of the resulting PKCS7 object. Its potential values are discussed at the end of this section.

int SMIME_write_PKCS7(BIO *bio, PKCS7 *p7, BIO *data, int flags);

The SMIME_write_PKCS7 function will write the PKCS7 object in the S/MIME encoding. The data is written to the BIO object passed in as the first argument. The object to write is the second. The other BIO object, the third argument, is the same object we used when calling PKCS7_sign. This allows the rest of the message data to be read and signed before writing the signature. The last argument is a set of flags of the same type as the signing function; they will be left for later discussion.

The verification process is, in essence, a reverse of the signing process. First, we read in a PKCS7 object, and then we call PKCS7_verify .

PKCS7 *SMIME_read_PKCS7(BIO *bio, BIO **bcont);

This function simply reads an S/MIME-encoded PKCS7 object from the BIO passed in as the first argument. The second argument is used to pass the caller back a pointer to a BIO that is opened for reading on the data in the PKCS7 object. This passed-back BIO will be important for our verification process.

int PKCS7_verify(PKCS7 *p7, STACK_OF(X509) *certs, X509_STORE *store, BIO *indata, BIO *out, int flags);

To verify a PKCS7 object, pass the object as the first argument to the PKCS7_verify. The second argument specifies a chain of certificates that we can use to verify the signature. The validity of these certificates is checked against the X509_STORE specified as the third argument. This store must be set up fully before attempting to verify an S/MIME message. The setup is identical to that of Example 10-7. The BIO object that is passed as the fourth argument is the same object we retrieved from our call to SMIME_read_PKCS7 . As the function PKCS7_verify processes the data read from this BIO, it writes the recovered message to the BIO used as the fifth argument. The flags are discussed below.

Now, using all we know about processing other types of OpenSSL objects and what we’ve just learned about PKCS#7 and S/MIME, we will dissect a small utility that can sign and verify text messages. The code appears in Example 10-8.

Example 10-8. A signing and verifying utility
#include <stdio.h>
#include <stdlib.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
 
/*
 * This code appearing before the main function is all for X509_STORE setup.
 */
 
/* these are defintions to make the example simpler */
#define CA_FILE  "CAfile.pem"
#define CA_DIR   "/etc/ssl"
#define CRL_FILE "CRLfile.pem"
 
int verify_callback(int ok, X509_STORE_CTX *stor)
{
    if (!ok)
        fprintf(stderr, "Error: %s\n",
                X509_verify_cert_error_string(stor->error));
    return ok;
}
 
X509_STORE *create_store(void)
{
    X509_STORE  *store;
    X509_LOOKUP *lookup;
 
    /* create the cert store and set the verify callback */
    if (!(store = X509_STORE_new(  )))
    {
        fprintf(stderr, "Error creating X509_STORE_CTX object\n");
        goto err;
    }
    X509_STORE_set_verify_cb_func(store, verify_callback);
 
    /* load the CA certificates and CRLs */
    if (X509_STORE_load_locations(store, CA_FILE, CA_DIR) != 1)
    {
        fprintf(stderr, "Error loading the CA file or directory\n");
        goto err;
    }
 
    if (X509_STORE_set_default_paths(store) != 1)
    {
        fprintf(stderr, "Error loading the system-wide CA certificates\n");
        goto err;
    }
    if (!(lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file(  ))))
    {
        fprintf(stderr, "Error creating X509_LOOKUP object\n");
        goto err;
    }
    if (X509_load_crl_file(lookup, CRL_FILE, X509_FILETYPE_PEM) != 1)
    {
        fprintf(stderr, "Error reading the CRL file\n");
        goto err;
    }
 
    /* set the flags of the store so that CRLs are consulted */
    X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK |
                         X509_V_FLAG_CRL_CHECK_ALL);
 
    return store;
err:
    return NULL;
}
 
int main(int argc, char *argv[])
{
    int            sign;
    X509           *cert;
    EVP_PKEY       *pkey;
    STACK_OF(X509) *chain = NULL;
    X509_STORE     *store;
    PKCS7          *pkcs7;
    FILE           *fp;
    BIO            *in, *out, *pkcs7_bio;
 
    OpenSSL_add_all_algorithms(  );
    ERR_load_crypto_strings(  );
    seed_prng(  );
 
    --argc, ++argv;
    if (argc < 2)
    {
        fprintf(stderr, "Usage: sv (sign|verify) [privkey.pem] cert.pem ...\n");
        goto err;
    }
    if (!strcmp(*argv, "sign"))
        sign = 1;
    else if (!strcmp(*argv, "verify"))
        sign = 0;
    else
    {
        fprintf(stderr, "Usage: sv (sign|verify) [privkey.pem] cert.pem ...\n");
        goto err;
    }
    --argc, ++argv;
 
    /* setup the BIO objects for stdin and stdout */
    if (!(in = BIO_new_fp(stdin, BIO_NOCLOSE)) ||
        !(out = BIO_new_fp(stdout, BIO_NOCLOSE)))
    {
        fprintf(stderr, "Error creating BIO objects\n");
        goto err;
    }
 
    if (sign)
    {
        /* read the signer private key */
        if (!(fp = fopen(*argv, "r")) ||
            !(pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)))
        {
            fprintf(stderr, "Error reading signer private key in %s\n", *argv);
            goto err;
        }
        fclose(fp);
        --argc, ++argv;
    }
    else
    {
        /* create the cert store and set the verify callback */
        if (!(store = create_store(  )))
            fprintf(stderr, "Error setting up X509_STORE object\n");
    }
 
    /* read the signer certificate */
    if (!(fp = fopen(*argv, "r")) ||
        !(cert = PEM_read_X509(fp, NULL, NULL, NULL)))
    {
        ERR_print_errors_fp(stderr);
        fprintf(stderr, "Error reading signer certificate in %s\n", *argv);
        goto err;
    }
    fclose(fp);
    --argc, ++argv;
 
    if (argc)
        chain = sk_X509_new_null(  );
    while (argc)
    {
        X509 *tmp;
 
        if (!(fp = fopen(*argv, "r")) ||
            !(tmp = PEM_read_X509(fp, NULL, NULL, NULL)))
        {
            fprintf(stderr, "Error reading chain certificate in %s\n", *argv);
            goto err;
        }
        sk_X509_push(chain, tmp);
        fclose(fp);
        --argc, ++argv;
    }
 
    if (sign)
    {
        if (!(pkcs7 = PKCS7_sign(cert, pkey, chain, in, 0)))
        {
            fprintf(stderr, "Error making the PKCS#7 object\n");
            goto err;
        }
        if (SMIME_write_PKCS7(out, pkcs7, in, 0) != 1)
        {
            fprintf(stderr, "Error writing the S/MIME data\n");
            goto err;
        }
    }
    else /* verify */
    {
        if (!(pkcs7 = SMIME_read_PKCS7(in, &pkcs7_bio)))
        {
            fprintf(stderr, "Error reading PKCS#7 object\n");
            goto err;
        }
        if (PKCS7_verify(pkcs7, chain, store, pkcs7_bio, out, 0) != 1)
        {
            fprintf(stderr, "Error writing PKCS#7 object\n");
            goto err;
        }
        else
            fprintf(stdout, "Certifiate and Signature verified!\n");
    }
  
    return 0;
err:
    return -1;
}

There should be no surprises in this code; it is a logical extension of what we already know about the various methods of processing private keys, certificates, and PKCS#7 objects.

This program is called with the first argument as either “sign” or “verify”. If signing mode is used, we will expect the next argument to be the private key, the following argument to be the corresponding certificate, and the rest can be chain certificates that we’ll add to the message. In verification mode, the third argument is expected to be the certificate, and the rest are extra certificates to check the signature.

We use the function create_store to represent the setup process for the certificate store abstractly. We read in the appropriate number of arguments based on the mode and add all the rest to a certificate stack. Finally, we either sign and emit the S/MIME message or read the S/MIME message and emit the original, verified message.

Encrypting and Decrypting

Again, we’re familiar with the general process; we need a peer’s public key to encrypt and our own private key to decrypt. The functions for reading and writing the PKCS#7 objects in the S/MIME encoding are unchanged, but we do have the new functions PKCS7_encrypt and PKCS7_decrypt . Before delving into the details of these two new functions, we should go back and think of the envelope interface we saw in Chapter 8. This interface allowed us to encrypt messages for other users with simple function calls and public key components, but in reality, the majority of the encryption was done using a symmetric cipher. These PKCS#7 functions do the same thing. They generate a random key and encrypt the data with it. Then the random key, or session key, is encrypted using the recipient’s public key and included with the message. As an extension, PKCS#7 allows us to send a single encrypted message to multiple users by simply encrypting the session key with each of the recipient’s public keys and including all of that data with the message. The example below will allow us to do this.

PKCS7 *PKCS7_encrypt(STACK_OF(X509) *certs, BIO *in, const EVP_CIPHER *cipher, int flags);

The first argument is a collection of public keys for the recipients. Each public key will be used to encrypt the message’s session key separately. The second argument specifies the BIO from which the message to encrypt will be read. The third argument specifies the symmetric algorithm to use, and the last are the flags, discussed below.

int PKCS7_decrypt(PKCS7 *p7, EVP_PKEY *pkey, X509 *cert, BIO *data, int flags);

The decryption function is equally simple. The PKCS7 object is passed in first; it is the product of a call to SMIME_read_PKCS7. The next two arguments are accounted for by the private key to perform the decryption and the corresponding certificate. The BIO object is used by the PKCS7_decrypt to write out the decrypted data. Again, the flags are discussed below.

We will look at another small utility, just as we did for signing and verifying, to make clear the kind of setup we need to do before calling these functions. Example 10-9 has that code.

Example 10-9. A utility to encrypt and decrypt S/MIME messages
#include <stdio.h>
#include <stdlib.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
 
int main(int argc, char *argv[])
{
    int              encrypt;
    PKCS7            *pkcs7;
    const EVP_CIPHER *cipher;
    STACK_OF(X509)   *certs;
    X509             *cert;
    EVP_PKEY         *pkey;
    FILE             *fp;
    BIO              *pkcs7_bio, *in, *out;
 
    OpenSSL_add_all_algorithms(  );
    ERR_load_crypto_strings(  );
    seed_prng(  );
 
    --argc, ++argv;
    if (argc < 2)
    {
        fprintf(stderr, "Usage: ed (encrypt|decrypt) [privkey.pem] cert.pem "
                        "...\n");
        goto err;
    }
    if (!strcmp(*argv, "encrypt"))
        encrypt = 1;
    else if(!strcmp(*argv, "decrypt"))
        encrypt = 0;
    else
    {
        fprintf(stderr, "Usage: ed (encrypt|decrypt) [privkey.pem] cert.pem "
                        "...\n");
        goto err;
    }
    --argc, ++argv;
 
    /* setup the BIO objects for stdin and stdout */
    if (!(in = BIO_new_fp(stdin, BIO_NOCLOSE)) ||
        !(out = BIO_new_fp(stdout, BIO_NOCLOSE)))
    {
        fprintf(stderr, "Error creating BIO objects\n");
        goto err;
    }
 
    if (encrypt)
    {
        /* choose cipher and read in all certificates as encryption targets */
        cipher = EVP_des_ede3_cbc(  );
        certs = sk_X509_new_null(  );
 
        while (argc)
        {
            X509 *tmp;
 
            if (!(fp = fopen(*argv, "r")) ||
                !(tmp = PEM_read_X509(fp, NULL, NULL, NULL)))
            {
                fprintf(stderr, "Error reading encryption certificate in %s\n",
                        *argv);
                goto err;
            }
            sk_X509_push(certs, tmp);
            fclose(fp);
            --argc, ++argv;
        }
 
        if (!(pkcs7 = PKCS7_encrypt(certs, in, cipher, 0)))
        {
            ERR_print_errors_fp(stderr);
            fprintf(stderr, "Error making the PKCS#7 object\n");
            goto err;
        }
        if (SMIME_write_PKCS7(out, pkcs7, in, 0) != 1)
        {
            fprintf(stderr, "Error writing the S/MIME data\n");
            goto err;
        }
    }
    else
    {
        if (!(fp = fopen(*argv, "r")) ||
            !(pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)))
        {
            fprintf(stderr, "Error reading private key in %s\n", *argv);
            goto err;
        }
        fclose(fp);
        --argc, ++argv;
        if (!(fp = fopen(*argv, "r")) ||
            !(cert = PEM_read_X509(fp, NULL, NULL, NULL)))
        {
            fprintf(stderr, "Error reading decryption certificate in %s\n",
                    *argv);
            goto err;
        }
        fclose(fp);
        --argc, ++argv;
 
        if (argc)
            fprintf(stderr, "Warning: excess parameters specified. "
                            "Ignoring...\n");
 
        if (!(pkcs7 = SMIME_read_PKCS7(in, &pkcs7_bio)))
        {
            fprintf(stderr, "Error reading PKCS#7 object\n");
            goto err;
        }
        if (PKCS7_decrypt(pkcs7, pkey, cert, out, 0) != 1)
        {
            fprintf(stderr, "Error decrypting PKCS#7 object\n");
            goto err;
        }
    }
 
    return 0;
err:
    return -1;
}

This program is similar to the one in Example 10-8. When in encryption mode, it expects all the arguments after the word “encrypt” to be certificate files for the recipients. In decryption mode, the argument after “decrypt” must be the private key filename. The following argument should be the corresponding certificate; all further arguments are ignored, and a warning is emitted.

Analyzing this program, we can see several similarities to the previous example. When encrypting, we create the recipient stack, create the PKCS7 object with it and the message, and then write the product out. Decryption requires us to get the private key and certificate before performing the PKCS#7 and S/MIME operations.

Combined Operations

Often, we will want to both sign and encrypt an S/MIME message. As it turns out, we can do this easily using the two utilities we’ve created. First, we sign the message, and then we encrypt it for the recipients. Take the following example. Assume we have a proper root certificate in the file CAfile.pem and a CRL in the file CRLfile.pem. We will further assume that we have two users: foo with certificate in foocert.pem and private key in fookey.pem, and bar with certificate and key files named similarly. Using the following command, foo can prepare his message for bar.

$ cat msg.txt | ./sv sign fookey.pem foocert.pem \
> | ./ed encrypt barcert.pem > msg.smime

This command line reads the original message from the file msg.txt and begins by signing it with foo’s private key. After this, the message is encrypted with the public key in the certificate of the recipient, i.e., bar’s certificate. We could easily add more certificate files to this part of the command line if we wish to send the message to multiple targets.

Upon receiving this S/MIME message, bar would execute the following command.

$ cat msg.smime | ./ed decrypt barkey.pem barcert.pem \
> | ./sv verify foocert.pem

This command will use bar’s private key first to decrypt the message. At this point, the verify routine uses foo’s certificate to verify the signature, and the message is displayed.

Of course, this is an elementary example, but it gives us some idea of how S/MIME messages can be nested. This is the simplest way of sending a signed and encrypted message.

PKCS#7 Flags

We delayed our discussion of the PKCS#7 flags until we had shown a real implementation using the functions. In addition, we’re now in a better position to understand the flags since we are more aware of the capabilities of PKCS#7 and S/MIME. The flags are all bit-valued and can be combined through the logical OR operation. Each of these PKCS7_... flags have SMIME_... aliases as well. We will limit our discussion to the more common and useful flags.

PKCS7_NOINTERN

The verification process will not use the certificates embedded in the object for signature verification, i.e., the peer’s certificate must be known beforehand for successful verification.

PKCS7_NOVERIFY

When verifying a PKCS#7 object, do not try to verify the signer’s certificate. The signature will still be checked.

PKCS7_NOCERTS

The signing process will not add any extra certificates to the generated object.

PKCS7_DETACHED

Do not include the signer’s certificate in the generated object when signing data.

PKCS7_NOSIGS

Do not verify the signature on the PKCS#7 object.

Some of these flags are dangerous, since they undermine security. We should strictly limit usage of the flags to applications running for testing or academic purposes.

PKCS#12

The PKCS#12 standard specifies a format for secure transportation of user identity information. This can be any sort of information, including certificates, passwords, and even private keys. The purpose for this standard is to allow user credentials to be portable while remaining secure.

The PKCS#12 standard allows for many different levels of security ranging from the use of a hardware security token to simpler password-based protection. For our purposes, we need only discuss a small subset of the features. The reason PKCS#12 is important to us is that many applications, namely common web browsers, use PKCS#12-formatted credentials.

For instance, if we develop an SSL-enabled web server, we want to be able to allow user authentication via client certificates. Using PKCS#12, we can generate the client credentials using OpenSSL and then import the data into a third-party web browser. This allows the browsing application to present the certificate to our SSL server and thus properly authenticate. The primary benefit of knowing how to perform this task is simple—we gain interoperability. We will limit our discussion of PKCS#12 to simply performing this task.

Wrapping Information into a PKCS#12 Object

This process is rather simple. It requires only one function call. The function takes all of the data presented and creates a password-protected PKCS12 object. With the created PKCS12 object, we can safely transport the data to a PKCS#12-compliant application. The application can then import the data, provided the password under which it was protected is supplied.

PKCS12 *PKCS12_create(char *pass, char *name, EVP_PKEY *pkey, X509 *cert, STACK_OF(X509) *ca, int nid_key, int nid_cert, int iter, int mac_iter, int keytype);

The PKCS12_create function takes many arguments, but only the first five are important; the rest can be safely left as 0. The first argument is the password to use in protecting the data. The second argument specifies a general name to identify the created set of credentials. The following three arguments are the main parts of the object, the private key, the certificate, and the certificate’s CA chain.

This function returns a properly formed PKCS12 object on success and NULL if an error occurs. To use this function successfully, we need only use the knowledge we already have to read in the data programmatically and then make this call. Once we’ve created the PKCS12 object, we will write it out to a file, in the most common case. The convention for PKCS#12 objects is to write them in DER format; thus, i2d_PKCS12_fp should be used.

int i2d_PKCS12_fp(FILE *fp, PKCS12 *p12);

Once we have the file, we can use it to import our credentials into any application that supports PKCS#12 objects. As a final note, we should use this method of exporting credentials only for user/client information. If we wanted to do something simpler, like add a CA certificate to a browser, we should just use i2d_X509_fp to write the single X509 object.

Importing Objects from PKCS#12 Data

Another common feature, the last we’ll discuss on the topic of PKCS#12, is building applications that can import user identity information via PKCS#12. For all of the reasons mentioned, it is a good idea to build this support into applications in which it’s appropriate. Again, OpenSSL provides one simple function call to serve our needs.

The first issue is reading the PKCS#12 file in from disk. The function d2i_PKCS12_fp performs this task.

PKCS12 *d2i_PKCS12_fp(FILE *fp, PKCS12 **p12);

We can simply use a NULL for the second argument, and a newly allocated and populated PKCS12 object is returned, as long as no errors are encountered in the file. Once we have this object, we can call PKCS12_parse to unwrap all of the identity objects that are encoded in it.

int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert, STACK_OF(X509) **ca);

The second argument must be the same passphrase under which the file was protected. The last arguments are all used to pass back pointers to the indicated objects: the private key, the certificate, and the certificate chain file. After this call is completed, the PKCS12 object can be freed, and we can use the unwrapped objects normally.

In the end, PKCS#12 provides a solid foundation to safely write and read user credentials and, at the same time, affords a degree of interoperability for user identity information.



[1] ASN.1 (Abstract Syntax Notation 1) is a language that is used to describe data structures. For our purposes, this definition is sufficient. A complete discussion of ASN.1 is beyond the scope of this book.