coapcore/
scope.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Expressions for access policy as evaluated for a particular security context.

use coap_message::{MessageOption, ReadableMessage};

/// A data item representing the server access policy as evaluated for a particular security context.
pub trait Scope: Sized + core::fmt::Debug {
    /// Returns true if a request may be performed by the bound security context.
    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool;

    /// Returns true if a bound security context should be preferably retained when hitting
    /// resource limits.
    fn is_admin(&self) -> bool {
        false
    }
}

impl Scope for core::convert::Infallible {
    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
        match *self {}
    }
}

/// Error type indicating that a scope could not be created from the given token scope.
///
/// As tokens are only accepted from trusted sources, the presence of this error typically
/// indicates a misconfigured trust anchor.
#[derive(Debug, Copy, Clone)]
pub struct InvalidScope;

/// A scope expression that allows all requests.
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug)]
pub struct AllowAll;

impl Scope for AllowAll {
    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
        true
    }
}

/// A scope expression that denies all requests.
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug)]
pub struct DenyAll;

impl Scope for DenyAll {
    fn request_is_allowed<M: ReadableMessage>(&self, _request: &M) -> bool {
        false
    }
}

const AIF_SCOPE_MAX_LEN: usize = 64;

/// A representation of an RFC9237 using the REST-specific model.
///
/// It is arbitrarily limited in length; future versions may give more flexibility, eg. by
/// referring to data in storage.
///
/// This type is constrained to valid CBOR representations of the REST-specific model; it may panic
/// if that constraint is not upheld.
///
/// ## Caveats
///
/// Using this is not very efficient; worst case, it iterates over all options for all AIF entries.
/// This could be mitigated by sorting the records at construction time.
///
/// This completely disregards proper URI splitting; this works for very simple URI references in
/// the AIF. This could be mitigated by switching to a CRI based model.
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone)]
pub struct AifValue([u8; AIF_SCOPE_MAX_LEN]);

impl AifValue {
    pub fn parse(bytes: &[u8]) -> Result<Self, InvalidScope> {
        let mut buffer = [0; AIF_SCOPE_MAX_LEN];

        buffer
            .get_mut(..bytes.len())
            .ok_or(InvalidScope)?
            .copy_from_slice(bytes);

        let mut decoder = minicbor::Decoder::new(bytes);
        for item in decoder
            .array_iter::<(&str, u32)>()
            .map_err(|_| InvalidScope)?
        {
            let (path, _mask) = item.map_err(|_| InvalidScope)?;
            if !path.starts_with("/") {
                return Err(InvalidScope);
            }
        }

        Ok(Self(buffer))
    }
}

impl Scope for AifValue {
    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool {
        let code: u8 = request.code().into();
        let (codebit, false) = 1u32.overflowing_shl(
            u32::from(code)
                .checked_sub(1)
                .expect("Request codes are != 0"),
        ) else {
            return false;
        };
        let mut decoder = minicbor::Decoder::new(&self.0);
        'outer: for item in decoder.array_iter::<(&str, u32)>().unwrap() {
            let (path, perms) = item.unwrap();
            if perms & codebit == 0 {
                continue;
            }
            // BIG FIXME: We're iterating over options without checking for critical options. If the
            // resource handler router consumes any different set of options, that disagreement might
            // give us a security issue.
            let mut pathopts = request
                .options()
                .filter(|o| o.number() == coap_numbers::option::URI_PATH)
                .peekable();
            if path == "/" && pathopts.peek().is_none() {
                // Special case: For consistency should be a single empty option.
                return true;
            }
            if !path.starts_with("/") {
                panic!("Invalid AIF");
            }
            let mut remainder = &path[1..];
            while !remainder.is_empty() {
                let (next_part, next_remainder) = match remainder.split_once('/') {
                    Some((next_part, next_remainder)) => (next_part, next_remainder),
                    None => (remainder, ""),
                };
                let Some(this_opt) = pathopts.next() else {
                    // Request path is shorter than this AIF record
                    continue 'outer;
                };
                if this_opt.value() != next_part.as_bytes() {
                    // Request path is just different from this AIF record
                    continue 'outer;
                }
                remainder = next_remainder;
            }
            if pathopts.next().is_none() {
                // Request path is longer than this AIF record
                return true;
            }
        }
        // No matches found
        false
    }

    fn is_admin(&self) -> bool {
        self.0[0] >= 0x83
    }
}

/// A scope that can use multiple backends, erasing its type.
///
/// (Think "`dyn Scope`" but without requiring dyn compatibility).
///
/// This is useful when combining multiple authentication methods, eg. allowing ACE tokens (that
/// need an [`AifValue`] to express their arbitrary scopes) as well as a configured admin key (that
/// has "all" permission, which are not expressible in an [`AifValue`].
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone)]
pub enum UnionScope {
    AifValue(AifValue),
    AllowAll,
    DenyAll,
}

impl Scope for UnionScope {
    fn request_is_allowed<M: ReadableMessage>(&self, request: &M) -> bool {
        match self {
            UnionScope::AifValue(v) => v.request_is_allowed(request),
            UnionScope::AllowAll => AllowAll.request_is_allowed(request),
            UnionScope::DenyAll => DenyAll.request_is_allowed(request),
        }
    }

    fn is_admin(&self) -> bool {
        match self {
            UnionScope::AifValue(v) => v.is_admin(),
            UnionScope::AllowAll => AllowAll.is_admin(),
            UnionScope::DenyAll => DenyAll.is_admin(),
        }
    }
}

impl From<AifValue> for UnionScope {
    fn from(value: AifValue) -> Self {
        UnionScope::AifValue(value)
    }
}

impl From<AllowAll> for UnionScope {
    fn from(_value: AllowAll) -> Self {
        UnionScope::AllowAll
    }
}

impl From<DenyAll> for UnionScope {
    fn from(_value: DenyAll) -> Self {
        UnionScope::DenyAll
    }
}

impl From<core::convert::Infallible> for UnionScope {
    fn from(value: core::convert::Infallible) -> Self {
        match value {}
    }
}