diff options
author | Pawel Salek <pawsa@damage.localdomain> | 2010-03-04 22:21:56 +0100 |
---|---|---|
committer | Pawel Salek <pawsa@damage.localdomain> | 2010-03-04 22:21:56 +0100 |
commit | af79dfa2d055053c1027e20cf08f0549dc2e9ada (patch) | |
tree | c1fbf971ebdf7913e0325313b701e7f2d2fa9c17 /smtp-tls.c | |
download | libesmtp-af79dfa2d055053c1027e20cf08f0549dc2e9ada.tar.gz libesmtp-af79dfa2d055053c1027e20cf08f0549dc2e9ada.tar.xz libesmtp-af79dfa2d055053c1027e20cf08f0549dc2e9ada.zip |
Initial Commit
Diffstat (limited to 'smtp-tls.c')
-rw-r--r-- | smtp-tls.c | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/smtp-tls.c b/smtp-tls.c new file mode 100644 index 0000000..90adfb4 --- /dev/null +++ b/smtp-tls.c @@ -0,0 +1,715 @@ +/* + * This file is part of libESMTP, a library for submission of RFC 2822 + * formatted electronic mail messages using the SMTP protocol described + * in RFC 2821. + * + * Copyright (C) 2001-2004 Brian Stafford <brian@stafford.uklinux.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +/* Support for the SMTP STARTTLS verb. + */ + + +#ifdef USE_TLS + +/* This stuff doesn't belong here */ +/* vvvvvvvvvvv */ +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> +#include <openssl/x509v3.h> +#include <openssl/err.h> +#include <missing.h> /* declarations for missing library functions */ +/* ^^^^^^^^^^^ */ + +#include <ctype.h> +#include "libesmtp-private.h" +#include "siobuf.h" +#include "protocol.h" + + +static int tls_init; +static SSL_CTX *starttls_ctx; +static smtp_starttls_passwordcb_t ctx_password_cb; +static void *ctx_password_cb_arg; + + +#ifdef USE_PTHREADS +#include <pthread.h> +static pthread_mutex_t starttls_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t *openssl_mutex; + +static void +openssl_mutexcb (int mode, int n, + const char *file __attribute__ ((unused)), + int line __attribute__ ((unused))) +{ + if (mode & CRYPTO_LOCK) + pthread_mutex_lock (&openssl_mutex[n]); + else + pthread_mutex_unlock (&openssl_mutex[n]); +} +#endif + +static int +starttls_init (void) +{ + if (tls_init) + return 1; + +#ifdef USE_PTHREADS + /* Set up mutexes for the OpenSSL library */ + if (openssl_mutex == NULL) + { + pthread_mutexattr_t attr; + int n; + + openssl_mutex = malloc (sizeof (pthread_mutex_t) * CRYPTO_num_locks ()); + if (openssl_mutex == NULL) + return 0; + pthread_mutexattr_init (&attr); + for (n = 0; n < CRYPTO_num_locks (); n++) + pthread_mutex_init (&openssl_mutex[n], &attr); + pthread_mutexattr_destroy (&attr); + CRYPTO_set_locking_callback (openssl_mutexcb); + } +#endif + tls_init = 1; + SSL_load_error_strings (); + SSL_library_init (); + return 1; +} + +/* This stuff is crude and doesn't belong here */ +/* vvvvvvvvvvv */ + +static const char * +get_home (void) +{ + return getenv ("HOME"); +} + +static char * +user_pathname (char buf[], size_t buflen, const char *tail) +{ + const char *home; + + home = get_home (); + snprintf (buf, buflen, "%s/.authenticate/%s", home, tail); + return buf; +} + +typedef enum { FILE_PROBLEM, FILE_NOT_PRESENT, FILE_OK } ckf_t; + +/* Check file exists, is a regular file and contains something */ +static ckf_t +check_file (const char *file) +{ + struct stat st; + + errno = 0; + if (stat (file, &st) < 0) + return (errno == ENOENT) ? FILE_NOT_PRESENT : FILE_PROBLEM; + /* File must be regular and contain something */ + if (!(S_ISREG (st.st_mode) && st.st_size > 0)) + return FILE_PROBLEM; + /* For now this check is paranoid. The way I figure it, the passwords + on the private keys will be intensely annoying, so people will + remove them. Therefore make them protect the files using very + restrictive file permissions. Only the owner should be able to + read or write the certificates and the user the app is running as + should own the files. */ + if ((st.st_mode & (S_IXUSR | S_IRWXG | S_IRWXO)) || st.st_uid != getuid ()) + return FILE_PROBLEM; + return FILE_OK; +} + +/* Check directory exists */ +static ckf_t +check_directory (const char *file) +{ + struct stat st; + + if (stat (file, &st) < 0) + return (errno == ENOENT) ? FILE_NOT_PRESENT : FILE_PROBLEM; + /* File must be a directory */ + if (!S_ISDIR (st.st_mode)) + return FILE_PROBLEM; + /* Paranoia - only permit owner rwx permissions */ + if ((st.st_mode & (S_IRWXG | S_IRWXO)) || st.st_uid != getuid ()) + return FILE_PROBLEM; + return FILE_OK; +} + +/* ^^^^^^^^^^^ */ + +/* Unusually this API does not require a smtp_session_t. The data + it sets is global. + + N.B. If this API is not called and OpenSSL requires a password, it + will supply a default callback which prompts on the user's tty. + This is likely to be undesired behaviour, so the app should + supply a callback using this function. + */ +int +smtp_starttls_set_password_cb (smtp_starttls_passwordcb_t cb, void *arg) +{ + SMTPAPI_CHECK_ARGS (cb != NULL, 0); + +#ifdef USE_PTHREADS + pthread_mutex_lock (&starttls_mutex); +#endif + ctx_password_cb = cb; + ctx_password_cb_arg = arg; +#ifdef USE_PTHREADS + pthread_mutex_unlock (&starttls_mutex); +#endif + return 1; +} + +static SSL_CTX * +starttls_create_ctx (smtp_session_t session) +{ + SSL_CTX *ctx; + char buf[2048]; + char buf2[2048]; + char *keyfile, *cafile, *capath; + ckf_t status; + + /* The decision not to support SSL v2 and v3 but instead to use only + TLSv1 is deliberate. This is in line with the intentions of RFC + 3207. Servers typically support SSL as well as TLS because some + versions of Netscape do not support TLS. I am assuming that all + currently deployed servers correctly support TLS. */ + ctx = SSL_CTX_new (TLSv1_client_method ()); + + /* Load our keys and certificates. To avoid messing with configuration + variables etc, use fixed paths for the certificate store. These are + as follows :- + + ~/.authenticate/private/smtp-starttls.pem + the user's certificate and private key + ~/.authenticate/ca.pem + the user's trusted CA list + ~/.authenticate/ca + the user's hashed CA directory + + Host specific stuff follows the same structure except that its + below ~/.authenticate/host.name + + It probably makes sense to check that the directories and/or files + are readable only by the user who owns them. + + This structure will certainly change. I'm on a voyage of discovery + here! Eventually, this code and the SASL stuff will become a + separate library used by libESMTP (libAUTH?). The general idea is + that ~/.authenticate will be used to store authentication + information for the user (eventually there might be a + ({/usr{/local}}/etc/authenticate for system wide stuff - CA lists + and CRLs for example). The "private" subdirectory is just to + separate out private info from others. There might be a "public" + directory too. Since the CA list is global (I think) I've put them + below .authenticate for now. Within the "private" and "public" + directories, certificates and other authentication data are named + according to their purpose (hence smtp-starttls.pem). Symlinks can + be used to avoid duplication where authentication tokens are shared + for several purposes. My reasoning here is that libESMTP (or any + client layered over the hypothetical libAUTH) will always want the + same authentication behaviour for a given service, regardless of + the application using it. + + XXX - The above isn't quite enough. Per-host directories are + required, e.g. a different smtp-starttls.pem might be needed for + different servers. This will not affect the trusted CAs though. + + XXX - (this comment belongs in auth-client.c) Ideally, the + ~/.authenticate hierarchy will be able to store SASL passwords + if required, preferably encrypted. Then the application would + not necessarily have to supply usernames and passwords via the + libESMTP API to be able to authenticate to a server. + */ + + /* Client certificate policy: if a client certificate is found at + ~/.authenticate/private/smtp-starttls.pem, it is presented to the + server if it requests it. The server may use the certificate to + perform authentication at its own discretion. */ + if (ctx_password_cb != NULL) + { + SSL_CTX_set_default_passwd_cb (ctx, ctx_password_cb); + SSL_CTX_set_default_passwd_cb_userdata (ctx, ctx_password_cb_arg); + } + keyfile = user_pathname (buf, sizeof buf, "private/smtp-starttls.pem"); + status = check_file (keyfile); + if (status == FILE_OK) + { + if (!SSL_CTX_use_certificate_file (ctx, keyfile, SSL_FILETYPE_PEM)) + { + /* FIXME: set an error code */ + return NULL; + } + if (!SSL_CTX_use_PrivateKey_file (ctx, keyfile, SSL_FILETYPE_PEM)) + { + int ok = 0; + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_NO_CLIENT_CERTIFICATE, + session->event_cb_arg, &ok); + if (!ok) + return NULL; + } + } + else if (status == FILE_PROBLEM) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_UNUSABLE_CLIENT_CERTIFICATE, + session->event_cb_arg, NULL); + return NULL; + } + + /* Server certificate policy: check the server certificate against the + trusted CA list to a depth of 1. */ + cafile = user_pathname (buf, sizeof buf, "ca.pem"); + status = check_file (cafile); + if (status == FILE_NOT_PRESENT) + cafile = NULL; + else if (status == FILE_PROBLEM) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_UNUSABLE_CA_LIST, + session->event_cb_arg, NULL); + return NULL; + } + capath = user_pathname (buf2, sizeof buf2, "ca"); + status = check_directory (capath); + if (status == FILE_NOT_PRESENT) + capath = NULL; + else if (status == FILE_PROBLEM) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_UNUSABLE_CA_LIST, + session->event_cb_arg, NULL); + return NULL; + } + + /* Load the CAs we trust */ + if (cafile != NULL || capath != NULL) + SSL_CTX_load_verify_locations (ctx, cafile, capath); + else + SSL_CTX_set_default_verify_paths (ctx); + + /* FIXME: load a source of randomness */ + + return ctx; +} + +static SSL * +starttls_create_ssl (smtp_session_t session) +{ + char buf[2048]; + char buf2[2048]; + char *keyfile; + SSL *ssl; + ckf_t status; + + ssl = SSL_new (session->starttls_ctx); + + /* Client certificate policy: if a host specific client certificate + is found at ~/.authenticate/host.name/private/smtp-starttls.pem, + it is presented to the server if it requests it. */ + + /* FIXME: when the default client certificate is loaded a passowrd may be + required. Then the code below might ask for one too. It + will be annoying when two passwords are needed and only one + is necessary. Also, the default certificate will only need + the password when the SSL_CTX is created. A host specific + certificate's password will be needed for every SMTP session + within an application. This needs a solution. */ + + /* FIXME: in protocol.c, record the canonic name of the host returned + by getaddrinfo. Use that instead of session->host. */ + snprintf (buf2, sizeof buf2, "%s/private/smtp-starttls.pem", session->host); + keyfile = user_pathname (buf, sizeof buf, buf2); + status = check_file (keyfile); + if (status == FILE_OK) + { + if (!SSL_use_certificate_file (ssl, keyfile, SSL_FILETYPE_PEM)) + { + /* FIXME: set an error code */ + return NULL; + } + if (!SSL_use_PrivateKey_file (ssl, keyfile, SSL_FILETYPE_PEM)) + { + int ok = 0; + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_NO_CLIENT_CERTIFICATE, + session->event_cb_arg, &ok); + if (!ok) + return NULL; + } + } + else if (status == FILE_PROBLEM) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_UNUSABLE_CLIENT_CERTIFICATE, + session->event_cb_arg, NULL); + return NULL; + } + + return ssl; +} + +/* App calls this to allow libESMTP to use an SSL_CTX it has already + initialised. NULL means use a default created by libESMTP. + If called at all, libESMTP assumes the application has initialised + openssl. Otherwise, libESMTP will initialise OpenSSL before calling + any of the SSL APIs. */ +int +smtp_starttls_set_ctx (smtp_session_t session, SSL_CTX *ctx) +{ + SMTPAPI_CHECK_ARGS (session != NULL, 0); + + tls_init = 1; /* Assume app has set up openssl */ + session->starttls_ctx = ctx; + return 1; +} + +/* how == 0: disabled, 1: if possible, 2: required */ +int +smtp_starttls_enable (smtp_session_t session, enum starttls_option how) +{ + SMTPAPI_CHECK_ARGS (session != NULL, 0); + + session->starttls_enabled = how; + if (how == Starttls_REQUIRED) + session->required_extensions |= EXT_STARTTLS; + else + session->required_extensions &= ~EXT_STARTTLS; + return 1; +} + +int +select_starttls (smtp_session_t session) +{ + if (session->using_tls || session->authenticated) + return 0; + /* FIXME: if the server has reported the TLS extension in a previous + session promote Starttls_ENABLED to Starttls_REQUIRED. + If this session does not offer STARTTLS, this will force + protocol.c to report the extension as not available and QUIT + as reccommended in RFC 3207. This requires some form of db + storage to record this for future sessions. */ + /* if (...) + session->starttls_enabled = Starttls_REQUIRED; */ + if (!(session->extensions & EXT_STARTTLS)) + return 0; + if (!session->starttls_enabled) + return 0; +#ifdef USE_PTHREADS + pthread_mutex_lock (&starttls_mutex); +#endif + if (starttls_ctx == NULL && starttls_init ()) + starttls_ctx = starttls_create_ctx (session); +#ifdef USE_PTHREADS + pthread_mutex_unlock (&starttls_mutex); +#endif + session->starttls_ctx = starttls_ctx; + return session->starttls_ctx != NULL; +} + +static int +match_component (const char *dom, const char *edom, + const char *ref, const char *eref) +{ + while (dom < edom && ref < eref) + { + /* Accept a final '*' in the reference as a wildcard */ + if (*ref == '*' && ref + 1 == eref) + break; + /* compare the domain name case insensitive */ + if (!(*dom == *ref || tolower (*dom) == tolower (*ref))) + return 0; + ref++, dom++; + } + return 1; +} + +/* Perform a domain name comparison where the reference may contain + wildcards. This implements the comparison from RFC 2818. + Each component of the domain name is matched separately, working from + right to left. + */ +static int +match_domain (const char *domain, const char *reference) +{ + const char *dom, *edom, *ref, *eref; + + eref = strchr (reference, '\0'); + edom = strchr (domain, '\0'); + while (eref > reference && edom > domain) + { + /* Find the rightmost component of the reference. */ + ref = memrchr (reference, '.', eref - reference - 1); + if (ref != NULL) + ref++; + else + ref = reference; + + /* Find the rightmost component of the domain name. */ + dom = memrchr (domain, '.', edom - domain - 1); + if (dom != NULL) + dom++; + else + dom = domain; + + if (!match_component (dom, edom, ref, eref)) + return 0; + edom = dom - 1; + eref = ref - 1; + } + return eref < reference && edom < domain; +} + +static int +check_acceptable_security (smtp_session_t session, SSL *ssl) +{ + X509 *cert; + char buf[256]; + int bits; + long vfy_result; + int ok; + + /* Check certificate validity. + */ + vfy_result = SSL_get_verify_result (ssl); + if (vfy_result != X509_V_OK) + { + ok = 0; + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_INVALID_PEER_CERTIFICATE, + session->event_cb_arg, vfy_result, &ok, ssl); + if (!ok) + return 0; +#if 0 + /* Not sure about the location of this call so leave it out for now + - from Pawel: the worst thing that can happen is that one can + get non-empty error log in wrong places. */ + ERR_clear_error (); /* we know what is going on, clear the error log */ +#endif + } + + /* Check cipher strength. Since we use only TLSv1 the cipher should + never be this weak. */ + bits = SSL_get_cipher_bits (ssl, NULL); + if (bits <= 40) + { + ok = 0; + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_WEAK_CIPHER, + session->event_cb_arg, bits, &ok); + if (!ok) + return 0; + } + + /* Check server credentials stored in the certificate. + */ + ok = 0; + cert = SSL_get_peer_certificate (ssl); + if (cert == NULL) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_NO_PEER_CERTIFICATE, + session->event_cb_arg, &ok); + } + else + { + int i, j, extcount; + + extcount = X509_get_ext_count (cert); + for (i = 0; i < extcount; i++) + { + const char *extstr; + X509_EXTENSION *ext = X509_get_ext (cert, i); + + extstr = OBJ_nid2sn (OBJ_obj2nid (X509_EXTENSION_get_object (ext))); + if (strcmp (extstr, "subjectAltName") == 0) + { + unsigned char *data; + STACK_OF(CONF_VALUE) *val; + CONF_VALUE *nval; + X509V3_EXT_METHOD *meth; + void *ext_str = NULL; + int stack_len; + + meth = X509V3_EXT_get (ext); + if (meth == NULL) + break; + data = ext->value->data; +#if (OPENSSL_VERSION_NUMBER > 0x00907000L) + if (meth->it) + ext_str = ASN1_item_d2i (NULL, &data, ext->value->length, + ASN1_ITEM_ptr (meth->it)); + else +#endif + ext_str = meth->d2i (NULL, &data, ext->value->length); + val = meth->i2v (meth, ext_str, NULL); + stack_len = sk_CONF_VALUE_num (val); + for (j = 0; j < stack_len; j++) + { + nval = sk_CONF_VALUE_value (val, j); + if (strcmp (nval->name, "DNS") == 0 + && match_domain (session->host, nval->value)) + { + ok = 1; + break; + } + } + } + if (ok) + break; + } + if (!ok) + { + /* Matching by subjectAltName failed, try commonName */ + X509_NAME_get_text_by_NID (X509_get_subject_name (cert), + NID_commonName, buf, sizeof buf); + if (!match_domain (session->host, buf) != 0) + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_WRONG_PEER_CERTIFICATE, + session->event_cb_arg, &ok, buf, ssl); + } + else + ok = 1; + } + X509_free (cert); + } + return ok; +} + +void +cmd_starttls (siobuf_t conn, smtp_session_t session) +{ + sio_write (conn, "STARTTLS\r\n", -1); + session->cmd_state = -1; +} + +void +rsp_starttls (siobuf_t conn, smtp_session_t session) +{ + int code; + SSL *ssl; + X509 *cert; + char buf[256]; + + code = read_smtp_response (conn, session, &session->mta_status, NULL); + if (code < 0) + { + session->rsp_state = S_quit; + return; + } + + if (code != 2) + { + if (code != 4 && code != 5) + set_error (SMTP_ERR_INVALID_RESPONSE_STATUS); + session->rsp_state = S_quit; + } + else if (sio_set_tlsclient_ssl (conn, (ssl = starttls_create_ssl (session)))) + { + session->using_tls = 1; + + /* Forget what we know about the server and reset protocol state. + */ + session->extensions = 0; + destroy_auth_mechanisms (session); + + if (!check_acceptable_security (session, ssl)) + session->rsp_state = S_quit; + else + { + if (session->event_cb != NULL) + (*session->event_cb) (session, SMTP_EV_STARTTLS_OK, + session->event_cb_arg, + ssl, SSL_get_cipher (ssl), + SSL_get_cipher_bits (ssl, NULL)); + cert = SSL_get_certificate (ssl); + if (cert != NULL) + { + /* Copy the common name [typically email address] from the + client certificate and use it to prime the SASL EXTERNAL + mechanism */ + X509_NAME_get_text_by_NID (X509_get_subject_name (cert), + NID_commonName, buf, sizeof buf); + X509_free (cert); + if (session->auth_context != NULL) + auth_set_external_id (session->auth_context, buf); + } + + /* Next state is EHLO */ + session->rsp_state = S_ehlo; + } + } + else + { + set_error (SMTP_ERR_CLIENT_ERROR); + session->rsp_state = -1; + } +} + +#else + +#define SSL_CTX void +#include "libesmtp-private.h" + +/* Fudge the declaration. The idea is that all builds of the library + export the same API, but that the unsupported features always fail. + This prototype is declared only if <openssl/ssl.h> is included. + Strict ANSI compiles require prototypes, so here it is! */ +int smtp_starttls_set_ctx (smtp_session_t session, SSL_CTX *ctx); + +int +smtp_starttls_set_ctx (smtp_session_t session, + SSL_CTX *ctx __attribute__ ((unused))) +{ + SMTPAPI_CHECK_ARGS (session != (smtp_session_t) 0, 0); + + return 0; +} + +int +smtp_starttls_enable (smtp_session_t session, + enum starttls_option how __attribute__ ((unused))) +{ + SMTPAPI_CHECK_ARGS (session != (smtp_session_t) 0, 0); + + return 0; +} + +int +smtp_starttls_set_password_cb (smtp_starttls_passwordcb_t cb + __attribute__ ((unused)), + void *arg __attribute__ ((unused))) +{ + return 0; +} + +#endif |