From 25deac189e0aeee942b5b171130f55cd0433c26c Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 25 Feb 2026 19:46:59 +0500 Subject: [PATCH] Fix nc not resetting when server issues new nonce without stale=true. When a server challenged with a new nonce but omitted stale=true, the nonce count (nc) carried over from the previous nonce instead of resetting to 1. Detect nonce changes by comparing the new nonce against the stored one, and regenerate cnonce/reset nc accordingly. --- libsofia-sip-ua/iptsec/auth_client.c | 3 +- libsofia-sip-ua/iptsec/test_auth_digest.c | 83 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/libsofia-sip-ua/iptsec/auth_client.c b/libsofia-sip-ua/iptsec/auth_client.c index a8cfedbb3..f26f2b13e 100644 --- a/libsofia-sip-ua/iptsec/auth_client.c +++ b/libsofia-sip-ua/iptsec/auth_client.c @@ -867,7 +867,8 @@ static int auc_digest_challenge(auth_client_t *ca, msg_auth_t const *ch) stale = ac->ac_stale || cda->cda_ac->ac_nonce == NULL; - if (ac->ac_qop && (cda->cda_cnonce == NULL || ac->ac_stale || ca->ca_clear )) { + if (ac->ac_qop && (cda->cda_cnonce == NULL || ac->ac_stale || ca->ca_clear + || su_strcmp(ac->ac_nonce, cda->cda_ac->ac_nonce) != 0)) { su_guid_t guid[1]; char *cnonce; size_t b64len = BASE64_MINSIZE(sizeof(guid)) + 1; diff --git a/libsofia-sip-ua/iptsec/test_auth_digest.c b/libsofia-sip-ua/iptsec/test_auth_digest.c index d84904605..424fc175e 100644 --- a/libsofia-sip-ua/iptsec/test_auth_digest.c +++ b/libsofia-sip-ua/iptsec/test_auth_digest.c @@ -1003,6 +1003,89 @@ int test_digest_client(void) TEST(as->as_status, 0); auth_mod_destroy(am); aucs = NULL; + /* Test nc reset on new nonce without stale=true. + * + * When a server issues a fresh 401 with a new nonce but without + * stale=true, the nc counter must reset to 00000001 for the new nonce. + * See RFC 2617 section 3.2.2. + */ + { + + TEST_1(am = auth_mod_create(root, + AUTHTAG_METHOD("Digest"), + AUTHTAG_REALM("ims3.so.noklab.net"), + AUTHTAG_DB(testpasswd), + AUTHTAG_QOP("auth"), + AUTHTAG_MAX_NCOUNT(5), + AUTHTAG_ALGORITHM("MD5"), + TAG_END())); + + /* Get initial 401 challenge (nonce A) */ + reinit_as(as); + auth_mod_check_client(am, as, NULL, ach); + TEST(as->as_status, 401); + TEST(auc_challenge(&aucs, home, (msg_auth_t *)as->as_response, + sip_authorization_class), 1); + TEST(auc_all_credentials(&aucs, "Digest", "\"ims3.so.noklab.net\"", + "user1", "secret"), 1); + + /* Authorize with nonce A — nc must be 00000001 */ + msg_header_remove(m2, (void *)sip, (void *)sip->sip_authorization); + TEST(auc_authorization(&aucs, m2, (msg_pub_t*)sip, rq->rq_method_name, + (url_t *)"sip:surf3@ims3.so.noklab.net", + sip->sip_payload), 1); + TEST_1(sip->sip_authorization); + TEST_S(msg_header_find_param(sip->sip_authorization->au_common, "nc="), + "00000001"); + + /* Server accepts auth with nonce A */ + reinit_as(as); + auth_mod_check_client(am, as, sip->sip_authorization, ach); + TEST(as->as_status, 0); + + /* Advance time so the new auth_mod generates a different nonce. + * Nonce includes the timestamp, so a 1-second shift is enough. */ + offset += 1; + + /* Destroy auth_mod to get a different nonce on next challenge */ + auth_mod_destroy(am); + TEST_1(am = auth_mod_create(root, + AUTHTAG_METHOD("Digest"), + AUTHTAG_REALM("ims3.so.noklab.net"), + AUTHTAG_DB(testpasswd), + AUTHTAG_QOP("auth"), + AUTHTAG_MAX_NCOUNT(5), + AUTHTAG_ALGORITHM("MD5"), + TAG_END())); + + /* Get new 401 challenge (nonce B, stale=false). + * auc_challenge returns 0 here because the new nonce without + * stale=true is not recognized as an update at the auc_challenge + * level. The nc reset still happens internally in + * auc_digest_challenge via the cnonce regeneration path. */ + reinit_as(as); + auth_mod_check_client(am, as, NULL, ach); + TEST(as->as_status, 401); + TEST(auc_challenge(&aucs, home, (msg_auth_t *)as->as_response, + sip_authorization_class), 0); + + /* Authorize with nonce B — nc MUST reset to 00000001, not 00000002 */ + msg_header_remove(m2, (void *)sip, (void *)sip->sip_authorization); + TEST(auc_authorization(&aucs, m2, (msg_pub_t*)sip, rq->rq_method_name, + (url_t *)"sip:surf3@ims3.so.noklab.net", + sip->sip_payload), 1); + TEST_1(sip->sip_authorization); + TEST_S(msg_header_find_param(sip->sip_authorization->au_common, "nc="), + "00000001"); + + /* Server accepts auth with nonce B */ + reinit_as(as); + auth_mod_check_client(am, as, sip->sip_authorization, ach); + TEST(as->as_status, 0); + + auth_mod_destroy(am); aucs = NULL; + } + /* Test empty realm */ TEST_1(am = auth_mod_create(root, AUTHTAG_METHOD("Digest"),