coapcore/
scope.rs

1//! Expressions for access policy as evaluated for a particular security context.
2//!
3//! This module provides the [`Scope`] trait, and generic implementations thereof.
4
5use coap_message::{MessageOption, ReadableMessage};
6
7/// A data item representing the server access policy as evaluated for a particular security context.
8pub trait Scope: Sized + core::fmt::Debug {
9    /// Returns true if a request may be performed by the bound security context.
10    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool;
11}
12
13impl Scope for core::convert::Infallible {
14    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
15        match *self {}
16    }
17}
18
19/// Error type indicating that a scope could not be created from the given token scope.
20///
21/// As tokens are only accepted from trusted sources, the presence of this error typically
22/// indicates a misconfigured trust anchor.
23#[derive(Debug, Copy, Clone)]
24pub struct InvalidScope;
25
26/// A scope expression that allows all requests.
27#[cfg_attr(feature = "defmt", derive(defmt::Format))]
28#[derive(Debug)]
29pub struct AllowAll;
30
31impl Scope for AllowAll {
32    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
33        true
34    }
35}
36
37/// A scope expression that denies all requests.
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39#[derive(Debug)]
40pub struct DenyAll;
41
42impl Scope for DenyAll {
43    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
44        false
45    }
46}
47
48const AIF_SCOPE_MAX_LEN: usize = 64;
49
50/// A representation of an RFC9237 using the REST-specific model.
51///
52/// It is arbitrarily limited in length; future versions may give more flexibility, eg. by
53/// referring to data in storage.
54///
55/// This type is constrained to valid CBOR representations of the REST-specific model; it may panic
56/// if that constraint is not upheld.
57///
58/// ## Caveats
59///
60/// Using this is not very efficient; worst case, it iterates over all options for all AIF entries.
61/// This could be mitigated by sorting the records at construction time.
62///
63/// This completely disregards proper URI splitting; this works for very simple URI references in
64/// the AIF. This could be mitigated by switching to a CRI based model.
65#[cfg_attr(feature = "defmt", derive(defmt::Format))]
66#[derive(Debug, Clone)]
67pub struct AifValue([u8; AIF_SCOPE_MAX_LEN]);
68
69impl AifValue {
70    /// Ingests an AIF scope, verifying that it satisfies the constraints of this type.
71    ///
72    /// # Errors
73    ///
74    /// This produces errors if the input (which is typically received from the network) is
75    /// malformed or contains unsupported items.
76    pub fn parse(bytes: &[u8]) -> Result<Self, InvalidScope> {
77        let mut buffer = [0; AIF_SCOPE_MAX_LEN];
78
79        buffer
80            .get_mut(..bytes.len())
81            .ok_or(InvalidScope)?
82            .copy_from_slice(bytes);
83
84        let mut decoder = minicbor::Decoder::new(bytes);
85        for item in decoder
86            .array_iter::<(&str, u32)>()
87            .map_err(|_| InvalidScope)?
88        {
89            let (path, _mask) = item.map_err(|_| InvalidScope)?;
90            if !path.starts_with('/') {
91                return Err(InvalidScope);
92            }
93        }
94
95        Ok(Self(buffer))
96    }
97}
98
99impl Scope for AifValue {
100    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool {
101        let code: u8 = request.code().into();
102        let (codebit, false) = 1u32.overflowing_shl(
103            u32::from(code)
104                .checked_sub(1)
105                .expect("Request codes are != 0"),
106        ) else {
107            return false;
108        };
109        let mut decoder = minicbor::Decoder::new(&self.0);
110        'outer: for item in decoder.array_iter::<(&str, u32)>().unwrap() {
111            let (path, perms) = item.unwrap();
112            if perms & codebit == 0 {
113                continue;
114            }
115            // BIG FIXME: We're iterating over options without checking for critical options. If the
116            // resource handler router consumes any different set of options, that disagreement might
117            // give us a security issue.
118            let mut pathopts = request
119                .options()
120                .filter(|o| o.number() == coap_numbers::option::URI_PATH)
121                .peekable();
122            if path == "/" && pathopts.peek().is_none() {
123                // Special case: For consistency should be a single empty option.
124                return true;
125            }
126            assert!(path.starts_with('/'), "Invalid AIF");
127            let mut remainder = &path[1..];
128            while !remainder.is_empty() {
129                let (next_part, next_remainder) = match remainder.split_once('/') {
130                    Some((next_part, next_remainder)) => (next_part, next_remainder),
131                    None => (remainder, ""),
132                };
133                let Some(this_opt) = pathopts.next() else {
134                    // Request path is shorter than this AIF record
135                    continue 'outer;
136                };
137                if this_opt.value() != next_part.as_bytes() {
138                    // Request path is just different from this AIF record
139                    continue 'outer;
140                }
141                remainder = next_remainder;
142            }
143            if pathopts.next().is_none() {
144                // Request path is longer than this AIF record
145                return true;
146            }
147        }
148        // No matches found
149        false
150    }
151}
152
153/// A scope that can use multiple backends, erasing its type.
154///
155/// (Think "`dyn Scope`" but without requiring dyn compatibility).
156///
157/// This is useful when combining multiple authentication methods, eg. allowing ACE tokens (that
158/// need an [`AifValue`] to express their arbitrary scopes) as well as a configured admin key (that
159/// has "all" permission, which are not expressible in an [`AifValue`].
160#[cfg_attr(feature = "defmt", derive(defmt::Format))]
161#[derive(Debug, Clone)]
162pub enum UnionScope {
163    /// Contains an [`AifValue`].
164    AifValue(AifValue),
165    /// Allows all requests.
166    AllowAll,
167    /// Denies all requests.
168    DenyAll,
169}
170
171impl Scope for UnionScope {
172    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool {
173        match self {
174            UnionScope::AifValue(v) => v.request_is_allowed(request),
175            UnionScope::AllowAll => AllowAll.request_is_allowed(request),
176            UnionScope::DenyAll => DenyAll.request_is_allowed(request),
177        }
178    }
179}
180
181impl From<AifValue> for UnionScope {
182    fn from(value: AifValue) -> Self {
183        UnionScope::AifValue(value)
184    }
185}
186
187impl From<AllowAll> for UnionScope {
188    fn from(_value: AllowAll) -> Self {
189        UnionScope::AllowAll
190    }
191}
192
193impl From<DenyAll> for UnionScope {
194    fn from(_value: DenyAll) -> Self {
195        UnionScope::DenyAll
196    }
197}
198
199impl From<core::convert::Infallible> for UnionScope {
200    fn from(value: core::convert::Infallible) -> Self {
201        match value {}
202    }
203}