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.
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.
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.
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.
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.
# 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.
#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.
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.
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.
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.
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.
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.
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.
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.
#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.
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.
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.
Create a new certificate and set all the necessary fields, such as public key, subject and issuer names, expiration date, etc.
Add applicable extensions to the certificate, including the requested
subjectAltName.
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.
#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.
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.
#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 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.
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.
#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.
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.
#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.
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.
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
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.
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.
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.
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.