Skip to main content

sipx_ua/
challenge.rs

1//! The server side of digest authentication (RFC 7616, RFC 8760).
2//!
3//! [`crate::auth`] answers a challenge. This issues one, and checks what comes back — the same
4//! formulas from the other end, which is why they are not written a second time here.
5//!
6//! **Scope is the primitives.** Which credential a username maps to, and what to do about a
7//! failure, belong to whoever is authenticating: a credential store is not this crate's business.
8//! So verification takes the password as an argument and returns a verdict, and the caller decides
9//! everything around it.
10
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13use sipx_sip::{HeaderName, Request};
14
15use crate::auth::{Algorithm, Credentials, respond};
16
17/// How long a nonce is good for by default.
18///
19/// Five minutes. Long enough that an ordinary REGISTER-then-refresh does not re-challenge, short
20/// enough that a captured `Authorization` is not a lasting credential. A longer window is not more
21/// convenient — a client that gets `stale=true` re-sends without prompting anyone.
22pub const DEFAULT_LIFETIME: Duration = Duration::from_secs(300);
23
24/// How many nonces the replay window remembers at once.
25///
26/// The window has to be bounded or it is a memory leak with a protocol in front of it. Evicting
27/// the oldest is safe in the direction that matters: a client whose nonce is evicted is challenged
28/// again with `stale=true` and retries without a human, whereas an unbounded map is an outage.
29const REPLAY_CAPACITY: usize = 4096;
30
31/// What a request presented in its `Authorization` or `Proxy-Authorization`.
32///
33/// Parsed but not trusted. Every field here is attacker-controlled; the only thing that makes any
34/// of it meaningful is [`Authenticator::verify`] recomputing the response from a password.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Presented {
37    /// Who the request claims to be.
38    pub username: String,
39    /// The realm it answered.
40    pub realm: String,
41    /// The nonce it answered.
42    pub nonce: String,
43    /// The URI the digest covers — **not** necessarily the request's own.
44    pub uri: String,
45    /// The computed response.
46    pub response: String,
47    /// The algorithm named, defaulting to MD5 when absent (RFC 7616 §3.4).
48    pub algorithm: Algorithm,
49    /// The nonce count, if `qop` was used.
50    pub nonce_count: Option<u32>,
51    /// The client nonce, if `qop` was used.
52    pub cnonce: Option<String>,
53    /// Whether `qop=auth` was claimed.
54    pub qop_auth: bool,
55}
56
57impl Presented {
58    /// Read the credentials out of a request.
59    ///
60    /// `proxy` selects `Proxy-Authorization` over `Authorization`. Which one is right is not a
61    /// detail: a UAS challenges with 401 and reads `Authorization`, a proxy challenges with 407 and
62    /// reads `Proxy-Authorization`, and a server that reads the wrong one authenticates nobody
63    /// while looking like it works.
64    #[must_use]
65    pub fn from_request(request: &Request, proxy: bool) -> Option<Self> {
66        let header = if proxy {
67            HeaderName::ProxyAuthorization
68        } else {
69            HeaderName::Authorization
70        };
71        let value = request.headers.value(&header)?;
72        Self::parse(&value)
73    }
74
75    /// Read a `Digest …` credentials value.
76    #[must_use]
77    pub fn parse(value: &[u8]) -> Option<Self> {
78        let text = String::from_utf8_lossy(value);
79        let rest = text.trim().strip_prefix("Digest")?.trim_start();
80        let param = |name: &str| parameter(rest, name);
81
82        let qop = param("qop");
83        Some(Self {
84            username: param("username")?,
85            realm: param("realm").unwrap_or_default(),
86            nonce: param("nonce")?,
87            uri: param("uri").unwrap_or_default(),
88            response: param("response")?,
89            algorithm: param("algorithm")
90                .as_deref()
91                .and_then(Algorithm::parse)
92                // RFC 7616 §3.4: an absent algorithm means MD5.
93                .unwrap_or(Algorithm::Md5),
94            nonce_count: param("nc").and_then(|nc| u32::from_str_radix(&nc, 16).ok()),
95            cnonce: param("cnonce"),
96            qop_auth: qop.as_deref() == Some("auth"),
97        })
98    }
99}
100
101/// Read one parameter out of a comma-separated credentials list, quoted or not.
102fn parameter(input: &str, name: &str) -> Option<String> {
103    let mut rest = input;
104    while !rest.is_empty() {
105        let rest_trimmed = rest.trim_start_matches([' ', '\t', ',']);
106        let (key, after) = rest_trimmed.split_once('=')?;
107        let key = key.trim();
108        let after = after.trim_start();
109        let (value, remainder) = if let Some(quoted) = after.strip_prefix('"') {
110            // A quoted string, honouring backslash escapes — a realm or username containing a
111            // quote would otherwise end the value early and shift every parameter after it.
112            let mut value = String::new();
113            let mut chars = quoted.char_indices();
114            let mut end = None;
115            while let Some((index, character)) = chars.next() {
116                match character {
117                    '\\' => {
118                        if let Some((_, escaped)) = chars.next() {
119                            value.push(escaped);
120                        }
121                    }
122                    '"' => {
123                        end = Some(index + 1);
124                        break;
125                    }
126                    other => value.push(other),
127                }
128            }
129            (value, quoted.get(end?..).unwrap_or_default())
130        } else {
131            let end = after.find(',').unwrap_or(after.len());
132            (
133                after.get(..end).unwrap_or_default().trim().to_owned(),
134                after.get(end..).unwrap_or_default(),
135            )
136        };
137        if key.eq_ignore_ascii_case(name) {
138            return Some(value);
139        }
140        rest = remainder;
141    }
142    None
143}
144
145/// What verification concluded.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum Verdict {
148    /// The credentials are correct and fresh.
149    Authenticated,
150    /// The credentials are correct and the nonce has expired.
151    ///
152    /// Challenge again with `stale=true` (RFC 7616 §3.3). The distinction is the whole reason
153    /// `stale` exists: a client told `stale=true` re-computes and re-sends by itself, and one told
154    /// only "401" prompts a human for a password that was never wrong.
155    Stale,
156    /// The credentials are not correct, or not this server's to accept.
157    Rejected(Reason),
158}
159
160/// Why verification failed.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum Reason {
163    /// The digest did not match the password.
164    ///
165    /// Deliberately does not distinguish "no such user" from "wrong password": the difference is a
166    /// user-enumeration oracle, and it is not information the far end is entitled to.
167    Mismatch,
168    /// The nonce was not issued by this server, or has been tampered with.
169    ForeignNonce,
170    /// The nonce count repeated with a *different* request — a replay.
171    Replay,
172    /// `qop=auth` was offered and the credentials did not use it, or vice versa.
173    QopMismatch,
174    /// The algorithm named is not one this server offered.
175    Algorithm,
176}
177
178/// A server that issues digest challenges and checks the answers.
179///
180/// Nonces are **self-describing**: each carries its issue time and a MAC over it, so this can
181/// recognise its own nonce and read its expiry without a table of every nonce ever issued. The only
182/// table is the replay window, which is bounded and holds nothing that has not been used.
183#[derive(Debug)]
184pub struct Authenticator {
185    realm: String,
186    secret: [u8; 32],
187    lifetime: Duration,
188    algorithm: Algorithm,
189    /// Nonce → the highest nonce-count seen and the response that came with it.
190    ///
191    /// The response is kept so a *retransmission* — the same request arriving twice, which is
192    /// ordinary over UDP — can be told from a replay. Same count and same response is the same
193    /// request; same count and a different response is somebody reusing a captured credential.
194    seen: std::collections::VecDeque<(String, u32, String)>,
195}
196
197impl Authenticator {
198    /// A server for a protection space.
199    ///
200    /// `secret` keys the nonce MAC. It must be **stable across restarts** if in-flight nonces are
201    /// to survive one, and **not shared** with another realm, or a nonce issued for one protection
202    /// space is accepted in the other.
203    #[must_use]
204    pub fn new(realm: impl Into<String>, secret: [u8; 32]) -> Self {
205        Self {
206            realm: realm.into(),
207            secret,
208            lifetime: DEFAULT_LIFETIME,
209            algorithm: Algorithm::Sha256,
210            seen: std::collections::VecDeque::new(),
211        }
212    }
213
214    /// A server with a freshly generated secret.
215    ///
216    /// Convenient, and it means every restart invalidates every outstanding nonce — which clients
217    /// recover from with `stale=true`, so it costs a round trip and not a login.
218    #[must_use]
219    pub fn with_random_secret(realm: impl Into<String>) -> Self {
220        use rand::Rng as _;
221        let mut secret = [0u8; 32];
222        rand::rng().fill(&mut secret);
223        Self::new(realm, secret)
224    }
225
226    /// Challenge with this algorithm.
227    ///
228    /// SHA-256 by default rather than MD5. RFC 8760 §2 exists because MD5 should not be the only
229    /// thing on offer, and a *server* choosing the default is the only place that choice can be
230    /// made — a client can only answer what it is asked.
231    #[must_use]
232    pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
233        self.algorithm = algorithm;
234        self
235    }
236
237    /// How long an issued nonce stays valid.
238    #[must_use]
239    pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
240        self.lifetime = lifetime;
241        self
242    }
243
244    /// The protection space.
245    #[must_use]
246    pub fn realm(&self) -> &str {
247        &self.realm
248    }
249
250    /// The header a challenge goes in: `WWW-Authenticate`, or `Proxy-Authenticate` for a proxy.
251    #[must_use]
252    pub fn challenge_header(proxy: bool) -> HeaderName {
253        if proxy {
254            HeaderName::ProxyAuthenticate
255        } else {
256            HeaderName::WwwAuthenticate
257        }
258    }
259
260    /// Mint a challenge value, as it goes in the header.
261    ///
262    /// `stale` says the previous credentials were right and only the nonce was old.
263    #[must_use]
264    pub fn challenge(&self, stale: bool) -> String {
265        self.challenge_at(stale, now())
266    }
267
268    /// [`Authenticator::challenge`] with the clock supplied, so a test can pin it.
269    #[must_use]
270    pub fn challenge_at(&self, stale: bool, now: u64) -> String {
271        let nonce = self.mint(now);
272        let mut header = format!(
273            r#"Digest realm="{}", nonce="{nonce}", qop="auth", algorithm={}"#,
274            self.realm.replace('\\', r"\\").replace('"', "\\\""),
275            self.algorithm.as_str()
276        );
277        if stale {
278            header.push_str(", stale=true");
279        }
280        header
281    }
282
283    /// A nonce that this server can later recognise as its own.
284    ///
285    /// `<issued-at in hex>.<HMAC-SHA-256 of it, under the secret, truncated>`. The MAC is what makes
286    /// the nonce unforgeable; the timestamp is what makes expiry checkable without a table.
287    fn mint(&self, now: u64) -> String {
288        let issued = format!("{now:016x}");
289        format!("{issued}.{}", self.mac(&issued))
290    }
291
292    /// The MAC over a nonce's issue time. Keyed properly rather than `H(secret || message)`, which
293    /// with a Merkle–Damgård hash is extensible by anyone who has seen one output.
294    fn mac(&self, issued: &str) -> String {
295        use hmac::{Hmac, Mac};
296        use sha2::Sha256;
297        let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&self.secret)
298            .unwrap_or_else(|_| unreachable!("HMAC accepts a key of any length"));
299        mac.update(issued.as_bytes());
300        mac.update(b":");
301        mac.update(self.realm.as_bytes());
302        let bytes = mac.finalize().into_bytes();
303        bytes.iter().fold(String::new(), |mut out, byte| {
304            use std::fmt::Write as _;
305            let _ = write!(out, "{byte:02x}");
306            out
307        })
308    }
309
310    /// When a nonce was issued, if it is one of ours.
311    ///
312    /// Constant-time on the MAC comparison. The nonce is public, so this is not about hiding it —
313    /// it is about not giving an attacker who can submit nonces a byte-at-a-time oracle for forging
314    /// one.
315    fn issued_at(&self, nonce: &str) -> Option<u64> {
316        use subtle::ConstantTimeEq as _;
317        let (issued, mac) = nonce.split_once('.')?;
318        let expected = self.mac(issued);
319        let matches: bool = expected.as_bytes().ct_eq(mac.as_bytes()).into();
320        matches.then(|| u64::from_str_radix(issued, 16).ok())?
321    }
322
323    /// Check the credentials a request presented.
324    ///
325    /// `password` is the one this server holds for `presented.username`; looking it up is the
326    /// caller's job, because a credential store is not this crate's business. `method` is the
327    /// request's, since the digest covers it.
328    pub fn verify(&mut self, presented: &Presented, method: &str, password: &str) -> Verdict {
329        self.verify_at(presented, method, password, now())
330    }
331
332    /// [`Authenticator::verify`] with the clock supplied.
333    pub fn verify_at(
334        &mut self,
335        presented: &Presented,
336        method: &str,
337        password: &str,
338        now: u64,
339    ) -> Verdict {
340        if presented.algorithm != self.algorithm {
341            return Verdict::Rejected(Reason::Algorithm);
342        }
343        // The challenge always offers `qop=auth`, so credentials without it are answering a
344        // question this server did not ask — and the RFC 2069 formula they would then use has no
345        // client nonce in it, which is the replay protection.
346        if !presented.qop_auth || presented.nonce_count.is_none() || presented.cnonce.is_none() {
347            return Verdict::Rejected(Reason::QopMismatch);
348        }
349        let Some(issued) = self.issued_at(&presented.nonce) else {
350            return Verdict::Rejected(Reason::ForeignNonce);
351        };
352
353        // The digest is checked *before* the clock. A wrong password on an expired nonce is a
354        // rejection, not a `stale` — answering `stale=true` there would tell an attacker that the
355        // only thing wrong with their guess was its timing.
356        let expected = respond(
357            &self.as_challenge(&presented.nonce),
358            &Credentials::new(presented.username.clone(), password.to_owned()),
359            method,
360            &presented.uri,
361            presented.nonce_count.unwrap_or(1),
362            presented.cnonce.as_deref().unwrap_or_default(),
363        );
364        let Some(computed) = parameter(&expected, "response") else {
365            return Verdict::Rejected(Reason::Mismatch);
366        };
367        if !constant_time_eq(computed.as_bytes(), presented.response.as_bytes()) {
368            return Verdict::Rejected(Reason::Mismatch);
369        }
370
371        if now.saturating_sub(issued) > self.lifetime.as_secs() {
372            return Verdict::Stale;
373        }
374
375        match self.record(presented) {
376            Ok(()) => Verdict::Authenticated,
377            Err(reason) => Verdict::Rejected(reason),
378        }
379    }
380
381    /// Advance the replay window, or refuse.
382    ///
383    /// RFC 7616 §3.4.3 makes `nc` count the requests sent with one nonce, so it must never go
384    /// backwards or repeat — *except* that a retransmission is the same request arriving twice,
385    /// which is ordinary over UDP and must still authenticate. The response digest tells them
386    /// apart: same count and the same digest is one request seen twice; same count and a different
387    /// digest is somebody reusing a captured credential against a different request.
388    fn record(&mut self, presented: &Presented) -> Result<(), Reason> {
389        let count = presented.nonce_count.unwrap_or(1);
390        if let Some(entry) = self
391            .seen
392            .iter_mut()
393            .find(|(nonce, _, _)| nonce == &presented.nonce)
394        {
395            if count > entry.1 {
396                entry.1 = count;
397                entry.2.clone_from(&presented.response);
398                return Ok(());
399            }
400            if count == entry.1 && entry.2 == presented.response {
401                return Ok(());
402            }
403            return Err(Reason::Replay);
404        }
405        // Bounded: the oldest nonce goes when the window is full. A client whose nonce is evicted
406        // is challenged again with `stale=true` and retries by itself, which is a round trip. An
407        // unbounded window is an outage.
408        if self.seen.len() >= REPLAY_CAPACITY {
409            self.seen.pop_front();
410        }
411        self.seen
412            .push_back((presented.nonce.clone(), count, presented.response.clone()));
413        Ok(())
414    }
415
416    /// This server's parameters as a client-side [`crate::auth::Challenge`], so the response is
417    /// computed by the *same code* the client uses.
418    ///
419    /// Writing the formula a second time here is how the two sides drift, and a server whose
420    /// verification disagrees with its own client rejects correct credentials.
421    fn as_challenge(&self, nonce: &str) -> crate::auth::Challenge {
422        crate::auth::Challenge {
423            realm: self.realm.clone(),
424            nonce: nonce.to_owned(),
425            opaque: None,
426            algorithm: self.algorithm,
427            qop_auth: true,
428            stale: false,
429            from_proxy: false,
430        }
431    }
432}
433
434fn constant_time_eq(one: &[u8], other: &[u8]) -> bool {
435    use subtle::ConstantTimeEq as _;
436    one.len() == other.len() && one.ct_eq(other).into()
437}
438
439/// Seconds since the epoch.
440fn now() -> u64 {
441    SystemTime::now()
442        .duration_since(UNIX_EPOCH)
443        .map_or(0, |since| since.as_secs())
444}
445
446#[cfg(test)]
447#[allow(
448    clippy::unwrap_used,
449    clippy::expect_used,
450    clippy::panic,
451    clippy::indexing_slicing
452)]
453mod tests {
454    use super::*;
455
456    const NOW: u64 = 1_700_000_000;
457    const PASSWORD: &str = "Circle Of Life";
458
459    fn server() -> Authenticator {
460        Authenticator::new("sipx.test", [7u8; 32])
461    }
462
463    /// Answer a challenge the way a real client would — through the client-side code.
464    fn answer(
465        server: &Authenticator,
466        challenge: &str,
467        method: &str,
468        nc: u32,
469        cnonce: &str,
470    ) -> Presented {
471        let parsed = crate::auth::Challenge::parse(challenge.as_bytes(), false).expect("parses");
472        let header = respond(
473            &parsed,
474            &Credentials::new("alice", PASSWORD),
475            method,
476            "sip:sipx.test",
477            nc,
478            cnonce,
479        );
480        let _ = server;
481        Presented::parse(header.as_bytes()).expect("parses")
482    }
483
484    #[test]
485    fn a_challenge_carries_a_realm_a_nonce_and_qop() {
486        let value = server().challenge_at(false, NOW);
487        assert!(value.starts_with("Digest "), "{value}");
488        assert!(value.contains(r#"realm="sipx.test""#), "{value}");
489        assert!(value.contains(r#"qop="auth""#), "{value}");
490        assert!(value.contains("algorithm=SHA-256"), "{value}");
491        assert!(!value.contains("stale"), "a fresh challenge is not stale");
492        assert!(
493            server().challenge_at(true, NOW).contains("stale=true"),
494            "a stale challenge says so"
495        );
496    }
497
498    #[test]
499    fn a_server_recognises_its_own_nonce_and_not_anyone_elses() {
500        let server = server();
501        let nonce = server.mint(NOW);
502        assert_eq!(server.issued_at(&nonce), Some(NOW));
503
504        // A different secret is a different server.
505        let other = Authenticator::new("sipx.test", [9u8; 32]);
506        assert_eq!(other.issued_at(&nonce), None, "a foreign nonce");
507
508        // The same secret in a different realm is also a different server: otherwise a nonce
509        // issued for one protection space authenticates in the other.
510        let other_realm = Authenticator::new("elsewhere.test", [7u8; 32]);
511        assert_eq!(other_realm.issued_at(&nonce), None);
512
513        // And a tampered one is nobody's.
514        let mut tampered = nonce.clone();
515        tampered.replace_range(0..1, "f");
516        assert_eq!(server.issued_at(&tampered), None);
517        assert_eq!(server.issued_at("not-a-nonce"), None);
518        assert_eq!(server.issued_at(""), None);
519    }
520
521    #[test]
522    fn correct_credentials_authenticate() {
523        let mut server = server();
524        let challenge = server.challenge_at(false, NOW);
525        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
526        assert_eq!(
527            server.verify_at(&presented, "REGISTER", PASSWORD, NOW),
528            Verdict::Authenticated
529        );
530    }
531
532    #[test]
533    fn a_wrong_password_is_rejected() {
534        let mut server = server();
535        let challenge = server.challenge_at(false, NOW);
536        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
537        assert_eq!(
538            server.verify_at(&presented, "REGISTER", "the wrong one", NOW),
539            Verdict::Rejected(Reason::Mismatch)
540        );
541    }
542
543    /// The digest covers the method, so credentials computed for one request do not authenticate
544    /// another. Without this an intercepted REGISTER's credentials would authorise an INVITE.
545    #[test]
546    fn credentials_for_one_method_do_not_authenticate_another() {
547        let mut server = server();
548        let challenge = server.challenge_at(false, NOW);
549        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
550        assert_eq!(
551            server.verify_at(&presented, "INVITE", PASSWORD, NOW),
552            Verdict::Rejected(Reason::Mismatch)
553        );
554    }
555
556    /// The story's failing-first test.
557    ///
558    /// RFC 7616 §3.4.3 counts requests per nonce, so a repeated count is a replay — except that a
559    /// retransmission *is* the same request arriving twice, which is ordinary over UDP. Rejecting
560    /// both is a stack that fails authentication whenever a packet is duplicated.
561    #[test]
562    fn a_replayed_nonce_count_is_rejected_but_a_retransmission_is_not() {
563        let mut server = server();
564        let challenge = server.challenge_at(false, NOW);
565        let first = answer(&server, &challenge, "REGISTER", 1, "abc");
566
567        assert_eq!(
568            server.verify_at(&first, "REGISTER", PASSWORD, NOW),
569            Verdict::Authenticated
570        );
571
572        // The identical request again: a retransmission. Same nonce, same count, same digest.
573        assert_eq!(
574            server.verify_at(&first, "REGISTER", PASSWORD, NOW),
575            Verdict::Authenticated,
576            "a retransmission must still authenticate, or a duplicated packet fails a login"
577        );
578
579        // A *different* request reusing that count: a replay.
580        let replayed = answer(&server, &challenge, "INVITE", 1, "abc");
581        assert_eq!(
582            server.verify_at(&replayed, "INVITE", PASSWORD, NOW),
583            Verdict::Rejected(Reason::Replay),
584            "the same count with a different request is somebody reusing a captured credential"
585        );
586
587        // And the count advancing is ordinary.
588        let next = answer(&server, &challenge, "REGISTER", 2, "abc");
589        assert_eq!(
590            server.verify_at(&next, "REGISTER", PASSWORD, NOW),
591            Verdict::Authenticated
592        );
593
594        // Going backwards is not.
595        let backwards = answer(&server, &challenge, "REGISTER", 1, "abc");
596        assert_eq!(
597            server.verify_at(&backwards, "REGISTER", PASSWORD, NOW),
598            Verdict::Rejected(Reason::Replay)
599        );
600    }
601
602    /// §3.3: `stale=true` says the password was right and only the nonce was old, so a client
603    /// re-sends instead of prompting a human.
604    #[test]
605    fn an_expired_nonce_with_correct_credentials_is_stale_rather_than_rejected() {
606        let mut server = server().with_lifetime(Duration::from_secs(60));
607        let challenge = server.challenge_at(false, NOW);
608        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
609        assert_eq!(
610            server.verify_at(&presented, "REGISTER", PASSWORD, NOW + 61),
611            Verdict::Stale
612        );
613    }
614
615    /// But a *wrong* password on an expired nonce is a rejection. Answering `stale` would tell an
616    /// attacker that the only thing wrong with their guess was its timing.
617    #[test]
618    fn an_expired_nonce_with_wrong_credentials_is_a_rejection_not_a_stale() {
619        let mut server = server().with_lifetime(Duration::from_secs(60));
620        let challenge = server.challenge_at(false, NOW);
621        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
622        assert_eq!(
623            server.verify_at(&presented, "REGISTER", "wrong", NOW + 61),
624            Verdict::Rejected(Reason::Mismatch)
625        );
626    }
627
628    #[test]
629    fn a_nonce_this_server_did_not_issue_is_refused() {
630        let mut server = server();
631        let elsewhere = Authenticator::new("sipx.test", [1u8; 32]);
632        let challenge = elsewhere.challenge_at(false, NOW);
633        let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
634        assert_eq!(
635            server.verify_at(&presented, "REGISTER", PASSWORD, NOW),
636            Verdict::Rejected(Reason::ForeignNonce)
637        );
638    }
639
640    #[test]
641    fn credentials_without_qop_are_refused_because_the_challenge_required_it() {
642        let mut server = server();
643        let nonce = server.mint(NOW);
644        let presented = Presented {
645            username: "alice".to_owned(),
646            realm: "sipx.test".to_owned(),
647            nonce,
648            uri: "sip:sipx.test".to_owned(),
649            response: "0".repeat(64),
650            algorithm: Algorithm::Sha256,
651            nonce_count: None,
652            cnonce: None,
653            qop_auth: false,
654        };
655        assert_eq!(
656            server.verify_at(&presented, "REGISTER", PASSWORD, NOW),
657            Verdict::Rejected(Reason::QopMismatch),
658            "the RFC 2069 formula has no client nonce in it, which is the replay protection"
659        );
660    }
661
662    #[test]
663    fn an_algorithm_this_server_did_not_offer_is_refused() {
664        let mut server = server(); // SHA-256
665        let nonce = server.mint(NOW);
666        let presented = Presented {
667            username: "alice".to_owned(),
668            realm: "sipx.test".to_owned(),
669            nonce,
670            uri: "sip:sipx.test".to_owned(),
671            response: "0".repeat(32),
672            algorithm: Algorithm::Md5,
673            nonce_count: Some(1),
674            cnonce: Some("abc".to_owned()),
675            qop_auth: true,
676        };
677        assert_eq!(
678            server.verify_at(&presented, "REGISTER", PASSWORD, NOW),
679            Verdict::Rejected(Reason::Algorithm)
680        );
681    }
682
683    #[test]
684    fn the_replay_window_does_not_grow_without_bound() {
685        let mut server = server();
686        for index in 0..(REPLAY_CAPACITY + 100) {
687            // A distinct nonce each time, as a fleet of clients would produce.
688            let challenge = server.challenge_at(false, NOW + index as u64);
689            let presented = answer(&server, &challenge, "REGISTER", 1, "abc");
690            let _ = server.verify_at(&presented, "REGISTER", PASSWORD, NOW + index as u64);
691        }
692        assert!(
693            server.seen.len() <= REPLAY_CAPACITY,
694            "the window held {} entries",
695            server.seen.len()
696        );
697    }
698
699    /// The parser has to survive a quoted value containing a comma or a quote — otherwise every
700    /// parameter after it shifts, and the response is read from the wrong place.
701    #[test]
702    fn a_quoted_value_containing_a_comma_or_a_quote_does_not_shift_the_rest() {
703        let value = br#"Digest username="al,ice", realm="a\"b", nonce="n", uri="sip:x", response="deadbeef", qop=auth, nc=00000002, cnonce="c""#;
704        let presented = Presented::parse(value).expect("parses");
705        assert_eq!(presented.username, "al,ice");
706        assert_eq!(presented.realm, "a\"b");
707        assert_eq!(presented.response, "deadbeef");
708        assert_eq!(presented.nonce_count, Some(2));
709        assert_eq!(presented.cnonce.as_deref(), Some("c"));
710        assert!(presented.qop_auth);
711    }
712
713    #[test]
714    fn credentials_without_a_username_or_a_response_are_not_credentials() {
715        assert!(Presented::parse(b"Digest realm=\"a\", nonce=\"n\"").is_none());
716        assert!(Presented::parse(b"Basic dXNlcjpwYXNz").is_none());
717        assert!(Presented::parse(b"").is_none());
718    }
719}