diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample index 7b4c7aac1eff4e9845e190ef31c315775e4d2e5b..874fcc390cdb7237851405bb6403deb0aa0fa95f 100644 --- a/configs/samples/pjsip.conf.sample +++ b/configs/samples/pjsip.conf.sample @@ -971,10 +971,11 @@ ; The input to the hash function must be in the ; following format: ; <username>:<realm>:<password> - ; For incoming authentication (asterisk is the server), + ; For incoming authentication (asterisk is the UAS), ; the realm must match either the realm set in this object ; or the default set in in the "global" object. - ; For outgoing authentication (asterisk is the client), + ; + ; For outgoing authentication (asterisk is the UAC), ; the realm must match what the server will be sending ; in their WWW-Authenticate header. It can't be blank ; unless you expect the server to be sending a blank @@ -985,16 +986,22 @@ ; Note the '-n'. You don't want a newline to be part ; of the hash. (default: "") ;password= ; PlainText password used for authentication (default: "") -;realm= ; For incoming authentication (asterisk is the server), +;realm= ; For incoming authentication (asterisk is the UAS), ; this is the realm to be sent on WWW-Authenticate ; headers. If not specified, the global object's ; "default_realm" will be used. - ; For outgoing authentication (asterisk is the client), this + ; + ; For outgoing authentication (asterisk is the UAS), this ; must either be the realm the server is expected to send, - ; or blank to automatically use the realm sent by the server. - ; If you have multiple auth object for an endpoint, the realm - ; is also used to match the auth object to the realm the - ; server sends. (default: "") + ; or left blank or contain a single '*' to automatically + ; use the realm sent by the server. If you have multiple + ; auth object for an endpoint, the realm is also used to + ; match the auth object to the realm the server sent. + ; Using the same auth section for inbound and outbound + ; authentication is not recommended. There is a difference in + ; meaning for an empty realm setting between inbound and outbound + ; authentication uses. + ; (default: "") ;type= ; Must be auth (default: "") ;username= ; Username to use for account (default: "") diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index 2020ca8782c3a9030564800602ebfcb93a7bc6cc..7083b25675abdec4c7c28f04e10df4be4fd1c2a8 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -2499,11 +2499,47 @@ int ast_sip_retrieve_auths(const struct ast_sip_auth_vector *auths, struct ast_s * Call this function once you have completed operating on auths * retrieved from \ref ast_sip_retrieve_auths * - * \param auths An vector of auth structures to clean up - * \param num_auths The number of auths in the vector + * \param auths An array of auth object pointers to clean up + * \param num_auths The number of auths in the array */ void ast_sip_cleanup_auths(struct ast_sip_auth *auths[], size_t num_auths); +/*! + * \brief Retrieve relevant SIP auth structures from sorcery as a vector + * + * \param auth_ids Vector of sorcery IDs of auth credentials to retrieve + * \param[out] auth_objects A pointer ast_sip_auth_objects_vector to hold the objects + * + * \retval 0 Success + * \retval -1 Number of auth objects found is less than the number of names supplied. + * + * \WARNING The number of auth objects retrieved may be less than the + * number of auth ids supplied if auth objects couldn't be found for + * some of them. + * + * \NOTE Since the ref count on all auith objects returned has been + * bumped, you must call ast_sip_cleanup_auth_objects_vector() to decrement + * the ref count on all of the auth objects in the vector, + * then call AST_VECTOR_FREE() on the vector itself. + * + */ +AST_VECTOR(ast_sip_auth_objects_vector, struct ast_sip_auth *); +int ast_sip_retrieve_auths_vector(const struct ast_sip_auth_vector *auth_ids, + struct ast_sip_auth_objects_vector *auth_objects); + +/*! + * \brief Clean up retrieved auth objects in vector + * + * Call this function once you have completed operating on auths + * retrieved from \ref ast_sip_retrieve_auths_vector. All + * auth objects will have their reference counts decremented and + * the vector size will be reset to 0. You must still call + * AST_VECTOR_FREE() on the vector itself. + * + * \param auth_objects A vector of auth structures to clean up + */ +#define ast_sip_cleanup_auth_objects_vector(auth_objects) AST_VECTOR_RESET(auth_objects, ao2_cleanup) + /*! * \brief Checks if the given content type matches type/subtype. * diff --git a/res/res_pjsip.c b/res/res_pjsip.c index 775b63f8d1af41bcc86c890a678144d23ee88c8e..3e9e4541ec764c87725b4d2002fba4b2dff5088d 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -1504,13 +1504,24 @@ This option specifies which of the password style config options should be read when trying to authenticate an endpoint inbound request. If set to <literal>userpass</literal> then we'll read from the 'password' option. For <literal>md5</literal> we'll read - from 'md5_cred'. If set to <literal>google_oauth</literal> then we'll read from the refresh_token/oauth_clientid/oauth_secret fields. + from 'md5_cred'. If set to <literal>google_oauth</literal> then we'll read from the + refresh_token/oauth_clientid/oauth_secret fields. The following values are valid: </para> <enumlist> <enum name="md5"/> <enum name="userpass"/> <enum name="google_oauth"/> </enumlist> + <para> + </para> + <note> + <para> + This setting only describes whether the password is in + plain text or has been pre-hashed with MD5. It doesn't describe + the acceptable digest algorithms we'll accept in a received + challenge. + </para> + </note> </description> </configOption> <configOption name="nonce_lifetime" default="32"> @@ -1542,11 +1553,12 @@ <para> </para> <para> - For outgoing authentication (asterisk is the client), + For outgoing authentication (asterisk is the UAC), the realm must match what the server will be sending in their WWW-Authenticate header. It can't be blank unless you expect the server to be sending a blank - realm in the header. + realm in the header. You can't use pre-hashed + paswords with a wildcard auth object. You can generate the hash with the following shell command: </para> @@ -1578,7 +1590,7 @@ <configOption name="realm" default=""> <synopsis>SIP realm for endpoint</synopsis> <description><para> - For incoming authentication (asterisk is the server), + For incoming authentication (asterisk is the UAS), this is the realm to be sent on WWW-Authenticate headers. If not specified, the <replaceable>global</replaceable> object's <variable>default_realm</variable> will be used. @@ -1586,12 +1598,12 @@ <para> </para> <para> - For outgoing authentication (asterisk is the client), this + For outgoing authentication (asterisk is the UAS), this must either be the realm the server is expected to send, - or blank to automatically use the realm sent by the server. - If you have multiple auth object for an endpoint, the realm - is also used to match the auth object to the realm the - server sent. + or left blank or contain a single '*' to automatically + use the realm sent by the server. If you have multiple + auth object for an endpoint, the realm is also used to + match the auth object to the realm the server sent. </para> <para> </para> @@ -1600,7 +1612,19 @@ Using the same auth section for inbound and outbound authentication is not recommended. There is a difference in meaning for an empty realm setting between inbound and outbound - authentication uses.</para></note> + authentication uses. + </para> + </note> + <para> + </para> + <note> + <para> + If more than one auth object with the same realm or + more than one wildcard auth object associated to + an endpoint, we can only use the first one of + each defined on the endpoint. + </para> + </note> </description> </configOption> <configOption name="type"> diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index 6d12c6f6ec841b566e5331bc3e6ea9e0a6d8e06f..6defa7cb96a542d75ae9a13dad7b90afe52bd71c 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -2382,6 +2382,25 @@ void ast_sip_cleanup_auths(struct ast_sip_auth *auths[], size_t num_auths) } } +int ast_sip_retrieve_auths_vector(const struct ast_sip_auth_vector *auth_ids, + struct ast_sip_auth_objects_vector *auth_objects) +{ + int i; + + for (i = 0; i < AST_VECTOR_SIZE(auth_ids); ++i) { + /* Using AST_VECTOR_GET is safe since the vector is immutable */ + const char *name = AST_VECTOR_GET(auth_ids, i); + struct ast_sip_auth *auth_object = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), SIP_SORCERY_AUTH_TYPE, name); + if (!auth_object) { + ast_log(LOG_WARNING, "Auth object '%s' could not be found\n", name); + } else { + AST_VECTOR_APPEND(auth_objects, auth_object); + } + } + + return AST_VECTOR_SIZE(auth_objects) == AST_VECTOR_SIZE(auth_ids) ? 0 : -1; +} + struct ast_sorcery *ast_sip_get_sorcery(void) { return sip_sorcery; diff --git a/res/res_pjsip_authenticator_digest.c b/res/res_pjsip_authenticator_digest.c index 518ef73bb146a2ce2cf8c86c40758fbc00527455..231a7e5aabf3914dce0b8d34fb930ccd0e0cb052 100644 --- a/res/res_pjsip_authenticator_digest.c +++ b/res/res_pjsip_authenticator_digest.c @@ -24,6 +24,7 @@ #include "asterisk/logger.h" #include "asterisk/module.h" #include "asterisk/strings.h" +#include "asterisk/test.h" /*** MODULEINFO <depend>pjproject</depend> @@ -296,7 +297,7 @@ static void setup_auth_srv(pj_pool_t *pool, pjsip_auth_srv *auth_server, const c */ enum digest_verify_result { /*! Authentication credentials incorrect */ - AUTH_FAIL, + AUTH_FAIL = 0, /*! Authentication credentials correct */ AUTH_SUCCESS, /*! Authentication credentials correct but nonce mismatch */ @@ -305,6 +306,12 @@ enum digest_verify_result { AUTH_NOAUTH, }; +static char *verify_result_str[] = { + "FAIL", + "SUCCESS", + "STALE", + "NOAUTH" +}; /*! * \brief astobj2 callback for verifying incoming credentials * @@ -320,6 +327,7 @@ static int verify(const struct ast_sip_auth *auth, pjsip_rx_data *rdata, pj_pool int response_code; pjsip_auth_srv auth_server; int stale = 0; + int res = AUTH_FAIL; if (!find_challenge(rdata, auth)) { /* Couldn't find a challenge with a sane nonce. @@ -336,17 +344,26 @@ static int verify(const struct ast_sip_auth *auth, pjsip_rx_data *rdata, pj_pool if (authed == PJ_SUCCESS) { if (stale) { - return AUTH_STALE; + res = AUTH_STALE; } else { - return AUTH_SUCCESS; + res = AUTH_SUCCESS; } } if (authed == PJSIP_EAUTHNOAUTH) { - return AUTH_NOAUTH; + res = AUTH_NOAUTH; } - return AUTH_FAIL; + ast_debug(3, "Realm: %s Username: %s Result: %s\n", + auth->realm, auth->auth_user, verify_result_str[res]); + + ast_test_suite_event_notify("INCOMING_AUTH_VERIFY_RESULT", + "Realm: %s\r\n" + "Username: %s\r\n" + "Status: %s", + auth->realm, auth->auth_user, verify_result_str[res]); + + return res; } /*! diff --git a/res/res_pjsip_outbound_authenticator_digest.c b/res/res_pjsip_outbound_authenticator_digest.c index b1011b00a3b5888210a609d4930094baebbe9834..3f8732c332e09f08069e0e07292741c3439c3a1f 100644 --- a/res/res_pjsip_outbound_authenticator_digest.c +++ b/res/res_pjsip_outbound_authenticator_digest.c @@ -30,131 +30,498 @@ #include "asterisk/logger.h" #include "asterisk/module.h" #include "asterisk/strings.h" +#include "asterisk/vector.h" -static pjsip_www_authenticate_hdr *get_auth_header(pjsip_rx_data *challenge, - const void *start) -{ - pjsip_hdr_e search_type; +pj_str_t supported_digest_algorithms[] = { + { "MD5", 3} +}; +/*! + * \internal + * \brief Determine proper authenticate header + * + * We need to search for different headers depending on whether + * the response code from the UAS/Proxy was 401 or 407. + */ +static pjsip_hdr_e get_auth_search_type(pjsip_rx_data *challenge) +{ if (challenge->msg_info.msg->line.status.code == PJSIP_SC_UNAUTHORIZED) { - search_type = PJSIP_H_WWW_AUTHENTICATE; + return PJSIP_H_WWW_AUTHENTICATE; } else if (challenge->msg_info.msg->line.status.code == PJSIP_SC_PROXY_AUTHENTICATION_REQUIRED) { - search_type = PJSIP_H_PROXY_AUTHENTICATE; + return PJSIP_H_PROXY_AUTHENTICATE; } else { ast_log(LOG_ERROR, "Status code %d was received when it should have been 401 or 407.\n", challenge->msg_info.msg->line.status.code); - return NULL ; + return PJSIP_H_OTHER; } +} - return pjsip_msg_find_hdr(challenge->msg_info.msg, search_type, start); +/*! + * \internal + * \brief Determine if digest algorithm in the header is one we support + * + * \retval 1 If we support the algorithm + * \retval 0 If we do not + * + */ +static int is_digest_algorithm_supported(pjsip_www_authenticate_hdr *auth_hdr) +{ + int digest; + /* An empty digest is assumed to be md5 */ + if (pj_strlen(&auth_hdr->challenge.digest.algorithm) == 0) { + return 1; + } + + for (digest = 0; digest < ARRAY_LEN(supported_digest_algorithms); digest++) { + if (pj_stricmp(&auth_hdr->challenge.digest.algorithm, &supported_digest_algorithms[digest]) == 0) { + return 1; + } + } + return 0; } -static int set_outbound_authentication_credentials(pjsip_auth_clt_sess *auth_sess, - const struct ast_sip_auth_vector *auth_vector, pjsip_rx_data *challenge, - pjsip_www_authenticate_hdr *auth_hdr) +/*! + * \internal + * \brief Initialize pjproject with a valid set of credentials + * + * RFC7616 and RFC8760 allow more than one WWW-Authenticate or + * Proxy-Authenticate header per realm, each with different digest + * algorithms (including new ones like SHA-256 and SHA-512-256). However, + * thankfully, a UAS can NOT send back multiple Authenticate headers for + * the same realm with the same digest algorithm. The UAS is also + * supposed to send the headers in order of preference with the first one + * being the most preferred. + * + * We're supposed to send an Authorization header for the first one we + * encounter for a realm that we can support. + * + * The UAS can also send multiple realms, especially when it's a proxy + * that has forked the request in which case the proxy will aggregate all + * of the Authenticate and then them all back to the UAC. + * + * It doesn't stop there though... Each realm can require a different + * username from the others. There's also nothing preventing each digest + * algorithm from having a unique password although I'm not sure if + * that adds any benefit. + * + * So now... For each Authenticate header we encounter, we have to + * determine if we support the digest algorithm and, if not, just skip the + * header. We then have to find an auth object that matches the realm AND + * the digest algorithm or find a wildcard object that matches the digest + * algorithm. If we find one, we add it to the results vector and read the + * next Authenticate header. If the next header is for the same realm AND + * we already added an auth object for that realm, we skip the header. + * Otherwise we repeat the process for the next header. + * + * In the end, we'll have accumulated a list of credentials we can pass to + * pjproject that it can use to add Authentication headers to a request. + * + * \NOTE: Neither we nor pjproject can currently handle digest algorithms + * other than MD5. We don't even have a place for it in the ast_sip_auth + * object. For this reason, we just skip processing any Authenticate + * header that's not MD5. When we support the others, we'll move the + * check into the loop that searches the objects. + */ +static pj_status_t set_outbound_authentication_credentials(pjsip_auth_clt_sess *auth_sess, + const struct ast_sip_auth_objects_vector *auth_objects_vector, pjsip_rx_data *challenge, + struct ast_str **realms) { - size_t auth_size = AST_VECTOR_SIZE(auth_vector); - struct ast_sip_auth **auths = ast_alloca(auth_size * sizeof(*auths)); - pjsip_cred_info *auth_creds = ast_alloca(auth_size * sizeof(*auth_creds)); - int res = 0; int i; + size_t auth_object_count; + pjsip_www_authenticate_hdr *auth_hdr = NULL; + pj_status_t res = PJ_SUCCESS; + pjsip_hdr_e search_type; + size_t cred_count; + pjsip_cred_info *creds_array; - if (ast_sip_retrieve_auths(auth_vector, auths)) { - res = -1; - goto cleanup; + /* + * Normally vector elements are pointers to something else, usually + * structures. In this case however, the elements are the + * structures themselves instead of pointers to them. This is due + * to the fact that pjsip_auth_clt_set_credentials() expects an + * array of structues, not an array of pointers to structures. + * Thankfully, vectors allow you to "steal" their underlying + * arrays, in this case an array of pjsip_cred_info structures, + * which we'll pass to pjsip_auth_clt_set_credentials() at the + * end. + */ + AST_VECTOR(cred_info, pjsip_cred_info) auth_creds; + + search_type = get_auth_search_type(challenge); + if (search_type == PJSIP_H_OTHER) { + /* + * The status code on the response wasn't 401 or 407 + * so there are no WWW-Authenticate or Proxy-Authenticate + * headers to process. + */ + return PJ_ENOTSUP; } - for (i = 0; i < auth_size; ++i) { - if (ast_strlen_zero(auths[i]->realm)) { - auth_creds[i].realm = auth_hdr->challenge.common.realm; + auth_object_count = AST_VECTOR_SIZE(auth_objects_vector); + if (auth_object_count == 0) { + /* This shouldn't happen but we'll check anyway. */ + return PJ_EINVAL; + } + + /* + * The number of pjsip_cred_infos we send to pjproject can + * vary based on the number of acceptable headers received + * and the number of acceptable auth objects on the endpoint + * so we just use a vector to accumulate them. + * + * NOTE: You have to call AST_VECTOR_FREE() on the vector + * but you don't have to free the elements because they're + * actual structures, not pointers to structures. + */ + if (AST_VECTOR_INIT(&auth_creds, 5) != 0) { + return PJ_ENOMEM; + } + + /* + * It's going to be rare that we actually have more than one + * WWW-Authentication header or more than one auth object to + * match to it so the following nested loop should be fine. + */ + while ((auth_hdr = pjsip_msg_find_hdr(challenge->msg_info.msg, + search_type, auth_hdr ? auth_hdr->next : NULL))) { + int exact_match_index = -1; + int wildcard_match_index = -1; + int match_index = 0; + pjsip_cred_info auth_cred; + struct ast_sip_auth *auth = NULL; + + memset(&auth_cred, 0, sizeof(auth_cred)); + /* + * Since we only support the MD5 algorithm at the current time, + * there's no sense searching for auth objects that match the algorithm. + * In fact, the auth_object structure doesn't even have a member + * for it. + * + * When we do support more algorithms, this check will need to be + * moved inside the auth object loop below. + * + * Note: The header may not have specified an algorithm at all in which + * case it's assumed to be MD5. is_digest_algorithm_supported() returns + * true for that case. + */ + if (!is_digest_algorithm_supported(auth_hdr)) { + ast_debug(3, "Skipping header with realm '%.*s' and unsupported '%.*s' algorithm \n", + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr, + (int)auth_hdr->challenge.digest.algorithm.slen, auth_hdr->challenge.digest.algorithm.ptr); + continue; + } + + /* + * Appending the realms is strictly so digest_create_request_with_auth() + * can display good error messages. Since we only support one algorithm, + * there can't be more than one header with the same realm. No need to worry + * about duplicate realms until then. + */ + if (*realms) { + ast_str_append(realms, 0, "%.*s, ", + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr); + } + + ast_debug(3, "Searching auths to find matching ones for header with realm '%.*s' and algorithm '%.*s'\n", + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr, + (int)auth_hdr->challenge.digest.algorithm.slen, auth_hdr->challenge.digest.algorithm.ptr); + + /* + * Now that we have a valid header, we can loop over the auths available to + * find either an exact realm match or, failing that, a wildcard auth (an + * auth with an empty or "*" realm). + * + * NOTE: We never use the global default realm when we're the UAC responding + * to a 401 or 407. We only use that when we're the UAS (handled elsewhere) + * and the auth object didn't have a realm. + */ + for (i = 0; i < auth_object_count; ++i) { + auth = AST_VECTOR_GET(auth_objects_vector, i); + + /* + * If this auth object's realm exactly matches the one + * from the header, we can just break out and use it. + * + * NOTE: If there's more than one auth object for an endpoint with + * a matching realm it's a misconfiguration. We'll only use the first. + */ + if (pj_stricmp2(&auth_hdr->challenge.digest.realm, auth->realm) == 0) { + ast_debug(3, "Found matching auth '%s' with realm '%s'\n", ast_sorcery_object_get_id(auth), + auth->realm); + exact_match_index = i; + /* + * If we found an exact realm match, there's no need to keep + * looking for a wildcard. + */ + break; + } + + /* + * If this auth object's realm is empty or a "*", it's a wildcard + * auth object. We going to save its index but keep iterating over + * the vector in case we find an exact match later. + * + * NOTE: If there's more than one wildcard auth object for an endpoint + * it's a misconfiguration. We'll only use the first. + */ + if (wildcard_match_index < 0 + && (ast_strlen_zero(auth->realm) || ast_strings_equal(auth->realm, "*"))) { + ast_debug(3, "Found wildcard auth '%s' for realm '%.*s'\n", ast_sorcery_object_get_id(auth), + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr); + wildcard_match_index = i; + } + } + + if (exact_match_index < 0 && wildcard_match_index < 0) { + /* + * Didn't find either a wildcard or an exact realm match. + * Move on to the next header. + */ + ast_debug(3, "No auth matching realm or no wildcard found for realm '%.*s'\n", + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr); + continue; + } + + if (exact_match_index >= 0) { + /* + * If we found an exact match, we'll always prefer that. + */ + match_index = exact_match_index; + auth = AST_VECTOR_GET(auth_objects_vector, match_index); + ast_debug(3, "Using matched auth '%s' with realm '%.*s'\n", ast_sorcery_object_get_id(auth), + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr); } else { - pj_cstr(&auth_creds[i].realm, auths[i]->realm); + /* + * We'll only use the wildcard if we didn't find an exact match. + */ + match_index = wildcard_match_index; + auth = AST_VECTOR_GET(auth_objects_vector, match_index); + ast_debug(3, "Using wildcard auth '%s' for realm '%.*s'\n", ast_sorcery_object_get_id(auth), + (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr); } - pj_cstr(&auth_creds[i].username, auths[i]->auth_user); - pj_cstr(&auth_creds[i].scheme, "digest"); - switch (auths[i]->type) { + + /* + * Copy the fields from the auth_object to the + * pjsip_cred_info structure. + */ + auth_cred.realm = auth_hdr->challenge.common.realm; + pj_cstr(&auth_cred.username, auth->auth_user); + pj_cstr(&auth_cred.scheme, "digest"); + switch (auth->type) { case AST_SIP_AUTH_TYPE_USER_PASS: - pj_cstr(&auth_creds[i].data, auths[i]->auth_pass); - auth_creds[i].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; + pj_cstr(&auth_cred.data, auth->auth_pass); + auth_cred.data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; break; case AST_SIP_AUTH_TYPE_MD5: - pj_cstr(&auth_creds[i].data, auths[i]->md5_creds); - auth_creds[i].data_type = PJSIP_CRED_DATA_DIGEST; + pj_cstr(&auth_cred.data, auth->md5_creds); + auth_cred.data_type = PJSIP_CRED_DATA_DIGEST; break; case AST_SIP_AUTH_TYPE_GOOGLE_OAUTH: /* nothing to do. handled seperately in res_pjsip_outbound_registration */ break; case AST_SIP_AUTH_TYPE_ARTIFICIAL: - ast_log(LOG_ERROR, "Trying to set artificial outbound auth credentials shouldn't happen.\n"); - break; + ast_log(LOG_ERROR, + "Trying to set artificial outbound auth credentials shouldn't happen.\n"); + continue; + } /* End auth object loop */ + + /* + * Because the vector contains actual structures and not pointers + * to structures, the call to AST_VECTOR_APPEND results in a simple + * assign of one structure to another, effectively copying the auth_cred + * structure contents to the array element. + * + * Also note that the calls to pj_cstr above set their respective + * auth_cred fields to the _pointers_ of their corresponding auth + * object fields. This is safe because the call to + * pjsip_auth_clt_set_credentials() below strdups them before we + * return to the calling function which decrements the reference + * counts. + */ + res = AST_VECTOR_APPEND(&auth_creds, auth_cred); + if (res != PJ_SUCCESS) { + res = PJ_ENOMEM; + goto cleanup; } + } /* End header loop */ + + if (*realms && ast_str_strlen(*realms)) { + /* + * Again, this is strictly so digest_create_request_with_auth() + * can display good error messages. + * + * Chop off the trailing ", " on the last realm. + */ + ast_str_truncate(*realms, ast_str_strlen(*realms) - 2); } - pjsip_auth_clt_set_credentials(auth_sess, auth_size, auth_creds); + if (AST_VECTOR_SIZE(&auth_creds) == 0) { + /* No matching auth objects were found. */ + res = PJSIP_ENOCREDENTIAL; + goto cleanup; + } + + /* + * Here's where we steal the cred info structures from the vector. + * + * The steal effectively returns a pointer to the underlying + * array of pjsip_cred_info structures which is exactly what we need + * to pass to pjsip_auth_clt_set_credentials(). + * + * <struct cred info><struct cred info>...<struct cred info> + * ^pointer + * + * Since we stole the array from the vector, we have to free it ourselves. + * + * We also have to copy the size before we steal because stealing + * resets the vector size to 0. + */ + cred_count = AST_VECTOR_SIZE(&auth_creds); + creds_array = AST_VECTOR_STEAL_ELEMENTS(&auth_creds); + + res = pjsip_auth_clt_set_credentials(auth_sess, cred_count, creds_array); + ast_free(creds_array); + if (res == PJ_SUCCESS) { + ast_debug(3, "Set %"PRIu64" credentials in auth session\n", cred_count); + } else { + ast_log(LOG_ERROR, "Failed to set %"PRIu64" credentials in auth session\n", cred_count); + } cleanup: - ast_sip_cleanup_auths(auths, auth_size); + AST_VECTOR_FREE(&auth_creds); return res; } -static int digest_create_request_with_auth(const struct ast_sip_auth_vector *auths, +/*! + * \internal + * \brief Create new tdata with auth based on original tdata + * \param auth_ids_vector Vector of auth IDs retrieved from endpoint + * \param challenge rdata of the response from the UAS with challenge + * \param old_request tdata from the original request + * \param new_request tdata of the new request with the auth + * + * This function is what's registered with ast_sip_register_outbound_authenticator() + * + * \retval 0 success + * \retval -1 failure + */ +static int digest_create_request_with_auth(const struct ast_sip_auth_vector *auth_ids_vector, pjsip_rx_data *challenge, pjsip_tx_data *old_request, pjsip_tx_data **new_request) { pjsip_auth_clt_sess auth_sess; pjsip_cseq_hdr *cseq; pj_status_t status; + struct ast_sip_auth_objects_vector auth_objects_vector; + size_t auth_object_count = 0; struct ast_sip_endpoint *endpoint; char *id = NULL; const char *id_type; - pjsip_www_authenticate_hdr *auth_hdr; - struct ast_str *realms; + struct ast_str *realms = NULL; pjsip_dialog *dlg; + int res = -1; + + /* + * Some older compilers have an issue with initializing structures with + * pjsip_auth_clt_sess auth_sess = { 0, }; + * so we'll just do it the old fashioned way. + */ + memset(&auth_sess, 0, sizeof(auth_sess)); dlg = pjsip_rdata_get_dlg(challenge); if (dlg) { + /* The only thing we use endpoint for is to get an id for error/debug messages */ endpoint = ast_sip_dialog_get_endpoint(dlg); id = endpoint ? ast_strdupa(ast_sorcery_object_get_id(endpoint)) : NULL; ao2_cleanup(endpoint); id_type = "Endpoint"; } + /* If there was no dialog, then this is probably a REGISTER so no endpoint */ if (!id) { + /* The only thing we use the address for is to get an id for error/debug messages */ id = ast_alloca(AST_SOCKADDR_BUFLEN); pj_sockaddr_print(&challenge->pkt_info.src_addr, id, AST_SOCKADDR_BUFLEN, 3); id_type = "Host"; } - auth_hdr = get_auth_header(challenge, NULL); - if (auth_hdr == NULL) { - ast_log(LOG_ERROR, "%s: '%s': Unable to find authenticate header in challenge.\n", - id_type, id); + if (!auth_ids_vector || AST_VECTOR_SIZE(auth_ids_vector) == 0) { + ast_log(LOG_ERROR, "%s: '%s': There were no auth ids available\n", id_type, id); + return -1; + } + + if (AST_VECTOR_INIT(&auth_objects_vector, AST_VECTOR_SIZE(auth_ids_vector)) != 0) { + ast_log(LOG_ERROR, "%s: '%s': Couldn't initialize auth object vector\n", id_type, id); return -1; } + /* + * We don't really care about ast_sip_retrieve_auths_vector()'s return code + * because we're checking the count of objects in the vector. + * + * Don't forget to call + * ast_sip_cleanup_auth_objects_vector(&auth_objects_vector); + * AST_VECTOR_FREE(&auth_objects_vector); + * when you're done with the vector + */ + ast_sip_retrieve_auths_vector(auth_ids_vector, &auth_objects_vector); + auth_object_count = AST_VECTOR_SIZE(&auth_objects_vector); + if (auth_object_count == 0) { + /* + * If none of the auth ids were found, we can't continue. + * We're OK if there's at least one left. + * ast_sip_retrieve_auths_vector() will print a warning for every + * id that wasn't found. + */ + res = -1; + goto cleanup; + } + if (pjsip_auth_clt_init(&auth_sess, ast_sip_get_pjsip_endpoint(), old_request->pool, 0) != PJ_SUCCESS) { ast_log(LOG_ERROR, "%s: '%s': Failed to initialize client authentication session\n", id_type, id); - return -1; + res = -1; + goto cleanup; } - if (set_outbound_authentication_credentials(&auth_sess, auths, challenge, auth_hdr)) { - ast_log(LOG_WARNING, "%s: '%s': Failed to set authentication credentials\n", - id_type, id); -#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT) - /* In case it is not a noop here in the future. */ - pjsip_auth_clt_deinit(&auth_sess); -#endif - return -1; + /* + * realms is used only for displaying good error messages. + */ + realms = ast_str_create(32); + if (!realms) { + res = -1; + goto cleanup; } + /* + * Load pjproject with the valid credentials for the Authentication headers + * received on the 401 or 407 response. + */ + status = set_outbound_authentication_credentials(&auth_sess, &auth_objects_vector, challenge, &realms); + switch (status) { + case PJ_SUCCESS: + break; + case PJSIP_ENOCREDENTIAL: + ast_log(LOG_WARNING, + "%s: '%s': No auth objects matching realm(s) '%s' from challenge found.\n", id_type, id, + realms ? ast_str_buffer(realms) : "<none>"); + res = -1; + goto cleanup; + default: + ast_log(LOG_WARNING, "%s: '%s': Failed to set authentication credentials\n", id_type, id); + res = -1; + goto cleanup; + } + + /* + * reinit_req actually creates the Authorization headers to send on + * the next request. If reinit_req already has a cached credential + * from an earlier successful authorization, it'll use it. Otherwise + * it'll create a new authorization and cache it. + */ status = pjsip_auth_clt_reinit_req(&auth_sess, challenge, old_request, new_request); -#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT) - /* Release any cached auths */ - pjsip_auth_clt_deinit(&auth_sess); -#endif switch (status) { case PJ_SUCCESS: @@ -167,22 +534,16 @@ static int digest_create_request_with_auth(const struct ast_sip_auth_vector *aut cseq = pjsip_msg_find_hdr((*new_request)->msg, PJSIP_H_CSEQ, NULL); ast_assert(cseq != NULL); ++cseq->cseq; - return 0; + res = 0; + goto cleanup; case PJSIP_ENOCREDENTIAL: - realms = ast_str_create(32); - if (realms) { - ast_str_append(&realms, 0, "%.*s", (int)auth_hdr->challenge.common.realm.slen, - auth_hdr->challenge.common.realm.ptr); - while((auth_hdr = get_auth_header(challenge, auth_hdr->next))) { - ast_str_append(&realms, 0, ",%.*s", (int)auth_hdr->challenge.common.realm.slen, - auth_hdr->challenge.common.realm.ptr); - } - } + /* + * This should be rare since set_outbound_authentication_credentials() + * did the matching but you never know. + */ ast_log(LOG_WARNING, - "%s: '%s': Unable to create request with auth. " - "No auth credentials for realm(s) '%s' in challenge.\n", id_type, id, - realms ? ast_str_buffer(realms) : "<unknown>"); - ast_free(realms); + "%s: '%s': No auth objects matching realm(s) '%s' from challenge found.\n", id_type, id, + realms ? ast_str_buffer(realms) : "<none>"); break; case PJSIP_EAUTHSTALECOUNT: ast_log(LOG_WARNING, @@ -198,8 +559,19 @@ static int digest_create_request_with_auth(const struct ast_sip_auth_vector *aut id_type, id); break; } + res = -1; + +cleanup: +#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT) + /* Release any cached auths */ + pjsip_auth_clt_deinit(&auth_sess); +#endif + + ast_sip_cleanup_auth_objects_vector(&auth_objects_vector); + AST_VECTOR_FREE(&auth_objects_vector); + ast_free(realms); - return -1; + return res; } static struct ast_sip_outbound_authenticator digest_authenticator = { diff --git a/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch b/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch new file mode 100644 index 0000000000000000000000000000000000000000..a2db2200e7c89f0708b1d71c30d6f133f98d4ad1 --- /dev/null +++ b/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch @@ -0,0 +1,212 @@ +From bdbeb7c4b2b11efc2e59f5dee7aa4360a2bc9fff Mon Sep 17 00:00:00 2001 +From: sauwming <ming@teluu.com> +Date: Thu, 22 Apr 2021 14:03:28 +0800 +Subject: [PATCH 90/90] Skip unsupported digest algorithm (#2408) + +Co-authored-by: Nanang Izzuddin <nanang@teluu.com> +--- + pjsip/src/pjsip/sip_auth_client.c | 32 +++++-- + tests/pjsua/scripts-sipp/uas-auth-two-algo.py | 7 ++ + .../pjsua/scripts-sipp/uas-auth-two-algo.xml | 83 +++++++++++++++++++ + 3 files changed, 117 insertions(+), 5 deletions(-) + create mode 100644 tests/pjsua/scripts-sipp/uas-auth-two-algo.py + create mode 100644 tests/pjsua/scripts-sipp/uas-auth-two-algo.xml + +diff --git a/pjsip/src/pjsip/sip_auth_client.c b/pjsip/src/pjsip/sip_auth_client.c +index 828b04db9..7eb2f5cd1 100644 +--- a/pjsip/src/pjsip/sip_auth_client.c ++++ b/pjsip/src/pjsip/sip_auth_client.c +@@ -1042,7 +1042,7 @@ static pj_status_t process_auth( pj_pool_t *req_pool, + pjsip_hdr *hdr; + pj_status_t status; + +- /* See if we have sent authorization header for this realm */ ++ /* See if we have sent authorization header for this realm (and scheme) */ + hdr = tdata->msg->hdr.next; + while (hdr != &tdata->msg->hdr) { + if ((hchal->type == PJSIP_H_WWW_AUTHENTICATE && +@@ -1052,7 +1052,8 @@ static pj_status_t process_auth( pj_pool_t *req_pool, + { + sent_auth = (pjsip_authorization_hdr*) hdr; + if (pj_stricmp(&hchal->challenge.common.realm, +- &sent_auth->credential.common.realm )==0) ++ &sent_auth->credential.common.realm)==0 && ++ pj_stricmp(&hchal->scheme, &sent_auth->scheme)==0) + { + /* If this authorization has empty response, remove it. */ + if (pj_stricmp(&sent_auth->scheme, &pjsip_DIGEST_STR)==0 && +@@ -1062,6 +1063,14 @@ static pj_status_t process_auth( pj_pool_t *req_pool, + hdr = hdr->next; + pj_list_erase(sent_auth); + continue; ++ } else ++ if (pj_stricmp(&sent_auth->scheme, &pjsip_DIGEST_STR)==0 && ++ pj_stricmp(&sent_auth->credential.digest.algorithm, ++ &hchal->challenge.digest.algorithm)!=0) ++ { ++ /* Same 'digest' scheme but different algo */ ++ hdr = hdr->next; ++ continue; + } else { + /* Found previous authorization attempt */ + break; +@@ -1155,9 +1164,10 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess, + { + pjsip_tx_data *tdata; + const pjsip_hdr *hdr; +- unsigned chal_cnt; ++ unsigned chal_cnt, auth_cnt; + pjsip_via_hdr *via; + pj_status_t status; ++ pj_status_t last_auth_err; + + PJ_ASSERT_RETURN(sess && rdata && old_request && new_request, + PJ_EINVAL); +@@ -1178,6 +1188,8 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess, + */ + hdr = rdata->msg_info.msg->hdr.next; + chal_cnt = 0; ++ auth_cnt = 0; ++ last_auth_err = PJSIP_EAUTHNOAUTH; + while (hdr != &rdata->msg_info.msg->hdr) { + pjsip_cached_auth *cached_auth; + const pjsip_www_authenticate_hdr *hchal; +@@ -1222,8 +1234,13 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess, + */ + status = process_auth(tdata->pool, hchal, tdata->msg->line.req.uri, + tdata, sess, cached_auth, &hauth); +- if (status != PJ_SUCCESS) +- return status; ++ if (status != PJ_SUCCESS) { ++ last_auth_err = status; ++ ++ /* Process next header. */ ++ hdr = hdr->next; ++ continue; ++ } + + if (pj_pool_get_used_size(cached_auth->pool) > + PJSIP_AUTH_CACHED_POOL_MAX_SIZE) +@@ -1236,12 +1253,17 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess, + + /* Process next header. */ + hdr = hdr->next; ++ auth_cnt++; + } + + /* Check if challenge is present */ + if (chal_cnt == 0) + return PJSIP_EAUTHNOCHAL; + ++ /* Check if any authorization header has been created */ ++ if (auth_cnt == 0) ++ return last_auth_err; ++ + /* Remove branch param in Via header. */ + via = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL); + via->branch_param.slen = 0; +diff --git a/tests/pjsua/scripts-sipp/uas-auth-two-algo.py b/tests/pjsua/scripts-sipp/uas-auth-two-algo.py +new file mode 100644 +index 000000000..c79c9f6d3 +--- /dev/null ++++ b/tests/pjsua/scripts-sipp/uas-auth-two-algo.py +@@ -0,0 +1,7 @@ ++# $Id$ ++# ++import inc_const as const ++ ++PJSUA = ["--null-audio --max-calls=1 --id=sip:a@localhost --username=a --realm=* --registrar=$SIPP_URI"] ++ ++PJSUA_EXPECTS = [[0, "registration success", ""]] +diff --git a/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml b/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml +new file mode 100644 +index 000000000..bd4871940 +--- /dev/null ++++ b/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml +@@ -0,0 +1,83 @@ ++<?xml version="1.0" encoding="ISO-8859-1" ?> ++<!DOCTYPE scenario SYSTEM "sipp.dtd"> ++ ++<scenario name="Basic UAS responder"> ++ <recv request="REGISTER" crlf="true"> ++ </recv> ++ ++ <send> ++ <![CDATA[ ++ SIP/2.0 100 Trying ++ [last_Via:];received=1.1.1.1;rport=1111 ++ [last_From:] ++ [last_To:];tag=[call_number] ++ [last_Call-ID:] ++ [last_CSeq:] ++ Content-Length: 0 ++ ]]> ++ </send> ++ ++ <send> ++ <![CDATA[ ++ SIP/2.0 401 Unauthorized ++ [last_Via:];received=1.1.1.1;rport=1111 ++ [last_From:] ++ [last_To:];tag=[call_number] ++ [last_Call-ID:] ++ [last_CSeq:] ++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=SHA-256, qop="auth" ++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=MD5, qop="auth" ++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=MD2, qop="auth" ++ Content-Length: 0 ++ ]]> ++ </send> ++ ++ <recv request="REGISTER" crlf="true"> ++ <action> ++ <ereg regexp=".*" ++ search_in="hdr" ++ header="Authorization:" ++ assign_to="have_auth" /> ++ </action> ++ </recv> ++ ++ <nop next="resp_okay" test="have_auth" /> ++ ++ <send next="end"> ++ <![CDATA[ ++ SIP/2.0 403 no auth ++ [last_Via:];received=1.1.1.1;rport=1111 ++ [last_From:] ++ [last_To:];tag=[call_number] ++ [last_Call-ID:] ++ [last_CSeq:] ++ [last_Contact:] ++ Content-Length: 0 ++ ]]> ++ </send> ++ ++ <label id="resp_okay" /> ++ ++ <send> ++ <![CDATA[ ++ SIP/2.0 200 OK ++ [last_Via:];received=1.1.1.1;rport=1111 ++ [last_From:] ++ [last_To:];tag=[call_number] ++ [last_Call-ID:] ++ [last_CSeq:] ++ [last_Contact:] ++ Content-Length: 0 ++ ]]> ++ </send> ++ ++ <label id="end" /> ++ ++ <!-- definition of the response time repartition table (unit is ms) --> ++ <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/> ++ ++ <!-- definition of the call length repartition table (unit is ms) --> ++ <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/> ++ ++</scenario> ++ +-- +2.31.1 +