Skip to main content

dig_stun/credential/
response.rs

1//! The error-response shape, both directions (`SPEC.md` §14.3.3, §14.9): how a server encodes the
2//! four DIG error shapes (bare refusal, challenge, stale, malformed), and how a client parses one
3//! back into a [`Challenge`] it can act on.
4
5use crate::codec::{StunError, TransactionId, MAGIC_COOKIE};
6use crate::credential::nonce::NONCE_LEN;
7use crate::credential::wire::{
8    base64url_encode, write_attr, write_header, ATTR_ERROR_CODE, ATTR_NONCE, ATTR_REALM,
9    BINDING_ERROR, ERR_BAD_REQUEST, ERR_STALE_NONCE, ERR_UNAUTHENTICATED, REALM,
10};
11
12/// A parsed Binding Error Response (`SPEC.md` §14.9). `realm`/`nonce` are `None` when the
13/// response simply did not carry that attribute — [`parse_challenge`] never errors for a missing
14/// optional attribute; only a message-level failure (wrong type, wrong txid, truncated) is an
15/// `Err`.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Challenge {
18    /// The `ERROR-CODE` class+number (400/401/438, or 0 if the response carried none at all).
19    pub code: u16,
20    /// The `REALM` attribute's UTF-8 value, if present and valid UTF-8.
21    pub realm: Option<String>,
22    /// The `NONCE` attribute value EXACTLY as carried (base64url text), if present.
23    pub nonce: Option<Vec<u8>>,
24}
25
26/// The reason phrase RFC 5389 §15.6 pairs with each code this crate emits (`SPEC.md` §14.3.3).
27fn reason_phrase(code: u16) -> &'static str {
28    match code {
29        ERR_UNAUTHENTICATED => "Unauthenticated",
30        ERR_STALE_NONCE => "Stale Nonce",
31        ERR_BAD_REQUEST => "Bad Request",
32        other => panic!(
33            "encode_challenge: unsupported error code {other} (caller error, not wire input)"
34        ),
35    }
36}
37
38/// Encode one of the four DIG error-response shapes (`SPEC.md` §14.3.3): pass `nonce = None` for a
39/// bare refusal (`code = 401`, 44 bytes) or a malformed refusal (`code = 400`, 40 bytes); pass
40/// `nonce = Some(..)` for a challenge (`code = 401`, 88 bytes) or a stale-nonce response
41/// (`code = 438`, 84 bytes) — both carry [`REALM`] and a fresh `NONCE`.
42///
43/// A response built with `nonce = Some(..)` never carries `XOR-MAPPED-ADDRESS`: handing the answer
44/// to a requester that has not yet proven anything is exactly what the credential exists to
45/// withhold (`SPEC.md` §14.3.3).
46pub fn encode_challenge(
47    txid: &TransactionId,
48    code: u16,
49    nonce: Option<&[u8; NONCE_LEN]>,
50) -> Vec<u8> {
51    let reason = reason_phrase(code);
52    let mut error_code_value = Vec::with_capacity(4 + reason.len());
53    error_code_value.extend_from_slice(&[0, 0, (code / 100) as u8, (code % 100) as u8]);
54    error_code_value.extend_from_slice(reason.as_bytes());
55
56    let mut attrs = Vec::new();
57    write_attr(&mut attrs, ATTR_ERROR_CODE, &error_code_value);
58    if let Some(raw_nonce) = nonce {
59        write_attr(&mut attrs, ATTR_REALM, REALM.as_bytes());
60        let encoded = base64url_encode(raw_nonce);
61        write_attr(&mut attrs, ATTR_NONCE, encoded.as_bytes());
62    }
63
64    let mut msg = Vec::with_capacity(20 + attrs.len());
65    write_header(&mut msg, BINDING_ERROR, attrs.len() as u16, txid);
66    msg.extend_from_slice(&attrs);
67    msg
68}
69
70/// Parse a Binding Error Response (`SPEC.md` §14.9) — the ONLY function in this crate that
71/// interprets message type `0x0111` ([`crate::parse_binding_response`] explicitly does not,
72/// `SPEC.md` §2.4).
73///
74/// Validates, in order: length, magic cookie, message type `== BINDING_ERROR`, and (when
75/// `expected_txid` matches the RFC 5389 contract every other parser here follows) the transaction
76/// id. A message that passes those checks always returns `Ok` — a missing or unparsable `REALM`/
77/// `NONCE` simply leaves that field `None` in the returned [`Challenge`]; the caller
78/// ([`crate::credential::query_reflexive_address_signed`]) is what turns an incomplete challenge
79/// into a refusal.
80pub fn parse_challenge(msg: &[u8], expected_txid: &TransactionId) -> Result<Challenge, StunError> {
81    if msg.len() < 20 {
82        return Err(StunError::Truncated);
83    }
84    let msg_type = u16::from_be_bytes([msg[0], msg[1]]);
85    let msg_len = u16::from_be_bytes([msg[2], msg[3]]) as usize;
86    let cookie = u32::from_be_bytes([msg[4], msg[5], msg[6], msg[7]]);
87    if cookie != MAGIC_COOKIE {
88        return Err(StunError::BadMagicCookie);
89    }
90    if msg_type != BINDING_ERROR {
91        return Err(StunError::UnexpectedType(msg_type));
92    }
93    let txid: TransactionId = msg[8..20].try_into().map_err(|_| StunError::Truncated)?;
94    if &txid != expected_txid {
95        return Err(StunError::TransactionIdMismatch);
96    }
97    if msg.len() < 20 + msg_len {
98        return Err(StunError::Truncated);
99    }
100
101    let area = &msg[20..20 + msg_len];
102    let mut code: u16 = 0;
103    let mut realm: Option<String> = None;
104    let mut nonce: Option<Vec<u8>> = None;
105
106    let mut off = 0usize;
107    while off + 4 <= area.len() {
108        let attr_type = u16::from_be_bytes([area[off], area[off + 1]]);
109        let attr_len = u16::from_be_bytes([area[off + 2], area[off + 3]]) as usize;
110        let val_start = off + 4;
111        let val_end = val_start + attr_len;
112        if val_end > area.len() {
113            break; // a truncated trailing attribute must not discard an ERROR-CODE already read
114        }
115        let value = &area[val_start..val_end];
116        match attr_type {
117            ATTR_ERROR_CODE if value.len() >= 4 => {
118                code = 100 * value[2] as u16 + value[3] as u16;
119            }
120            ATTR_REALM => {
121                if let Ok(s) = std::str::from_utf8(value) {
122                    realm = Some(s.to_string());
123                }
124            }
125            ATTR_NONCE => {
126                nonce = Some(value.to_vec());
127            }
128            _ => {}
129        }
130        off = val_end + ((4 - (attr_len % 4)) % 4);
131    }
132
133    Ok(Challenge { code, realm, nonce })
134}