From 63dbb99337d0423253cb1ead0dcc3da54af5d13e Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Mon, 21 Apr 2014 16:36:56 -0400 Subject: [PATCH] Add mod_session support By setting GssapiUseSessions we enable the module to store a bearer token with the user and gss names in the client, this way we can allow clients to perform authentication once but then remain authenticaed for the duration of the session or until the original credentials expire. The Secure cookie used to store the token is encrypted using a randomly generated AES key at process startup. This means multiple apache servers will not be able to use the same cookie, however the client will reauth transparently if the cookie cannot be read. --- src/Makefile.am | 2 +- src/crypto.c | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/crypto.h | 17 ++++ src/mod_auth_gssapi.c | 65 +++++++++++----- src/mod_auth_gssapi.h | 25 +++++- src/sessions.c | 187 ++++++++++++++++++++++++++++++++++++++++++++ src/sessions.h | 10 +++ 7 files changed, 493 insertions(+), 23 deletions(-) create mode 100644 src/crypto.c create mode 100644 src/crypto.h create mode 100644 src/sessions.c create mode 100644 src/sessions.h diff --git a/src/Makefile.am b/src/Makefile.am index 95b85b3..2273194 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,5 +1,5 @@ all-local: - @APXS@ -c ${LIBS} ${CFLAGS} mod_auth_gssapi.c + @APXS@ -c ${LIBS} ${CFLAGS} mod_auth_gssapi.c sessions.c crypto.c install-exec-local: if test ! -d ${APXS_LIBEXECDIR}; then mkdir -p ${APXS_LIBEXECDIR}; fi diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 0000000..9be58e5 --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,210 @@ +/* Copyright (C) 2014 mod_auth_gssapi authors - See COPYING for (C) terms */ + +#include +#include +#include +#include +#include "crypto.h" + +struct seal_key { + const EVP_CIPHER *cipher; + const EVP_MD *md; + unsigned char *ekey; + unsigned char *hkey; +}; + +apr_status_t SEAL_KEY_CREATE(struct seal_key **skey) +{ + struct seal_key *n; + int ret; + + n = calloc(1, sizeof(*n)); + if (!n) return ENOMEM; + + n->cipher = EVP_aes_128_cbc(); + if (!n->cipher) { + free(n); + return EFAULT; + } + + n->md = EVP_sha256(); + if (!n->md) { + free(n); + return EFAULT; + } + + n->ekey = malloc(n->cipher->key_len); + if (!n->ekey) { + free(n); + return ENOMEM; + } + + n->hkey = malloc(n->cipher->key_len); + if (!n->hkey) { + free(n); + return ENOMEM; + } + + ret = RAND_bytes(n->ekey, n->cipher->key_len); + if (ret == 0) { + free(n->ekey); + free(n->hkey); + free(n); + return EFAULT; + } + + ret = RAND_bytes(n->hkey, n->cipher->key_len); + if (ret == 0) { + free(n->ekey); + free(n->hkey); + free(n); + return EFAULT; + } + + *skey = n; + return 0; +} + +apr_status_t SEAL_BUFFER(apr_pool_t *p, struct seal_key *skey, + struct databuf *plain, struct databuf *cipher) +{ + apr_status_t err = EFAULT; + EVP_CIPHER_CTX ctx = { 0 }; + HMAC_CTX hmac_ctx = { 0 }; + uint8_t rbuf[16]; + unsigned int len; + int outlen, totlen; + int ret; + + EVP_CIPHER_CTX_init(&ctx); + + /* confounder to avoid exposing random numbers directly to clients + * as IVs */ + ret = RAND_bytes(rbuf, 16); + if (ret == 0) goto done; + + if (cipher->length == 0) { + /* add space for confounder and padding and MAC */ + cipher->length = (plain->length / 16 + 2) * 16; + cipher->value = apr_palloc(p, cipher->length + skey->md->md_size); + if (!cipher->value) { + err = ENOMEM; + goto done; + } + } + + ret = EVP_EncryptInit_ex(&ctx, skey->cipher, NULL, skey->ekey, NULL); + if (ret == 0) goto done; + totlen = 0; + + outlen = cipher->length; + ret = EVP_EncryptUpdate(&ctx, cipher->value, &outlen, rbuf, 16); + if (ret == 0) goto done; + totlen += outlen; + + outlen = cipher->length - totlen; + ret = EVP_EncryptUpdate(&ctx, &cipher->value[totlen], &outlen, + plain->value, plain->length); + if (ret == 0) goto done; + totlen += outlen; + + outlen = cipher->length - totlen; + ret = EVP_EncryptFinal_ex(&ctx, &cipher->value[totlen], &outlen); + if (ret == 0) goto done; + totlen += outlen; + + /* now MAC the buffer */ + HMAC_CTX_init(&hmac_ctx); + + ret = HMAC_Init_ex(&hmac_ctx, skey->hkey, + skey->cipher->key_len, skey->md, NULL); + if (ret == 0) goto done; + + ret = HMAC_Update(&hmac_ctx, cipher->value, totlen); + if (ret == 0) goto done; + + ret = HMAC_Final(&hmac_ctx, &cipher->value[totlen], &len); + if (ret == 0) goto done; + + cipher->length = totlen + len; + err = 0; + +done: + EVP_CIPHER_CTX_cleanup(&ctx); + HMAC_CTX_cleanup(&hmac_ctx); + return err; +} + +apr_status_t UNSEAL_BUFFER(apr_pool_t *p, struct seal_key *skey, + struct databuf *cipher, struct databuf *plain) +{ + apr_status_t err = EFAULT; + EVP_CIPHER_CTX ctx = { 0 }; + HMAC_CTX hmac_ctx = { 0 }; + unsigned char mac[skey->md->md_size]; + unsigned int len; + int outlen, totlen; + volatile bool equal = true; + int ret, i; + + /* check MAC first */ + HMAC_CTX_init(&hmac_ctx); + + ret = HMAC_Init_ex(&hmac_ctx, skey->hkey, + skey->cipher->key_len, skey->md, NULL); + if (ret == 0) goto done; + + cipher->length -= skey->md->md_size; + + ret = HMAC_Update(&hmac_ctx, cipher->value, cipher->length); + if (ret == 0) goto done; + + ret = HMAC_Final(&hmac_ctx, mac, &len); + if (ret == 0) goto done; + + if (len != skey->md->md_size) goto done; + for (i = 0; i < skey->md->md_size; i++) { + if (cipher->value[cipher->length + i] != mac[i]) equal = false; + /* not breaking intentionally, + * or we would allow an oracle attack */ + } + if (!equal) goto done; + + EVP_CIPHER_CTX_init(&ctx); + + if (plain->length == 0) { + plain->length = cipher->length; + plain->value = apr_palloc(p, plain->length); + if (!plain->value) { + err = ENOMEM; + goto done; + } + } + + ret = EVP_DecryptInit_ex(&ctx, skey->cipher, NULL, skey->ekey, NULL); + if (ret == 0) goto done; + + totlen = 0; + outlen = plain->length; + ret = EVP_DecryptUpdate(&ctx, plain->value, &outlen, + cipher->value, cipher->length); + if (ret == 0) goto done; + + totlen += outlen; + outlen = plain->length - totlen; + ret = EVP_DecryptFinal_ex(&ctx, plain->value, &outlen); + if (ret == 0) goto done; + + totlen += outlen; + /* now remove the confounder */ + totlen -= 16; + memmove(plain->value, plain->value + 16, totlen); + + plain->length = totlen; + err = 0; + +done: + EVP_CIPHER_CTX_cleanup(&ctx); + HMAC_CTX_cleanup(&hmac_ctx); + return err; +} diff --git a/src/crypto.h b/src/crypto.h new file mode 100644 index 0000000..a8b5ca0 --- /dev/null +++ b/src/crypto.h @@ -0,0 +1,17 @@ +/* Copyright (C) 2014 mod_auth_gssapi authors - See COPYING for (C) terms */ + +#include +#include + +struct seal_key; + +struct databuf { + unsigned char *value; + int length; +}; + +apr_status_t SEAL_KEY_CREATE(struct seal_key **skey); +apr_status_t SEAL_BUFFER(apr_pool_t *p, struct seal_key *skey, + struct databuf *plain, struct databuf *cipher); +apr_status_t UNSEAL_BUFFER(apr_pool_t *p, struct seal_key *skey, + struct databuf *cipher, struct databuf *plain); diff --git a/src/mod_auth_gssapi.c b/src/mod_auth_gssapi.c index e8c1966..7e8df96 100644 --- a/src/mod_auth_gssapi.c +++ b/src/mod_auth_gssapi.c @@ -24,6 +24,7 @@ #include "mod_auth_gssapi.h" + module AP_MODULE_DECLARE_DATA auth_gssapi_module; APR_DECLARE_OPTIONAL_FN(int, ssl_is_https, (conn_rec *)); @@ -71,24 +72,16 @@ static char *mag_error(request_rec *req, const char *msg, static APR_OPTIONAL_FN_TYPE(ssl_is_https) *mag_is_https = NULL; -static int mag_post_config(apr_pool_t *cfg, apr_pool_t *log, +static int mag_post_config(apr_pool_t *cfgpool, apr_pool_t *log, apr_pool_t *temp, server_rec *s) { /* FIXME: create mutex to deal with connections and contexts ? */ mag_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); + mag_post_config_session(); return OK; } - -struct mag_conn { - apr_pool_t *parent; - gss_ctx_id_t ctx; - bool established; - char *user_name; - char *gss_name; -}; - static int mag_pre_connection(conn_rec *c, void *csd) { struct mag_conn *mc; @@ -138,6 +131,7 @@ static int mag_auth(request_rec *req) gss_name_t client = GSS_C_NO_NAME; gss_cred_id_t delegated_cred = GSS_C_NO_CREDENTIAL; uint32_t flags; + uint32_t vtime; uint32_t maj, min; char *reply; size_t replen; @@ -151,6 +145,11 @@ static int mag_auth(request_rec *req) return DECLINED; } + /* ignore auth for subrequests */ + if (!ap_is_initial_req(req)) { + return OK; + } + cfg = ap_get_module_config(req->per_dir_config, &auth_gssapi_module); if (cfg->ssl_only) { @@ -166,11 +165,24 @@ static int mag_auth(request_rec *req) req->connection->conn_config, &auth_gssapi_module); if (!mc) { - return DECLINED; + ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, 0, req, + "Failed to retrieve connection context!"); + goto done; } + } + + /* if available, session always supersedes connection bound data */ + mag_check_session(req, cfg, &mc); + + if (mc) { + /* register the context in the memory pool, so it can be freed + * when the connection/request is terminated */ + apr_pool_userdata_set(mc, "mag_conn_ptr", + mag_conn_destroy, mc->parent); + if (mc->established) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, 0, req, - "Connection bound pre-authentication found."); + "Already established context found!"); apr_table_set(req->subprocess_env, "GSS_NAME", mc->gss_name); req->ap_auth_type = apr_pstrdup(req->pool, "Negotiate"); req->user = apr_pstrdup(req->pool, mc->user_name); @@ -199,7 +211,7 @@ static int mag_auth(request_rec *req) maj = gss_accept_sec_context(&min, pctx, GSS_C_NO_CREDENTIAL, &input, GSS_C_NO_CHANNEL_BINDINGS, - &client, &mech_type, &output, &flags, NULL, + &client, &mech_type, &output, &flags, &vtime, &delegated_cred); if (GSS_ERROR(maj)) { ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, @@ -208,19 +220,17 @@ static int mag_auth(request_rec *req) goto done; } - /* register the context in the connection pool, so it can be freed - * when the connection is terminated */ - apr_pool_userdata_set(mc, "mag_conn_ptr", mag_conn_destroy, mc->parent); - if (maj == GSS_S_CONTINUE_NEEDED) { - if (!cfg->gss_conn_ctx) { + if (!mc) { ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, - "Mechanism needs continuation but " - "GssapiConnectionBound is off."); + "Mechanism needs continuation but neither " + "GssapiConnectionBound nor " + "GssapiUseSessions are available"); gss_delete_sec_context(&min, pctx, GSS_C_NO_BUFFER); gss_release_buffer(&min, &output); output.length = 0; } + /* auth not complete send token and wait next packet */ goto done; } @@ -263,6 +273,11 @@ static int mag_auth(request_rec *req) mc->user_name = apr_pstrdup(mc->parent, req->user); mc->gss_name = apr_pstrdup(mc->parent, clientname); mc->established = true; + if (vtime == GSS_C_INDEFINITE || vtime < MIN_SESS_EXP_TIME) { + vtime = MIN_SESS_EXP_TIME; + } + mc->expiration = time(NULL) + vtime; + mag_attempt_session(req, cfg, mc); } ret = OK; @@ -298,6 +313,7 @@ static void *mag_create_dir_config(apr_pool_t *p, char *dir) cfg = (struct mag_config *)apr_pcalloc(p, sizeof(struct mag_config)); if (!cfg) return NULL; + cfg->pool = p; return cfg; } @@ -323,6 +339,13 @@ static const char *mag_conn_ctx(cmd_parms *parms, void *mconfig, int on) return NULL; } +static const char *mag_use_sess(cmd_parms *parms, void *mconfig, int on) +{ + struct mag_config *cfg = (struct mag_config *)mconfig; + cfg->use_sessions = on ? true : false; + return NULL; +} + static const char *mag_cred_store(cmd_parms *parms, void *mconfig, const char *w) { @@ -376,6 +399,8 @@ static const command_rec mag_commands[] = { "Work only if connection is SSL Secured"), AP_INIT_FLAG("GssapiConnectionBound", mag_conn_ctx, NULL, OR_AUTHCFG, "Authentication is bound to the TCP connection"), + AP_INIT_FLAG("GssapiUseSessions", mag_use_sess, NULL, OR_AUTHCFG, + "Authentication uses mod_sessions to hold status"), AP_INIT_ITERATE("GssapiCredStore", mag_cred_store, NULL, OR_AUTHCFG, "Credential Store"), { NULL } diff --git a/src/mod_auth_gssapi.h b/src/mod_auth_gssapi.h index 2022061..6a21254 100644 --- a/src/mod_auth_gssapi.h +++ b/src/mod_auth_gssapi.h @@ -2,16 +2,21 @@ #include #include +#include #include #include +#define APR_WANT_STRFUNC +#include "apr_want.h" +#include +#include + #include #include #include #include #include -#include -#include +#include /* apache's httpd.h drags in empty PACKAGE_* variables. * undefine them to avoid annoying compile warnings as they @@ -23,10 +28,26 @@ #undef PACKAGE_VERSION #include "config.h" +#include "crypto.h" +#include "sessions.h" + +#define MIN_SESS_EXP_TIME 300 /* 5 minutes validity minimum */ + struct mag_config { + apr_pool_t *pool; bool ssl_only; bool map_to_local; bool gss_conn_ctx; + bool use_sessions; gss_key_value_set_desc cred_store; + struct seal_key *mag_skey; }; +struct mag_conn { + apr_pool_t *parent; + gss_ctx_id_t ctx; + bool established; + const char *user_name; + const char *gss_name; + time_t expiration; +}; diff --git a/src/sessions.c b/src/sessions.c new file mode 100644 index 0000000..766ca66 --- /dev/null +++ b/src/sessions.c @@ -0,0 +1,187 @@ +/* Copyright (C) 2014 mod_auth_gssapi authors - See COPYING for (C) terms */ + +#include "mod_auth_gssapi.h" + +static APR_OPTIONAL_FN_TYPE(ap_session_load) *mag_sess_load_fn = NULL; +static APR_OPTIONAL_FN_TYPE(ap_session_get) *mag_sess_get_fn = NULL; +static APR_OPTIONAL_FN_TYPE(ap_session_set) *mag_sess_set_fn = NULL; + +void mag_post_config_session(void) +{ + mag_sess_load_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_load); + mag_sess_get_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_get); + mag_sess_set_fn = APR_RETRIEVE_OPTIONAL_FN(ap_session_set); +} + +static apr_status_t mag_session_load(request_rec *req, session_rec **sess) +{ + if (mag_sess_load_fn) { + return mag_sess_load_fn(req, sess); + } + return DECLINED; +} + +static apr_status_t mag_session_get(request_rec *req, session_rec *sess, + const char *key, const char **value) +{ + if (mag_sess_get_fn) { + return mag_sess_get_fn(req, sess, key, value); + } + return DECLINED; +} + +static apr_status_t mag_session_set(request_rec *req, session_rec *sess, + const char *key, const char *value) +{ + if (mag_sess_set_fn) { + return mag_sess_set_fn(req, sess, key, value); + } + return DECLINED; +} + +#define MAG_BEARER_KEY "MagBearerToken" + +void mag_check_session(request_rec *req, + struct mag_config *cfg, struct mag_conn **conn) +{ + struct mag_conn *mc; + apr_status_t rc; + session_rec *sess = NULL; + const char *sessval = NULL; + int declen; + struct databuf ctxbuf = { 0 }; + struct databuf cipherbuf = { 0 }; + char *next, *last; + time_t expiration; + + rc = mag_session_load(req, &sess); + if (rc != OK || sess == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_INFO|APLOG_NOERRNO, 0, req, + "Sessions not available, no cookies!"); + return; + } + + mc = *conn; + if (!mc) { + mc = apr_pcalloc(req->pool, sizeof(struct mag_conn)); + if (!mc) return; + + mc->parent = req->pool; + *conn = mc; + } + + rc = mag_session_get(req, sess, MAG_BEARER_KEY, &sessval); + if (rc != OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, + "Failed to get session data!"); + return; + } + if (!sessval) { + /* no session established, just return */ + return; + } + + if (!cfg->mag_skey) { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, req, + "Session key not available, no cookies!"); + /* we do not have a key, just return */ + return; + } + + /* decode it */ + declen = apr_base64_decode_len(sessval); + cipherbuf.value = apr_palloc(req->pool, declen); + if (!cipherbuf.value) return; + cipherbuf.length = (int)apr_base64_decode((char *)cipherbuf.value, sessval); + + rc = UNSEAL_BUFFER(req->pool, cfg->mag_skey, &cipherbuf, &ctxbuf); + if (rc != OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, + "Failed to unseal session data!"); + return; + } + + /* get time */ + next = apr_strtok((char *)ctxbuf.value, ":", &last); + expiration = (time_t)apr_atoi64(next); + if (expiration < time(NULL)) { + /* credentials fully expired, return nothing */ + return; + } + + /* user name is next */ + next = apr_strtok(NULL, ":", &last); + mc->user_name = apr_pstrdup(mc->parent, next); + if (!mc->user_name) return; + + /* gssapi name (often a principal) is last. + * (because it may contain the separator as a valid char we + * just read last as is, without further tokenizing */ + mc->gss_name = apr_pstrdup(mc->parent, last); + if (!mc->gss_name) return; + + /* OK we have a valid token */ + mc->established = true; +} + +void mag_attempt_session(request_rec *req, + struct mag_config *cfg, struct mag_conn *mc) +{ + session_rec *sess = NULL; + char *sessval = NULL; + struct databuf plainbuf = { 0 }; + struct databuf cipherbuf = { 0 }; + struct databuf ctxbuf = { 0 }; + apr_status_t rc; + + /* we save the session only if the authentication is established */ + + if (!mc->established) return; + rc = mag_session_load(req, &sess); + if (rc != OK || sess == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, req, + "Sessions not available, can't send cookies!"); + return; + } + + if (!cfg->mag_skey) { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, req, + "Session key not available, generating new one."); + rc = SEAL_KEY_CREATE(&cfg->mag_skey); + if (rc != OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, + "Failed to create sealing key!"); + return; + } + } + + sessval = apr_psprintf(req->pool, "%ld:%s:%s", + (long)mc->expiration, mc->user_name, mc->gss_name); + if (!sessval) return; + + plainbuf.length = strlen(sessval) + 1; + plainbuf.value = (unsigned char *)sessval; + + rc = SEAL_BUFFER(req->pool, cfg->mag_skey, &plainbuf, &cipherbuf); + if (rc != OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, + "Failed to seal session data!"); + return; + } + + ctxbuf.length = apr_base64_encode_len(cipherbuf.length); + ctxbuf.value = apr_pcalloc(req->pool, ctxbuf.length); + if (!ctxbuf.value) return; + + ctxbuf.length = apr_base64_encode((char *)ctxbuf.value, + (char *)cipherbuf.value, + cipherbuf.length); + + rc = mag_session_set(req, sess, MAG_BEARER_KEY, (char *)ctxbuf.value); + if (rc != OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req, + "Failed to set session data!"); + return; + } +} + diff --git a/src/sessions.h b/src/sessions.h new file mode 100644 index 0000000..f3b398e --- /dev/null +++ b/src/sessions.h @@ -0,0 +1,10 @@ +/* Copyright (C) 2014 mod_auth_gssapi authors - See COPYING for (C) terms */ + +struct mag_config; +struct mag_conn; + +void mag_post_config_session(void); +void mag_check_session(request_rec *req, + struct mag_config *cfg, struct mag_conn **conn); +void mag_attempt_session(request_rec *req, + struct mag_config *cfg, struct mag_conn *mc); -- 2.1.4