diff options
Diffstat (limited to 'pjnath/src/pjnath/stun_auth.c')
-rw-r--r-- | pjnath/src/pjnath/stun_auth.c | 631 |
1 files changed, 631 insertions, 0 deletions
diff --git a/pjnath/src/pjnath/stun_auth.c b/pjnath/src/pjnath/stun_auth.c new file mode 100644 index 0000000..9041186 --- /dev/null +++ b/pjnath/src/pjnath/stun_auth.c @@ -0,0 +1,631 @@ +/* $Id: stun_auth.c 3553 2011-05-05 06:14:19Z nanang $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +#include <pjnath/stun_auth.h> +#include <pjnath/errno.h> +#include <pjlib-util/hmac_sha1.h> +#include <pjlib-util/md5.h> +#include <pjlib-util/sha1.h> +#include <pj/assert.h> +#include <pj/log.h> +#include <pj/pool.h> +#include <pj/string.h> + +#define THIS_FILE "stun_auth.c" + +/* Duplicate credential */ +PJ_DEF(void) pj_stun_auth_cred_dup( pj_pool_t *pool, + pj_stun_auth_cred *dst, + const pj_stun_auth_cred *src) +{ + dst->type = src->type; + + switch (src->type) { + case PJ_STUN_AUTH_CRED_STATIC: + pj_strdup(pool, &dst->data.static_cred.realm, + &src->data.static_cred.realm); + pj_strdup(pool, &dst->data.static_cred.username, + &src->data.static_cred.username); + dst->data.static_cred.data_type = src->data.static_cred.data_type; + pj_strdup(pool, &dst->data.static_cred.data, + &src->data.static_cred.data); + pj_strdup(pool, &dst->data.static_cred.nonce, + &src->data.static_cred.nonce); + break; + case PJ_STUN_AUTH_CRED_DYNAMIC: + pj_memcpy(&dst->data.dyn_cred, &src->data.dyn_cred, + sizeof(src->data.dyn_cred)); + break; + } +} + + +/* + * Duplicate request credential. + */ +PJ_DEF(void) pj_stun_req_cred_info_dup( pj_pool_t *pool, + pj_stun_req_cred_info *dst, + const pj_stun_req_cred_info *src) +{ + pj_strdup(pool, &dst->realm, &src->realm); + pj_strdup(pool, &dst->username, &src->username); + pj_strdup(pool, &dst->nonce, &src->nonce); + pj_strdup(pool, &dst->auth_key, &src->auth_key); +} + + +/* Calculate HMAC-SHA1 key for long term credential, by getting + * MD5 digest of username, realm, and password. + */ +static void calc_md5_key(pj_uint8_t digest[16], + const pj_str_t *realm, + const pj_str_t *username, + const pj_str_t *passwd) +{ + /* The 16-byte key for MESSAGE-INTEGRITY HMAC is formed by taking + * the MD5 hash of the result of concatenating the following five + * fields: (1) The username, with any quotes and trailing nulls + * removed, (2) A single colon, (3) The realm, with any quotes and + * trailing nulls removed, (4) A single colon, and (5) The + * password, with any trailing nulls removed. + */ + pj_md5_context ctx; + pj_str_t s; + + pj_md5_init(&ctx); + +#define REMOVE_QUOTE(s) if (s.slen && *s.ptr=='"') \ + s.ptr++, s.slen--; \ + if (s.slen && s.ptr[s.slen-1]=='"') \ + s.slen--; + + /* Add username */ + s = *username; + REMOVE_QUOTE(s); + pj_md5_update(&ctx, (pj_uint8_t*)s.ptr, s.slen); + + /* Add single colon */ + pj_md5_update(&ctx, (pj_uint8_t*)":", 1); + + /* Add realm */ + s = *realm; + REMOVE_QUOTE(s); + pj_md5_update(&ctx, (pj_uint8_t*)s.ptr, s.slen); + +#undef REMOVE_QUOTE + + /* Another colon */ + pj_md5_update(&ctx, (pj_uint8_t*)":", 1); + + /* Add password */ + pj_md5_update(&ctx, (pj_uint8_t*)passwd->ptr, passwd->slen); + + /* Done */ + pj_md5_final(&ctx, digest); +} + + +/* + * Create authentication key to be used for encoding the message with + * MESSAGE-INTEGRITY. + */ +PJ_DEF(void) pj_stun_create_key(pj_pool_t *pool, + pj_str_t *key, + const pj_str_t *realm, + const pj_str_t *username, + pj_stun_passwd_type data_type, + const pj_str_t *data) +{ + PJ_ASSERT_ON_FAIL(pool && key && username && data, return); + + if (realm && realm->slen) { + if (data_type == PJ_STUN_PASSWD_PLAIN) { + key->ptr = (char*) pj_pool_alloc(pool, 16); + calc_md5_key((pj_uint8_t*)key->ptr, realm, username, data); + key->slen = 16; + } else { + pj_strdup(pool, key, data); + } + } else { + pj_assert(data_type == PJ_STUN_PASSWD_PLAIN); + pj_strdup(pool, key, data); + } +} + + +PJ_INLINE(pj_uint16_t) GET_VAL16(const pj_uint8_t *pdu, unsigned pos) +{ + return (pj_uint16_t) ((pdu[pos] << 8) + pdu[pos+1]); +} + + +PJ_INLINE(void) PUT_VAL16(pj_uint8_t *buf, unsigned pos, pj_uint16_t hval) +{ + buf[pos+0] = (pj_uint8_t) ((hval & 0xFF00) >> 8); + buf[pos+1] = (pj_uint8_t) ((hval & 0x00FF) >> 0); +} + + +/* Send 401 response */ +static pj_status_t create_challenge(pj_pool_t *pool, + const pj_stun_msg *msg, + int err_code, + const char *errstr, + const pj_str_t *realm, + const pj_str_t *nonce, + pj_stun_msg **p_response) +{ + pj_stun_msg *response; + pj_str_t tmp_nonce; + pj_str_t err_msg; + pj_status_t rc; + + rc = pj_stun_msg_create_response(pool, msg, err_code, + (errstr?pj_cstr(&err_msg, errstr):NULL), + &response); + if (rc != PJ_SUCCESS) + return rc; + + /* SHOULD NOT add REALM, NONCE, USERNAME, and M-I on 400 response */ + if (err_code!=400 && realm && realm->slen) { + rc = pj_stun_msg_add_string_attr(pool, response, + PJ_STUN_ATTR_REALM, + realm); + if (rc != PJ_SUCCESS) + return rc; + + /* long term must include nonce */ + if (!nonce || nonce->slen == 0) { + tmp_nonce = pj_str("pjstun"); + nonce = &tmp_nonce; + } + } + + if (err_code!=400 && nonce && nonce->slen) { + rc = pj_stun_msg_add_string_attr(pool, response, + PJ_STUN_ATTR_NONCE, + nonce); + if (rc != PJ_SUCCESS) + return rc; + } + + *p_response = response; + + return PJ_SUCCESS; +} + + +/* Verify credential in the request */ +PJ_DEF(pj_status_t) pj_stun_authenticate_request(const pj_uint8_t *pkt, + unsigned pkt_len, + const pj_stun_msg *msg, + pj_stun_auth_cred *cred, + pj_pool_t *pool, + pj_stun_req_cred_info *p_info, + pj_stun_msg **p_response) +{ + pj_stun_req_cred_info tmp_info; + const pj_stun_msgint_attr *amsgi; + unsigned i, amsgi_pos; + pj_bool_t has_attr_beyond_mi; + const pj_stun_username_attr *auser; + const pj_stun_realm_attr *arealm; + const pj_stun_realm_attr *anonce; + pj_hmac_sha1_context ctx; + pj_uint8_t digest[PJ_SHA1_DIGEST_SIZE]; + pj_stun_status err_code; + const char *err_text = NULL; + pj_status_t status; + + /* msg and credential MUST be specified */ + PJ_ASSERT_RETURN(pkt && pkt_len && msg && cred, PJ_EINVAL); + + /* If p_response is specified, pool MUST be specified. */ + PJ_ASSERT_RETURN(!p_response || pool, PJ_EINVAL); + + if (p_response) + *p_response = NULL; + + if (!PJ_STUN_IS_REQUEST(msg->hdr.type)) + p_response = NULL; + + if (p_info == NULL) + p_info = &tmp_info; + + pj_bzero(p_info, sizeof(pj_stun_req_cred_info)); + + /* Get realm and nonce from credential */ + p_info->realm.slen = p_info->nonce.slen = 0; + if (cred->type == PJ_STUN_AUTH_CRED_STATIC) { + p_info->realm = cred->data.static_cred.realm; + p_info->nonce = cred->data.static_cred.nonce; + } else if (cred->type == PJ_STUN_AUTH_CRED_DYNAMIC) { + status = cred->data.dyn_cred.get_auth(cred->data.dyn_cred.user_data, + pool, &p_info->realm, + &p_info->nonce); + if (status != PJ_SUCCESS) + return status; + } else { + pj_assert(!"Invalid credential type"); + return PJ_EBUG; + } + + /* Look for MESSAGE-INTEGRITY while counting the position */ + amsgi_pos = 0; + has_attr_beyond_mi = PJ_FALSE; + amsgi = NULL; + for (i=0; i<msg->attr_count; ++i) { + if (msg->attr[i]->type == PJ_STUN_ATTR_MESSAGE_INTEGRITY) { + amsgi = (const pj_stun_msgint_attr*) msg->attr[i]; + } else if (amsgi) { + has_attr_beyond_mi = PJ_TRUE; + break; + } else { + amsgi_pos += ((msg->attr[i]->length+3) & ~0x03) + 4; + } + } + + if (amsgi == NULL) { + /* According to rfc3489bis-10 Sec 10.1.2/10.2.2, we should return 400 + for short term, and 401 for long term. + The rule has been changed from rfc3489bis-06 + */ + err_code = p_info->realm.slen ? PJ_STUN_SC_UNAUTHORIZED : + PJ_STUN_SC_BAD_REQUEST; + goto on_auth_failed; + } + + /* Next check that USERNAME is present */ + auser = (const pj_stun_username_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_USERNAME, 0); + if (auser == NULL) { + /* According to rfc3489bis-10 Sec 10.1.2/10.2.2, we should return 400 + for both short and long term, since M-I is present. + The rule has been changed from rfc3489bis-06 + */ + err_code = PJ_STUN_SC_BAD_REQUEST; + err_text = "Missing USERNAME"; + goto on_auth_failed; + } + + /* Get REALM, if any */ + arealm = (const pj_stun_realm_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_REALM, 0); + + /* Reject with 400 if we have long term credential and the request + * is missing REALM attribute. + */ + if (p_info->realm.slen && arealm==NULL) { + err_code = PJ_STUN_SC_BAD_REQUEST; + err_text = "Missing REALM"; + goto on_auth_failed; + } + + /* Check if username match */ + if (cred->type == PJ_STUN_AUTH_CRED_STATIC) { + pj_bool_t username_ok; + username_ok = !pj_strcmp(&auser->value, + &cred->data.static_cred.username); + if (username_ok) { + pj_strdup(pool, &p_info->username, + &cred->data.static_cred.username); + pj_stun_create_key(pool, &p_info->auth_key, &p_info->realm, + &auser->value, cred->data.static_cred.data_type, + &cred->data.static_cred.data); + } else { + /* Username mismatch */ + /* According to rfc3489bis-10 Sec 10.1.2/10.2.2, we should + * return 401 + */ + err_code = PJ_STUN_SC_UNAUTHORIZED; + goto on_auth_failed; + } + } else if (cred->type == PJ_STUN_AUTH_CRED_DYNAMIC) { + pj_stun_passwd_type data_type = PJ_STUN_PASSWD_PLAIN; + pj_str_t password; + pj_status_t rc; + + rc = cred->data.dyn_cred.get_password(msg, + cred->data.dyn_cred.user_data, + (arealm?&arealm->value:NULL), + &auser->value, pool, + &data_type, &password); + if (rc == PJ_SUCCESS) { + pj_strdup(pool, &p_info->username, &auser->value); + pj_stun_create_key(pool, &p_info->auth_key, + (arealm?&arealm->value:NULL), &auser->value, + data_type, &password); + } else { + err_code = PJ_STUN_SC_UNAUTHORIZED; + goto on_auth_failed; + } + } else { + pj_assert(!"Invalid credential type"); + return PJ_EBUG; + } + + + + /* Get NONCE attribute */ + anonce = (pj_stun_nonce_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_NONCE, 0); + + /* Check for long term/short term requirements. */ + if (p_info->realm.slen != 0 && arealm == NULL) { + /* Long term credential is required and REALM is not present */ + err_code = PJ_STUN_SC_BAD_REQUEST; + err_text = "Missing REALM"; + goto on_auth_failed; + + } else if (p_info->realm.slen != 0 && arealm != NULL) { + /* We want long term, and REALM is present */ + + /* NONCE must be present. */ + if (anonce == NULL && p_info->nonce.slen) { + err_code = PJ_STUN_SC_BAD_REQUEST; + err_text = "Missing NONCE"; + goto on_auth_failed; + } + + /* Verify REALM matches */ + if (pj_stricmp(&arealm->value, &p_info->realm)) { + /* REALM doesn't match */ + err_code = PJ_STUN_SC_UNAUTHORIZED; + err_text = "Invalid REALM"; + goto on_auth_failed; + } + + /* Valid case, will validate the message integrity later */ + + } else if (p_info->realm.slen == 0 && arealm != NULL) { + /* We want to use short term credential, but client uses long + * term credential. The draft doesn't mention anything about + * switching between long term and short term. + */ + + /* For now just accept the credential, anyway it will probably + * cause wrong message integrity value later. + */ + } else if (p_info->realm.slen==0 && arealm == NULL) { + /* Short term authentication is wanted, and one is supplied */ + + /* Application MAY request NONCE to be supplied */ + if (p_info->nonce.slen != 0) { + err_code = PJ_STUN_SC_UNAUTHORIZED; + err_text = "NONCE required"; + goto on_auth_failed; + } + } + + /* If NONCE is present, validate it */ + if (anonce) { + pj_bool_t ok; + + if (cred->type == PJ_STUN_AUTH_CRED_DYNAMIC && + cred->data.dyn_cred.verify_nonce != NULL) + { + ok=cred->data.dyn_cred.verify_nonce(msg, + cred->data.dyn_cred.user_data, + (arealm?&arealm->value:NULL), + &auser->value, + &anonce->value); + } else if (cred->type == PJ_STUN_AUTH_CRED_DYNAMIC) { + ok = PJ_TRUE; + } else { + if (p_info->nonce.slen) { + ok = !pj_strcmp(&anonce->value, &p_info->nonce); + } else { + ok = PJ_TRUE; + } + } + + if (!ok) { + err_code = PJ_STUN_SC_STALE_NONCE; + goto on_auth_failed; + } + } + + /* Now calculate HMAC of the message. */ + pj_hmac_sha1_init(&ctx, (pj_uint8_t*)p_info->auth_key.ptr, + p_info->auth_key.slen); + +#if PJ_STUN_OLD_STYLE_MI_FINGERPRINT + /* Pre rfc3489bis-06 style of calculation */ + pj_hmac_sha1_update(&ctx, pkt, 20); +#else + /* First calculate HMAC for the header. + * The calculation is different depending on whether FINGERPRINT attribute + * is present in the message. + */ + if (has_attr_beyond_mi) { + pj_uint8_t hdr_copy[20]; + pj_memcpy(hdr_copy, pkt, 20); + PUT_VAL16(hdr_copy, 2, (pj_uint16_t)(amsgi_pos + 24)); + pj_hmac_sha1_update(&ctx, hdr_copy, 20); + } else { + pj_hmac_sha1_update(&ctx, pkt, 20); + } +#endif /* PJ_STUN_OLD_STYLE_MI_FINGERPRINT */ + + /* Now update with the message body */ + pj_hmac_sha1_update(&ctx, pkt+20, amsgi_pos); +#if PJ_STUN_OLD_STYLE_MI_FINGERPRINT + // This is no longer necessary as per rfc3489bis-08 + if ((amsgi_pos+20) & 0x3F) { + pj_uint8_t zeroes[64]; + pj_bzero(zeroes, sizeof(zeroes)); + pj_hmac_sha1_update(&ctx, zeroes, 64-((amsgi_pos+20) & 0x3F)); + } +#endif + pj_hmac_sha1_final(&ctx, digest); + + + /* Compare HMACs */ + if (pj_memcmp(amsgi->hmac, digest, 20)) { + /* HMAC value mismatch */ + /* According to rfc3489bis-10 Sec 10.1.2 we should return 401 */ + err_code = PJ_STUN_SC_UNAUTHORIZED; + err_text = "MESSAGE-INTEGRITY mismatch"; + goto on_auth_failed; + } + + /* Everything looks okay! */ + return PJ_SUCCESS; + +on_auth_failed: + if (p_response) { + create_challenge(pool, msg, err_code, err_text, + &p_info->realm, &p_info->nonce, p_response); + } + return PJ_STATUS_FROM_STUN_CODE(err_code); +} + + +/* Determine if STUN message can be authenticated */ +PJ_DEF(pj_bool_t) pj_stun_auth_valid_for_msg(const pj_stun_msg *msg) +{ + unsigned msg_type = msg->hdr.type; + const pj_stun_errcode_attr *err_attr; + + /* STUN requests and success response can be authenticated */ + if (!PJ_STUN_IS_ERROR_RESPONSE(msg_type) && + !PJ_STUN_IS_INDICATION(msg_type)) + { + return PJ_TRUE; + } + + /* STUN Indication cannot be authenticated */ + if (PJ_STUN_IS_INDICATION(msg_type)) + return PJ_FALSE; + + /* Authentication for STUN error responses depend on the error + * code. + */ + err_attr = (const pj_stun_errcode_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_ERROR_CODE, 0); + if (err_attr == NULL) { + PJ_LOG(4,(THIS_FILE, "STUN error code attribute not present in " + "error response")); + return PJ_TRUE; + } + + switch (err_attr->err_code) { + case PJ_STUN_SC_BAD_REQUEST: /* 400 (Bad Request) */ + case PJ_STUN_SC_UNAUTHORIZED: /* 401 (Unauthorized) */ + case PJ_STUN_SC_STALE_NONCE: /* 438 (Stale Nonce) */ + + /* Due to the way this response is generated here, we can't really + * authenticate 420 (Unknown Attribute) response */ + case PJ_STUN_SC_UNKNOWN_ATTRIBUTE: + return PJ_FALSE; + default: + return PJ_TRUE; + } +} + + +/* Authenticate MESSAGE-INTEGRITY in the response */ +PJ_DEF(pj_status_t) pj_stun_authenticate_response(const pj_uint8_t *pkt, + unsigned pkt_len, + const pj_stun_msg *msg, + const pj_str_t *key) +{ + const pj_stun_msgint_attr *amsgi; + unsigned i, amsgi_pos; + pj_bool_t has_attr_beyond_mi; + pj_hmac_sha1_context ctx; + pj_uint8_t digest[PJ_SHA1_DIGEST_SIZE]; + + PJ_ASSERT_RETURN(pkt && pkt_len && msg && key, PJ_EINVAL); + + /* First check that MESSAGE-INTEGRITY is present */ + amsgi = (const pj_stun_msgint_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_MESSAGE_INTEGRITY, 0); + if (amsgi == NULL) { + return PJ_STATUS_FROM_STUN_CODE(PJ_STUN_SC_UNAUTHORIZED); + } + + + /* Check that message length is valid */ + if (msg->hdr.length < 24) { + return PJNATH_EINSTUNMSGLEN; + } + + /* Look for MESSAGE-INTEGRITY while counting the position */ + amsgi_pos = 0; + has_attr_beyond_mi = PJ_FALSE; + amsgi = NULL; + for (i=0; i<msg->attr_count; ++i) { + if (msg->attr[i]->type == PJ_STUN_ATTR_MESSAGE_INTEGRITY) { + amsgi = (const pj_stun_msgint_attr*) msg->attr[i]; + } else if (amsgi) { + has_attr_beyond_mi = PJ_TRUE; + break; + } else { + amsgi_pos += ((msg->attr[i]->length+3) & ~0x03) + 4; + } + } + + if (amsgi == NULL) { + return PJ_STATUS_FROM_STUN_CODE(PJ_STUN_SC_BAD_REQUEST); + } + + /* Now calculate HMAC of the message. */ + pj_hmac_sha1_init(&ctx, (pj_uint8_t*)key->ptr, key->slen); + +#if PJ_STUN_OLD_STYLE_MI_FINGERPRINT + /* Pre rfc3489bis-06 style of calculation */ + pj_hmac_sha1_update(&ctx, pkt, 20); +#else + /* First calculate HMAC for the header. + * The calculation is different depending on whether FINGERPRINT attribute + * is present in the message. + */ + if (has_attr_beyond_mi) { + pj_uint8_t hdr_copy[20]; + pj_memcpy(hdr_copy, pkt, 20); + PUT_VAL16(hdr_copy, 2, (pj_uint16_t)(amsgi_pos+24)); + pj_hmac_sha1_update(&ctx, hdr_copy, 20); + } else { + pj_hmac_sha1_update(&ctx, pkt, 20); + } +#endif /* PJ_STUN_OLD_STYLE_MI_FINGERPRINT */ + + /* Now update with the message body */ + pj_hmac_sha1_update(&ctx, pkt+20, amsgi_pos); +#if PJ_STUN_OLD_STYLE_MI_FINGERPRINT + // This is no longer necessary as per rfc3489bis-08 + if ((amsgi_pos+20) & 0x3F) { + pj_uint8_t zeroes[64]; + pj_bzero(zeroes, sizeof(zeroes)); + pj_hmac_sha1_update(&ctx, zeroes, 64-((amsgi_pos+20) & 0x3F)); + } +#endif + pj_hmac_sha1_final(&ctx, digest); + + /* Compare HMACs */ + if (pj_memcmp(amsgi->hmac, digest, 20)) { + /* HMAC value mismatch */ + return PJ_STATUS_FROM_STUN_CODE(PJ_STUN_SC_UNAUTHORIZED); + } + + /* Everything looks okay! */ + return PJ_SUCCESS; +} + |