Skip to main content

rsipstack/dialog/
authenticate.rs

1use super::DialogId;
2use crate::sip::headers::auth::{Algorithm, AuthQop, Qop};
3use crate::sip::prelude::{HasHeaders, HeadersExt, ToTypedHeader};
4use crate::sip::typed::{Authorization, ProxyAuthorization};
5use crate::sip::DigestGenerator;
6use crate::sip::{Header, Method, Param, Response};
7use crate::transaction::key::{TransactionKey, TransactionRole};
8use crate::transaction::transaction::Transaction;
9use crate::transaction::{make_via_branch, random_text, CNONCE_LEN};
10use crate::Result;
11
12/// SIP Authentication Credentials
13///
14/// `Credential` contains the authentication information needed for SIP
15/// digest authentication. This is used when a SIP server challenges
16/// a request with a 401 Unauthorized or 407 Proxy Authentication Required
17/// response.
18///
19/// # Fields
20///
21/// * `username` - The username for authentication
22/// * `password` - The password for authentication
23/// * `realm` - Optional authentication realm (extracted from challenge)
24///
25/// # Examples
26///
27/// ## Basic Usage
28///
29/// ```rust,no_run
30/// # use rsipstack::dialog::authenticate::Credential;
31/// # fn example() -> rsipstack::Result<()> {
32/// let credential = Credential {
33///     username: "alice".to_string(),
34///     password: "secret123".to_string(),
35///     realm: Some("example.com".to_string()),
36/// };
37/// # Ok(())
38/// # }
39/// ```
40///
41/// ## Usage with Registration
42///
43/// ```rust,no_run
44/// # use rsipstack::dialog::authenticate::Credential;
45/// # fn example() -> rsipstack::Result<()> {
46/// let credential = Credential {
47///     username: "alice".to_string(),
48///     password: "secret123".to_string(),
49///     realm: None, // Will be extracted from server challenge
50/// };
51///
52/// // Use credential with registration
53/// // let registration = Registration::new(endpoint.inner.clone(), Some(credential));
54/// # Ok(())
55/// # }
56/// ```
57///
58/// ## Usage with INVITE
59///
60/// ```rust,no_run
61/// # use rsipstack::dialog::authenticate::Credential;
62/// # use rsipstack::dialog::invitation::InviteOption;
63/// # fn example() -> rsipstack::Result<()> {
64/// # let sdp_bytes = vec![];
65/// # let credential = Credential {
66/// #     username: "alice".to_string(),
67/// #     password: "secret123".to_string(),
68/// #     realm: Some("example.com".to_string()),
69/// # };
70/// let invite_option = InviteOption {
71///     caller: rsipstack::sip::Uri::try_from("sip:alice@example.com")?,
72///     callee: rsipstack::sip::Uri::try_from("sip:bob@example.com")?,
73///     content_type: Some("application/sdp".to_string()),
74///     offer: Some(sdp_bytes),
75///     contact: rsipstack::sip::Uri::try_from("sip:alice@192.168.1.100:5060")?,
76///     credential: Some(credential),
77///     ..Default::default()
78/// };
79/// # Ok(())
80/// # }
81/// ```
82#[derive(Clone)]
83pub struct Credential {
84    pub username: String,
85    pub password: String,
86    pub realm: Option<String>,
87}
88
89/// Handle client-side authentication challenge
90///
91/// This function processes a 401 Unauthorized or 407 Proxy Authentication Required
92/// response and creates a new transaction with proper authentication headers.
93/// It implements SIP digest authentication according to RFC 3261 and RFC 2617.
94///
95/// # Parameters
96///
97/// * `new_seq` - New CSeq number for the authenticated request
98/// * `tx` - Original transaction that received the authentication challenge
99/// * `resp` - Authentication challenge response (401 or 407)
100/// * `cred` - User credentials for authentication
101///
102/// # Returns
103///
104/// * `Ok(Transaction)` - New transaction with authentication headers
105/// * `Err(Error)` - Failed to process authentication challenge
106///
107/// # Examples
108///
109/// ## Automatic Authentication Handling
110///
111/// ```rust,no_run
112/// # use rsipstack::dialog::authenticate::{handle_client_authenticate, Credential};
113/// # use rsipstack::transaction::transaction::Transaction;
114/// # use rsipstack::sip::Response;
115/// # async fn example() -> rsipstack::Result<()> {
116/// # let new_seq = 1u32;
117/// # let original_tx: Transaction = todo!();
118/// # let auth_challenge_response: Response = todo!();
119/// # let credential = Credential {
120/// #     username: "alice".to_string(),
121/// #     password: "secret123".to_string(),
122/// #     realm: Some("example.com".to_string()),
123/// # };
124/// // This is typically called automatically by dialog methods
125/// let new_tx = handle_client_authenticate(
126///     new_seq,
127///     &original_tx,
128///     auth_challenge_response,
129///     &credential
130/// ).await?;
131///
132/// // Send the authenticated request
133/// new_tx.send().await?;
134/// # Ok(())
135/// # }
136/// ```
137///
138/// ## Manual Authentication Flow
139///
140/// ```rust,no_run
141/// # use rsipstack::dialog::authenticate::{handle_client_authenticate, Credential};
142/// # use rsipstack::transaction::transaction::Transaction;
143/// # use rsipstack::sip::{SipMessage, StatusCode, Response};
144/// # async fn example() -> rsipstack::Result<()> {
145/// # let mut tx: Transaction = todo!();
146/// # let credential = Credential {
147/// #     username: "alice".to_string(),
148/// #     password: "secret123".to_string(),
149/// #     realm: Some("example.com".to_string()),
150/// # };
151/// # let new_seq = 2u32;
152/// // Send initial request
153/// tx.send().await?;
154///
155/// while let Some(message) = tx.receive().await {
156///     match message {
157///         SipMessage::Response(resp) => {
158///             match resp.status_code {
159///                 StatusCode::Unauthorized | StatusCode::ProxyAuthenticationRequired => {
160///                     // Handle authentication challenge
161///                     let auth_tx = handle_client_authenticate(
162///                         new_seq, &tx, resp, &credential
163///                     ).await?;
164///
165///                     // Send authenticated request
166///                     auth_tx.send().await?;
167///                     tx = auth_tx;
168///                 },
169///                 StatusCode::OK => {
170///                     println!("Request successful");
171///                     break;
172///                 },
173///                 _ => {
174///                     println!("Request failed: {}", resp.status_code);
175///                     break;
176///                 }
177///             }
178///         },
179///         _ => {}
180///     }
181/// }
182/// # Ok(())
183/// # }
184/// ```
185///
186/// This function handles SIP authentication challenges and creates authenticated requests.
187pub async fn handle_client_authenticate(
188    new_seq: u32,
189    tx: &Transaction,
190    resp: Response,
191    cred: &Credential,
192) -> Result<Transaction> {
193    let header = match resp.www_authenticate_header() {
194        Some(h) => Header::WwwAuthenticate(h.clone()),
195        None => {
196            let code = resp.status_code.clone();
197            let proxy_header =
198                crate::sip_header_opt!(resp.headers().iter(), Header::ProxyAuthenticate);
199            let proxy_header = proxy_header.ok_or(crate::Error::DialogError(
200                "missing proxy/www authenticate".to_string(),
201                DialogId::try_from(tx)?,
202                code,
203            ))?;
204            Header::ProxyAuthenticate(proxy_header.clone())
205        }
206    };
207
208    let mut new_req = tx.original.clone();
209    new_req.cseq_header_mut()?.mut_seq(new_seq)?;
210
211    let challenge: crate::sip::typed::WwwAuthenticate = match &header {
212        Header::WwwAuthenticate(h) => h.typed()?,
213        Header::ProxyAuthenticate(h) => {
214            let t = h.typed()?;
215            crate::sip::typed::WwwAuthenticate {
216                scheme: t.scheme,
217                realm: t.realm,
218                domain: t.domain,
219                nonce: t.nonce,
220                opaque: t.opaque,
221                stale: t.stale,
222                algorithm: t.algorithm,
223                qop: t.qop,
224                charset: t.charset,
225            }
226        }
227        _ => unreachable!(),
228    };
229
230    let cnonce = random_text(CNONCE_LEN);
231    let auth_qop = match challenge.qop {
232        Some(Qop::Auth) => Some(AuthQop::Auth { cnonce, nc: 1 }),
233        Some(Qop::AuthInt) => Some(AuthQop::AuthInt { cnonce, nc: 1 }),
234        _ => None,
235    };
236
237    // Use MD5 as default algorithm if none specified (RFC 2617 compatibility)
238    let algorithm = challenge
239        .algorithm
240        .unwrap_or(crate::sip::headers::auth::Algorithm::Md5);
241
242    let response = DigestGenerator {
243        username: cred.username.as_str(),
244        password: cred.password.as_str(),
245        algorithm,
246        nonce: challenge.nonce.as_str(),
247        method: &tx.original.method,
248        qop: auth_qop.as_ref(),
249        uri: &tx.original.uri,
250        realm: challenge.realm.as_str(),
251    }
252    .compute();
253
254    let auth = Authorization {
255        scheme: challenge.scheme,
256        username: cred.username.clone(),
257        realm: challenge.realm,
258        nonce: challenge.nonce,
259        uri: tx.original.uri.clone(),
260        response,
261        algorithm: Some(algorithm),
262        opaque: challenge.opaque,
263        qop: auth_qop,
264    };
265
266    let mut via_header = tx.original.via_header()?.clone().typed()?;
267    let params = &mut via_header.params;
268    params.retain(|p| !matches!(p, crate::sip::Param::Branch(_)));
269    params.push(make_via_branch());
270    if !params.iter().any(|p| matches!(p, Param::Rport(_))) {
271        params.push(Param::Rport(None));
272    }
273    new_req.headers_mut().unique_push(via_header.into());
274
275    new_req.headers_mut().retain(|h| {
276        !matches!(
277            h,
278            Header::ProxyAuthenticate(_)
279                | Header::Authorization(_)
280                | Header::WwwAuthenticate(_)
281                | Header::ProxyAuthorization(_)
282        )
283    });
284
285    match header {
286        Header::WwwAuthenticate(_) => {
287            new_req.headers_mut().unique_push(auth.into());
288        }
289        Header::ProxyAuthenticate(_) => {
290            new_req.headers_mut().unique_push(
291                ProxyAuthorization {
292                    scheme: auth.scheme,
293                    username: auth.username,
294                    realm: auth.realm,
295                    nonce: auth.nonce,
296                    uri: auth.uri,
297                    response: auth.response,
298                    algorithm: auth.algorithm,
299                    opaque: auth.opaque,
300                    qop: auth.qop,
301                }
302                .into(),
303            );
304        }
305        _ => unreachable!(),
306    }
307    let key = TransactionKey::from_request(&new_req, TransactionRole::Client)?;
308    let mut new_tx = Transaction::new_client(
309        key,
310        new_req,
311        tx.endpoint_inner.clone(),
312        tx.connection.clone(),
313    );
314    new_tx.destination = tx.destination.clone();
315    Ok(new_tx)
316}
317
318/// Compute the digest hash value using the specified algorithm.
319///
320/// This is a standalone hash function that supports MD5, SHA-256, and SHA-512
321/// algorithms as specified in RFC 2617 and RFC 7616.
322fn hash_value(algorithm: Algorithm, value: &str) -> String {
323    use md5::Md5;
324    use sha2::{Digest, Sha256, Sha512};
325
326    match algorithm {
327        Algorithm::Md5 | Algorithm::Md5Sess => {
328            let mut hasher = Md5::new();
329            hasher.update(value.as_bytes());
330            encode_lower_hex(hasher.finalize())
331        }
332        Algorithm::Sha256 | Algorithm::Sha256Sess => {
333            let mut hasher = Sha256::new();
334            hasher.update(value.as_bytes());
335            encode_lower_hex(hasher.finalize())
336        }
337        Algorithm::Sha512 | Algorithm::Sha512Sess => {
338            let mut hasher = Sha512::new();
339            hasher.update(value.as_bytes());
340            encode_lower_hex(hasher.finalize())
341        }
342    }
343}
344
345fn encode_lower_hex(bytes: impl AsRef<[u8]>) -> String {
346    bytes
347        .as_ref()
348        .iter()
349        .map(|byte| format!("{:02x}", byte))
350        .collect()
351}
352
353/// Compute the digest response using raw URI string.
354///
355/// This function computes the SIP digest authentication response using the
356/// **raw URI string** rather than a parsed and re-serialized `Uri`. This is
357/// critical because some SIP devices (e.g., Unify OpenScape phones) use
358/// lowercase transport parameters like `transport=tls` in their digest URI,
359/// while rsip's `Uri::Display` always normalizes to uppercase (`transport=TLS`).
360/// Using the parsed URI would produce a different hash and cause authentication
361/// to fail.
362///
363/// # Parameters
364///
365/// * `username` - The authentication username
366/// * `password` - The authentication password
367/// * `realm` - The authentication realm
368/// * `nonce` - The server-provided nonce
369/// * `method` - The SIP method (REGISTER, INVITE, etc.)
370/// * `uri_raw` - The **raw** URI string exactly as provided by the client
371/// * `algorithm` - The hash algorithm to use
372/// * `qop` - Optional quality of protection
373///
374/// # Returns
375///
376/// The computed digest response string.
377///
378/// # Examples
379///
380/// ```rust,no_run
381/// # use rsipstack::dialog::authenticate::compute_digest;
382/// # use rsipstack::sip::headers::auth::Algorithm;
383/// let response = compute_digest(
384///     "alice",
385///     "secret123",
386///     "example.com",
387///     "dcd98b7102dd2f0e8b11d0f600bfb0c093",
388///     &rsipstack::sip::Method::Register,
389///     "sip:example.com:5061;transport=tls",
390///     Algorithm::Md5,
391///     None,
392/// );
393/// ```
394pub fn compute_digest(
395    username: &str,
396    password: &str,
397    realm: &str,
398    nonce: &str,
399    method: &Method,
400    uri_raw: &str,
401    algorithm: Algorithm,
402    qop: Option<&AuthQop>,
403) -> String {
404    let ha1 = hash_value(algorithm, &format!("{}:{}:{}", username, realm, password));
405    let ha2 = match qop {
406        None | Some(AuthQop::Auth { .. }) => {
407            hash_value(algorithm, &format!("{}:{}", method, uri_raw))
408        }
409        _ => hash_value(
410            algorithm,
411            &format!("{}:{}:d41d8cd98f00b204e9800998ecf8427e", method, uri_raw),
412        ),
413    };
414
415    let value = match qop {
416        Some(AuthQop::Auth { cnonce, nc }) => {
417            format!("{}:{}:{:08}:{}:{}:{}", ha1, nonce, nc, cnonce, "auth", ha2)
418        }
419        Some(AuthQop::AuthInt { cnonce, nc }) => {
420            format!(
421                "{}:{}:{:08}:{}:{}:{}",
422                ha1, nonce, nc, cnonce, "auth-int", ha2
423            )
424        }
425        None => format!("{}:{}:{}", ha1, nonce, ha2),
426    };
427
428    hash_value(algorithm, &value)
429}
430
431/// Extract the raw `uri` value from a SIP Authorization/Proxy-Authorization header.
432///
433/// Uses rsip's `AuthTokenizer` to parse the header, which preserves the original
434/// case of parameter values. This is necessary because `rsipstack::sip::Uri::Display`
435/// normalizes transport parameters to uppercase (e.g., `transport=tls` → `transport=TLS`),
436/// which breaks digest authentication verification when the client used a different case.
437///
438/// # Parameters
439///
440/// * `header_value` - The raw header value string (e.g., `Digest username="alice", ...`)
441///
442/// # Returns
443///
444/// The raw URI string if found, or `None`.
445///
446/// # Examples
447///
448/// ```rust
449/// # use rsipstack::dialog::authenticate::extract_digest_uri_raw;
450/// let header = r#"Digest username="111",realm="pbx.e36",nonce="abc",uri="sip:pbx.e36:5061;transport=tls",response="xxx",algorithm=MD5"#;
451/// let uri = extract_digest_uri_raw(header);
452/// assert_eq!(uri, Some("sip:pbx.e36:5061;transport=tls".to_string()));
453/// ```
454pub fn extract_digest_uri_raw(header_value: &str) -> Option<String> {
455    use crate::sip::headers::typed::tokenizers::AuthTokenizer;
456    use crate::sip::headers::typed::Tokenize;
457
458    let tokenizer = AuthTokenizer::tokenize(header_value).ok()?;
459    tokenizer
460        .params
461        .iter()
462        .find(|(key, _)| key.eq_ignore_ascii_case("uri"))
463        .map(|(_, value)| value.to_string())
464}
465
466/// Verify a SIP digest authentication response.
467///
468/// This function verifies the digest response from a SIP Authorization or
469/// Proxy-Authorization header using the **raw URI string** to avoid case
470/// normalization issues. Some SIP devices use lowercase transport parameters
471/// (e.g., `transport=tls`) while rsip normalizes them to uppercase (`TLS`),
472/// which would cause digest verification to fail if the parsed URI were used.
473///
474/// # Parameters
475///
476/// * `auth` - The parsed `Authorization` header (used for username, realm, nonce, etc.)
477/// * `password` - The expected password for the user
478/// * `method` - The SIP method from the request
479/// * `raw_header_value` - The raw Authorization header value string, used to extract the URI
480///
481/// # Returns
482///
483/// `true` if the digest response matches, `false` otherwise.
484///
485/// # Examples
486///
487/// ```rust,no_run
488/// # use rsipstack::dialog::authenticate::verify_digest;
489/// # use rsipstack::sip::typed::Authorization;
490/// # use rsipstack::sip::prelude::ToTypedHeader;
491/// # fn example() -> rsipstack::Result<()> {
492/// # let auth_header_value = "";
493/// # let auth: Authorization = todo!();
494/// let is_valid = verify_digest(
495///     &auth,
496///     "secret123",
497///     &rsipstack::sip::Method::Register,
498///     auth_header_value,
499/// );
500///
501/// if is_valid {
502///     println!("Authentication successful");
503/// } else {
504///     println!("Authentication failed");
505/// }
506/// # Ok(())
507/// # }
508/// ```
509pub fn verify_digest(
510    auth: &Authorization,
511    password: &str,
512    method: &Method,
513    raw_header_value: &str,
514) -> bool {
515    let algorithm = auth.algorithm.unwrap_or(Algorithm::Md5);
516
517    // Extract the raw URI from the header to preserve original case
518    // This is critical because DigestGenerator uses Uri::Display which
519    // normalizes transport params to uppercase (e.g., transport=tls -> transport=TLS)
520    let uri_str = match extract_digest_uri_raw(raw_header_value) {
521        Some(uri) => uri,
522        None => {
523            // Fallback to the parsed URI (which may have case normalization issues)
524            auth.uri.to_string()
525        }
526    };
527
528    let expected = compute_digest(
529        &auth.username,
530        password,
531        &auth.realm,
532        &auth.nonce,
533        method,
534        &uri_str,
535        algorithm,
536        auth.qop.as_ref(),
537    );
538
539    expected == auth.response
540}