1 // Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
2 // SPDX-License-Identifier: MIT OR Apache-2.0
4 //! `Digest` authentication scheme, as in
5 //! [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616).
7 use std
::{convert::TryFrom, fmt::Write as _, io::Write as _}
;
12 char_classes
, ChallengeRef
, ParamValue
, PasswordParams
, C_ATTR
, C_ESCAPABLE
, C_QDTEXT
,
15 /// "Quality of protection" value.
17 /// The values here can be used in a bitmask as in [`DigestClient::qop`].
18 #[derive(Copy, Clone, Debug)]
25 /// Authentication with integrity protection.
27 /// "Integrity protection" means protection of the request entity body.
32 /// Returns a string form as expected over the wire.
33 fn as_str(self) -> &'
static str {
36 Qop
::AuthInt
=> "auth-int",
41 /// A set of zero or more [`Qop`]s.
42 #[derive(Copy, Clone, PartialEq, Eq)]
43 pub struct QopSet(u8);
45 impl std
::fmt
::Debug
for QopSet
{
46 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
47 let mut l
= f
.debug_set();
48 if (self.0 & Qop
::Auth
as u8) != 0 {
51 if (self.0 & Qop
::AuthInt
as u8) != 0 {
58 impl std
::ops
::BitAnd
<Qop
> for QopSet
{
61 fn bitand(self, rhs
: Qop
) -> Self::Output
{
62 (self.0 & (rhs
as u8)) != 0
66 /// Client for a `Digest` challenge, as in [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616).
68 /// Most of the information here is taken from the `WWW-Authenticate` or
69 /// `Proxy-Authenticate` header. This also internally maintains a nonce counter.
71 /// ## Implementation notes
73 /// * Recalculates `H(A1)` on each [`DigestClient::respond`] call. It'd be
74 /// more CPU-efficient to calculate `H(A1)` only once by supplying the
75 /// username and password at construction time or by caching (username,
76 /// password) -> `H(A1)` mappings internally. `DigestClient` prioritizes
77 /// simplicity instead.
78 /// * There's no support yet for parsing the `Authentication-Info` and
79 /// `Proxy-Authentication-Info` header fields described by [RFC 7616 section
80 /// 3.5](https://datatracker.ietf.org/doc/html/rfc7616#section-3.5).
82 /// * Always responds using `UTF-8`, and thus doesn't use or keep around the `charset`
83 /// parameter. The RFC only allows that parameter to be set to `UTF-8` anyway.
84 /// * Supports [RFC 2069](https://datatracker.ietf.org/doc/html/rfc2069) compatibility as in
85 /// [RFC 2617 section 3.2.2.1](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1),
86 /// even though RFC 7616 drops it. There are still RTSP cameras being sold
87 /// in 2021 that use the RFC 2069-style calculations.
88 /// * Supports RFC 7616 `userhash`, even though it seems impractical and only
89 /// marginally useful. The server must index the userhash for each supported
90 /// algorithm or calculate it on-the-fly for all users in the database.
91 /// * The `-sess` algorithm variants haven't been tested; there's no example
94 /// ## Security considerations
96 /// We strongly advise *servers* against implementing `Digest`:
98 /// * It's actively harmful in that it prevents the server from securing their
99 /// password storage via salted password hashes. See [RFC 7616 Section
100 /// 5.2](https://datatracker.ietf.org/doc/html/rfc7616#section-5.2).
101 /// When your server offers `Digest` authentication, it is advertising that
102 /// it stores plaintext passwords!
103 /// * It's no replacement for TLS in terms of protecting confidentiality of
104 /// the password, much less confidentiality of any other information.
106 /// For *clients*, when a server supports both `Digest` and `Basic`, we advise
107 /// using `Digest`. It provides (slightly) more confidentiality of passwords
110 /// Some servers *only* support `Digest`. E.g.,
111 /// [ONVIF](https://www.onvif.org/profiles/specifications/) mandates the
112 /// `Digest` scheme. It doesn't prohibit implementing other schemes, but some
113 /// cameras meet the specification's requirement and do no more.
114 #[derive(Eq, PartialEq)]
115 pub struct DigestClient
{
116 /// Holds unescaped versions of all string fields.
118 /// Using a single `String` minimizes the size of the `DigestClient`
119 /// itself and/or any option/enum it may be wrapped in. It also minimizes
120 /// padding bytes after each allocation. The fields as stored as follows:
122 /// 1. `realm`: `[0, domain_start)`
123 /// 2. `domain`: `[domain_start, opaque_start)`
124 /// 3. `opaque`: `[opaque_start, nonce_start)`
125 /// 4. `nonce`: `[nonce_start, buf.len())`
128 // Positions described in `buf` comment above. See respective methods' doc
129 // comments for more information. These are stored as `u16` to save space,
130 // and because it's unreasonable for them to be large.
135 // Non-string fields. See respective methods' doc comments for more information.
136 algorithm
: Algorithm
,
139 rfc2069_compat
: bool
,
146 /// Returns a string to be displayed to users so they know which username
147 /// and password to use.
149 /// This string should contain at least the name of
150 /// the host performing the authentication and might additionally
151 /// indicate the collection of users who might have access. An
152 /// example is `registered_users@example.com`. (See [Section 2.2 of
153 /// RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235#section-2.2) for
156 pub fn realm(&self) -> &str {
157 &self.buf
[..self.domain_start
as usize]
160 /// Returns the domain, a space-separated list of URIs, as specified in RFC
161 /// 3986, that define the protection space.
163 /// If the domain parameter is absent, returns an empty string, which is semantically
164 /// identical according to the RFC.
166 pub fn domain(&self) -> &str {
167 &self.buf
[self.domain_start
as usize..self.opaque_start
as usize]
170 /// Returns the nonce, a server-specified string which should be uniquely
171 /// generated each time a 401 response is made.
173 pub fn nonce(&self) -> &str {
174 &self.buf
[self.nonce_start
as usize..]
177 /// Returns string of data, specified by the server, that SHOULD be returned
178 /// by the client unchanged in the Authorization header field of subsequent
179 /// requests with URIs in the same protection space.
181 /// Currently an empty `opaque` is treated as an absent one.
183 pub fn opaque(&self) -> Option
<&str> {
184 if self.opaque_start
== self.nonce_start
{
187 Some(&self.buf
[self.opaque_start
as usize..self.nonce_start
as usize])
191 /// Returns a flag indicating that the previous request from the client was
192 /// rejected because the nonce value was stale.
194 pub fn stale(&self) -> bool
{
198 /// Returns true if using [RFC 2069](https://datatracker.ietf.org/doc/html/rfc2069)
199 /// compatibility mode as in [RFC 2617 section
200 /// 3.2.2.1](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1).
202 /// If so, `request-digest` is calculated without the nonce count, conce, or qop.
204 pub fn rfc2069_compat(&self) -> bool
{
208 /// Returns the algorithm used to produce the digest and an unkeyed digest.
210 pub fn algorithm(&self) -> Algorithm
{
214 /// Returns if the session style `A1` will be used.
216 pub fn session(&self) -> bool
{
220 /// Returns the acceptable `qop` (quality of protection) values.
222 pub fn qop(&self) -> QopSet
{
226 /// Returns the number of times the server-supplied nonce has been used by
227 /// [`DigestClient::respond`].
229 pub fn nonce_count(&self) -> u32 {
233 /// Responds to the challenge with the supplied parameters.
235 /// The caller should use the returned string as an `Authorization` or
236 /// `Proxy-Authorization` header value.
238 pub fn respond(&mut self, p
: &PasswordParams
) -> Result
<String
, String
> {
239 self.respond_inner(p
, &new_random_cnonce())
242 /// Responds using a fixed cnonce **for testing only**.
244 /// In production code, use [`DigestClient::respond`] instead, which generates a new
245 /// random cnonce value.
247 pub fn respond_with_testing_cnonce(
251 ) -> Result
<String
, String
> {
252 self.respond_inner(p
, cnonce
)
255 /// Helper for respond methods.
257 /// We don't simply implement this as `respond_with_testing_cnonce` and have
258 /// `respond` delegate to that method because it'd be confusing/alarming if
259 /// that method name ever shows up in production stack traces.
260 /// and have `respond` delegate to the testing version. We don't do that because
261 fn respond_inner(&mut self, p
: &PasswordParams
, cnonce
: &str) -> Result
<String
, String
> {
262 let realm
= self.realm();
263 let mut h_a1
= self.algorithm
.h(&[
264 p
.username
.as_bytes(),
268 p
.password
.as_bytes(),
271 h_a1
= self.algorithm
.h(&[
274 self.nonce().as_bytes(),
280 // Select the best available qop and calculate H(A2) as in
281 // [https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.3].
283 if let (Some(body
), true) = (p
.body
, self.qop
& Qop
::AuthInt
) {
286 .h(&[p
.method
.as_bytes(), b
":", p
.uri
.as_bytes(), b
":", body
]);
288 } else if self.qop
& Qop
::Auth
{
291 .h(&[p
.method
.as_bytes(), b
":", p
.uri
.as_bytes()]);
294 return Err("no supported/available qop".into());
297 let nc
= self.nc
.checked_add(1).ok_or("nonce count exhausted")?
;
298 let mut hex_nc
= [0u8; 8];
299 let _
= write
!(&mut hex_nc
[..], "{:08x}", nc
);
300 let str_hex_nc
= match std
::str::from_utf8(&hex_nc
[..]) {
302 Err(_
) => unreachable
!(),
305 // https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1
306 let response
= if self.rfc2069_compat
{
310 self.nonce().as_bytes(),
318 self.nonce().as_bytes(),
324 qop
.as_str().as_bytes(),
330 let mut out
= String
::with_capacity(128);
331 out
.push_str("Digest ");
335 .h(&[p
.username
.as_bytes(), b
":", realm
.as_bytes()]);
336 append_quoted_key_value(&mut out
, "username", &hashed
)?
;
337 append_unquoted_key_value(&mut out
, "userhash", "true");
338 } else if is_valid_quoted_value(p
.username
) {
339 append_quoted_key_value(&mut out
, "username", p
.username
)?
;
341 append_extended_key_value(&mut out
, "username", p
.username
);
343 append_quoted_key_value(&mut out
, "realm", self.realm())?
;
344 append_quoted_key_value(&mut out
, "uri", p
.uri
)?
;
345 append_quoted_key_value(&mut out
, "nonce", self.nonce())?
;
346 if !self.rfc2069_compat
{
347 append_unquoted_key_value(&mut out
, "algorithm", self.algorithm
.as_str(self.session
));
348 append_unquoted_key_value(&mut out
, "nc", str_hex_nc
);
349 append_quoted_key_value(&mut out
, "cnonce", cnonce
)?
;
350 append_unquoted_key_value(&mut out
, "qop", qop
.as_str());
352 append_quoted_key_value(&mut out
, "response", &response
)?
;
353 if let Some(o
) = self.opaque() {
354 append_quoted_key_value(&mut out
, "opaque", o
)?
;
356 out
.truncate(out
.len() - 2); // remove final ", "
362 impl TryFrom
<&ChallengeRef
<'_
>> for DigestClient
{
365 fn try_from(value
: &ChallengeRef
<'_
>) -> Result
<Self, Self::Error
> {
366 if !value
.scheme
.eq_ignore_ascii_case("Digest") {
368 "DigestClientContext doesn't support challenge scheme {:?}",
373 let mut unused_len
= 0;
374 let mut realm
= None
;
375 let mut domain
= None
;
376 let mut nonce
= None
;
377 let mut opaque
= None
;
378 let mut stale
= false;
379 let mut algorithm_and_session
= None
;
380 let mut qop_str
= None
;
381 let mut userhash_str
= None
;
383 // Parse response header field parameters as in
384 // [https://datatracker.ietf.org/doc/html/rfc7616#section-3.3].
385 for (k
, v
) in &value
.params
{
386 // Note that "stale" and "algorithm" can be directly compared
387 // without unescaping because RFC 7616 section 3.3 says "For
388 // historical reasons, a sender MUST NOT generate the quoted string
389 // syntax values for the following parameters: stale and algorithm."
390 if store_param(k
, v
, "realm", &mut realm
, &mut buf_len
)?
391 || store_param(k
, v
, "domain", &mut domain
, &mut buf_len
)?
392 || store_param(k
, v
, "nonce", &mut nonce
, &mut buf_len
)?
393 || store_param(k
, v
, "opaque", &mut opaque
, &mut buf_len
)?
394 || store_param(k
, v
, "qop", &mut qop_str
, &mut unused_len
)?
395 || store_param(k
, v
, "userhash", &mut userhash_str
, &mut unused_len
)?
398 } else if k
.eq_ignore_ascii_case("stale") {
399 stale
= v
.escaped
.eq_ignore_ascii_case("true");
400 } else if k
.eq_ignore_ascii_case("algorithm") {
401 algorithm_and_session
= Some(Algorithm
::parse(v
.escaped
)?
);
404 let realm
= realm
.ok_or("missing required parameter realm")?
;
405 let nonce
= nonce
.ok_or("missing required parameter nonce")?
;
406 if buf_len
> u16::MAX
as usize {
407 // Incredibly unlikely, but just for completeness.
409 "Unescaped parameters' length {} exceeds u16::MAX!",
414 let algorithm_and_session
= algorithm_and_session
.unwrap_or((Algorithm
::Md5
, false));
416 let mut buf
= String
::with_capacity(buf_len
);
417 let mut qop
= QopSet(0);
418 let rfc2069_compat
= if let Some(qop_str
) = qop_str
{
419 let qop_str
= qop_str
.unescaped_with_scratch(&mut buf
);
420 for v
in qop_str
.split('
,'
) {
422 if v
.eq_ignore_ascii_case("auth") {
423 qop
.0 |= Qop
::Auth
as u8;
424 } else if v
.eq_ignore_ascii_case("auth-int") {
425 qop
.0 |= Qop
::AuthInt
as u8;
429 return Err(format
!("no supported qop in {:?}", qop_str
));
434 // An absent qop is treated as "auth", according to
435 // https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.3
436 qop
.0 |= Qop
::Auth
as u8;
440 if let Some(userhash_str
) = userhash_str
{
441 let userhash_str
= userhash_str
.unescaped_with_scratch(&mut buf
);
442 userhash
= userhash_str
.eq_ignore_ascii_case("true");
447 realm
.append_unescaped(&mut buf
);
448 let domain_start
= buf
.len();
449 if let Some(d
) = domain
{
450 d
.append_unescaped(&mut buf
);
452 let opaque_start
= buf
.len();
453 if let Some(o
) = opaque
{
454 o
.append_unescaped(&mut buf
);
456 let nonce_start
= buf
.len();
457 nonce
.append_unescaped(&mut buf
);
459 buf
: buf
.into_boxed_str(),
460 domain_start
: domain_start
as u16,
461 opaque_start
: opaque_start
as u16,
462 nonce_start
: nonce_start
as u16,
463 algorithm
: algorithm_and_session
.0,
464 session
: algorithm_and_session
.1,
474 impl std
::fmt
::Debug
for DigestClient
{
475 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
476 f
.debug_struct("DigestClient")
477 .field("realm", &self.realm())
478 .field("domain", &self.domain())
479 .field("opaque", &self.opaque())
480 .field("nonce", &self.nonce())
481 .field("algorithm", &self.algorithm
.as_str(self.session
))
482 .field("stale", &self.stale
)
483 .field("qop", &self.qop
)
484 .field("rfc2069_compat", &self.rfc2069_compat
)
485 .field("userhash", &self.userhash
)
486 .field("nc", &self.nc
)
491 /// Helper for `DigestClient::try_from` which stashes away a `&ParamValue`.
492 fn store_param
<'v
, 'tmp
>(
494 v
: &'v ParamValue
<'v
>,
495 expected_k
: &'tmp
str,
496 set_v
: &'tmp
mut Option
<&'v ParamValue
<'v
>>,
497 add_len
: &'tmp
mut usize,
498 ) -> Result
<bool
, String
> {
499 if !k
.eq_ignore_ascii_case(expected_k
) {
503 return Err(format
!("duplicate parameter {:?}", k
));
505 *add_len
+= v
.unescaped_len();
510 fn is_valid_quoted_value(s
: &str) -> bool
{
511 for &b
in s
.as_bytes() {
512 if char_classes(b
) & (C_QDTEXT
| C_ESCAPABLE
) == 0 {
519 fn append_extended_key_value(out
: &mut String
, key
: &str, value
: &str) {
521 out
.push_str("*=UTF-8''");
522 for &b
in value
.as_bytes() {
523 if (char_classes(b
) & C_ATTR
) != 0 {
524 out
.push(char::from(b
));
526 let _
= write
!(out
, "%{:02X}", b
);
532 fn append_unquoted_key_value(out
: &mut String
, key
: &str, value
: &str) {
539 fn append_quoted_key_value(out
: &mut String
, key
: &str, value
: &str) -> Result
<(), String
> {
542 let mut first_unwritten
= 0;
543 let bytes
= value
.as_bytes();
544 for (i
, &b
) in bytes
.iter().enumerate() {
545 // Note that bytes >= 128 are in neither C_QDTEXT nor C_ESCAPABLE, so every allowed byte
546 // is a full character.
547 let class
= char_classes(b
);
548 if (class
& C_QDTEXT
) != 0 {
550 } else if (class
& C_ESCAPABLE
) != 0 {
551 out
.push_str(&value
[first_unwritten
..i
]);
553 out
.push(char::from(b
));
554 first_unwritten
= i
+ 1;
556 return Err(format
!("invalid {} value {:?}", key
, value
));
559 out
.push_str(&value
[first_unwritten
..]);
560 out
.push_str("\", ");
564 /// Supported algorithm from the [HTTP Digest Algorithm Values
565 /// registry](https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml).
567 /// This doesn't store whether the session variant (`<Algorithm>-sess`) was
568 /// requested; see [`DigestClient::session`] for that.
569 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
578 /// Parses a string into a tuple of `Algorithm` and a bool representing
579 /// whether the `-sess` suffix is present.
580 fn parse(s
: &str) -> Result
<(Self, bool
), String
> {
582 "MD5" => (Algorithm
::Md5
, false),
583 "MD5-sess" => (Algorithm
::Md5
, true),
584 "SHA-256" => (Algorithm
::Sha256
, false),
585 "SHA-256-sess" => (Algorithm
::Sha256
, true),
586 "SHA-512-256" => (Algorithm
::Sha512Trunc256
, false),
587 "SHA-512-256-sess" => (Algorithm
::Sha512Trunc256
, true),
588 _
=> return Err(format
!("unknown algorithm {:?}", s
)),
592 fn as_str(&self, session
: bool
) -> &'
static str {
593 match (self, session
) {
594 (Algorithm
::Md5
, false) => "MD5",
595 (Algorithm
::Md5
, true) => "MD5-sess",
596 (Algorithm
::Sha256
, false) => "SHA-256",
597 (Algorithm
::Sha256
, true) => "SHA-256-sess",
598 (Algorithm
::Sha512Trunc256
, false) => "SHA-512-256",
599 (Algorithm
::Sha512Trunc256
, true) => "SHA-512-256-sess",
603 fn h(&self, items
: &[&[u8]]) -> String
{
605 Algorithm
::Md5
=> h(md5
::Md5
::new(), items
),
606 Algorithm
::Sha256
=> h(sha2
::Sha256
::new(), items
),
607 Algorithm
::Sha512Trunc256
=> h(sha2
::Sha512_256
::new(), items
),
612 fn h
<D
: Digest
>(mut d
: D
, items
: &[&[u8]]) -> String
{
616 hex
::encode(d
.finalize())
619 fn new_random_cnonce() -> String
{
620 let raw
: [u8; 16] = rand
::random();
621 hex
::encode(&raw
[..])
627 use pretty_assertions
::assert_eq
;
629 /// Tests the example from [RFC 7616 section 3.9.1: SHA-256 and
630 /// MD5](https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.1).
632 fn sha256_and_md5() {
633 let www_authenticate
= "\
635 realm=\"http-auth@example.org\", \
636 qop=\"auth, auth-int\", \
638 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
639 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\", \
641 realm=\"http-auth@example.org\", \
642 qop=\"auth, auth-int\", \
644 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
645 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
646 let challenges
= dbg
!(crate::parse_challenges(www_authenticate
).unwrap());
647 assert_eq
!(challenges
.len(), 2);
648 let ctxs
: Result
<Vec
<_
>, _
> = challenges
.iter().map(DigestClient
::try_from
).collect();
649 let mut ctxs
= dbg
!(ctxs
.unwrap());
650 assert_eq
!(ctxs
[1].realm(), "http-auth@example.org");
651 assert_eq
!(ctxs
[1].domain(), "");
654 "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
658 Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
660 assert_eq
!(ctxs
[1].stale(), false);
661 assert_eq
!(ctxs
[1].algorithm(), Algorithm
::Md5
);
662 assert_eq
!(ctxs
[1].qop().0, (Qop
::Auth
as u8) | (Qop
::AuthInt
as u8));
663 assert_eq
!(ctxs
[1].nonce_count(), 0);
664 let params
= crate::PasswordParams
{
666 password
: "Circle of Life",
667 uri
: "/dir/index.html",
673 .respond_with_testing_cnonce(
675 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
678 "Digest username=\"Mufasa\", \
679 realm=\"http-auth@example.org\", \
680 uri=\"/dir/index.html\", \
681 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
684 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
686 response=\"753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1\", \
687 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
689 assert_eq
!(ctxs
[0].nc
, 1);
692 .respond_with_testing_cnonce(
694 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
697 "Digest username=\"Mufasa\", \
698 realm=\"http-auth@example.org\", \
699 uri=\"/dir/index.html\", \
700 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
703 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
705 response=\"8ca523f5e9506fed4657c9700eebdbec\", \
706 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
708 assert_eq
!(ctxs
[1].nc
, 1);
711 /// Tests a made-up example with `MD5-sess`. There's no example in the RFC,
712 /// and these values haven't been tested against any other implementation.
713 /// But having the test here ensures we don't accidentally change the
717 let www_authenticate
= "\
719 realm=\"http-auth@example.org\", \
720 qop=\"auth, auth-int\", \
721 algorithm=MD5-sess, \
722 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
723 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
724 let challenges
= dbg
!(crate::parse_challenges(www_authenticate
).unwrap());
725 assert_eq
!(challenges
.len(), 1);
726 let ctxs
: Result
<Vec
<_
>, _
> = challenges
.iter().map(DigestClient
::try_from
).collect();
727 let mut ctxs
= dbg
!(ctxs
.unwrap());
728 assert_eq
!(ctxs
[0].realm(), "http-auth@example.org");
729 assert_eq
!(ctxs
[0].domain(), "");
732 "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
736 Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
738 assert_eq
!(ctxs
[0].stale(), false);
739 assert_eq
!(ctxs
[0].algorithm(), Algorithm
::Md5
);
740 assert_eq
!(ctxs
[0].session(), true);
741 assert_eq
!(ctxs
[0].qop().0, (Qop
::Auth
as u8) | (Qop
::AuthInt
as u8));
742 assert_eq
!(ctxs
[0].nonce_count(), 0);
743 let params
= crate::PasswordParams
{
745 password
: "Circle of Life",
746 uri
: "/dir/index.html",
752 .respond_with_testing_cnonce(
754 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
757 "Digest username=\"Mufasa\", \
758 realm=\"http-auth@example.org\", \
759 uri=\"/dir/index.html\", \
760 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
761 algorithm=MD5-sess, \
763 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
765 response=\"e783283f46242139c486a698fec7211d\", \
766 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
768 assert_eq
!(ctxs
[0].nc
, 1);
771 /// Tests the example from [RFC 7616 section 3.9.2: SHA-512-256, Charset, and
772 /// Userhash](https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.2).
774 fn sha512_256_charset() {
775 let www_authenticate
= "\
777 realm=\"api@example.org\", \
779 algorithm=SHA-512-256, \
780 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
781 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", \
784 let challenges
= dbg
!(crate::parse_challenges(www_authenticate
).unwrap());
785 assert_eq
!(challenges
.len(), 1);
786 let ctxs
: Result
<Vec
<_
>, _
> = challenges
.iter().map(DigestClient
::try_from
).collect();
787 let mut ctxs
= dbg
!(ctxs
.unwrap());
788 assert_eq
!(ctxs
.len(), 1);
789 assert_eq
!(ctxs
[0].realm(), "api@example.org");
790 assert_eq
!(ctxs
[0].domain(), "");
793 "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
797 Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS")
799 assert_eq
!(ctxs
[0].stale
, false);
800 assert_eq
!(ctxs
[0].userhash
, true);
801 assert_eq
!(ctxs
[0].algorithm
, Algorithm
::Sha512Trunc256
);
802 assert_eq
!(ctxs
[0].qop
.0, Qop
::Auth
as u8);
803 assert_eq
!(ctxs
[0].nc
, 0);
804 let params
= crate::PasswordParams
{
805 username
: "J\u{E4}s\u{F8}n Doe",
806 password
: "Secret, or not?",
812 // Note the username and response values in the RFC are *wrong*!
813 // https://www.rfc-editor.org/errata/eid4897
816 .respond_with_testing_cnonce(
818 "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
823 username=\"793263caabb707a56211940d90411ea4a575adeccb7e360aeb624ed06ece9b0b\", \
825 realm=\"api@example.org\", \
827 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
828 algorithm=SHA-512-256, \
830 cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
832 response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
833 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
835 assert_eq
!(ctxs
[0].nc
, 1);
836 ctxs
[0].userhash
= false;
840 .respond_with_testing_cnonce(
842 "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
847 username*=UTF-8''J%C3%A4s%C3%B8n%20Doe, \
848 realm=\"api@example.org\", \
850 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
851 algorithm=SHA-512-256, \
853 cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
855 response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
856 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
858 assert_eq
!(ctxs
[0].nc
, 1);
863 // https://datatracker.ietf.org/doc/html/rfc2069#section-2.4
864 // The response there is wrong! See https://www.rfc-editor.org/errata/eid749
865 let www_authenticate
= "\
867 realm=\"testrealm@host.com\", \
868 nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
869 opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
870 let challenges
= dbg
!(crate::parse_challenges(www_authenticate
).unwrap());
871 assert_eq
!(challenges
.len(), 1);
872 let ctxs
: Result
<Vec
<_
>, _
> = challenges
.iter().map(DigestClient
::try_from
).collect();
873 let mut ctxs
= dbg
!(ctxs
.unwrap());
874 assert_eq
!(ctxs
.len(), 1);
875 assert_eq
!(ctxs
[0].qop
.0, Qop
::Auth
as u8);
876 assert_eq
!(ctxs
[0].rfc2069_compat
, true);
877 let params
= crate::PasswordParams
{
879 password
: "CircleOfLife",
880 uri
: "/dir/index.html",
886 .respond_with_testing_cnonce(¶ms
, "unused")
890 username=\"Mufasa\", \
891 realm=\"testrealm@host.com\", \
892 uri=\"/dir/index.html\", \
893 nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
894 response=\"1949323746fe6a43ef61f9606e7febea\", \
895 opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
897 assert_eq
!(ctxs
[0].nc
, 1);
900 // See sizes with: cargo test -- --nocapture digest::tests::size
903 // This type should have a niche.
905 dbg
!(std
::mem
::size_of
::<DigestClient
>()),
906 dbg
!(std
::mem
::size_of
::<Option
<DigestClient
>>()),