1use crate::extract::FromRequest;
9use crate::password::constant_time_eq;
10use crate::response::IntoResponse;
11use crate::{Method, Request, RequestContext};
12use core::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DigestAlgorithm {
16 Md5,
17 Md5Sess,
18 Sha256,
19 Sha256Sess,
20}
21
22impl DigestAlgorithm {
23 #[must_use]
24 pub fn parse(s: &str) -> Option<Self> {
25 if s.eq_ignore_ascii_case("md5") {
26 Some(Self::Md5)
27 } else if s.eq_ignore_ascii_case("md5-sess") {
28 Some(Self::Md5Sess)
29 } else if s.eq_ignore_ascii_case("sha-256") {
30 Some(Self::Sha256)
31 } else if s.eq_ignore_ascii_case("sha-256-sess") {
32 Some(Self::Sha256Sess)
33 } else {
34 None
35 }
36 }
37
38 #[must_use]
39 pub fn is_sess(self) -> bool {
40 matches!(self, Self::Md5Sess | Self::Sha256Sess)
41 }
42
43 #[must_use]
44 fn response_hex_len(self) -> usize {
45 match self {
46 Self::Md5 | Self::Md5Sess => 32,
47 Self::Sha256 | Self::Sha256Sess => 64,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DigestQop {
54 Auth,
55 AuthInt,
56}
57
58impl DigestQop {
59 #[must_use]
60 pub fn parse(s: &str) -> Option<Self> {
61 if s.eq_ignore_ascii_case("auth") {
62 Some(Self::Auth)
63 } else if s.eq_ignore_ascii_case("auth-int") {
64 Some(Self::AuthInt)
65 } else {
66 None
67 }
68 }
69
70 #[must_use]
71 pub fn as_str(self) -> &'static str {
72 match self {
73 Self::Auth => "auth",
74 Self::AuthInt => "auth-int",
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct DigestAuth {
81 pub username: String,
82 pub realm: Option<String>,
83 pub nonce: String,
84 pub uri: String,
85 pub response: String,
86 pub opaque: Option<String>,
87 pub algorithm: DigestAlgorithm,
88 pub qop: Option<DigestQop>,
89 pub nc: Option<String>,
90 pub cnonce: Option<String>,
91}
92
93#[derive(Debug, Clone)]
94pub struct DigestAuthError {
95 pub kind: DigestAuthErrorKind,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum DigestAuthErrorKind {
100 MissingHeader,
101 InvalidUtf8,
102 InvalidScheme,
103 InvalidFormat(&'static str),
104 MissingField(&'static str),
105 UnsupportedQop,
106 UnsupportedAlgorithm,
107 InvalidNc,
108 InvalidResponseHex,
109}
110
111impl fmt::Display for DigestAuthError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match &self.kind {
114 DigestAuthErrorKind::MissingHeader => write!(f, "Missing Authorization header"),
115 DigestAuthErrorKind::InvalidUtf8 => write!(f, "Invalid Authorization header encoding"),
116 DigestAuthErrorKind::InvalidScheme => {
117 write!(f, "Authorization header must use Digest scheme")
118 }
119 DigestAuthErrorKind::InvalidFormat(m) => write!(f, "Invalid Digest header: {m}"),
120 DigestAuthErrorKind::MissingField(k) => write!(f, "Digest header missing field: {k}"),
121 DigestAuthErrorKind::UnsupportedQop => write!(f, "Unsupported Digest qop"),
122 DigestAuthErrorKind::UnsupportedAlgorithm => write!(f, "Unsupported Digest algorithm"),
123 DigestAuthErrorKind::InvalidNc => write!(f, "Invalid Digest nc value"),
124 DigestAuthErrorKind::InvalidResponseHex => write!(f, "Invalid Digest response value"),
125 }
126 }
127}
128
129impl std::error::Error for DigestAuthError {}
130
131impl IntoResponse for DigestAuthError {
132 fn into_response(self) -> crate::response::Response {
133 use crate::response::{Response, ResponseBody, StatusCode};
134
135 let detail = match self.kind {
136 DigestAuthErrorKind::MissingHeader => "Not authenticated",
137 DigestAuthErrorKind::InvalidUtf8 => "Invalid authentication credentials",
138 DigestAuthErrorKind::InvalidScheme => "Invalid authentication credentials",
139 DigestAuthErrorKind::InvalidFormat(_) => "Invalid authentication credentials",
140 DigestAuthErrorKind::MissingField(_) => "Invalid authentication credentials",
141 DigestAuthErrorKind::UnsupportedQop => "Invalid authentication credentials",
142 DigestAuthErrorKind::UnsupportedAlgorithm => "Invalid authentication credentials",
143 DigestAuthErrorKind::InvalidNc => "Invalid authentication credentials",
144 DigestAuthErrorKind::InvalidResponseHex => "Invalid authentication credentials",
145 };
146
147 let body = serde_json::json!({ "detail": detail });
148 Response::with_status(StatusCode::UNAUTHORIZED)
149 .header(
150 "www-authenticate",
151 b"Digest realm=\"api\", qop=\"auth\", algorithm=MD5".to_vec(),
152 )
153 .header("content-type", b"application/json".to_vec())
154 .body(ResponseBody::Bytes(body.to_string().into_bytes()))
155 }
156}
157
158impl FromRequest for DigestAuth {
159 type Error = DigestAuthError;
160
161 async fn from_request(_ctx: &RequestContext, req: &mut Request) -> Result<Self, Self::Error> {
162 let auth_header = req.headers().get("authorization").ok_or(DigestAuthError {
163 kind: DigestAuthErrorKind::MissingHeader,
164 })?;
165 let auth_str = std::str::from_utf8(auth_header).map_err(|_| DigestAuthError {
166 kind: DigestAuthErrorKind::InvalidUtf8,
167 })?;
168 Self::parse(auth_str)
169 }
170}
171
172impl DigestAuth {
173 pub fn parse(header_value: &str) -> Result<Self, DigestAuthError> {
175 let mut it = header_value.splitn(2, char::is_whitespace);
176 let scheme = it.next().unwrap_or("");
177 if !scheme.eq_ignore_ascii_case("digest") {
178 return Err(DigestAuthError {
179 kind: DigestAuthErrorKind::InvalidScheme,
180 });
181 }
182 let rest = it.next().unwrap_or("").trim();
183 if rest.is_empty() {
184 return Err(DigestAuthError {
185 kind: DigestAuthErrorKind::InvalidFormat("missing parameters"),
186 });
187 }
188
189 let params = parse_kv_list(rest).map_err(|m| DigestAuthError {
190 kind: DigestAuthErrorKind::InvalidFormat(m),
191 })?;
192
193 let username = params
194 .get("username")
195 .ok_or(DigestAuthError {
196 kind: DigestAuthErrorKind::MissingField("username"),
197 })?
198 .clone();
199
200 let nonce = params
201 .get("nonce")
202 .ok_or(DigestAuthError {
203 kind: DigestAuthErrorKind::MissingField("nonce"),
204 })?
205 .clone();
206
207 let uri = params
208 .get("uri")
209 .ok_or(DigestAuthError {
210 kind: DigestAuthErrorKind::MissingField("uri"),
211 })?
212 .clone();
213
214 let response = params
215 .get("response")
216 .ok_or(DigestAuthError {
217 kind: DigestAuthErrorKind::MissingField("response"),
218 })?
219 .clone();
220
221 let realm = params.get("realm").map(ToString::to_string);
222 let opaque = params.get("opaque").map(ToString::to_string);
223
224 let algorithm = match params.get("algorithm") {
225 Some(v) => DigestAlgorithm::parse(v).ok_or(DigestAuthError {
226 kind: DigestAuthErrorKind::UnsupportedAlgorithm,
227 })?,
228 None => DigestAlgorithm::Md5,
229 };
230
231 if response.len() != algorithm.response_hex_len() || !is_hex(&response) {
232 return Err(DigestAuthError {
233 kind: DigestAuthErrorKind::InvalidResponseHex,
234 });
235 }
236
237 let qop = match params.get("qop") {
238 Some(v) => Some(DigestQop::parse(v).ok_or(DigestAuthError {
239 kind: DigestAuthErrorKind::UnsupportedQop,
240 })?),
241 None => None,
242 };
243
244 let nc = params.get("nc").map(|v| v.to_ascii_lowercase());
245 if let Some(nc) = &nc {
246 if nc.len() != 8 || !nc.as_bytes().iter().all(u8::is_ascii_hexdigit) {
247 return Err(DigestAuthError {
248 kind: DigestAuthErrorKind::InvalidNc,
249 });
250 }
251 }
252
253 let cnonce = params.get("cnonce").map(ToString::to_string);
254 if qop.is_some() {
255 if nc.is_none() {
256 return Err(DigestAuthError {
257 kind: DigestAuthErrorKind::MissingField("nc"),
258 });
259 }
260 if cnonce.is_none() {
261 return Err(DigestAuthError {
262 kind: DigestAuthErrorKind::MissingField("cnonce"),
263 });
264 }
265 }
266
267 Ok(Self {
268 username,
269 realm,
270 nonce,
271 uri,
272 response: response.to_ascii_lowercase(),
273 opaque,
274 algorithm,
275 qop,
276 nc,
277 cnonce,
278 })
279 }
280
281 pub fn compute_expected_response(
287 &self,
288 method: Method,
289 realm: &str,
290 password: &str,
291 ) -> Result<String, DigestAuthError> {
292 let qop = match self.qop {
293 Some(DigestQop::Auth) => Some("auth"),
294 Some(DigestQop::AuthInt) => {
295 return Err(DigestAuthError {
296 kind: DigestAuthErrorKind::UnsupportedQop,
297 });
298 }
299 None => None,
300 };
301
302 let ha1_0 = hash_hex(
303 self.algorithm,
304 format_args!("{}:{}:{}", self.username, realm, password),
305 );
306 let ha1 = if self.algorithm.is_sess() {
307 let Some(cnonce) = self.cnonce.as_deref() else {
308 return Err(DigestAuthError {
309 kind: DigestAuthErrorKind::MissingField("cnonce"),
310 });
311 };
312 hash_hex(
313 self.algorithm,
314 format_args!("{}:{}:{}", ha1_0, self.nonce, cnonce),
315 )
316 } else {
317 ha1_0
318 };
319
320 let ha2 = hash_hex(
321 self.algorithm,
322 format_args!("{}:{}", method.as_str(), self.uri),
323 );
324
325 let response = if let Some(qop) = qop {
326 let Some(nc) = self.nc.as_deref() else {
327 return Err(DigestAuthError {
328 kind: DigestAuthErrorKind::MissingField("nc"),
329 });
330 };
331 let Some(cnonce) = self.cnonce.as_deref() else {
332 return Err(DigestAuthError {
333 kind: DigestAuthErrorKind::MissingField("cnonce"),
334 });
335 };
336 hash_hex(
337 self.algorithm,
338 format_args!("{}:{}:{}:{}:{}:{}", ha1, self.nonce, nc, cnonce, qop, ha2),
339 )
340 } else {
341 hash_hex(
343 self.algorithm,
344 format_args!("{}:{}:{}", ha1, self.nonce, ha2),
345 )
346 };
347
348 Ok(response)
349 }
350
351 pub fn verify(
353 &self,
354 method: Method,
355 realm: &str,
356 password: &str,
357 ) -> Result<bool, DigestAuthError> {
358 let expected = self.compute_expected_response(method, realm, password)?;
359 Ok(constant_time_eq(
360 expected.as_bytes(),
361 self.response.as_bytes(),
362 ))
363 }
364
365 pub fn verify_for_challenge(
367 &self,
368 method: Method,
369 realm: &str,
370 nonce: &str,
371 password: &str,
372 ) -> Result<bool, DigestAuthError> {
373 if self.nonce != nonce {
374 return Ok(false);
375 }
376 if let Some(header_realm) = self.realm.as_deref() {
377 if header_realm != realm {
378 return Ok(false);
379 }
380 }
381 self.verify(method, realm, password)
382 }
383}
384
385fn is_hex(s: &str) -> bool {
386 !s.is_empty() && s.as_bytes().iter().all(u8::is_ascii_hexdigit)
387}
388
389fn parse_kv_list(input: &str) -> Result<std::collections::HashMap<String, String>, &'static str> {
390 let mut out = std::collections::HashMap::new();
391 let bytes = input.as_bytes();
392 let mut i = 0usize;
393
394 while i < bytes.len() {
395 while i < bytes.len() && (bytes[i].is_ascii_whitespace() || bytes[i] == b',') {
397 i += 1;
398 }
399 if i >= bytes.len() {
400 break;
401 }
402
403 let key_start = i;
405 while i < bytes.len()
406 && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'-' || bytes[i] == b'_')
407 {
408 i += 1;
409 }
410 if i == key_start {
411 return Err("expected key");
412 }
413 let key = std::str::from_utf8(&bytes[key_start..i]).map_err(|_| "non-utf8 key")?;
414 let key = key.to_ascii_lowercase();
415
416 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
417 i += 1;
418 }
419 if i >= bytes.len() || bytes[i] != b'=' {
420 return Err("expected '='");
421 }
422 i += 1;
423 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
424 i += 1;
425 }
426 if i >= bytes.len() {
427 return Err("expected value");
428 }
429
430 let value = if bytes[i] == b'"' {
431 i += 1;
432 let mut buf = String::new();
433 let mut closed = false;
434 while i < bytes.len() {
435 let b = bytes[i];
436 i += 1;
437 match b {
438 b'\\' => {
439 if i >= bytes.len() {
440 return Err("invalid escape");
441 }
442 let esc = bytes[i];
443 i += 1;
444 buf.push(esc as char);
445 }
446 b'"' => {
447 closed = true;
448 break;
449 }
450 _ => buf.push(b as char),
451 }
452 }
453 if !closed {
454 return Err("unterminated quoted value");
455 }
456 buf
457 } else {
458 let v_start = i;
459 while i < bytes.len() && bytes[i] != b',' {
460 i += 1;
461 }
462 let raw = std::str::from_utf8(&bytes[v_start..i]).map_err(|_| "non-utf8 value")?;
463 raw.trim().to_string()
464 };
465
466 out.insert(key, value);
467 }
468
469 Ok(out)
470}
471
472fn hash_hex(alg: DigestAlgorithm, args: fmt::Arguments<'_>) -> String {
473 let s = args.to_string();
474 match alg {
475 DigestAlgorithm::Md5 | DigestAlgorithm::Md5Sess => {
476 let d = md5(s.as_bytes());
477 hex_lower(&d)
478 }
479 DigestAlgorithm::Sha256 | DigestAlgorithm::Sha256Sess => {
480 let d = sha256(s.as_bytes());
481 hex_lower(&d)
482 }
483 }
484}
485
486fn hex_lower<const N: usize>(bytes: &[u8; N]) -> String {
487 const HEX: &[u8; 16] = b"0123456789abcdef";
488 let mut out = Vec::with_capacity(N * 2);
489 for &b in bytes {
490 out.push(HEX[(b >> 4) as usize]);
491 out.push(HEX[(b & 0x0f) as usize]);
492 }
493 String::from_utf8(out).expect("hex is ascii")
494}
495
496#[allow(clippy::many_single_char_names)]
501fn md5(data: &[u8]) -> [u8; 16] {
502 let mut a0: u32 = 0x67452301;
504 let mut b0: u32 = 0xefcdab89;
505 let mut c0: u32 = 0x98badcfe;
506 let mut d0: u32 = 0x10325476;
507
508 let bit_len = (data.len() as u64) * 8;
509 let mut msg = Vec::with_capacity((data.len() + 9).div_ceil(64) * 64);
510 msg.extend_from_slice(data);
511 msg.push(0x80);
512 while (msg.len() % 64) != 56 {
513 msg.push(0);
514 }
515 msg.extend_from_slice(&bit_len.to_le_bytes());
516
517 for chunk in msg.chunks_exact(64) {
518 let mut m = [0u32; 16];
519 for (i, word) in m.iter_mut().enumerate() {
520 let j = i * 4;
521 *word = u32::from_le_bytes([chunk[j], chunk[j + 1], chunk[j + 2], chunk[j + 3]]);
522 }
523
524 let mut a = a0;
525 let mut b = b0;
526 let mut c = c0;
527 let mut d = d0;
528
529 for i in 0..64 {
530 let (f, g) = match i {
531 0..=15 => ((b & c) | ((!b) & d), i),
532 16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
533 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
534 _ => (c ^ (b | (!d)), (7 * i) % 16),
535 };
536
537 let tmp = d;
538 d = c;
539 c = b;
540 b = b.wrapping_add(
541 (a.wrapping_add(f).wrapping_add(MD5_K[i]).wrapping_add(m[g])).rotate_left(MD5_S[i]),
542 );
543 a = tmp;
544 }
545
546 a0 = a0.wrapping_add(a);
547 b0 = b0.wrapping_add(b);
548 c0 = c0.wrapping_add(c);
549 d0 = d0.wrapping_add(d);
550 }
551
552 let mut out = [0u8; 16];
553 out[0..4].copy_from_slice(&a0.to_le_bytes());
554 out[4..8].copy_from_slice(&b0.to_le_bytes());
555 out[8..12].copy_from_slice(&c0.to_le_bytes());
556 out[12..16].copy_from_slice(&d0.to_le_bytes());
557 out
558}
559
560const MD5_S: [u32; 64] = [
561 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
562 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
563 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
564];
565
566const MD5_K: [u32; 64] = [
567 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
568 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
569 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
570 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
571 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
572 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
573 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
574 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
575];
576
577#[allow(clippy::many_single_char_names)]
578fn sha256(data: &[u8]) -> [u8; 32] {
579 let mut state: [u32; 8] = [
580 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
581 0x5be0cd19,
582 ];
583
584 let bit_len = (data.len() as u64) * 8;
585 let mut padded = data.to_vec();
586 padded.push(0x80);
587 while (padded.len() % 64) != 56 {
588 padded.push(0);
589 }
590 padded.extend_from_slice(&bit_len.to_be_bytes());
591
592 for chunk in padded.chunks(64) {
593 let mut words = [0u32; 64];
594 for (i, word) in words.iter_mut().enumerate().take(16) {
595 let offset = i * 4;
596 *word = u32::from_be_bytes([
597 chunk[offset],
598 chunk[offset + 1],
599 chunk[offset + 2],
600 chunk[offset + 3],
601 ]);
602 }
603 for i in 16..64 {
604 let sigma0 = words[i - 15].rotate_right(7)
605 ^ words[i - 15].rotate_right(18)
606 ^ (words[i - 15] >> 3);
607 let sigma1 = words[i - 2].rotate_right(17)
608 ^ words[i - 2].rotate_right(19)
609 ^ (words[i - 2] >> 10);
610 words[i] = words[i - 16]
611 .wrapping_add(sigma0)
612 .wrapping_add(words[i - 7])
613 .wrapping_add(sigma1);
614 }
615
616 let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
617 for i in 0..64 {
618 let sigma1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
619 let choose = (e & f) ^ ((!e) & g);
620 let temp1 = h
621 .wrapping_add(sigma1)
622 .wrapping_add(choose)
623 .wrapping_add(SHA256_K[i])
624 .wrapping_add(words[i]);
625 let sigma0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
626 let majority = (a & b) ^ (a & c) ^ (b & c);
627 let temp2 = sigma0.wrapping_add(majority);
628
629 h = g;
630 g = f;
631 f = e;
632 e = d.wrapping_add(temp1);
633 d = c;
634 c = b;
635 b = a;
636 a = temp1.wrapping_add(temp2);
637 }
638
639 state[0] = state[0].wrapping_add(a);
640 state[1] = state[1].wrapping_add(b);
641 state[2] = state[2].wrapping_add(c);
642 state[3] = state[3].wrapping_add(d);
643 state[4] = state[4].wrapping_add(e);
644 state[5] = state[5].wrapping_add(f);
645 state[6] = state[6].wrapping_add(g);
646 state[7] = state[7].wrapping_add(h);
647 }
648
649 let mut out = [0u8; 32];
650 for (i, value) in state.iter().enumerate() {
651 out[i * 4..i * 4 + 4].copy_from_slice(&value.to_be_bytes());
652 }
653 out
654}
655
656const SHA256_K: [u32; 64] = [
657 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
658 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
659 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
660 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
661 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
662 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
663 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
664 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
665];
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use crate::response::IntoResponse;
671
672 #[test]
673 fn rfc_2617_mufasa_vector_md5_auth() {
674 let hdr = concat!(
676 "Digest username=\"Mufasa\",",
677 " realm=\"testrealm@host.com\",",
678 " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",",
679 " uri=\"/dir/index.html\",",
680 " qop=auth,",
681 " nc=00000001,",
682 " cnonce=\"0a4f113b\",",
683 " response=\"6629fae49393a05397450978507c4ef1\",",
684 " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""
685 );
686
687 let d = DigestAuth::parse(hdr).expect("parse");
688 assert_eq!(d.algorithm, DigestAlgorithm::Md5);
689 assert_eq!(d.qop, Some(DigestQop::Auth));
690
691 let ok = d
692 .verify(Method::Get, "testrealm@host.com", "Circle Of Life")
693 .expect("verify");
694 assert!(ok);
695 }
696
697 #[test]
698 fn md5_known_vector_empty() {
699 let d = md5(b"");
701 assert_eq!(hex_lower(&d), "d41d8cd98f00b204e9800998ecf8427e");
702 }
703
704 #[test]
705 fn parse_uppercase_response_is_accepted_and_normalized() {
706 let hdr = concat!(
707 "Digest username=\"Mufasa\",",
708 " realm=\"testrealm@host.com\",",
709 " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",",
710 " uri=\"/dir/index.html\",",
711 " qop=auth,",
712 " nc=00000001,",
713 " cnonce=\"0a4f113b\",",
714 " response=\"6629FAE49393A05397450978507C4EF1\""
715 );
716 let d = DigestAuth::parse(hdr).expect("parse");
717 assert_eq!(d.response, "6629fae49393a05397450978507c4ef1");
718 }
719
720 #[test]
721 fn verify_for_challenge_rejects_nonce_mismatch() {
722 let hdr = concat!(
723 "Digest username=\"Mufasa\",",
724 " realm=\"testrealm@host.com\",",
725 " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",",
726 " uri=\"/dir/index.html\",",
727 " qop=auth,",
728 " nc=00000001,",
729 " cnonce=\"0a4f113b\",",
730 " response=\"6629fae49393a05397450978507c4ef1\""
731 );
732 let d = DigestAuth::parse(hdr).expect("parse");
733 let ok = d
734 .verify_for_challenge(
735 Method::Get,
736 "testrealm@host.com",
737 "different_nonce",
738 "Circle Of Life",
739 )
740 .expect("verify");
741 assert!(!ok);
742 }
743
744 #[test]
745 fn from_request_missing_header_produces_401() {
746 let cx = asupersync::Cx::for_testing();
747 let ctx = RequestContext::new(cx, 17);
748 let mut req = Request::new(Method::Get, "/");
749 let err = futures_executor::block_on(DigestAuth::from_request(&ctx, &mut req)).unwrap_err();
750 assert_eq!(err.kind, DigestAuthErrorKind::MissingHeader);
751 assert_eq!(err.into_response().status().as_u16(), 401);
752 }
753
754 #[test]
755 fn parse_rejects_unterminated_quoted_value() {
756 let hdr = "Digest username=\"Mufasa, nonce=\"abc\", uri=\"/\", response=\"0123456789abcdef0123456789abcdef\"";
757 let err = DigestAuth::parse(hdr).expect_err("unterminated quoted values must be rejected");
758 assert!(matches!(err.kind, DigestAuthErrorKind::InvalidFormat(_)));
759 }
760}