diff options
Diffstat (limited to 'src/tls/tls_certkey.c')
-rw-r--r-- | src/tls/tls_certkey.c | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/src/tls/tls_certkey.c b/src/tls/tls_certkey.c new file mode 100644 index 0000000..09a35e0 --- /dev/null +++ b/src/tls/tls_certkey.c @@ -0,0 +1,721 @@ +/*++ +/* NAME +/* tls_certkey 3 +/* SUMMARY +/* public key certificate and private key loader +/* SYNOPSIS +/* #define TLS_INTERNAL +/* #include <tls.h> +/* +/* int tls_set_ca_certificate_info(ctx, CAfile, CApath) +/* SSL_CTX *ctx; +/* const char *CAfile; +/* const char *CApath; +/* +/* int tls_set_my_certificate_key_info(ctx, chain_files, +/* cert_file, key_file, +/* dcert_file, dkey_file, +/* eccert_file, eckey_file) +/* SSL_CTX *ctx; +/* const char *chain_files; +/* const char *cert_file; +/* const char *key_file; +/* const char *dcert_file; +/* const char *dkey_file; +/* const char *eccert_file; +/* const char *eckey_file; +/* +/* int tls_load_pem_chain(ssl, pem, origin); +/* SSL *ssl; +/* const char *pem; +/* const char *origin; +/* DESCRIPTION +/* OpenSSL supports two options to specify CA certificates: +/* either one file CAfile that contains all CA certificates, +/* or a directory CApath with separate files for each +/* individual CA, with symbolic links named after the hash +/* values of the certificates. The second option is not +/* convenient with a chrooted process. +/* +/* tls_set_ca_certificate_info() loads the CA certificate +/* information for the specified TLS server or client context. +/* The result is -1 on failure, 0 on success. +/* +/* tls_set_my_certificate_key_info() loads the public key +/* certificates and private keys for the specified TLS server +/* or client context. Up to 3 pairs of key pairs (RSA, DSA and +/* ECDSA) may be specified; each certificate and key pair must +/* match. The chain_files argument makes it possible to load +/* keys and certificates for more than 3 algorithms, via either +/* a single file, or a list of multiple files. The result is -1 +/* on failure, 0 on success. +/* +/* tls_load_pem_chain() loads one or more (key, cert, [chain]) +/* triples from an in-memory PEM blob. The "origin" argument +/* is used for error logging, to identify the provenance of the +/* PEM blob. "ssl" must be non-zero, and the keys and certificates +/* will be loaded into that object. +/* LICENSE +/* .ad +/* .fi +/* This software is free. You can do with it whatever you want. +/* The original author kindly requests that you acknowledge +/* the use of his software. +/* AUTHOR(S) +/* Originally written by: +/* Lutz Jaenicke +/* BTU Cottbus +/* Allgemeine Elektrotechnik +/* Universitaetsplatz 3-4 +/* D-03044 Cottbus, Germany +/* +/* Updated by: +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +#ifdef USE_TLS + +/* Utility library. */ + +#include <msg.h> + +/* Global library. */ + +#include <mail_params.h> + +/* TLS library. */ + +#define TLS_INTERNAL +#include <tls.h> + +#define PEM_LOAD_STATE_NOGO -2 /* Unusable object or sequence */ +#define PEM_LOAD_STATE_FAIL -1 /* Error in libcrypto */ +#define PEM_LOAD_STATE_DONE 0 /* End of PEM file, return value only */ +#define PEM_LOAD_STATE_INIT 1 /* No PEM objects seen */ +#define PEM_LOAD_STATE_PKEY 2 /* Last object was a private key */ +#define PEM_LOAD_STATE_CERT 3 /* Last object was a certificate */ +#define PEM_LOAD_STATE_BOTH 4 /* Unordered, key + first cert seen */ + +#define PEM_LOAD_READ_LAST 0 /* Reading last file */ +#define PEM_LOAD_READ_MORE 1 /* More files to be read */ + +typedef struct pem_load_state_t { + const char *origin; /* PEM chain origin description */ + const char *source; /* PEM BIO origin description */ + const char *keysrc; /* Source of last key */ + BIO *pembio; /* PEM input stream */ + SSL_CTX *ctx; /* SSL connection factory */ + SSL *ssl; /* SSL connection handle */ + EVP_PKEY *pkey; /* current key */ + X509 *cert; /* current certificate */ + x509_stack_t *chain; /* current chain */ + int keynum; /* Index of last key */ + int objnum; /* Index in current source */ + int state; /* Current state, never "DONE" */ + int mixed; /* Single file with key anywhere */ +} pem_load_state_t; + +/* init_pem_load_state - fill in initial pem_load_state structure */ + +static void init_pem_load_state(pem_load_state_t *st, SSL_CTX *ctx, SSL *ssl, + const char *origin) +{ + st->origin = origin; + st->source = origin; + st->keysrc = 0; + st->pembio = 0; + st->ctx = ctx; + st->ssl = ssl; + st->pkey = 0; + st->cert = 0; + st->chain = 0; + st->keynum = 0; + st->objnum = 0; + st->state = PEM_LOAD_STATE_INIT; + st->mixed = 0; +} + +/* use_chain - load cert, key and chain into ctx or ssl */ + +static int use_chain(pem_load_state_t *st) +{ + int ret; + int replace = 0; + + /* + * With replace == 0, an error is returned if the algorithm slot is + * already taken, and a previous key + chain of the same type would be + * clobbered. + */ + if (st->ctx) + ret = SSL_CTX_use_cert_and_key(st->ctx, st->cert, st->pkey, st->chain, + replace); + else + ret = SSL_use_cert_and_key(st->ssl, st->cert, st->pkey, st->chain, + replace); + + /* + * SSL_[CTX_]_use_cert_key() uprefs all the objects in question, so we + * must free ours. + */ + X509_free(st->cert); + st->cert = 0; + EVP_PKEY_free(st->pkey); + st->pkey = 0; + sk_X509_pop_free(st->chain, X509_free); + st->chain = 0; + + return ret; +} + +/* load_cert - decode and load a DER-encoded X509 certificate */ + +static void load_cert(pem_load_state_t *st, unsigned char *buf, + long buflen) +{ + const unsigned char *p = buf; + X509 *cert = d2i_X509(0, &p, buflen); + + /* + * When expecting one or more keys, each key must precede the associated + * certificate (chain). + */ + if (!st->mixed && st->state == PEM_LOAD_STATE_INIT) { + msg_warn("error loading chain from %s: key not first", st->source); + if (cert) + X509_free(cert); + st->state = PEM_LOAD_STATE_NOGO; + return; + } + if (!cert) { + msg_warn("error loading certificate (PEM object number %d) from %s", + st->objnum, st->source); + st->state = PEM_LOAD_STATE_FAIL; + return; + } + if (p - buf != buflen) { + msg_warn("error loading certificate (PEM object number %d) from %s:" + " excess data", st->objnum, st->source); + X509_free(cert); + st->state = PEM_LOAD_STATE_NOGO; + return; + } + + /* + * The first certificate after a new key becomes the leaf certificate for + * that key. Subsequent certificates are added to the issuer chain. + * + * In "mixed" mode, the first certificate is either after the key, or else + * comes first. + */ + switch (st->state) { + case PEM_LOAD_STATE_PKEY: + st->cert = cert; + st->state = st->mixed ? PEM_LOAD_STATE_BOTH : PEM_LOAD_STATE_CERT; + return; + case PEM_LOAD_STATE_INIT: + st->cert = cert; + st->state = PEM_LOAD_STATE_CERT; + return; + case PEM_LOAD_STATE_CERT: + case PEM_LOAD_STATE_BOTH: + if ((!st->chain && (st->chain = sk_X509_new_null()) == 0) + || !sk_X509_push(st->chain, cert)) { + X509_free(cert); + st->state = PEM_LOAD_STATE_FAIL; + } + return; + } +} + +/* load_pkey - decode and load a DER-encoded private key */ + +static void load_pkey(pem_load_state_t *st, int pkey_type, + unsigned char *buf, long buflen) +{ + const char *myname = "load_pkey"; + const unsigned char *p = buf; + PKCS8_PRIV_KEY_INFO *p8; + EVP_PKEY *pkey = 0; + + /* + * Keys are either algorithm-specific, or else (ideally) algorithm + * agnostic, in which case they are wrapped as PKCS#8 objects with an + * algorithm OID. + */ + if (pkey_type != NID_undef) { + pkey = d2i_PrivateKey(pkey_type, 0, &p, buflen); + } else { + p8 = d2i_PKCS8_PRIV_KEY_INFO(NULL, &p, buflen); + if (p8) { + pkey = EVP_PKCS82PKEY(p8); + PKCS8_PRIV_KEY_INFO_free(p8); + } + } + + /* + * Except in "mixed" mode, where a single key appears anywhere in a file + * with multiple certificates, a given key is either at the first object + * we process, or occurs after a previous key and one or more associated + * certificates. Thus, encountering a key in a state other than "INIT" + * or "CERT" is an error, except in "mixed" mode where a second key is + * ignored with a warning. + */ + switch (st->state) { + case PEM_LOAD_STATE_CERT: + + /* + * When processing the key of a "next" chain, we're in the "CERT" + * state, and first complete the processing of the previous chain. + */ + if (!st->mixed && !use_chain(st)) { + msg_warn("error loading certificate chain: " + "key at index %d in %s does not match the certificate", + st->keynum, st->keysrc); + st->state = PEM_LOAD_STATE_FAIL; + return; + } + /* FALLTHROUGH */ + case PEM_LOAD_STATE_INIT: + + if (!pkey) { + msg_warn("error loading private key (PEM object number %d) from %s", + st->objnum, st->source); + st->state = PEM_LOAD_STATE_FAIL; + return; + } + /* Reject unexpected data beyond the end of the DER-encoded object */ + if (p - buf != buflen) { + msg_warn("error loading private key (PEM object number %d) from" + " %s: excess data", st->objnum, st->source); + EVP_PKEY_free(pkey); + st->state = PEM_LOAD_STATE_NOGO; + return; + } + /* All's well, update the state */ + st->pkey = pkey; + if (st->state == PEM_LOAD_STATE_INIT) + st->state = PEM_LOAD_STATE_PKEY; + else if (st->mixed) + st->state = PEM_LOAD_STATE_BOTH; + else + st->state = PEM_LOAD_STATE_PKEY; + return; + + case PEM_LOAD_STATE_PKEY: + case PEM_LOAD_STATE_BOTH: + if (pkey) + EVP_PKEY_free(pkey); + + /* XXX: Legacy behavior was silent, should we stay silent? */ + if (st->mixed) { + msg_warn("ignoring 2nd key at index %d in %s after 1st at %d", + st->objnum, st->source, st->keynum); + return; + } + /* else back-to-back keys */ + msg_warn("error loading certificate chain: " + "key at index %d in %s not followed by a certificate", + st->keynum, st->keysrc); + st->state = PEM_LOAD_STATE_NOGO; + return; + + default: + msg_error("%s: internal error: bad state: %d", myname, st->state); + st->state = PEM_LOAD_STATE_NOGO; + return; + } +} + +/* load_pem_object - load next pkey or cert from open BIO */ + +static int load_pem_object(pem_load_state_t *st) +{ + char *name = 0; + char *header = 0; + unsigned char *buf = 0; + long buflen; + int pkey_type = NID_undef; + + if (!PEM_read_bio(st->pembio, &name, &header, &buf, &buflen)) { + if (ERR_GET_REASON(ERR_peek_last_error()) != PEM_R_NO_START_LINE) + return (st->state = PEM_LOAD_STATE_FAIL); + + ERR_clear_error(); + /* Clean EOF, preserve stored state for any next input file */ + return (PEM_LOAD_STATE_DONE); + } + if (strcmp(name, PEM_STRING_X509) == 0 + || strcmp(name, PEM_STRING_X509_OLD) == 0) { + load_cert(st, buf, buflen); + } else if (strcmp(name, PEM_STRING_PKCS8INF) == 0 + || ((pkey_type = EVP_PKEY_RSA) != NID_undef + && strcmp(name, PEM_STRING_RSA) == 0) + || ((pkey_type = EVP_PKEY_EC) != NID_undef + && strcmp(name, PEM_STRING_ECPRIVATEKEY) == 0) + || ((pkey_type = EVP_PKEY_DSA) != NID_undef + && strcmp(name, PEM_STRING_DSA) == 0)) { + load_pkey(st, pkey_type, buf, buflen); + } else if (!st->mixed) { + msg_warn("loading %s: ignoring PEM type: %s", st->source, name); + } + OPENSSL_free(name); + OPENSSL_free(header); + OPENSSL_free(buf); + return (st->state); +} + +/* load_pem_bio - load all key/certs from bio and free the bio */ + +static int load_pem_bio(pem_load_state_t *st, int more) +{ + int state = st->state; + + /* Don't report old news */ + ERR_clear_error(); + + /* + * When "more" is PEM_LOAD_READ_MORE, more files will be loaded after the + * current file, and final processing for the last key and chain is + * deferred. + * + * When "more" is PEM_LOAD_READ_LAST, this is the last file in the list, and + * we validate the final chain. + * + * When st->mixed is true, this is the only file, and its key can occur at + * any location. In this case we load at most one key. + */ + for (st->objnum = 1; state > PEM_LOAD_STATE_DONE; ++st->objnum) { + state = load_pem_object(st); + if ((st->mixed && st->keynum == 0 && + (state == PEM_LOAD_STATE_PKEY || state == PEM_LOAD_STATE_BOTH)) + || (!st->mixed && state == PEM_LOAD_STATE_PKEY)) { + /* Squirrel-away the current key location */ + st->keynum = st->objnum; + st->keysrc = st->source; + } + } + /* We're responsible for unconditionally freeing the BIO */ + BIO_free(st->pembio); + + /* Success with current file, go back for more? */ + if (more == PEM_LOAD_READ_MORE && state >= PEM_LOAD_STATE_DONE) + return 0; + + /* + * If all is well so far, complete processing for the final chain. + */ + switch (st->state) { + case PEM_LOAD_STATE_FAIL: + tls_print_errors(); + break; + default: + break; + case PEM_LOAD_STATE_INIT: + msg_warn("No PEM data in %s", st->origin); + break; + case PEM_LOAD_STATE_PKEY: + msg_warn("No certs for key at index %d in %s", st->keynum, st->keysrc); + break; + case PEM_LOAD_STATE_CERT: + if (st->mixed) { + msg_warn("No private key found in %s", st->origin); + break; + } + /* FALLTHROUGH */ + case PEM_LOAD_STATE_BOTH: + /* use_chain() frees the key and certs, and zeroes the pointers */ + if (use_chain(st)) + return (0); + msg_warn("key at index %d in %s does not match next certificate", + st->keynum, st->keysrc); + tls_print_errors(); + break; + } + /* Free any left-over unused keys and certs */ + EVP_PKEY_free(st->pkey); + X509_free(st->cert); + sk_X509_pop_free(st->chain, X509_free); + + msg_warn("error loading private keys and certificates from: %s: %s", + st->origin, st->ctx ? "disabling TLS support" : + "aborting TLS handshake"); + return (-1); +} + +/* load_chain_files - load sequence of (key, cert, [chain]) from files */ + +static int load_chain_files(SSL_CTX *ctx, const char *chain_files) +{ + pem_load_state_t st; + ARGV *files = argv_split(chain_files, CHARS_COMMA_SP); + char **filep; + int ret = 0; + int more; + + init_pem_load_state(&st, ctx, 0, chain_files); + for (filep = files->argv; ret == 0 && *filep; ++filep) { + st.source = *filep; + if ((st.pembio = BIO_new_file(st.source, "r")) == NULL) { + msg_warn("error opening chain file: %s: %m", st.source); + st.state = PEM_LOAD_STATE_NOGO; + break; + } + more = filep[1] ? PEM_LOAD_READ_MORE : PEM_LOAD_READ_LAST; + /* load_pem_bio() frees the BIO */ + ret = load_pem_bio(&st, more); + } + argv_free(files); + return (ret); +} + +/* load_mixed_file - load certs with single key anywhere in the file */ + +static int load_mixed_file(SSL_CTX *ctx, const char *file) +{ + pem_load_state_t st; + + init_pem_load_state(&st, ctx, 0, file); + if ((st.pembio = BIO_new_file(st.source, "r")) == NULL) { + msg_warn("error opening chain file: %s: %m", st.source); + return (-1); + } + st.mixed = 1; + /* load_pem_bio() frees the BIO */ + return load_pem_bio(&st, PEM_LOAD_READ_LAST); +} + +/* tls_set_ca_certificate_info - load Certification Authority certificates */ + +int tls_set_ca_certificate_info(SSL_CTX *ctx, const char *CAfile, + const char *CApath) +{ + if (*CAfile == 0) + CAfile = 0; + if (*CApath == 0) + CApath = 0; + +#define CA_PATH_FMT "%s%s%s" +#define CA_PATH_ARGS(var, nextvar) \ + var ? #var "=\"" : "", \ + var ? var : "", \ + var ? (nextvar ? "\", " : "\"") : "" + + if (CAfile || CApath) { + if (!SSL_CTX_load_verify_locations(ctx, CAfile, CApath)) { + msg_info("cannot load Certification Authority data, " + CA_PATH_FMT CA_PATH_FMT ": disabling TLS support", + CA_PATH_ARGS(CAfile, CApath), + CA_PATH_ARGS(CApath, 0)); + tls_print_errors(); + return (-1); + } + if (var_tls_append_def_CA && !SSL_CTX_set_default_verify_paths(ctx)) { + msg_info("cannot set default OpenSSL certificate verification " + "paths: disabling TLS support"); + tls_print_errors(); + return (-1); + } + } + return (0); +} + +/* set_cert_stuff - specify certificate and key information */ + +static int set_cert_stuff(SSL_CTX *ctx, const char *cert_type, + const char *cert_file, + const char *key_file) +{ + + /* + * When the certfile and keyfile are one and the same, load both in a + * single pass, avoiding potential race conditions during key rollover. + */ + if (strcmp(cert_file, key_file) == 0) + return (load_mixed_file(ctx, cert_file) == 0); + + /* + * We need both the private key (in key_file) and the public key + * certificate (in cert_file). + * + * Code adapted from OpenSSL apps/s_cb.c. + */ + ERR_clear_error(); + if (SSL_CTX_use_certificate_chain_file(ctx, cert_file) <= 0) { + msg_warn("cannot get %s certificate from file \"%s\": " + "disabling TLS support", cert_type, cert_file); + tls_print_errors(); + return (0); + } + if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) <= 0) { + msg_warn("cannot get %s private key from file \"%s\": " + "disabling TLS support", cert_type, key_file); + tls_print_errors(); + return (0); + } + + /* + * Sanity check. + */ + if (!SSL_CTX_check_private_key(ctx)) { + msg_warn("%s private key in %s does not match public key in %s: " + "disabling TLS support", cert_type, key_file, cert_file); + return (0); + } + return (1); +} + +/* tls_set_my_certificate_key_info - load client or server certificates/keys */ + +int tls_set_my_certificate_key_info(SSL_CTX *ctx, const char *chain_files, + const char *cert_file, + const char *key_file, + const char *dcert_file, + const char *dkey_file, + const char *eccert_file, + const char *eckey_file) +{ + + /* The "chain_files" parameter overrides all the legacy parameters */ + if (chain_files && *chain_files) + return load_chain_files(ctx, chain_files); + + /* + * Lack of certificates is fine so long as we are prepared to use + * anonymous ciphers. + */ + if (*cert_file && !set_cert_stuff(ctx, "RSA", cert_file, key_file)) + return (-1); /* logged */ + if (*dcert_file && !set_cert_stuff(ctx, "DSA", dcert_file, dkey_file)) + return (-1); /* logged */ +#ifndef OPENSSL_NO_ECDH + if (*eccert_file && !set_cert_stuff(ctx, "ECDSA", eccert_file, eckey_file)) + return (-1); /* logged */ +#else + if (*eccert_file) + msg_warn("ECDSA not supported. Ignoring ECDSA certificate file \"%s\"", + eccert_file); +#endif + return (0); +} + +/* tls_load_pem_chain - load in-memory PEM client or server chain */ + +int tls_load_pem_chain(SSL *ssl, const char *pem, const char *origin) +{ + static VSTRING *obuf; + pem_load_state_t st; + + if (!obuf) + obuf = vstring_alloc(100); + vstring_sprintf(obuf, "SNI data for %s", origin); + init_pem_load_state(&st, 0, ssl, vstring_str(obuf)); + + if ((st.pembio = BIO_new_mem_buf(pem, -1)) == NULL) { + msg_warn("error opening memory BIO for %s", st.origin); + tls_print_errors(); + return (-1); + } + /* load_pem_bio() frees the BIO */ + return (load_pem_bio(&st, PEM_LOAD_READ_LAST)); +} + +#ifdef TEST + +static NORETURN usage(void) +{ + fprintf(stderr, "usage: tls_certkey [-m] <chainfiles>\n"); + exit(1); +} + +int main(int argc, char *argv[]) +{ + int ch; + int mixed = 0; + int ret; + char *key_file = 0; + SSL_CTX *ctx; + + if (!(ctx = SSL_CTX_new(TLS_client_method()))) { + tls_print_errors(); + exit(1); + } + while ((ch = GETOPT(argc, argv, "mk:")) > 0) { + switch (ch) { + case 'k': + key_file = optarg; + break; + case 'm': + mixed = 1; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (argc < 1) + usage(); + + if (key_file) + ret = set_cert_stuff(ctx, "any", argv[0], key_file) == 0; + else if (mixed) + ret = load_mixed_file(ctx, argv[0]); + else + ret = load_chain_files(ctx, argv[0]); + + if (ret != 0) + exit(1); + + if (SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST) != 1) { + fprintf(stderr, "error selecting first certificate\n"); + tls_print_errors(); + exit(1); + } + do { + STACK_OF(X509) *chain; + int i; + + if (SSL_CTX_get0_chain_certs(ctx, &chain) != 1) { + fprintf(stderr, "error locating certificate chain\n"); + tls_print_errors(); + exit(1); + } + for (i = 0; i <= sk_X509_num(chain); ++i) { + char buf[CCERT_BUFSIZ]; + X509 *cert; + + if (i > 0) + cert = sk_X509_value(chain, i - 1); + else + cert = SSL_CTX_get0_certificate(ctx); + + printf("depth = %d\n", i); + + X509_NAME_oneline(X509_get_issuer_name(cert), buf, sizeof(buf)); + printf("issuer = %s\n", buf); + + X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf)); + printf("subject = %s\n\n", buf); + } + } while (SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT) != 0); + + exit(0); +} + +#endif + +#endif |