diff options
Diffstat (limited to 'usr.sbin/smtpd/ssl.c')
-rw-r--r-- | usr.sbin/smtpd/ssl.c | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/usr.sbin/smtpd/ssl.c b/usr.sbin/smtpd/ssl.c new file mode 100644 index 00000000000..28a5284d250 --- /dev/null +++ b/usr.sbin/smtpd/ssl.c @@ -0,0 +1,496 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/param.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <ctype.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> +#include <openssl/engine.h> +#include <openssl/err.h> + +#include "smtpd.h" + +#define SSL_CIPHERS "HIGH:!ADH" + +void ssl_error(const char *); +char *ssl_load_file(const char *, off_t *); +SSL_CTX *ssl_ctx_create(void); +void ssl_session_accept(int, short, void *); +void ssl_read(int, short, void *); +void ssl_write(int, short, void *); +int ssl_bufferevent_add(struct event *, int); + +extern void bufferevent_read_pressure_cb(struct evbuffer *, size_t, + size_t, void *); + +void +ssl_read(int fd, short event, void *p) +{ + struct bufferevent *bufev = p; + struct session *s = bufev->cbarg; + int ret; + int ssl_err; + short what; + size_t len; + char rbuf[READ_BUF_SIZE]; + int howmuch = READ_BUF_SIZE; + + what = EVBUFFER_READ; + ret = ssl_err = 0; + + if (event == EV_TIMEOUT) { + what |= EVBUFFER_TIMEOUT; + goto err; + } + + if (bufev->wm_read.high != 0) + howmuch = MIN(sizeof(rbuf), bufev->wm_read.high); + + ret = SSL_read(s->s_ssl, rbuf, howmuch); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + goto retry; + case SSL_ERROR_WANT_WRITE: + goto retry; + default: + if (ret == 0) + what |= EVBUFFER_EOF; + else { + ssl_error("ssl_read"); + what |= EVBUFFER_ERROR; + } + goto err; + } + } + + if (evbuffer_add(bufev->input, rbuf, ret) == -1) { + what |= EVBUFFER_ERROR; + goto err; + } + + ssl_bufferevent_add(&bufev->ev_read, bufev->timeout_read); + + len = EVBUFFER_LENGTH(bufev->input); + if (bufev->wm_read.low != 0 && len < bufev->wm_read.low) + return; + if (bufev->wm_read.high != 0 && len > bufev->wm_read.high) { + struct evbuffer *buf = bufev->input; + event_del(&bufev->ev_read); + evbuffer_setcb(buf, bufferevent_read_pressure_cb, bufev); + return; + } + + if (bufev->readcb != NULL) + (*bufev->readcb)(bufev, bufev->cbarg); + return; + +retry: + ssl_bufferevent_add(&bufev->ev_read, bufev->timeout_read); + return; + +err: + (*bufev->errorcb)(bufev, what, bufev->cbarg); +} + + +void +ssl_write(int fd, short event, void *p) +{ + struct bufferevent *bufev = p; + struct session *s = bufev->cbarg; + int ret; + int ssl_err; + short what; + + ret = 0; + what = EVBUFFER_WRITE; + + if (event == EV_TIMEOUT) { + what |= EV_TIMEOUT; + goto err; + } + + if (EVBUFFER_LENGTH(bufev->output)) { + if (s->s_buf == NULL) { + s->s_buflen = EVBUFFER_LENGTH(bufev->output); + if ((s->s_buf = malloc(s->s_buflen)) == NULL) { + what |= EVBUFFER_ERROR; + goto err; + } + memcpy(s->s_buf, EVBUFFER_DATA(bufev->output), + s->s_buflen); + } + + ret = SSL_write(s->s_ssl, s->s_buf, s->s_buflen); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + goto retry; + case SSL_ERROR_WANT_WRITE: + goto retry; + default: + if (ret == 0) + what |= EVBUFFER_EOF; + else { + ssl_error("ssl_write"); + what |= EVBUFFER_ERROR; + } + goto err; + } + } + evbuffer_drain(bufev->output, ret); + } + if (s->s_buf != NULL) { + free(s->s_buf); + s->s_buf = NULL; + s->s_buflen = 0; + } + if (EVBUFFER_LENGTH(bufev->output) != 0) + ssl_bufferevent_add(&bufev->ev_write, bufev->timeout_write); + + if (bufev->writecb != NULL && + EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low) + (*bufev->writecb)(bufev, bufev->cbarg); + return; + +retry: + if (s->s_buflen != 0) + ssl_bufferevent_add(&bufev->ev_write, bufev->timeout_write); + return; + +err: + if (s->s_buf != NULL) { + free(s->s_buf); + s->s_buf = NULL; + s->s_buflen = 0; + } + (*bufev->errorcb)(bufev, what, bufev->cbarg); +} + +int +ssl_bufferevent_add(struct event *ev, int timeout) +{ + struct timeval tv; + struct timeval *ptv = NULL; + + if (timeout) { + timerclear(&tv); + tv.tv_sec = timeout; + ptv = &tv; + } + + return (event_add(ev, ptv)); +} + +int +ssl_cmp(struct ssl *s1, struct ssl *s2) +{ + return (strcmp(s1->ssl_name, s2->ssl_name)); +} + +SPLAY_GENERATE(ssltree, ssl, ssl_nodes, ssl_cmp); + +char * +ssl_load_file(const char *name, off_t *len) +{ + struct stat st; + off_t size; + char *buf = NULL; + int fd; + + if ((fd = open(name, O_RDONLY)) == -1) + return (NULL); + if (fstat(fd, &st) != 0) + goto fail; + size = st.st_size; + if ((buf = calloc(1, size + 1)) == NULL) + goto fail; + if (read(fd, buf, size) != size) + goto fail; + close(fd); + + *len = size + 1; + return (buf); + +fail: + if (buf != NULL) + free(buf); + close(fd); + return (NULL); +} + +SSL_CTX * +ssl_ctx_create(void) +{ + SSL_CTX *ctx; + + ctx = SSL_CTX_new(SSLv23_method()); + if (ctx == NULL) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not create SSL context"); + } + + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + SSL_CTX_set_timeout(ctx, SMTPD_SESSION_TIMEOUT); + SSL_CTX_set_options(ctx, SSL_OP_ALL); + SSL_CTX_set_options(ctx, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + if (!SSL_CTX_set_cipher_list(ctx, SSL_CIPHERS)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not set cipher list"); + } + return (ctx); +} + +int +ssl_load_certfile(struct smtpd *env, const char *name) +{ + struct ssl *s; + struct ssl key; + char certfile[PATH_MAX]; + + if (strlcpy(key.ssl_name, name, sizeof(key.ssl_name)) + >= sizeof(key.ssl_name)) { + log_warn("ssl_load_certfile: certificate name truncated"); + return -1; + } + + s = SPLAY_FIND(ssltree, &env->sc_ssl, &key); + if (s != NULL) + return 0; + + if ((s = calloc(1, sizeof(*s))) == NULL) + fatal(NULL); + + (void)strlcpy(s->ssl_name, key.ssl_name, sizeof(s->ssl_name)); + + if (snprintf(certfile, sizeof(certfile), + "/etc/mail/certs/%s.crt", name) == -1) { + free(s); + return (-1); + } + + if ((s->ssl_cert = ssl_load_file(certfile, &s->ssl_cert_len)) == NULL) { + free(s); + return (-1); + } + + if (snprintf(certfile, sizeof(certfile), + "/etc/mail/certs/%s.key", name) == -1) { + free(s->ssl_cert); + free(s); + return -1; + } + + if ((s->ssl_key = ssl_load_file(certfile, &s->ssl_key_len)) == NULL) { + free(s->ssl_cert); + free(s); + return (-1); + } + + if (s->ssl_cert == NULL || s->ssl_key == NULL) + fatal("invalid certificates"); + + SPLAY_INSERT(ssltree, &env->sc_ssl, s); + + return (0); +} + +void +ssl_init(void) +{ + SSL_library_init(); + SSL_load_error_strings(); + + OpenSSL_add_all_algorithms(); + + /* Init hardware crypto engines. */ + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); +} + +void +ssl_setup(struct smtpd *env, struct listener *l) +{ + struct ssl key; + + if (!(l->flags & F_SSL)) + return; + + if (strlcpy(key.ssl_name, l->ssl_cert_name, sizeof(key.ssl_name)) + >= sizeof(key.ssl_name)) + fatal("ssl_setup: certificate name truncated"); + + if ((l->ssl = SPLAY_FIND(ssltree, &env->sc_ssl, &key)) == NULL) + fatal("ssl_setup: certificate tree corrupted"); + + l->ssl_ctx = ssl_ctx_create(); + + if (!ssl_ctx_use_certificate_chain(l->ssl_ctx, + l->ssl->ssl_cert, l->ssl->ssl_cert_len)) + goto err; + if (!ssl_ctx_use_private_key(l->ssl_ctx, + l->ssl->ssl_key, l->ssl->ssl_key_len)) + goto err; + + if (!SSL_CTX_check_private_key(l->ssl_ctx)) + goto err; + if (!SSL_CTX_set_session_id_context(l->ssl_ctx, + (const unsigned char *)l->ssl_cert_name, strlen(l->ssl_cert_name) + 1)) + goto err; + + log_debug("ssl_setup: ssl setup finished for listener: %p", l); + return; + +err: + if (l->ssl_ctx != NULL) + SSL_CTX_free(l->ssl_ctx); + ssl_error("ssl_setup"); + fatal("ssl_setup: cannot set SSL up"); + return; +} + +void +ssl_error(const char *where) +{ + unsigned long code; + char errbuf[128]; + extern int debug; + + if (!debug) + return; + for (; (code = ERR_get_error()) != 0 ;) { + ERR_error_string_n(code, errbuf, sizeof(errbuf)); + log_debug("SSL library error: %s: %s", where, errbuf); + } +} + +void +ssl_session_accept(int fd, short event, void *p) +{ + struct session *s = p; + int ret; + int retry_flag; + int ssl_err; + + if (event == EV_TIMEOUT) { + log_debug("ssl_session_accept: session timed out"); + session_destroy(s); + return; + } + + retry_flag = ssl_err = 0; + + log_debug("ssl_session_accept: accepting client"); + ret = SSL_accept(s->s_ssl); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + retry_flag = EV_READ; + goto retry; + case SSL_ERROR_WANT_WRITE: + retry_flag = EV_WRITE; + goto retry; + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_SYSCALL: + if (ret == 0) { + session_destroy(s); + return; + } + /* FALLTHROUGH */ + default: + ssl_error("ssl_session_accept"); + session_destroy(s); + return; + } + } + + event_set(&s->s_bev->ev_read, s->s_fd, EV_READ, ssl_read, s->s_bev); + event_set(&s->s_bev->ev_write, s->s_fd, EV_WRITE, ssl_write, s->s_bev); + + log_info("ssl_session_accept: accepted ssl client"); + s->s_flags |= F_SECURE; + session_pickup(s, NULL); + return; +retry: + event_add(&s->s_ev, &s->s_tv); +} + +void +ssl_session_init(struct session *s) +{ + struct listener *l; + SSL *ssl; + + l = s->s_l; + + if (!(l->flags & F_SSL)) + return; + + log_debug("ssl_session_init: switching to SSL"); + ssl = SSL_new(l->ssl_ctx); + if (ssl == NULL) + goto err; + + if (!SSL_set_ssl_method(ssl, SSLv23_server_method())) + goto err; + if (!SSL_set_fd(ssl, s->s_fd)) + goto err; + SSL_set_accept_state(ssl); + + s->s_ssl = ssl; + + s->s_tv.tv_sec = SMTPD_SESSION_TIMEOUT; + s->s_tv.tv_usec = 0; + event_set(&s->s_ev, s->s_fd, EV_READ|EV_TIMEOUT, ssl_session_accept, s); + event_add(&s->s_ev, &s->s_tv); + return; + + err: + if (ssl != NULL) + SSL_free(ssl); + ssl_error("ssl_session_init"); +} + +void +ssl_session_destroy(struct session *s) +{ + SSL_free(s->s_ssl); +} |