dig_stun/credential/decide.rs
1//! The server decision table (`SPEC.md` §14.7) — a pure function from what a request classified
2//! as, plus its already-computed nonce/signature results, to what the server does. `decide` never
3//! parses a datagram and never verifies a signature itself; it only combines results its caller
4//! already has, which is what makes the "verification is reached only for Signed+Fresh" property
5//! (`SPEC.md` §14.5 step 4) a fact about the CALLER'S control flow rather than something this
6//! function could violate.
7
8use crate::credential::nonce::NonceCheck;
9use crate::credential::request::RequestKind;
10use crate::credential::signature::VerifiedIdentity;
11use crate::credential::wire::{CredentialError, ERR_STALE_NONCE, ERR_UNAUTHENTICATED};
12
13/// A server deployment's credential requirement (`SPEC.md` §14.8) — never wire-negotiated, set
14/// once per deployment. The two modes differ in exactly one decision: what a [`RequestKind::Bare`]
15/// request gets (`SPEC.md` §14.7 rows 1-2).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum CredentialMode {
18 /// Bare requests are answered exactly as before `0.2.0`; identity and signed requests are
19 /// additionally handled. The migration starting point (`SPEC.md` §14.8 step 1).
20 Advisory,
21 /// Bare requests are refused (`401`, no nonce). The migration endpoint (`SPEC.md` §14.8 step
22 /// 2), flipped only once an operator has measured how little traffic it would cost.
23 Required,
24}
25
26/// What the server does with one classified request (`SPEC.md` §14.7). Exhaustive: a fourth
27/// outcome is a decision-table change tracked as a breaking change (`SPEC.md` §12).
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ServerDecision {
30 /// Send the ordinary §2.7 success response — byte-identical whether or not a credential was
31 /// presented. `identity` is `Some` only when a signature verified (row 7); the response itself
32 /// carries no acknowledgement of it.
33 Answer {
34 /// The verified requester, when the request carried and passed a signature.
35 identity: Option<VerifiedIdentity>,
36 },
37 /// Send [`crate::credential::encode_challenge`] with a freshly issued nonce — the requester
38 /// has more work to do to be answered.
39 Challenge {
40 /// `401` (needs to sign, or signed wrong) or `438` (nonce aged out).
41 code: u16,
42 },
43 /// Send [`crate::credential::encode_challenge`] with `nonce: None` — the requester will not be
44 /// answered on this datagram at all.
45 Refuse {
46 /// `401` (bare request under [`CredentialMode::Required`]) or `400` (malformed, decided by
47 /// the caller directly from a [`CredentialError::Malformed`] — `decide` is never called
48 /// for that case, since there is no [`RequestKind`] to pass it).
49 code: u16,
50 },
51}
52
53/// The decision table (`SPEC.md` §14.7, rows 1-7 — row 8, "any `Malformed`", has no
54/// `RequestKind` to classify and so is handled by the caller directly from
55/// [`crate::credential::classify_request`]'s `Err` rather than through this function).
56///
57/// `nonce` and `verified` are `None` whenever they do not apply — a [`RequestKind::Bare`] or
58/// [`RequestKind::Identity`] request never has a nonce to check or a signature to verify, and this
59/// function never asks the caller to have computed either for those. Every non-`Signed`+`Fresh`
60/// row is decided WITHOUT `verified` needing to be `Some` at all — reflecting, at the type level,
61/// that a correct caller invokes [`crate::credential::verify_signed_request`] only when it is about
62/// to reach this branch.
63pub fn decide(
64 mode: CredentialMode,
65 kind: &RequestKind<'_>,
66 nonce: Option<NonceCheck>,
67 verified: Option<Result<VerifiedIdentity, CredentialError>>,
68) -> ServerDecision {
69 match kind {
70 RequestKind::Bare => match mode {
71 CredentialMode::Advisory => ServerDecision::Answer { identity: None }, // row 1
72 CredentialMode::Required => ServerDecision::Refuse {
73 code: ERR_UNAUTHENTICATED, // row 2
74 },
75 },
76 RequestKind::Identity { .. } => ServerDecision::Challenge {
77 code: ERR_UNAUTHENTICATED, // row 3: challenged in BOTH modes
78 },
79 RequestKind::Signed { .. } => match nonce {
80 Some(NonceCheck::Fresh) => match verified {
81 Some(Ok(identity)) => ServerDecision::Answer {
82 identity: Some(identity), // row 7
83 },
84 _ => ServerDecision::Challenge {
85 code: ERR_UNAUTHENTICATED, // row 6 (Err(BadSignature)); defensive default if
86 // `verified` was left `None` for a Fresh nonce
87 },
88 },
89 Some(NonceCheck::Stale) => ServerDecision::Challenge {
90 code: ERR_STALE_NONCE, // row 5
91 },
92 Some(NonceCheck::Invalid) | None => ServerDecision::Challenge {
93 code: ERR_UNAUTHENTICATED, // row 4
94 },
95 },
96 }
97}