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}