1use crate::{ClientAuthenticator, RequestParts, ResponseParts};
2use bytesstr::BytesStr;
3use sha2::Digest;
4use sip_types::header::typed::{
5 Algorithm, AlgorithmValue, AuthChallenge, DigestChallenge, DigestResponse, QopOption,
6 QopResponse, Username,
7};
8use sip_types::header::HeaderError;
9use sip_types::print::{AppendCtx, PrintCtx, UriContext};
10use sip_types::{Headers, Name};
11use std::collections::HashMap;
12
13#[derive(Debug, thiserror::Error)]
14pub enum DigestError {
15 #[error("failed to authenticate realms: {0:?}")]
16 FailedToAuthenticate(Vec<BytesStr>),
17 #[error("encountered unsupported algorithm {0}")]
18 UnsupportedAlgorithm(BytesStr),
19 #[error("missing credentials for realm {0}")]
20 MissingCredentials(BytesStr),
21 #[error("unsupported qop")]
22 UnsupportedQop,
23 #[error(transparent)]
24 Header(HeaderError),
25}
26
27#[derive(Default, Clone)]
31pub struct DigestCredentials {
32 default: Option<DigestUser>,
33 map: HashMap<String, DigestUser>,
34}
35
36impl DigestCredentials {
37 pub fn new() -> Self {
38 Self {
39 default: None,
40 map: HashMap::new(),
41 }
42 }
43
44 pub fn set_default(&mut self, credentials: DigestUser) {
46 self.default = Some(credentials)
47 }
48
49 pub fn add_for_realm<R>(&mut self, realm: R, credentials: DigestUser)
51 where
52 R: Into<String>,
53 {
54 self.map.insert(realm.into(), credentials);
55 }
56
57 pub fn get_for_realm(&self, realm: &str) -> Option<&DigestUser> {
62 self.map.get(realm).or(self.default.as_ref())
63 }
64
65 pub fn remove_for_realm(&mut self, realm: &str) {
67 self.map.remove(realm);
68 }
69}
70
71#[derive(Clone)]
72pub struct DigestUser {
73 user: String,
74 password: Vec<u8>,
75}
76
77impl DigestUser {
78 pub fn new<U, P>(user: U, password: P) -> Self
79 where
80 U: Into<String>,
81 P: Into<Vec<u8>>,
82 {
83 Self {
84 user: user.into(),
85 password: password.into(),
86 }
87 }
88}
89
90pub struct DigestAuthenticator {
92 pub credentials: DigestCredentials,
93 qop_responses: Vec<(BytesStr, QopEntry)>,
94 responses: Vec<ResponseEntry>,
95
96 pub enforce_qop: bool,
98 pub reject_md5: bool,
100}
101
102struct QopEntry {
103 ha1: String,
104 ha2: String,
105 hash: HashFn,
106}
107
108struct ChallengedRealm {
113 realm: BytesStr,
114 challenges: Vec<(bool, AuthChallenge)>,
115}
116
117pub struct ResponseEntry {
119 pub realm: BytesStr,
120 pub header: DigestResponse,
121
122 pub use_count: u32,
127
128 is_proxy: bool,
129}
130
131impl ClientAuthenticator for DigestAuthenticator {
132 type Error = DigestError;
133
134 fn authorize_request(&mut self, request_headers: &mut Headers) {
135 for response in &mut self.responses {
136 let name = if response.is_proxy {
137 Name::PROXY_AUTHORIZATION
138 } else {
139 Name::AUTHORIZATION
140 };
141
142 if response.use_count > 0 {
144 let digest_realm = &response.header.realm;
145
146 if let Some(qop_response) = &mut response.header.qop_response {
148 qop_response.nc += 1;
149
150 let (_, qop_entry) = self
151 .qop_responses
152 .iter_mut()
153 .find(|(realm, _)| realm == digest_realm)
154 .expect("qop_entry must be some");
155
156 match qop_response.qop {
157 QopOption::Auth | QopOption::AuthInt => {
158 let hash = (qop_entry.hash)(
159 format!(
160 "{}:{}:{:08X}:{}:auth:{}",
161 qop_entry.ha1,
162 response.header.nonce,
163 qop_response.nc,
164 qop_response.cnonce,
165 qop_entry.ha2
166 )
167 .as_bytes(),
168 );
169
170 response.header.response = hash.into();
171 }
172 QopOption::Other(_) => unreachable!(),
173 };
174 }
175 }
176
177 response.use_count += 1;
178
179 request_headers.insert_type(name, &response.header);
180 }
181 }
182
183 fn handle_rejection(
184 &mut self,
185 rejected_request: RequestParts<'_>,
186 reject_response: ResponseParts<'_>,
187 ) -> Result<(), DigestError> {
188 let mut challenged_realms = vec![];
189
190 self.read_challenges(false, reject_response.headers, &mut challenged_realms)?;
191 self.read_challenges(true, reject_response.headers, &mut challenged_realms)?;
192
193 let mut failed_realms = vec![];
194
195 'outer: for challenged_realm in challenged_realms {
196 for (is_proxy, challenge) in challenged_realm.challenges {
197 let AuthChallenge::Digest(challenge) = challenge else {
198 continue;
199 };
200
201 let result = self.handle_challenge(rejected_request, challenge);
202
203 let response = match result {
204 Ok(response) => response,
205 Err(e) => {
206 log::warn!("failed to handle challenge {}", e);
207 continue;
208 }
209 };
210
211 let realm = challenged_realm.realm;
212
213 if let Some(i) = self
215 .responses
216 .iter()
217 .position(|response| response.realm == realm)
218 {
219 self.responses.remove(i);
220 }
221
222 let entry = ResponseEntry {
223 realm,
224 header: response,
225 use_count: 0,
226 is_proxy,
227 };
228
229 self.responses.push(entry);
230
231 continue 'outer;
232 }
233
234 failed_realms.push(challenged_realm.realm);
235 }
236
237 if !failed_realms.is_empty() {
238 return Err(DigestError::FailedToAuthenticate(failed_realms));
239 }
240
241 Ok(())
242 }
243}
244
245impl DigestAuthenticator {
246 pub fn new(credentials: DigestCredentials) -> Self {
247 Self {
248 credentials,
249 qop_responses: vec![],
250 responses: vec![],
251 enforce_qop: false,
252 reject_md5: false,
253 }
254 }
255
256 fn read_challenges(
258 &mut self,
259 is_proxy: bool,
260 headers: &Headers,
261 dst: &mut Vec<ChallengedRealm>,
262 ) -> Result<(), DigestError> {
263 let challenge_name = if is_proxy {
264 Name::PROXY_AUTHENTICATE
265 } else {
266 Name::WWW_AUTHENTICATE
267 };
268
269 let challenges = headers
270 .try_get::<Vec<AuthChallenge>>(challenge_name)
271 .map(|val| val.map_err(DigestError::Header))
272 .transpose()?
273 .unwrap_or_default();
274
275 for challenge in challenges {
276 let realm = match &challenge {
277 AuthChallenge::Digest(digest_challenge) => &digest_challenge.realm,
278 AuthChallenge::Other(..) => {
279 continue;
280 }
281 };
282
283 if let Some(challenged_realm) = dst
284 .iter_mut()
285 .find(|challenged_realm| &challenged_realm.realm == realm)
286 {
287 challenged_realm.challenges.push((is_proxy, challenge));
288 } else {
289 dst.push(ChallengedRealm {
290 realm: realm.clone(),
291 challenges: vec![(is_proxy, challenge)],
292 });
293 }
294 }
295
296 Ok(())
297 }
298
299 fn handle_challenge(
300 &mut self,
301 request_parts: RequestParts<'_>,
302 challenge: DigestChallenge,
303 ) -> Result<DigestResponse, DigestError> {
304 let previous_response = self
313 .responses
314 .iter()
315 .find(|response| response.realm == challenge.realm);
316
317 let authenticate = if let Some(previous_response) = previous_response {
318 previous_response.header.nonce != challenge.nonce
319 } else {
320 true
321 };
322
323 if authenticate {
324 self.handle_digest_challenge(challenge, request_parts)
325 } else {
326 Err(DigestError::FailedToAuthenticate(vec![challenge.realm]))
327 }
328 }
329
330 fn handle_digest_challenge(
331 &mut self,
332 digest_challenge: DigestChallenge,
333 request_parts: RequestParts<'_>,
334 ) -> Result<DigestResponse, DigestError> {
335 let algorithm_value = match digest_challenge.algorithm.clone() {
336 Algorithm::AkaNamespace((_, av)) => av,
337 Algorithm::AlgorithmValue(av) => av,
338 };
339
340 let (hash, is_session): (HashFn, bool) = match algorithm_value {
341 AlgorithmValue::MD5 => {
342 if self.reject_md5 {
343 return Err(DigestError::UnsupportedAlgorithm(BytesStr::from_static(
344 "MD5",
345 )));
346 } else {
347 (hash_md5, false)
348 }
349 }
350 AlgorithmValue::MD5Sess => {
351 if self.reject_md5 {
352 return Err(DigestError::UnsupportedAlgorithm(BytesStr::from_static(
353 "MD5",
354 )));
355 } else {
356 (hash_md5, true)
357 }
358 }
359 AlgorithmValue::SHA256 => (hash_sha256, false),
360 AlgorithmValue::SHA256Sess => (hash_sha256, true),
361 AlgorithmValue::SHA512256 => (hash_sha512_trunc256, false),
362 AlgorithmValue::SHA512256Sess => (hash_sha512_trunc256, true),
363 AlgorithmValue::Other(other) => return Err(DigestError::UnsupportedAlgorithm(other)),
364 };
365
366 let response = self.digest_respond(digest_challenge, request_parts, is_session, hash)?;
367
368 Ok(response)
369 }
370
371 fn digest_respond(
372 &mut self,
373 mut challenge: DigestChallenge,
374 request_parts: RequestParts<'_>,
375 is_session: bool,
376 hash: HashFn,
377 ) -> Result<DigestResponse, DigestError> {
378 let digest_user = self
379 .credentials
380 .get_for_realm(&challenge.realm)
381 .ok_or_else(|| DigestError::MissingCredentials(challenge.realm.clone()))?
382 .clone();
383
384 let cnonce = BytesStr::from(uuid::Uuid::new_v4().simple().to_string());
385
386 let mut ha1 = hash(
387 [
388 format!("{}:{}:", digest_user.user, challenge.realm).as_bytes(),
389 &digest_user.password,
390 ]
391 .concat()
392 .as_slice(),
393 );
394
395 if is_session {
396 ha1 = format!("{}:{}:{}", ha1, challenge.nonce, cnonce);
397 }
398
399 let ctx = PrintCtx {
400 method: Some(&request_parts.line.method),
401 uri: Some(UriContext::ReqUri),
402 };
403
404 let uri = request_parts.line.uri.print_ctx(ctx).to_string();
405
406 if challenge.qop.is_empty() && self.enforce_qop {
408 challenge.qop.push(QopOption::Auth)
409 }
410
411 let (response, qop_response) = if !challenge.qop.is_empty() {
412 if challenge.qop.contains(&QopOption::AuthInt) {
413 let ha2 = hash(
414 format!(
415 "{}:{}:{}",
416 &request_parts.line.method,
417 uri,
418 hash(request_parts.body)
419 )
420 .as_bytes(),
421 );
422
423 let nc = 1;
424
425 let response = hash(
426 format!(
427 "{}:{}:{:08X}:{}:auth-int:{}",
428 ha1, challenge.nonce, nc, cnonce, ha2
429 )
430 .as_bytes(),
431 );
432
433 self.save_qop_response(challenge.realm.clone(), ha1, ha2, hash);
434
435 let qop_response = QopResponse {
436 qop: QopOption::AuthInt,
437 cnonce,
438 nc,
439 };
440
441 (response, Some(qop_response))
442 } else if challenge.qop.contains(&QopOption::Auth) {
443 let a2 = format!("{}:{}", &request_parts.line.method, uri);
444 let ha2 = hash(a2.as_bytes());
445
446 let nc = 1;
447
448 let response = hash(
449 format!(
450 "{}:{}:{:08X}:{}:auth:{}",
451 ha1, challenge.nonce, nc, cnonce, ha2
452 )
453 .as_bytes(),
454 );
455
456 self.save_qop_response(challenge.realm.clone(), ha1, ha2, hash);
457
458 let qop_response = QopResponse {
459 qop: QopOption::Auth,
460 cnonce,
461 nc,
462 };
463
464 (response, Some(qop_response))
465 } else {
466 return Err(DigestError::UnsupportedQop);
467 }
468 } else {
469 let a2 = format!("{}:{}", &request_parts.line.method, uri);
470
471 (
472 hash(format!("{}:{}:{}", ha1, challenge.nonce, hash(a2.as_bytes())).as_bytes()),
473 None,
474 )
475 };
476
477 let username = if challenge.userhash {
478 let username_hash =
480 hash(format!("{}:{}", digest_user.user, challenge.realm).as_bytes())
481 .as_str()
482 .into();
483
484 Username::Username(username_hash)
485 } else {
486 Username::new(digest_user.user.as_str().into())
487 };
488
489 Ok(DigestResponse {
490 username,
491 realm: challenge.realm,
492 nonce: challenge.nonce,
493 uri: uri.into(),
494 response: response.into(),
495 algorithm: challenge.algorithm,
496 opaque: challenge.opaque,
497 qop_response,
498 userhash: challenge.userhash,
499 other: vec![],
500 })
501 }
502
503 fn save_qop_response(
504 &mut self,
505 challenge_realm: BytesStr,
506 ha1: String,
507 ha2: String,
508 hash: HashFn,
509 ) {
510 let qop_entry = QopEntry { ha1, ha2, hash };
511
512 if let Some((_, old_qop_entry)) = self
513 .qop_responses
514 .iter_mut()
515 .find(|(realm, _)| *realm == challenge_realm)
516 {
517 *old_qop_entry = qop_entry;
518 } else {
519 self.qop_responses
520 .push((challenge_realm.clone(), qop_entry))
521 }
522 }
523}
524
525fn hash_md5(i: &[u8]) -> String {
526 format!("{:x}", md5::compute(i))
527}
528
529fn hash_sha256(i: &[u8]) -> String {
530 let mut hasher = sha2::Sha256::new();
531 hasher.update(i);
532 format!("{:x}", hasher.finalize())
533}
534
535fn hash_sha512_trunc256(i: &[u8]) -> String {
536 let mut hasher = sha2::Sha512_256::new();
537 hasher.update(i);
538 format!("{:x}", hasher.finalize())
539}
540
541type HashFn = fn(&[u8]) -> String;
542
543#[cfg(test)]
544mod test {
545 use super::*;
546 use sip_types::{
547 header::typed::AuthResponse,
548 msg::{RequestLine, StatusLine},
549 uri::SipUri,
550 Headers, Method, Name, StatusCode,
551 };
552
553 fn test_authenticator() -> DigestAuthenticator {
554 let mut credentials = DigestCredentials::new();
555
556 credentials.add_for_realm("example.org", DigestUser::new("user123", "password123"));
557
558 DigestAuthenticator::new(credentials)
559 }
560
561 #[test]
562 fn digest_challenge() {
563 let mut authenticator = test_authenticator();
564
565 let mut headers = Headers::new();
566
567 headers.insert_type(
568 Name::WWW_AUTHENTICATE,
569 &AuthChallenge::Digest(DigestChallenge {
570 realm: "example.org".into(),
571 domain: None,
572 nonce: "YWmh5GFpoLjiTDCA1hTSSygkgdj99aHE".into(),
573 opaque: None,
574 stale: false,
575 algorithm: Algorithm::AlgorithmValue(AlgorithmValue::MD5),
576 qop: vec![],
577 userhash: false,
578 other: vec![],
579 }),
580 );
581
582 let line = RequestLine {
583 method: Method::REGISTER,
584 uri: "sip:example.org".parse::<SipUri>().unwrap(),
585 };
586
587 authenticator
588 .handle_rejection(
589 RequestParts {
590 line: &line,
591 headers: &Headers::new(),
592 body: &[],
593 },
594 ResponseParts {
595 line: &StatusLine {
596 code: StatusCode::UNAUTHORIZED,
597 reason: None,
598 },
599 headers: &headers,
600 body: &[],
601 },
602 )
603 .unwrap();
604
605 let mut response_headers = Headers::new();
606 authenticator.authorize_request(&mut response_headers);
607
608 let authorization = response_headers
609 .get::<AuthResponse>(Name::AUTHORIZATION)
610 .unwrap();
611
612 match authorization {
613 AuthResponse::Digest(DigestResponse {
614 username,
615 realm,
616 nonce,
617 uri,
618 response,
619 algorithm,
620 opaque,
621 qop_response,
622 userhash,
623 other,
624 }) => {
625 assert_eq!(username, Username::Username("user123".into()));
626 assert_eq!(realm, "example.org");
627 assert_eq!(nonce, "YWmh5GFpoLjiTDCA1hTSSygkgdj99aHE");
628 assert_eq!(uri, "sip:example.org");
629 assert_eq!(response, "bc185e4893f17f12dc53153d2a62e6a6");
630 assert_eq!(algorithm, Algorithm::AlgorithmValue(AlgorithmValue::MD5));
631 assert_eq!(opaque, None);
632 assert_eq!(qop_response, None);
633 assert!(!userhash);
634 assert_eq!(other, vec![]);
635 }
636 _ => panic!(),
637 }
638 }
639
640 #[test]
641 fn digest_challenge_and_response() {
642 let mut authenticator = test_authenticator();
643
644 let mut headers = Headers::new();
645
646 headers.insert_type(
647 Name::WWW_AUTHENTICATE,
648 &AuthChallenge::Digest(DigestChallenge {
649 realm: "example.org".into(),
650 domain: None,
651 nonce: "YWmh5GFpoLjiTDCA1hTSSygkgdj99aHE".into(),
652 opaque: None,
653 stale: false,
654 algorithm: Algorithm::AlgorithmValue(AlgorithmValue::MD5),
655 qop: vec![QopOption::AuthInt],
656 userhash: false,
657 other: vec![],
658 }),
659 );
660
661 let uri: SipUri = "sip:example.org".parse().unwrap();
662
663 let line = RequestLine {
664 method: Method::REGISTER,
665 uri,
666 };
667
668 authenticator
669 .handle_rejection(
670 RequestParts {
671 line: &line,
672 headers: &Headers::new(),
673 body: &[],
674 },
675 ResponseParts {
676 line: &StatusLine {
677 code: StatusCode::UNAUTHORIZED,
678 reason: None,
679 },
680 headers: &headers,
681 body: &[],
682 },
683 )
684 .unwrap();
685
686 let mut response_headers = Headers::new();
687 authenticator.authorize_request(&mut response_headers);
688
689 let response = response_headers
690 .get::<AuthResponse>(Name::AUTHORIZATION)
691 .unwrap();
692
693 let resp_value = match response {
694 AuthResponse::Digest(DigestResponse {
695 username,
696 realm,
697 nonce,
698 uri,
699 response, algorithm,
701 opaque,
702 qop_response,
703 userhash,
704 other,
705 }) => {
706 assert_eq!(username, Username::Username("user123".into()));
707 assert_eq!(realm, "example.org");
708 assert_eq!(nonce, "YWmh5GFpoLjiTDCA1hTSSygkgdj99aHE");
709 assert_eq!(uri, "sip:example.org");
710 assert_eq!(algorithm, Algorithm::AlgorithmValue(AlgorithmValue::MD5));
711 assert_eq!(opaque, None);
712 let qop_response = qop_response.unwrap();
713 assert_eq!(qop_response.qop, QopOption::AuthInt);
714 assert_eq!(qop_response.nc, 1);
715 assert!(!userhash);
716 assert_eq!(other, vec![]);
717 response
718 }
719 _ => panic!("Expected digest"),
720 };
721
722 let mut response_headers = Headers::new();
723 authenticator.authorize_request(&mut response_headers);
724
725 let response = response_headers
726 .get::<AuthResponse>(Name::AUTHORIZATION)
727 .unwrap();
728
729 match response {
730 AuthResponse::Digest(DigestResponse {
731 username,
732 realm,
733 nonce,
734 uri,
735 response, algorithm,
737 opaque,
738 qop_response,
739 userhash,
740 other,
741 }) => {
742 assert_eq!(username, Username::Username("user123".into()));
743 assert_eq!(realm, "example.org");
744 assert_eq!(nonce, "YWmh5GFpoLjiTDCA1hTSSygkgdj99aHE");
745 assert_eq!(uri, "sip:example.org");
746
747 assert_eq!(algorithm, Algorithm::AlgorithmValue(AlgorithmValue::MD5));
748 assert_eq!(opaque, None);
749 let qop_response = qop_response.unwrap();
750 assert_eq!(qop_response.qop, QopOption::AuthInt);
751 assert_eq!(qop_response.nc, 2);
752 assert!(!userhash);
753 assert_eq!(other, vec![]);
754 assert_ne!(resp_value, response)
755 }
756 _ => panic!("Expected digest"),
757 }
758 }
759}