2013-04-25 18:25:31 +00:00
|
|
|
/*
|
|
|
|
* Asterisk -- An open source telephony toolkit.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2013, Digium, Inc.
|
|
|
|
*
|
|
|
|
* Mark Michelson <mmichelson@digium.com>
|
|
|
|
*
|
|
|
|
* See http://www.asterisk.org for more information about
|
|
|
|
* the Asterisk project. Please do not directly contact
|
|
|
|
* any of the maintainers of this project for assistance;
|
|
|
|
* the project provides a web site, mailing lists and IRC
|
|
|
|
* channels for your use.
|
|
|
|
*
|
|
|
|
* This program is free software, distributed under the terms of
|
|
|
|
* the GNU General Public License Version 2. See the LICENSE file
|
|
|
|
* at the top of the source tree.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "asterisk.h"
|
|
|
|
|
|
|
|
#include <pjsip.h>
|
|
|
|
|
2013-07-30 18:14:50 +00:00
|
|
|
#include "asterisk/res_pjsip.h"
|
2013-04-25 18:25:31 +00:00
|
|
|
#include "asterisk/logger.h"
|
|
|
|
#include "asterisk/module.h"
|
|
|
|
#include "asterisk/strings.h"
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
#include "asterisk/test.h"
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
/*!
|
|
|
|
* \file
|
|
|
|
* \brief PJSIP UAS Authentication
|
|
|
|
*
|
|
|
|
* This module handles authentication when Asterisk is the UAS.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
/*** MODULEINFO
|
|
|
|
<depend>pjproject</depend>
|
2013-07-30 18:14:50 +00:00
|
|
|
<depend>res_pjsip</depend>
|
2013-04-25 18:25:31 +00:00
|
|
|
<support_level>core</support_level>
|
|
|
|
***/
|
|
|
|
|
2023-09-23 02:32:43 +09:00
|
|
|
static char default_realm[AST_SIP_AUTH_MAX_REALM_LENGTH + 1];
|
res_pjsip: Add ability to identify by Authorization username
A feature of chan_sip that service providers relied upon was the ability to
identify by the Authorization username. This is most often used when customers
have a PBX that needs to register rather than identify by IP address. From my
own experiance, this is pretty common with small businesses who otherwise
don't need a static IP.
In this scenario, a register from the customer's PBX may succeed because From
will usually contain the PBXs account id but an INVITE will contain the caller
id. With nothing recognizable in From, the service provider's Asterisk can
never match to an endpoint and the INVITE just stays unauthorized.
The fixes:
A new value "auth_username" has been added to endpoint/identify_by that
will use the username and digest fields in the Authorization header
instead of username and domain in the the From header to match an endpoint,
or the To header to match an aor. This code as added to
res_pjsip_endpoint_identifier_user rather than creating a new module.
Although identify_by was always a comma-separated list, there was only
1 choice so order wasn't preserved. So to keep the order, a vector was added
to the end of ast_sip_endpoint. This is only used by res_pjsip_registrar
to find the aor. The res_pjsip_endpoint_identifier_* modules are called in
globals/endpoint_identifier_order.
Along the way, the logic in res_pjsip_registrar was corrected to match
most-specific to least-specific as res_pjsip_endpoint_identifier_user does.
The order is:
username@domain
username@domain_alias
username
Auth by username does present 1 problem however, the first INVITE won't have
an Authorization header so the distributor, not finding a match on anything,
sends a securty_alert. It still sends a 401 with a challenge so the next
INVITE will have the Authorization header and presumably succeed. As a result
though, that first security alert is actually a false alarm.
To address this, a new feature has been added to pjsip_distributor that keeps
track of unidentified requests and only sends the security alert if a
configurable number of unidentified requests come from the same IP in a
configurable amout of time. Those configuration options have been added to
the global config object. This feature is only used when auth_username
is enabled.
Finally, default_realm was added to the globals object to replace the hard
coded "asterisk" used when an endpoint is not yet identified.
The testsuite tests all pass but new tests are forthcoming for this new
feature.
ASTERISK-25835 #close
Reported-by: Ross Beer
Change-Id: I30ba62d208e6f63439600916fcd1c08a365ed69d
2016-03-07 17:34:31 -07:00
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
AO2_GLOBAL_OBJ_STATIC(entity_id);
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Determine if authentication is required
|
|
|
|
*
|
|
|
|
* Authentication is required if the endpoint has at least one auth
|
|
|
|
* section specified
|
|
|
|
*/
|
|
|
|
static int digest_requires_authentication(struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata)
|
|
|
|
{
|
2013-12-10 15:15:13 +00:00
|
|
|
RAII_VAR(struct ast_sip_endpoint *, artificial, ast_sip_get_artificial_endpoint(), ao2_cleanup);
|
|
|
|
|
|
|
|
return endpoint == artificial || AST_VECTOR_SIZE(&endpoint->inbound_auths) > 0;
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void auth_store_cleanup(void *data)
|
|
|
|
{
|
|
|
|
struct ast_sip_auth **auth = data;
|
|
|
|
|
|
|
|
ao2_cleanup(*auth);
|
|
|
|
ast_free(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Thread-local storage for \ref ast_sip_auth
|
|
|
|
*
|
|
|
|
* The PJSIP authentication API is a bit annoying. When you set
|
|
|
|
* up an authentication server, you specify a lookup callback to
|
|
|
|
* call into when verifying incoming credentials. The problem
|
|
|
|
* with this callback is that it only gives you the realm and
|
|
|
|
* authentication username. In 2.0.5, there is a new version of
|
|
|
|
* the callback you can use that gives the pjsip_rx_data in
|
|
|
|
* addition.
|
|
|
|
*
|
|
|
|
* Unfortunately, the data we actually \b need is the
|
|
|
|
* \ref ast_sip_auth we are currently observing. So we have two
|
|
|
|
* choices:
|
|
|
|
* 1) Use the current PJSIP API and use thread-local storage
|
|
|
|
* to temporarily store our SIP authentication information. Then
|
|
|
|
* in the callback, we can retrieve the authentication info and
|
|
|
|
* use as needed. Given our threading model, this is safe.
|
|
|
|
* 2) Use the 2.0.5 API and temporarily store the authentication
|
|
|
|
* information in the rdata's endpoint_info. Then in the callback,
|
|
|
|
* we can retrieve the authentication info from the rdata.
|
|
|
|
*
|
|
|
|
* I've chosen option 1 since it does not require backporting
|
|
|
|
* any APIs from future versions of PJSIP, plus I feel the
|
|
|
|
* thread-local option is a bit cleaner.
|
|
|
|
*/
|
|
|
|
AST_THREADSTORAGE_CUSTOM(auth_store, NULL, auth_store_cleanup);
|
|
|
|
|
|
|
|
/*!
|
2017-01-08 20:32:17 -06:00
|
|
|
* \brief Store shallow copy authentication information in thread-local storage
|
2013-04-25 18:25:31 +00:00
|
|
|
*/
|
2017-01-08 20:32:17 -06:00
|
|
|
static int store_auth(const struct ast_sip_auth *auth)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
2017-01-08 20:32:17 -06:00
|
|
|
const struct ast_sip_auth **pointing;
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
pointing = ast_threadstorage_get(&auth_store, sizeof(pointing));
|
2017-01-08 20:32:17 -06:00
|
|
|
if (!pointing) {
|
2013-04-25 18:25:31 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
*pointing = auth;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
2017-01-08 20:32:17 -06:00
|
|
|
* \brief Remove shallow copy authentication information from thread-local storage
|
2013-04-25 18:25:31 +00:00
|
|
|
*/
|
|
|
|
static int remove_auth(void)
|
|
|
|
{
|
|
|
|
struct ast_sip_auth **pointing;
|
2017-01-08 20:32:17 -06:00
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
pointing = ast_threadstorage_get(&auth_store, sizeof(pointing));
|
|
|
|
if (!pointing) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
*pointing = NULL;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
2017-01-08 20:32:17 -06:00
|
|
|
* \brief Retrieve shallow copy authentication information from thread-local storage
|
2013-04-25 18:25:31 +00:00
|
|
|
*/
|
2017-01-08 20:32:17 -06:00
|
|
|
static const struct ast_sip_auth *get_auth(void)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
|
|
|
struct ast_sip_auth **auth;
|
2017-01-08 20:32:17 -06:00
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
auth = ast_threadstorage_get(&auth_store, sizeof(auth));
|
2017-01-08 20:32:17 -06:00
|
|
|
if (auth) {
|
2013-04-25 18:25:31 +00:00
|
|
|
return *auth;
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
static struct pjsip_authorization_hdr *get_authorization_hdr(
|
|
|
|
const char *auth_id, const char *realm, const pjsip_rx_data *rdata)
|
|
|
|
{
|
|
|
|
const char *src_name = rdata->pkt_info.src_name;
|
|
|
|
struct pjsip_authorization_hdr *auth_hdr =
|
|
|
|
(pjsip_authorization_hdr *) &rdata->msg_info.msg->hdr;
|
|
|
|
SCOPE_ENTER(3, "%s:%s: realm: %s\n", auth_id, src_name, realm);
|
|
|
|
|
|
|
|
while ((auth_hdr = pjsip_msg_find_hdr(rdata->msg_info.msg,
|
|
|
|
PJSIP_H_AUTHORIZATION, auth_hdr ? auth_hdr->next : NULL))) {
|
|
|
|
if (pj_strcmp2(&auth_hdr->credential.common.realm, realm) == 0) {
|
|
|
|
SCOPE_EXIT_RTN_VALUE(auth_hdr, "%s:%s: realm: %s Found header\n",
|
|
|
|
auth_id, src_name, realm);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
SCOPE_EXIT_RTN_VALUE(NULL, "%s:%s: realm: %s No auth header found\n",
|
|
|
|
auth_id, src_name, realm);
|
|
|
|
}
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
/*!
|
|
|
|
* \brief Lookup callback for authentication verification
|
|
|
|
*
|
|
|
|
* This function is called when we call pjsip_auth_srv_verify(). It
|
|
|
|
* expects us to verify that the realm and account name from the
|
2024-10-17 08:02:08 -06:00
|
|
|
* Authorization header are correct and that we can support the digest
|
|
|
|
* algorithm specified. We are then supposed to supply a password or
|
|
|
|
* password_digest for the algorithm.
|
|
|
|
*
|
|
|
|
* The auth object must have previously been saved to thread-local storage.
|
2013-04-25 18:25:31 +00:00
|
|
|
*
|
|
|
|
* \param pool A memory pool we can use for allocations
|
2024-10-17 08:02:08 -06:00
|
|
|
* \param param Contains the realm, username, rdata and auth header
|
|
|
|
* \param cred_info The credentials we need to fill in
|
2013-04-25 18:25:31 +00:00
|
|
|
* \retval PJ_SUCCESS Successful authentication
|
|
|
|
* \retval other Unsuccessful
|
|
|
|
*/
|
2024-10-17 08:02:08 -06:00
|
|
|
static pj_status_t digest_lookup(pj_pool_t *pool,
|
|
|
|
const pjsip_auth_lookup_cred_param *param,
|
|
|
|
pjsip_cred_info *cred_info)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
2024-10-17 08:02:08 -06:00
|
|
|
const struct ast_sip_auth *auth = get_auth();
|
|
|
|
const char *realm = S_OR(auth->realm, default_realm);
|
|
|
|
const char *creds;
|
|
|
|
const char *auth_name = (auth ? ast_sorcery_object_get_id(auth) : "none");
|
|
|
|
struct pjsip_authorization_hdr *auth_hdr = get_authorization_hdr(auth_name, realm, param->rdata);
|
|
|
|
const pjsip_auth_algorithm *algorithm =
|
|
|
|
ast_sip_auth_get_algorithm_by_iana_name(&auth_hdr->credential.digest.algorithm);
|
|
|
|
const char *src_name = param->rdata->pkt_info.src_name;
|
|
|
|
SCOPE_ENTER(4, "%s:%s:"
|
|
|
|
" srv realm: " PJSTR_PRINTF_SPEC
|
|
|
|
" auth realm: %s"
|
|
|
|
" hdr realm: " PJSTR_PRINTF_SPEC
|
|
|
|
" auth user: %s"
|
|
|
|
" hdr user: " PJSTR_PRINTF_SPEC
|
|
|
|
" algorithm: " PJSTR_PRINTF_SPEC
|
|
|
|
"\n",
|
|
|
|
auth_name, src_name,
|
|
|
|
PJSTR_PRINTF_VAR(param->realm),
|
|
|
|
realm,
|
|
|
|
PJSTR_PRINTF_VAR(auth_hdr->credential.common.realm),
|
|
|
|
auth->auth_user,
|
|
|
|
PJSTR_PRINTF_VAR(param->acc_name),
|
|
|
|
PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2017-01-08 20:32:17 -06:00
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
if (!auth) {
|
2024-10-17 08:02:08 -06:00
|
|
|
/* This can only happen if the auth object was not saved to thread-local storage */
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: No auth object found\n",
|
|
|
|
auth_name, src_name);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2013-07-02 17:06:06 +00:00
|
|
|
if (auth->type == AST_SIP_AUTH_TYPE_ARTIFICIAL) {
|
2024-10-17 08:02:08 -06:00
|
|
|
/*
|
|
|
|
* This shouldn't happen because this function can only be invoked
|
|
|
|
* if there was an Authorization header in the incoming request.
|
|
|
|
*/
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: Artificial auth object\n",
|
|
|
|
auth_name, src_name);
|
2013-07-02 17:06:06 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
if (pj_strcmp2(¶m->realm, realm) != 0) {
|
|
|
|
/*
|
|
|
|
* This shouldn't happen because param->realm was passed in from the auth
|
|
|
|
* when we called pjsip_auth_srv_init2.
|
|
|
|
*/
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: Realm '%s' mismatch\n",
|
|
|
|
auth_name, src_name, realm);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
2024-10-17 08:02:08 -06:00
|
|
|
|
|
|
|
if (pj_strcmp2(¶m->acc_name, auth->auth_user) != 0) {
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: Username '%s' mismatch\n",
|
|
|
|
auth_name, src_name, auth->auth_user);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
if (!ast_sip_auth_is_algorithm_available(auth, &auth->supported_algorithms_uas,
|
|
|
|
algorithm->algorithm_type)) {
|
|
|
|
/*
|
|
|
|
* This shouldn't happen because we shouldn't have sent a challenge for
|
|
|
|
* an unsupported algorithm.
|
|
|
|
*/
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: Algorithm '" PJSTR_PRINTF_SPEC
|
|
|
|
"' not supported or auth doesn't contain appropriate credentials\n",
|
|
|
|
auth_name, src_name, PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
2024-10-17 08:02:08 -06:00
|
|
|
|
|
|
|
pj_strdup2(pool, &cred_info->realm, realm);
|
|
|
|
pj_strdup2(pool, &cred_info->username, auth->auth_user);
|
|
|
|
|
|
|
|
creds = ast_sip_auth_get_creds(auth, algorithm->algorithm_type, &cred_info->data_type);
|
|
|
|
if (!creds) {
|
|
|
|
/*
|
|
|
|
* This shouldn't happen because we checked the auth object when we
|
|
|
|
* loaded it to make sure it had the appropriate credentials for each
|
|
|
|
* algorithm in supported_algorithms_uas.
|
|
|
|
*/
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJSIP_SC_FORBIDDEN, "%s:%s: No plain text or digest password found for algorithm '" PJSTR_PRINTF_SPEC "'\n",
|
|
|
|
auth_name, src_name, PJSTR_PRINTF_VAR(algorithm->iana_name));
|
|
|
|
}
|
|
|
|
pj_strdup2(pool, &cred_info->data, creds);
|
|
|
|
#ifdef HAVE_PJSIP_AUTH_NEW_DIGESTS
|
|
|
|
if (cred_info->data_type == PJSIP_CRED_DATA_DIGEST) {
|
|
|
|
cred_info->algorithm_type = algorithm->algorithm_type;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
SCOPE_EXIT_RTN_VALUE(PJ_SUCCESS, "%s:%s: Success. Data type: %s Algorithm '" PJSTR_PRINTF_SPEC "'\n",
|
|
|
|
auth_name, src_name, cred_info->data_type ? "digest" : "plain text", PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Calculate a nonce
|
|
|
|
*
|
|
|
|
* We use this in order to create authentication challenges. We also use this in order
|
|
|
|
* to verify that an incoming request with credentials could be in response to one
|
|
|
|
* of our challenges.
|
|
|
|
*
|
|
|
|
* The nonce is calculated from a timestamp, the source IP address, the source port, a
|
|
|
|
* unique ID for us, and the realm. This helps to ensure that the incoming request
|
|
|
|
* is from the same source that the nonce was calculated for. Including the realm
|
|
|
|
* ensures that multiple challenges to the same request have different nonces.
|
|
|
|
*
|
2021-11-16 10:40:37 +01:00
|
|
|
* \param nonce
|
|
|
|
* \param timestamp A UNIX timestamp expressed as a string
|
2013-04-25 18:25:31 +00:00
|
|
|
* \param rdata The incoming request
|
|
|
|
* \param realm The realm for which authentication should occur
|
|
|
|
*/
|
2024-10-17 08:02:08 -06:00
|
|
|
static int build_nonce(struct ast_str **nonce, const char *timestamp,
|
|
|
|
const pjsip_rx_data *rdata, const char *realm)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
|
|
|
struct ast_str *str = ast_str_alloca(256);
|
|
|
|
RAII_VAR(char *, eid, ao2_global_obj_ref(entity_id), ao2_cleanup);
|
2014-01-03 21:13:30 +00:00
|
|
|
char hash[33];
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2016-05-05 11:37:37 -05:00
|
|
|
/*
|
|
|
|
* Note you may be tempted to think why not include the port. The reason
|
|
|
|
* is that when using TCP the port can potentially differ from before.
|
|
|
|
*/
|
2013-04-25 18:25:31 +00:00
|
|
|
ast_str_append(&str, 0, "%s", timestamp);
|
|
|
|
ast_str_append(&str, 0, ":%s", rdata->pkt_info.src_name);
|
|
|
|
ast_str_append(&str, 0, ":%s", eid);
|
|
|
|
ast_str_append(&str, 0, ":%s", realm);
|
|
|
|
ast_md5_hash(hash, ast_str_buffer(str));
|
|
|
|
|
|
|
|
ast_str_append(nonce, 0, "%s/%s", timestamp, hash);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Ensure that a nonce on an incoming request is sane.
|
|
|
|
*
|
|
|
|
* The nonce in an incoming Authorization header needs to pass some scrutiny in order
|
|
|
|
* for us to consider accepting it. What we do is re-build a nonce based on request
|
|
|
|
* data and a realm and see if it matches the nonce they sent us.
|
|
|
|
* \param candidate The nonce on an incoming request
|
|
|
|
* \param rdata The incoming request
|
|
|
|
* \param auth The auth credentials we are trying to match against.
|
|
|
|
* \retval 0 Nonce does not pass validity checks
|
|
|
|
* \retval 1 Nonce passes validity check
|
|
|
|
*/
|
|
|
|
static int check_nonce(const char *candidate, const pjsip_rx_data *rdata, const struct ast_sip_auth *auth)
|
|
|
|
{
|
|
|
|
char *copy = ast_strdupa(candidate);
|
|
|
|
char *timestamp = strsep(©, "/");
|
|
|
|
int timestamp_int;
|
|
|
|
time_t now = time(NULL);
|
|
|
|
struct ast_str *calculated = ast_str_alloca(64);
|
|
|
|
|
|
|
|
if (!copy) {
|
|
|
|
/* Clearly a bad nonce! */
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sscanf(timestamp, "%30d", ×tamp_int) != 1) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((int) now - timestamp_int > auth->nonce_lifetime) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
build_nonce(&calculated, timestamp, rdata, S_OR(auth->realm, default_realm));
|
2013-04-25 18:25:31 +00:00
|
|
|
ast_debug(3, "Calculated nonce %s. Actual nonce is %s\n", ast_str_buffer(calculated), candidate);
|
|
|
|
if (strcmp(ast_str_buffer(calculated), candidate)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Result of digest verification
|
|
|
|
*/
|
|
|
|
enum digest_verify_result {
|
|
|
|
/*! Authentication credentials incorrect */
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
AUTH_FAIL = 0,
|
2013-04-25 18:25:31 +00:00
|
|
|
/*! Authentication credentials correct */
|
|
|
|
AUTH_SUCCESS,
|
|
|
|
/*! Authentication credentials correct but nonce mismatch */
|
|
|
|
AUTH_STALE,
|
2013-11-07 23:42:31 +00:00
|
|
|
/*! Authentication credentials were not provided */
|
|
|
|
AUTH_NOAUTH,
|
2013-04-25 18:25:31 +00:00
|
|
|
};
|
|
|
|
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
static char *verify_result_str[] = {
|
|
|
|
"FAIL",
|
|
|
|
"SUCCESS",
|
|
|
|
"STALE",
|
|
|
|
"NOAUTH"
|
|
|
|
};
|
2024-10-17 08:02:08 -06:00
|
|
|
|
|
|
|
static enum digest_verify_result find_authorization(const char *endpoint_id,
|
|
|
|
const struct ast_sip_auth *auth, const pjsip_rx_data *rdata)
|
|
|
|
{
|
|
|
|
const char *auth_id = ast_sorcery_object_get_id(auth);
|
|
|
|
const char *src_name = rdata->pkt_info.src_name;
|
|
|
|
const char *realm = S_OR(auth->realm, default_realm);
|
|
|
|
struct pjsip_authorization_hdr *auth_hdr =
|
|
|
|
(pjsip_authorization_hdr *) &rdata->msg_info.msg->hdr;
|
|
|
|
enum digest_verify_result res = AUTH_NOAUTH;
|
|
|
|
int authorization_found = 0;
|
|
|
|
char nonce[64];
|
|
|
|
SCOPE_ENTER(3, "%s:%s:%s: realm: %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm);
|
|
|
|
|
|
|
|
while ((auth_hdr = pjsip_msg_find_hdr(rdata->msg_info.msg,
|
|
|
|
PJSIP_H_AUTHORIZATION, auth_hdr ? auth_hdr->next : NULL))) {
|
|
|
|
ast_copy_pj_str(nonce, &auth_hdr->credential.digest.nonce, sizeof(nonce));
|
|
|
|
ast_trace(-1, "%s:%s:%s: Checking nonce %s hdr-realm: " PJSTR_PRINTF_SPEC " hdr-algo: " PJSTR_PRINTF_SPEC " \n",
|
|
|
|
endpoint_id, auth_id, src_name, nonce,
|
|
|
|
PJSTR_PRINTF_VAR(auth_hdr->credential.digest.realm),
|
|
|
|
PJSTR_PRINTF_VAR(auth_hdr->credential.digest.algorithm));
|
|
|
|
authorization_found++;
|
|
|
|
if (check_nonce(nonce, rdata, auth)
|
|
|
|
&& pj_strcmp2(&auth_hdr->credential.digest.realm, realm) == 0) {
|
|
|
|
res = AUTH_SUCCESS;
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
res = AUTH_STALE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!authorization_found) {
|
|
|
|
ast_trace(-1, "%s:%s:%s: No Authorization header found\n",
|
|
|
|
endpoint_id, auth_id, src_name);
|
|
|
|
res = AUTH_NOAUTH;
|
|
|
|
}
|
|
|
|
|
|
|
|
SCOPE_EXIT_RTN_VALUE(res, "%s:%s:%s: realm: %s Result %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm, verify_result_str[res]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Common code for initializing a pjsip_auth_srv
|
|
|
|
*/
|
|
|
|
static void setup_auth_srv(pj_pool_t *pool, pjsip_auth_srv *auth_server, const char *realm)
|
|
|
|
{
|
|
|
|
pjsip_auth_srv_init_param *param = pj_pool_alloc(pool, sizeof(*param));
|
|
|
|
pj_str_t *pj_realm = pj_pool_alloc(pool, sizeof(*pj_realm));
|
|
|
|
|
|
|
|
pj_cstr(pj_realm, realm);
|
|
|
|
param->realm = pj_realm;
|
|
|
|
param->lookup2 = digest_lookup;
|
|
|
|
param->options = 0;
|
|
|
|
|
|
|
|
pjsip_auth_srv_init2(pool, auth_server, param);
|
|
|
|
}
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
/*!
|
2024-10-17 08:02:08 -06:00
|
|
|
* \brief Verify incoming credentials
|
2013-04-25 18:25:31 +00:00
|
|
|
*
|
2024-10-17 08:02:08 -06:00
|
|
|
* \param endpoint_id For logging
|
|
|
|
* \param auth The ast_sip_auth to check against
|
|
|
|
* \param rdata The incoming request
|
|
|
|
* \param pool A pool to use for the auth server
|
|
|
|
* \return One of digest_verify_result
|
2013-04-25 18:25:31 +00:00
|
|
|
*/
|
2024-10-17 08:02:08 -06:00
|
|
|
static int verify(const char *endpoint_id, const struct ast_sip_auth *auth,
|
|
|
|
pjsip_rx_data *rdata, pj_pool_t *pool)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
2024-10-17 08:02:08 -06:00
|
|
|
const char *auth_id = ast_sorcery_object_get_id(auth);
|
|
|
|
const char *realm = S_OR(auth->realm, default_realm);
|
|
|
|
const char *src_name = rdata->pkt_info.src_name;
|
2013-04-25 18:25:31 +00:00
|
|
|
pj_status_t authed;
|
|
|
|
int response_code;
|
|
|
|
pjsip_auth_srv auth_server;
|
|
|
|
int stale = 0;
|
2024-10-17 08:02:08 -06:00
|
|
|
enum digest_verify_result res = AUTH_FAIL;
|
|
|
|
SCOPE_ENTER(3, "%s:%s:%s: realm: %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm);
|
|
|
|
|
|
|
|
res = find_authorization(endpoint_id, auth, rdata);
|
|
|
|
if (res == AUTH_NOAUTH)
|
|
|
|
{
|
|
|
|
ast_test_suite_event_notify("INCOMING_AUTH_VERIFY_RESULT",
|
|
|
|
"Realm: %s\r\n"
|
|
|
|
"Username: %s\r\n"
|
|
|
|
"Status: %s",
|
|
|
|
realm, auth->auth_user, verify_result_str[res]);
|
|
|
|
SCOPE_EXIT_RTN_VALUE(res, "%s:%s:%s: No Authorization header found\n",
|
|
|
|
endpoint_id, auth_id, src_name);
|
|
|
|
}
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
if (res == AUTH_STALE) {
|
|
|
|
/* Couldn't find an authorization with a sane nonce.
|
2013-04-25 18:25:31 +00:00
|
|
|
* Nonce mismatch may just be due to staleness.
|
|
|
|
*/
|
|
|
|
stale = 1;
|
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
setup_auth_srv(pool, &auth_server, realm);
|
2013-04-25 18:25:31 +00:00
|
|
|
store_auth(auth);
|
2024-10-17 08:02:08 -06:00
|
|
|
/* pjsip_auth_srv_verify will invoke digest_lookup */
|
|
|
|
authed = SCOPE_CALL_WITH_RESULT(-1, pj_status_t, pjsip_auth_srv_verify, &auth_server, rdata, &response_code);
|
2013-04-25 18:25:31 +00:00
|
|
|
remove_auth();
|
|
|
|
if (authed == PJ_SUCCESS) {
|
|
|
|
if (stale) {
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
res = AUTH_STALE;
|
2013-04-25 18:25:31 +00:00
|
|
|
} else {
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
res = AUTH_SUCCESS;
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
2024-10-17 08:02:08 -06:00
|
|
|
} else {
|
|
|
|
char err[256];
|
|
|
|
res = AUTH_FAIL;
|
|
|
|
pj_strerror(authed, err, sizeof(err));
|
|
|
|
ast_trace(-1, "%s:%s:%s: authed: %s\n", endpoint_id, auth_id, src_name, err);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
2013-11-07 23:42:31 +00:00
|
|
|
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
ast_test_suite_event_notify("INCOMING_AUTH_VERIFY_RESULT",
|
|
|
|
"Realm: %s\r\n"
|
|
|
|
"Username: %s\r\n"
|
|
|
|
"Status: %s",
|
2024-10-17 08:02:08 -06:00
|
|
|
realm, auth->auth_user, verify_result_str[res]);
|
res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
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).
Thankfully however 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 headers and then send 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.
Changes:
* Added a new API ast_sip_retrieve_auths_vector() that takes in
a vector of auth ids (usually supplied on a call to
ast_sip_create_request_with_auth()) and populates another
vector with the actual objects.
* Refactored res_pjsip_outbound_authenticator_digest to handle
multiple Authenticate headers and set the stage for handling
additional digest algorithms.
* Added a pjproject patch that allows them to ignore digest
algorithms they don't support. This patch has already been
merged upstream.
* Updated documentation for auth objects in the XML and
in pjsip.conf.sample.
* Although res_pjsip_authenticator_digest isn't affected
by this change, some debugging and a testsuite AMI event
was added to facilitate testing.
Discovered during OpenSIPit 2021.
ASTERISK-29397
Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
2021-04-15 09:43:48 -06:00
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
SCOPE_EXIT_RTN_VALUE(res, "%s:%s:%s: Realm: %s Username: %s Result: %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm,
|
|
|
|
auth->auth_user, verify_result_str[res]);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
2024-10-17 08:02:08 -06:00
|
|
|
* \brief Send a WWW-Authenticate challenge
|
2013-04-25 18:25:31 +00:00
|
|
|
*
|
2024-10-17 08:02:08 -06:00
|
|
|
* \param endpoint_id For logging
|
|
|
|
* \param auth The auth object to use for the challenge
|
2013-04-25 18:25:31 +00:00
|
|
|
* \param tdata The response to add the challenge to
|
|
|
|
* \param rdata The request the challenge is in response to
|
|
|
|
* \param is_stale Indicates whether nonce on incoming request was stale
|
2024-10-17 08:02:08 -06:00
|
|
|
* \param algorithm_type The algorithm to use for the challenge
|
2013-04-25 18:25:31 +00:00
|
|
|
*/
|
2024-10-17 08:02:08 -06:00
|
|
|
static void challenge(const char *endpoint_id, struct ast_sip_auth *auth,
|
|
|
|
pjsip_tx_data *tdata, const pjsip_rx_data *rdata, int is_stale,
|
|
|
|
const pjsip_auth_algorithm *algorithm)
|
2013-04-25 18:25:31 +00:00
|
|
|
{
|
|
|
|
pj_str_t qop;
|
|
|
|
pj_str_t pj_nonce;
|
|
|
|
pjsip_auth_srv auth_server;
|
|
|
|
struct ast_str *nonce = ast_str_alloca(256);
|
|
|
|
char time_buf[32];
|
|
|
|
time_t timestamp = time(NULL);
|
2024-10-17 08:02:08 -06:00
|
|
|
pj_status_t res;
|
|
|
|
const char *realm = S_OR(auth->realm, default_realm);
|
|
|
|
const char *auth_id = ast_sorcery_object_get_id(auth);
|
|
|
|
const char *src_name = rdata->pkt_info.src_name;
|
|
|
|
SCOPE_ENTER(5, "%s:%s:%s: realm: %s time: %d algorithm: " PJSTR_PRINTF_SPEC " stale? %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm, (int)timestamp,
|
|
|
|
PJSTR_PRINTF_VAR(algorithm->iana_name), is_stale ? "yes" : "no");
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
snprintf(time_buf, sizeof(time_buf), "%d", (int) timestamp);
|
|
|
|
|
2013-07-02 17:06:06 +00:00
|
|
|
build_nonce(&nonce, time_buf, rdata, realm);
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2013-07-02 17:06:06 +00:00
|
|
|
setup_auth_srv(tdata->pool, &auth_server, realm);
|
2013-04-25 18:25:31 +00:00
|
|
|
|
|
|
|
pj_cstr(&pj_nonce, ast_str_buffer(nonce));
|
|
|
|
pj_cstr(&qop, "auth");
|
2024-10-17 08:02:08 -06:00
|
|
|
#ifdef HAVE_PJSIP_AUTH_NEW_DIGESTS
|
|
|
|
res = pjsip_auth_srv_challenge2(&auth_server, &qop, &pj_nonce,
|
|
|
|
NULL, is_stale ? PJ_TRUE : PJ_FALSE, tdata, algorithm->algorithm_type);
|
|
|
|
#else
|
|
|
|
res = pjsip_auth_srv_challenge(&auth_server, &qop, &pj_nonce,
|
|
|
|
NULL, is_stale ? PJ_TRUE : PJ_FALSE, tdata);
|
|
|
|
#endif
|
|
|
|
SCOPE_EXIT_RTN("%s:%s:%s: Sending challenge for realm: %s algorithm: " PJSTR_PRINTF_SPEC
|
|
|
|
" %s\n",
|
|
|
|
endpoint_id, auth_id, src_name, realm, PJSTR_PRINTF_VAR(algorithm->iana_name),
|
|
|
|
res == PJ_SUCCESS ? "succeeded" : "failed");
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
static char *check_auth_result_str[] = {
|
|
|
|
"CHALLENGE",
|
|
|
|
"SUCCESS",
|
|
|
|
"FAILED",
|
|
|
|
"ERROR",
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
/*!
|
|
|
|
* \brief Check authentication using Digest scheme
|
|
|
|
*
|
|
|
|
* This function will check an incoming message against configured authentication
|
|
|
|
* options. If \b any of the incoming Authorization headers result in successful
|
|
|
|
* authentication, then authentication is considered successful.
|
2013-07-02 17:06:06 +00:00
|
|
|
*
|
2025-01-28 09:14:34 -07:00
|
|
|
* \warning The return code from the function is used by the distributor to
|
|
|
|
* determine which log messages (if any) are emitted. Many admins will be
|
|
|
|
* using log parsers like fail2ban to block IPs that are repeatedly failing
|
|
|
|
* to authenticate so changing the return code could have unintended
|
|
|
|
* consequences.
|
|
|
|
*
|
|
|
|
* \retval AST_SIP_AUTHENTICATION_SUCCESS There was an Authorization header
|
|
|
|
* in the request and it verified successfully with at least one auth object
|
|
|
|
* on the endpoint. No further challenges sent.
|
|
|
|
*
|
|
|
|
* \retval AST_SIP_AUTHENTICATION_CHALLENGE There was NO Authorization header
|
|
|
|
* in the incoming request. We sent a 401 with one or more challenges.
|
|
|
|
*
|
|
|
|
* \retval AST_SIP_AUTHENTICATION_FAILED There were one or more Authorization
|
|
|
|
* headers in the request but they all failed to verify with any auth object
|
|
|
|
* on the endpoint. We sent a 401 with one or more challenges.
|
|
|
|
*
|
|
|
|
* \retval AST_SIP_AUTHENTICATION_ERROR An internal error occurred. No challenges
|
|
|
|
* were sent.
|
|
|
|
*
|
2013-04-25 18:25:31 +00:00
|
|
|
* \see ast_sip_check_authentication
|
|
|
|
*/
|
|
|
|
static enum ast_sip_check_auth_result digest_check_auth(struct ast_sip_endpoint *endpoint,
|
|
|
|
pjsip_rx_data *rdata, pjsip_tx_data *tdata)
|
|
|
|
{
|
2013-07-02 17:06:06 +00:00
|
|
|
struct ast_sip_auth **auths;
|
|
|
|
enum digest_verify_result *verify_res;
|
2017-01-08 20:32:17 -06:00
|
|
|
struct ast_sip_endpoint *artificial_endpoint;
|
2025-01-17 09:20:16 -07:00
|
|
|
enum ast_sip_check_auth_result res = AST_SIP_AUTHENTICATION_ERROR;
|
2017-01-08 20:32:17 -06:00
|
|
|
int idx;
|
|
|
|
int is_artificial;
|
2013-11-07 23:42:31 +00:00
|
|
|
int failures = 0;
|
2013-12-09 16:10:05 +00:00
|
|
|
size_t auth_size;
|
2024-10-17 08:02:08 -06:00
|
|
|
const char *endpoint_id = ast_sorcery_object_get_id(endpoint);
|
|
|
|
char *src_name = rdata->pkt_info.src_name;
|
|
|
|
SCOPE_ENTER(3, "%s:%s\n", endpoint_id, src_name);
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2013-12-09 16:10:05 +00:00
|
|
|
auth_size = AST_VECTOR_SIZE(&endpoint->inbound_auths);
|
2017-01-08 20:32:17 -06:00
|
|
|
ast_assert(0 < auth_size);
|
2013-12-09 16:10:05 +00:00
|
|
|
|
|
|
|
auths = ast_alloca(auth_size * sizeof(*auths));
|
|
|
|
verify_res = ast_alloca(auth_size * sizeof(*verify_res));
|
2013-07-02 17:06:06 +00:00
|
|
|
|
2017-01-08 20:32:17 -06:00
|
|
|
artificial_endpoint = ast_sip_get_artificial_endpoint();
|
|
|
|
if (!artificial_endpoint) {
|
|
|
|
/* Should not happen except possibly if we are shutting down. */
|
2024-10-17 08:02:08 -06:00
|
|
|
SCOPE_EXIT_RTN_VALUE(AST_SIP_AUTHENTICATION_ERROR);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2017-01-08 20:32:17 -06:00
|
|
|
is_artificial = endpoint == artificial_endpoint;
|
|
|
|
ao2_ref(artificial_endpoint, -1);
|
|
|
|
if (is_artificial) {
|
2024-10-17 08:02:08 -06:00
|
|
|
ast_trace(3, "%s:%s: Using artificial endpoint for authentication\n",
|
|
|
|
endpoint_id, src_name);
|
2017-01-08 20:32:17 -06:00
|
|
|
ast_assert(auth_size == 1);
|
2013-07-02 17:06:06 +00:00
|
|
|
auths[0] = ast_sip_get_artificial_auth();
|
2017-01-08 20:32:17 -06:00
|
|
|
if (!auths[0]) {
|
|
|
|
/* Should not happen except possibly if we are shutting down. */
|
2024-10-17 08:02:08 -06:00
|
|
|
SCOPE_EXIT_RTN_VALUE(AST_SIP_AUTHENTICATION_ERROR);
|
2017-01-08 20:32:17 -06:00
|
|
|
}
|
|
|
|
} else {
|
2024-10-17 08:02:08 -06:00
|
|
|
ast_trace(3, "%s:%s: Using endpoint for authentication\n",
|
|
|
|
endpoint_id, src_name);
|
2017-01-08 20:32:17 -06:00
|
|
|
memset(auths, 0, auth_size * sizeof(*auths));
|
2024-10-17 08:02:08 -06:00
|
|
|
/*
|
|
|
|
* If ast_sip_retrieve_auths returns a failure we still need
|
|
|
|
* to cleanup the auths array because it may have been partially
|
|
|
|
* filled in.
|
|
|
|
*/
|
2017-01-08 20:32:17 -06:00
|
|
|
if (ast_sip_retrieve_auths(&endpoint->inbound_auths, auths)) {
|
2024-10-17 08:02:08 -06:00
|
|
|
ast_sip_cleanup_auths(auths, auth_size);
|
|
|
|
SCOPE_EXIT_RTN_VALUE(AST_SIP_AUTHENTICATION_ERROR,
|
|
|
|
"%s:%s: Failed to retrieve some or all auth objects from endpoint\n",
|
|
|
|
endpoint_id, src_name);
|
2017-01-08 20:32:17 -06:00
|
|
|
}
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
/*
|
2025-01-28 09:14:34 -07:00
|
|
|
* Verify any Authorization headers in the incoming request against the
|
|
|
|
* auth objects on the endpoint. If there aren't any Authorization headers
|
|
|
|
* verify() will return AUTH_NOAUTH.
|
|
|
|
*
|
2024-10-17 08:02:08 -06:00
|
|
|
* NOTE: The only reason to use multiple auth objects as a UAS might
|
|
|
|
* be to send challenges for multiple realms however we currently don't
|
|
|
|
* know of anyone actually doing this.
|
|
|
|
*/
|
|
|
|
for (idx = 0; idx < auth_size; ++idx) {
|
|
|
|
struct ast_sip_auth *auth = auths[idx];
|
|
|
|
const char *auth_id = ast_sorcery_object_get_id(auth);
|
2025-01-28 09:14:34 -07:00
|
|
|
SCOPE_ENTER(4, "%s:%s:%s: Auth %d of %d: Verifying\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size);
|
|
|
|
|
|
|
|
verify_res[idx] = SCOPE_CALL_WITH_RESULT(-1, int, verify, endpoint_id, auth, rdata, tdata->pool);
|
|
|
|
switch((int)verify_res[idx]) {
|
|
|
|
case AUTH_SUCCESS:
|
|
|
|
res = AST_SIP_AUTHENTICATION_SUCCESS;
|
|
|
|
break;
|
|
|
|
case AUTH_FAIL:
|
|
|
|
failures++;
|
|
|
|
break;
|
|
|
|
case AUTH_NOAUTH:
|
|
|
|
case AUTH_STALE:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
SCOPE_EXIT("%s:%s:%s: Auth %d of %d: Result: %s Failure count: %d\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size,
|
|
|
|
verify_result_str[verify_res[idx]], failures);
|
2024-10-17 08:02:08 -06:00
|
|
|
|
2017-01-08 20:32:17 -06:00
|
|
|
/*
|
2025-01-28 09:14:34 -07:00
|
|
|
* If there was a success or there was no Authorization header in the
|
|
|
|
* incoming request, we can stop verifying the rest of the auth objects.
|
2017-01-08 20:32:17 -06:00
|
|
|
*/
|
2025-01-28 09:14:34 -07:00
|
|
|
if (verify_res[idx] == AUTH_SUCCESS || verify_res[idx] == AUTH_NOAUTH) {
|
|
|
|
break;
|
2013-09-13 14:44:43 +00:00
|
|
|
}
|
2025-01-28 09:14:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (res == AST_SIP_AUTHENTICATION_SUCCESS) {
|
|
|
|
ast_sip_cleanup_auths(auths, auth_size);
|
|
|
|
SCOPE_EXIT_RTN_VALUE(res, "%s:%s: Result: %s\n",
|
|
|
|
endpoint_id, src_name,
|
|
|
|
check_auth_result_str[res]);
|
|
|
|
}
|
|
|
|
ast_trace(-1, "%s:%s: Done with verification. Failures: %d of %d\n",
|
|
|
|
endpoint_id, src_name, failures, (int)auth_size);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If none of the Authorization headers in the incoming request were
|
|
|
|
* successfully verified, or there were no Authorization headers in the
|
|
|
|
* request, we need to send challenges for each auth object
|
|
|
|
* on the endpoint.
|
|
|
|
*/
|
|
|
|
for (idx = 0; idx < auth_size; ++idx) {
|
|
|
|
int i = 0;
|
|
|
|
struct ast_sip_auth *auth = auths[idx];
|
|
|
|
const char *realm = S_OR(auth->realm, default_realm);
|
|
|
|
const char *auth_id = ast_sorcery_object_get_id(auth);
|
|
|
|
SCOPE_ENTER(4, "%s:%s:%s: Auth %d of %d: Sending challenges\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size);
|
2017-01-08 20:32:17 -06:00
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
for (i = 0; i < AST_VECTOR_SIZE(&auth->supported_algorithms_uas); i++) {
|
|
|
|
pjsip_auth_algorithm_type algorithm_type = AST_VECTOR_GET(&auth->supported_algorithms_uas, i);
|
|
|
|
const pjsip_auth_algorithm *algorithm = ast_sip_auth_get_algorithm_by_type(algorithm_type);
|
|
|
|
pjsip_www_authenticate_hdr *auth_hdr = NULL;
|
|
|
|
int already_sent_challenge = 0;
|
2025-01-28 09:14:34 -07:00
|
|
|
SCOPE_ENTER(5, "%s:%s:%s: Auth %d of %d: Challenging with " PJSTR_PRINTF_SPEC "\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size,
|
|
|
|
PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2024-10-17 08:02:08 -06:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Per RFC 7616, if we've already sent a challenge for this realm
|
|
|
|
* and algorithm, we must not send another.
|
|
|
|
*/
|
|
|
|
while ((auth_hdr = pjsip_msg_find_hdr(tdata->msg,
|
|
|
|
PJSIP_H_WWW_AUTHENTICATE, auth_hdr ? auth_hdr->next : NULL))) {
|
|
|
|
if (pj_strcmp2(&auth_hdr->challenge.common.realm, realm) == 0 &&
|
|
|
|
!pj_stricmp(&auth_hdr->challenge.digest.algorithm, &algorithm->iana_name)) {
|
2025-01-28 09:14:34 -07:00
|
|
|
ast_trace(-1, "%s:%s:%s: Auth %d of %d: Not sending duplicate challenge for realm: %s algorithm: "
|
2024-10-17 08:02:08 -06:00
|
|
|
PJSTR_PRINTF_SPEC "\n",
|
2025-01-28 09:14:34 -07:00
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size,
|
|
|
|
realm, PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2024-10-17 08:02:08 -06:00
|
|
|
already_sent_challenge = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (already_sent_challenge) {
|
|
|
|
SCOPE_EXIT_EXPR(continue);
|
|
|
|
}
|
|
|
|
|
|
|
|
SCOPE_CALL(5, challenge, endpoint_id, auth, tdata, rdata,
|
|
|
|
verify_res[idx] == AUTH_STALE, algorithm);
|
2025-01-17 09:20:16 -07:00
|
|
|
res = AST_SIP_AUTHENTICATION_CHALLENGE;
|
2024-10-17 08:02:08 -06:00
|
|
|
|
2025-01-28 09:14:34 -07:00
|
|
|
SCOPE_EXIT("%s:%s:%s: Auth %d of %d: Challenged with " PJSTR_PRINTF_SPEC "\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size,
|
|
|
|
PJSTR_PRINTF_VAR(algorithm->iana_name));
|
2013-11-07 23:42:31 +00:00
|
|
|
}
|
2025-01-28 09:14:34 -07:00
|
|
|
SCOPE_EXIT("%s:%s:%s: Auth %d of %d: Done with challenges\n",
|
|
|
|
endpoint_id, auth_id, src_name, idx + 1, (int)auth_size);
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 08:02:08 -06:00
|
|
|
/*
|
|
|
|
* If we've sent challenges for multiple auth objects, we currently
|
|
|
|
* return SUCCESS when the first one succeeds. We may want to change
|
|
|
|
* this in the future to require that all succeed but as stated above,
|
|
|
|
* currently we don't have a use case for even using more than one
|
|
|
|
* auth object as a UAS.
|
|
|
|
*/
|
2013-07-02 17:06:06 +00:00
|
|
|
|
2025-01-17 09:20:16 -07:00
|
|
|
/*
|
|
|
|
* If the authentication failed for any reason, we want to send
|
|
|
|
* a 401 with a challenge. If it was because there was no
|
|
|
|
* Authorization header or there was a stale nonce, fine. That's not
|
|
|
|
* unusual so we return AST_SIP_AUTHENTICATION_CHALLENGE. If it
|
|
|
|
* failed because of a user/password mismatch then we return
|
|
|
|
* AST_SIP_AUTHENTICATION_FAILED which causes the distributor to
|
|
|
|
* print a "Failed to authenticate" message.
|
|
|
|
*/
|
2013-12-09 16:10:05 +00:00
|
|
|
if (failures == auth_size) {
|
2013-11-07 23:42:31 +00:00
|
|
|
res = AST_SIP_AUTHENTICATION_FAILED;
|
|
|
|
}
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2013-12-09 16:10:05 +00:00
|
|
|
ast_sip_cleanup_auths(auths, auth_size);
|
2024-10-17 08:02:08 -06:00
|
|
|
SCOPE_EXIT_RTN_VALUE(res, "%s:%s: Result: %s\n",
|
|
|
|
endpoint_id, src_name,
|
|
|
|
check_auth_result_str[res]);
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static struct ast_sip_authenticator digest_authenticator = {
|
|
|
|
.requires_authentication = digest_requires_authentication,
|
|
|
|
.check_authentication = digest_check_auth,
|
|
|
|
};
|
|
|
|
|
|
|
|
static int build_entity_id(void)
|
|
|
|
{
|
2014-09-29 21:18:54 +00:00
|
|
|
char *eid;
|
2013-04-25 18:25:31 +00:00
|
|
|
|
2014-09-29 21:18:54 +00:00
|
|
|
eid = ao2_alloc(AST_UUID_STR_LEN, NULL);
|
|
|
|
if (!eid) {
|
2013-04-25 18:25:31 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2014-09-29 21:18:54 +00:00
|
|
|
ast_uuid_generate_str(eid, AST_UUID_STR_LEN);
|
2013-04-25 18:25:31 +00:00
|
|
|
ao2_global_obj_replace_unref(entity_id, eid);
|
2014-09-29 21:18:54 +00:00
|
|
|
ao2_ref(eid, -1);
|
2013-04-25 18:25:31 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
res_pjsip: Add ability to identify by Authorization username
A feature of chan_sip that service providers relied upon was the ability to
identify by the Authorization username. This is most often used when customers
have a PBX that needs to register rather than identify by IP address. From my
own experiance, this is pretty common with small businesses who otherwise
don't need a static IP.
In this scenario, a register from the customer's PBX may succeed because From
will usually contain the PBXs account id but an INVITE will contain the caller
id. With nothing recognizable in From, the service provider's Asterisk can
never match to an endpoint and the INVITE just stays unauthorized.
The fixes:
A new value "auth_username" has been added to endpoint/identify_by that
will use the username and digest fields in the Authorization header
instead of username and domain in the the From header to match an endpoint,
or the To header to match an aor. This code as added to
res_pjsip_endpoint_identifier_user rather than creating a new module.
Although identify_by was always a comma-separated list, there was only
1 choice so order wasn't preserved. So to keep the order, a vector was added
to the end of ast_sip_endpoint. This is only used by res_pjsip_registrar
to find the aor. The res_pjsip_endpoint_identifier_* modules are called in
globals/endpoint_identifier_order.
Along the way, the logic in res_pjsip_registrar was corrected to match
most-specific to least-specific as res_pjsip_endpoint_identifier_user does.
The order is:
username@domain
username@domain_alias
username
Auth by username does present 1 problem however, the first INVITE won't have
an Authorization header so the distributor, not finding a match on anything,
sends a securty_alert. It still sends a 401 with a challenge so the next
INVITE will have the Authorization header and presumably succeed. As a result
though, that first security alert is actually a false alarm.
To address this, a new feature has been added to pjsip_distributor that keeps
track of unidentified requests and only sends the security alert if a
configurable number of unidentified requests come from the same IP in a
configurable amout of time. Those configuration options have been added to
the global config object. This feature is only used when auth_username
is enabled.
Finally, default_realm was added to the globals object to replace the hard
coded "asterisk" used when an endpoint is not yet identified.
The testsuite tests all pass but new tests are forthcoming for this new
feature.
ASTERISK-25835 #close
Reported-by: Ross Beer
Change-Id: I30ba62d208e6f63439600916fcd1c08a365ed69d
2016-03-07 17:34:31 -07:00
|
|
|
static void global_loaded(const char *object_type)
|
|
|
|
{
|
|
|
|
ast_sip_get_default_realm(default_realm, sizeof(default_realm));
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \brief Observer which is used to update our default_realm when the global setting changes */
|
|
|
|
static struct ast_sorcery_observer global_observer = {
|
|
|
|
.loaded = global_loaded,
|
|
|
|
};
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
static int reload_module(void)
|
|
|
|
{
|
|
|
|
if (build_entity_id()) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int load_module(void)
|
|
|
|
{
|
|
|
|
if (build_entity_id()) {
|
|
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
|
|
}
|
res_pjsip: Add ability to identify by Authorization username
A feature of chan_sip that service providers relied upon was the ability to
identify by the Authorization username. This is most often used when customers
have a PBX that needs to register rather than identify by IP address. From my
own experiance, this is pretty common with small businesses who otherwise
don't need a static IP.
In this scenario, a register from the customer's PBX may succeed because From
will usually contain the PBXs account id but an INVITE will contain the caller
id. With nothing recognizable in From, the service provider's Asterisk can
never match to an endpoint and the INVITE just stays unauthorized.
The fixes:
A new value "auth_username" has been added to endpoint/identify_by that
will use the username and digest fields in the Authorization header
instead of username and domain in the the From header to match an endpoint,
or the To header to match an aor. This code as added to
res_pjsip_endpoint_identifier_user rather than creating a new module.
Although identify_by was always a comma-separated list, there was only
1 choice so order wasn't preserved. So to keep the order, a vector was added
to the end of ast_sip_endpoint. This is only used by res_pjsip_registrar
to find the aor. The res_pjsip_endpoint_identifier_* modules are called in
globals/endpoint_identifier_order.
Along the way, the logic in res_pjsip_registrar was corrected to match
most-specific to least-specific as res_pjsip_endpoint_identifier_user does.
The order is:
username@domain
username@domain_alias
username
Auth by username does present 1 problem however, the first INVITE won't have
an Authorization header so the distributor, not finding a match on anything,
sends a securty_alert. It still sends a 401 with a challenge so the next
INVITE will have the Authorization header and presumably succeed. As a result
though, that first security alert is actually a false alarm.
To address this, a new feature has been added to pjsip_distributor that keeps
track of unidentified requests and only sends the security alert if a
configurable number of unidentified requests come from the same IP in a
configurable amout of time. Those configuration options have been added to
the global config object. This feature is only used when auth_username
is enabled.
Finally, default_realm was added to the globals object to replace the hard
coded "asterisk" used when an endpoint is not yet identified.
The testsuite tests all pass but new tests are forthcoming for this new
feature.
ASTERISK-25835 #close
Reported-by: Ross Beer
Change-Id: I30ba62d208e6f63439600916fcd1c08a365ed69d
2016-03-07 17:34:31 -07:00
|
|
|
|
|
|
|
ast_sorcery_observer_add(ast_sip_get_sorcery(), "global", &global_observer);
|
|
|
|
ast_sorcery_reload_object(ast_sip_get_sorcery(), "global");
|
|
|
|
|
2013-04-25 18:25:31 +00:00
|
|
|
if (ast_sip_register_authenticator(&digest_authenticator)) {
|
|
|
|
ao2_global_obj_release(entity_id);
|
|
|
|
return AST_MODULE_LOAD_DECLINE;
|
|
|
|
}
|
|
|
|
return AST_MODULE_LOAD_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int unload_module(void)
|
|
|
|
{
|
res_pjsip: Add ability to identify by Authorization username
A feature of chan_sip that service providers relied upon was the ability to
identify by the Authorization username. This is most often used when customers
have a PBX that needs to register rather than identify by IP address. From my
own experiance, this is pretty common with small businesses who otherwise
don't need a static IP.
In this scenario, a register from the customer's PBX may succeed because From
will usually contain the PBXs account id but an INVITE will contain the caller
id. With nothing recognizable in From, the service provider's Asterisk can
never match to an endpoint and the INVITE just stays unauthorized.
The fixes:
A new value "auth_username" has been added to endpoint/identify_by that
will use the username and digest fields in the Authorization header
instead of username and domain in the the From header to match an endpoint,
or the To header to match an aor. This code as added to
res_pjsip_endpoint_identifier_user rather than creating a new module.
Although identify_by was always a comma-separated list, there was only
1 choice so order wasn't preserved. So to keep the order, a vector was added
to the end of ast_sip_endpoint. This is only used by res_pjsip_registrar
to find the aor. The res_pjsip_endpoint_identifier_* modules are called in
globals/endpoint_identifier_order.
Along the way, the logic in res_pjsip_registrar was corrected to match
most-specific to least-specific as res_pjsip_endpoint_identifier_user does.
The order is:
username@domain
username@domain_alias
username
Auth by username does present 1 problem however, the first INVITE won't have
an Authorization header so the distributor, not finding a match on anything,
sends a securty_alert. It still sends a 401 with a challenge so the next
INVITE will have the Authorization header and presumably succeed. As a result
though, that first security alert is actually a false alarm.
To address this, a new feature has been added to pjsip_distributor that keeps
track of unidentified requests and only sends the security alert if a
configurable number of unidentified requests come from the same IP in a
configurable amout of time. Those configuration options have been added to
the global config object. This feature is only used when auth_username
is enabled.
Finally, default_realm was added to the globals object to replace the hard
coded "asterisk" used when an endpoint is not yet identified.
The testsuite tests all pass but new tests are forthcoming for this new
feature.
ASTERISK-25835 #close
Reported-by: Ross Beer
Change-Id: I30ba62d208e6f63439600916fcd1c08a365ed69d
2016-03-07 17:34:31 -07:00
|
|
|
ast_sorcery_observer_remove(ast_sip_get_sorcery(), "global", &global_observer);
|
2013-04-25 18:25:31 +00:00
|
|
|
ast_sip_unregister_authenticator(&digest_authenticator);
|
|
|
|
ao2_global_obj_release(entity_id);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2013-07-30 18:14:50 +00:00
|
|
|
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP authentication resource",
|
2015-05-05 20:49:04 -04:00
|
|
|
.support_level = AST_MODULE_SUPPORT_CORE,
|
|
|
|
.load = load_module,
|
|
|
|
.unload = unload_module,
|
|
|
|
.reload = reload_module,
|
2016-05-06 12:54:17 -04:00
|
|
|
.load_pri = AST_MODPRI_CHANNEL_DEPEND - 5,
|
2017-11-19 17:30:49 -05:00
|
|
|
.requires = "res_pjsip",
|
2013-04-25 18:25:31 +00:00
|
|
|
);
|