Skip to main content

dig_stun/credential/
request.rs

1//! The request shape, both directions (`SPEC.md` §14.3, §14.5, §14.9): how a server classifies an
2//! incoming Binding request's DIG attributes, and how a client encodes its own identity / signed
3//! asks.
4
5use crate::codec::{parse_binding_request, TransactionId, BINDING_REQUEST};
6use crate::credential::signature::StunSigner;
7use crate::credential::wire::{
8    is_valid_signature_der_len, is_valid_spki_der, write_attr, write_header, CredentialError,
9    ATTR_DIG_IDENTITY, ATTR_DIG_SIGNATURE, ATTR_NONCE, CREDENTIAL_VERSION,
10};
11
12/// A classified incoming Binding request (`SPEC.md` §14.5). Exhaustive: a fourth shape would be a
13/// wire-format change requiring a version bump (`SPEC.md` §12).
14///
15/// Every variant borrows directly from the datagram [`classify_request`] was given — nothing here
16/// allocates.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RequestKind<'a> {
19    /// No DIG attribute at all — an ordinary, uncredentialed Binding request.
20    Bare,
21    /// A `DIG-IDENTITY` alone, with no `NONCE` and no `DIG-SIGNATURE` — the first datagram of the
22    /// client's challenge/response exchange (`SPEC.md` §14.9 step 2).
23    Identity {
24        /// The 91-byte SPKI DER (no version-byte prefix), already shape-validated.
25        spki: &'a [u8],
26    },
27    /// `DIG-IDENTITY` + `NONCE` + `DIG-SIGNATURE`, in that order, with the signature last.
28    Signed {
29        /// The 91-byte SPKI DER (no version-byte prefix), already shape-validated.
30        spki: &'a [u8],
31        /// The `NONCE` attribute value EXACTLY as carried (base64url text) — the same bytes
32        /// [`crate::credential::signing_message`] expects.
33        nonce: &'a [u8],
34        /// The DER-encoded ECDSA signature (no version-byte prefix), already length-validated.
35        signature: &'a [u8],
36    },
37}
38
39/// Classify an incoming Binding request's DIG attributes (`SPEC.md` §14.5).
40///
41/// Performs `SPEC.md` §2.5's ordinary checks FIRST ([`parse_binding_request`]) — any failure there
42/// is returned wrapped as [`CredentialError::Stun`] before a single DIG attribute is inspected.
43/// Then walks the attribute area once: an unknown attribute type is silently ignored (this
44/// server's RFC 5389 stateless-ignore latitude, unchanged); a `DIG-IDENTITY`/`DIG-SIGNATURE`
45/// violating its wire shape, a duplicated DIG attribute, any attribute after `DIG-SIGNATURE`, or a
46/// combination other than the three named in [`RequestKind`] is [`CredentialError::Malformed`].
47///
48/// Allocates nothing; verifies nothing (that is [`crate::credential::verify_signed_request`],
49/// called by the caller only for [`RequestKind::Signed`] with a fresh nonce).
50pub fn classify_request(
51    datagram: &[u8],
52) -> Result<(TransactionId, RequestKind<'_>), CredentialError> {
53    let txid = parse_binding_request(datagram)?;
54    // parse_binding_request already proved datagram.len() >= 20 + msg_len, so this slice is safe.
55    let msg_len = u16::from_be_bytes([datagram[2], datagram[3]]) as usize;
56    let area = &datagram[20..20 + msg_len];
57    let kind = classify_attrs(area)?;
58    Ok((txid, kind))
59}
60
61fn classify_attrs(area: &[u8]) -> Result<RequestKind<'_>, CredentialError> {
62    let mut identity: Option<&[u8]> = None;
63    let mut nonce: Option<&[u8]> = None;
64    let mut signature: Option<&[u8]> = None;
65    let mut signature_seen = false;
66
67    let mut off = 0usize;
68    while off + 4 <= area.len() {
69        // DIG-SIGNATURE MUST be the last attribute (`SPEC.md` §14.3.2) — once we have recorded
70        // one, ANY further attribute of ANY type (including a repeated DIG-SIGNATURE) is a
71        // violation, checked before we even look at this attribute's type.
72        if signature_seen {
73            return Err(CredentialError::Malformed);
74        }
75
76        let attr_type = u16::from_be_bytes([area[off], area[off + 1]]);
77        let attr_len = u16::from_be_bytes([area[off + 2], area[off + 3]]) as usize;
78        let val_start = off + 4;
79        let val_end = val_start + attr_len;
80        if val_end > area.len() {
81            return Err(CredentialError::Malformed);
82        }
83        let value = &area[val_start..val_end];
84
85        match attr_type {
86            ATTR_DIG_IDENTITY => {
87                if identity.is_some() {
88                    return Err(CredentialError::Malformed);
89                }
90                if value.len() != 1 + crate::credential::wire::P256_SPKI_LEN
91                    || value[0] != CREDENTIAL_VERSION
92                    || !is_valid_spki_der(&value[1..])
93                {
94                    return Err(CredentialError::Malformed);
95                }
96                identity = Some(&value[1..]);
97            }
98            ATTR_NONCE => {
99                if nonce.is_some() {
100                    return Err(CredentialError::Malformed);
101                }
102                nonce = Some(value);
103            }
104            ATTR_DIG_SIGNATURE => {
105                if value.is_empty()
106                    || value[0] != CREDENTIAL_VERSION
107                    || !is_valid_signature_der_len(&value[1..])
108                {
109                    return Err(CredentialError::Malformed);
110                }
111                signature = Some(&value[1..]);
112                signature_seen = true;
113            }
114            _ => {} // unknown attribute: ignored, per this server's stateless-ignore latitude
115        }
116
117        off = val_end + ((4 - (attr_len % 4)) % 4);
118    }
119
120    match (identity, nonce, signature) {
121        (None, None, None) => Ok(RequestKind::Bare),
122        (Some(spki), None, None) => Ok(RequestKind::Identity { spki }),
123        (Some(spki), Some(nonce), Some(signature)) => Ok(RequestKind::Signed {
124            spki,
125            nonce,
126            signature,
127        }),
128        _ => Err(CredentialError::Malformed), // any other combination (SPEC.md §14.5)
129    }
130}
131
132/// Encode a `DIG-IDENTITY`-only Binding request — 116 bytes: the 20-byte header plus one 96-byte
133/// attribute (`SPEC.md` §14.9 step 2). `spki_der` MUST be exactly the 91 bytes of `SPEC.md`
134/// §14.3.1; callers that hold a [`StunSigner`] get this from `signer.spki_der()`.
135pub fn encode_identity_request(txid: &TransactionId, spki_der: &[u8]) -> Vec<u8> {
136    debug_assert!(
137        is_valid_spki_der(spki_der),
138        "encode_identity_request requires a valid 91-byte P-256 SPKI DER"
139    );
140
141    let mut identity_value = Vec::with_capacity(1 + spki_der.len());
142    identity_value.push(CREDENTIAL_VERSION);
143    identity_value.extend_from_slice(spki_der);
144
145    let mut attrs = Vec::new();
146    write_attr(&mut attrs, ATTR_DIG_IDENTITY, &identity_value);
147
148    let mut msg = Vec::with_capacity(20 + attrs.len());
149    write_header(&mut msg, BINDING_REQUEST, attrs.len() as u16, txid);
150    msg.extend_from_slice(&attrs);
151    msg
152}
153
154/// Encode a fully signed Binding request — `DIG-IDENTITY` + `NONCE` + `DIG-SIGNATURE`, in that
155/// order, at most 228 bytes (`SPEC.md` §14.9 step 4). `nonce_attr_value` is echoed back EXACTLY as
156/// received in a challenge; `signer` supplies both the SPKI and the signature over
157/// [`crate::credential::signing_message`].
158pub fn encode_signed_request(
159    txid: &TransactionId,
160    nonce_attr_value: &[u8],
161    signer: &dyn StunSigner,
162) -> Vec<u8> {
163    let spki = signer.spki_der();
164    let message = crate::credential::signature::signing_message(txid, nonce_attr_value, spki);
165    let sig_der = signer.sign(&message);
166
167    let mut identity_value = Vec::with_capacity(1 + spki.len());
168    identity_value.push(CREDENTIAL_VERSION);
169    identity_value.extend_from_slice(spki);
170
171    let mut signature_value = Vec::with_capacity(1 + sig_der.len());
172    signature_value.push(CREDENTIAL_VERSION);
173    signature_value.extend_from_slice(&sig_der);
174
175    let mut attrs = Vec::new();
176    write_attr(&mut attrs, ATTR_DIG_IDENTITY, &identity_value);
177    write_attr(&mut attrs, ATTR_NONCE, nonce_attr_value);
178    write_attr(&mut attrs, ATTR_DIG_SIGNATURE, &signature_value);
179
180    let mut msg = Vec::with_capacity(20 + attrs.len());
181    write_header(&mut msg, BINDING_REQUEST, attrs.len() as u16, txid);
182    msg.extend_from_slice(&attrs);
183    msg
184}