coapcore/
seccfg.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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
//! Descriptions of ACE Authorization Servers (AS) and other trust anchors, as viewed from the
//! Resource Server (RS) which coapcore runs on.

use defmt_or_log::{debug, error, trace};

use crate::ace::HeaderMap;
use crate::error::{CredentialError, CredentialErrorDetail};

pub const MAX_AUD_SIZE: usize = 8;

/// Error type of [`ServerSecurityConfig::render_not_allowed`].
///
/// This represents a failure to express the Request Creation Hints of ACE in a message. Unlike
/// most CoAP rendering errors, this can not just fall back to rendering that produces an Internal
/// Server Error, as that would be misunderstood by the client to mean that the requested operation
/// was being performed and failed at runtime (whereas with this error, the requested operation was
/// not performed). Therefore, no error details can be communicated to the client reliably.
///
/// Implementers are encouraged to log an error when returning this.
pub struct NotAllowedRenderingFailed;

/// A single or collection of authorization servers that a handler trusts to create ACE tokens.
pub trait ServerSecurityConfig: crate::Sealed {
    /// True if the type will at any time need to process tokens at /authz-info
    ///
    /// This is used by the handler implementation to shortcut through some message processing
    /// paths.
    const PARSES_TOKENS: bool;

    /// The way scopes issued with this system as audience by this AS are expressed here.
    type Scope: crate::scope::Scope;

    /// Unprotects a symmetriclly encrypted token and processes the contained [CWT Claims
    /// Set][crate::ace::CwtClaimsSet] into a [`Self::Scope`] and returns the claims.
    ///
    /// The steps are performed together rather than in separate functions because it is yet
    /// unclear how data would precisely be carried around. (Previous iterations of this API had a
    /// `ScopeGenerator` associated type that would carry such data, but that did not scale well to
    /// different kinds of tokens).
    ///
    /// As part of such a dissection it would be preferable to return a decryption key and let the
    /// `ace` module do the decryption, but the key is not dyn safe, and
    /// [`aead::AeadInPlace`](https://docs.rs/aead/latest/aead/trait.AeadInPlace.html) can not be
    /// enum'd around different potential key types because the associated types are fixed length.
    /// (Returning a key in some COSE crypto abstraction may work better).
    ///
    /// Note that the full AAD (COSE's AAD including the external AAD) is built by the caller; the
    /// headers are only passed in to enable the AS to select the right key.
    ///
    /// The buffer is given as heapless buffer rather than an an
    /// [`aead::Buffer`](https://docs.rs/aead/latest/aead/trait.Buffer.html) because the latter is
    /// not on the latest heaples version in its released version.
    #[allow(
        unused_variables,
        reason = "Names are human visible part of API description"
    )]
    #[expect(
        rustdoc::private_intra_doc_links,
        reason = "Method is sealed by private types"
    )]
    // The method is already sealed by the use of a HeaderMap and CwtClaimsSet, but that may become
    // more public over time, and that should not impct this method's publicness.
    fn decrypt_symmetric_token<'buf>(
        &self,
        headers: &HeaderMap,
        aad: &[u8],
        ciphertext_buffer: &'buf mut [u8],
        _: crate::PrivateMethod,
    ) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
        Err(CredentialErrorDetail::KeyNotPresent.into())
    }

    /// Verify the signature on a symmetrically encrypted token
    ///
    /// `signed_payload` is the payload part of the signed CWT; while it is part of `signed_data` and
    /// can be recovered from it, `signed_data` currently typically resides in a copied buffer
    /// created for signature verification, and signed_payload is around inside the caller for
    /// longer. As common with signed data, it should only be parsed once the signature has been
    /// verified.
    #[allow(
        unused_variables,
        reason = "Names are human visible part of API description"
    )]
    fn verify_asymmetric_token<'b>(
        &self,
        headers: &HeaderMap,
        signed_data: &[u8],
        signature: &[u8],
        signed_payload: &'b [u8],
        _: crate::PrivateMethod,
    ) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'b>), CredentialError> {
        Err(CredentialErrorDetail::KeyNotPresent.into())
    }

    fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
        None
    }

    /// Expands an EDHOC `ID_CRED_x` into a parsed `CRED_x` along with the associated
    /// authorizations.
    #[allow(
        unused_variables,
        reason = "Names are human visible part of API description"
    )]
    fn expand_id_cred_x(
        &self,
        id_cred_x: lakers::IdCred,
    ) -> Option<(lakers::Credential, Self::Scope)> {
        None
    }

    /// Generates the scope representing unauthenticated access.
    fn nosec_authorization(&self) -> Option<Self::Scope> {
        None
    }

    /// Render the "not allowed" message in this scenario.
    ///
    /// The default (or any error) renderer produces a generic 4.01 Unauthorized in the handler;
    /// specifics can be useful in ACE scenarios to return a Request Creation Hint.
    #[allow(
        unused_variables,
        reason = "Names are human visible part of API description"
    )]
    fn render_not_allowed<M: coap_message::MutableWritableMessage>(
        &self,
        message: &mut M,
    ) -> Result<(), NotAllowedRenderingFailed> {
        Err(NotAllowedRenderingFailed)
    }
}

/// The default empty configuration that denies all access.
pub struct DenyAll;

impl crate::Sealed for DenyAll {}

impl ServerSecurityConfig for DenyAll {
    const PARSES_TOKENS: bool = false;

    type Scope = core::convert::Infallible;
}

/// An SSC representing unconditionally allowed access, including unencrypted.
pub struct AllowAll;

impl crate::Sealed for AllowAll {}

impl ServerSecurityConfig for AllowAll {
    const PARSES_TOKENS: bool = false;

    type Scope = crate::scope::AllowAll;

    fn nosec_authorization(&self) -> Option<Self::Scope> {
        Some(crate::scope::AllowAll)
    }
}

/// An implementation of [`ServerSecurityConfig`] that can be extended using builder methods.
///
/// This is very much in flux, and will need further exploration as to inhowmuch this can be
/// type-composed from components.
pub struct ConfigBuilder {
    /// Symmetric used when tokens are symmetrically encrypted with AES-CCM-16-128-256
    as_key_31: Option<[u8; 32]>,
    /// Asymmetric key used when tokens are signed with ES256
    ///
    /// Alogn with the key, this also holds the audience value of this RS (as signed tokens only
    /// make sense when the same signing key is used with multiple recipients).
    as_key_neg7: Option<([u8; 32], [u8; 32], heapless::String<MAX_AUD_SIZE>)>,
    unauthenticated_scope: Option<crate::scope::UnionScope>,
    own_edhoc_credential: Option<(lakers::Credential, lakers::BytesP256ElemLen)>,
    known_edhoc_clients: Option<(lakers::Credential, crate::scope::UnionScope)>,
    request_creation_hints: &'static [u8],
}

impl crate::Sealed for ConfigBuilder {}

impl ServerSecurityConfig for ConfigBuilder {
    // We can't know at build time, assume yes
    const PARSES_TOKENS: bool = true;

    type Scope = crate::scope::UnionScope;

    fn decrypt_symmetric_token<'buf>(
        &self,
        headers: &HeaderMap,
        aad: &[u8],
        ciphertext_buffer: &'buf mut [u8],
        _: crate::PrivateMethod,
    ) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'buf>), CredentialError> {
        use ccm::aead::AeadInPlace;
        use ccm::KeyInit;

        let key = self.as_key_31.ok_or_else(|| {
            error!("Symmetrically encrypted token was sent, but no symmetric key is configured.");
            CredentialErrorDetail::KeyNotPresent
        })?;

        // FIXME: should be something Aes256Ccm::TagLength
        const TAG_SIZE: usize = 16;
        const NONCE_SIZE: usize = 13;

        pub type Aes256Ccm = ccm::Ccm<aes::Aes256, ccm::consts::U16, ccm::consts::U13>;
        let cipher = Aes256Ccm::new((&key).into());

        let nonce: &[u8; NONCE_SIZE] = headers
            .iv
            .ok_or_else(|| {
                error!("IV missing from token.");
                CredentialErrorDetail::InconsistentDetails
            })?
            .try_into()
            .map_err(|_| {
                error!("Token's IV length mismatches algorithm.");
                CredentialErrorDetail::InconsistentDetails
            })?;

        let ciphertext_len = ciphertext_buffer
            .len()
            .checked_sub(TAG_SIZE)
            .ok_or_else(|| {
                error!("Token's ciphertext too short for the algorithm's tag.");
                CredentialErrorDetail::InconsistentDetails
            })?;

        let (ciphertext, tag) = ciphertext_buffer.split_at_mut(ciphertext_len);

        cipher
            .decrypt_in_place_detached(nonce.into(), aad, ciphertext, ccm::Tag::from_slice(tag))
            .map_err(|_| {
                error!("Token decryption failed.");
                CredentialErrorDetail::VerifyFailed
            })?;

        let claims: crate::ace::CwtClaimsSet = minicbor::decode(ciphertext)
            .map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;

        let scope = crate::scope::AifValue::parse(claims.scope)
            .map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;

        Ok((scope.into(), claims))
    }

    fn verify_asymmetric_token<'b>(
        &self,
        headers: &HeaderMap,
        signed_data: &[u8],
        signature: &[u8],
        signed_payload: &'b [u8],
        _: crate::PrivateMethod,
    ) -> Result<(Self::Scope, crate::ace::CwtClaimsSet<'b>), CredentialError> {
        if headers.alg != Some(-7) {
            // ES256
            return Err(CredentialErrorDetail::UnsupportedAlgorithm.into());
        }

        let Some((x, y, rs_audience)) = self.as_key_neg7.as_ref() else {
            return Err(CredentialErrorDetail::KeyNotPresent.into());
        };

        use p256::ecdsa::{signature::Verifier, VerifyingKey};
        let as_key = VerifyingKey::from_encoded_point(
            &p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false),
        )
        .map_err(|_| CredentialErrorDetail::InconsistentDetails)?;
        let signature = p256::ecdsa::Signature::from_slice(signature)
            .map_err(|_| CredentialErrorDetail::InconsistentDetails)?;

        as_key
            .verify(signed_data, &signature)
            .map_err(|_| CredentialErrorDetail::VerifyFailed)?;

        let claims: crate::ace::CwtClaimsSet = minicbor::decode(signed_payload)
            .map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;

        if claims.aud != Some(rs_audience) {
            // FIXME describe better? "Verified but we're not the audience?"
            return Err(CredentialErrorDetail::VerifyFailed.into());
        }

        let scope = crate::scope::AifValue::parse(claims.scope)
            .map_err(|_| CredentialErrorDetail::UnsupportedExtension)?;

        Ok((scope.into(), claims))
    }

    fn nosec_authorization(&self) -> Option<Self::Scope> {
        self.unauthenticated_scope.clone()
    }

    fn own_edhoc_credential(&self) -> Option<(lakers::Credential, lakers::BytesP256ElemLen)> {
        self.own_edhoc_credential
    }

    fn expand_id_cred_x(
        &self,
        id_cred_x: lakers::IdCred,
    ) -> Option<(lakers::Credential, Self::Scope)> {
        trace!(
            "Evaluating peer's credential {=[u8]:02x}", // :02x could be :cbor
            id_cred_x.as_full_value()
        );

        #[expect(
            clippy::single_element_loop,
            reason = "Expected to be extended to actual loop soon"
        )]
        for (credential, scope) in &[self.known_edhoc_clients.as_ref()?] {
            trace!("Comparing to {=[u8]:02x}", credential.bytes.as_slice()); // :02x could be :cbor
            if id_cred_x.reference_only() {
                // ad Ok: If our credential has no KID, it can't be recognized in this branch
                if credential.by_kid() == Ok(id_cred_x) {
                    debug!("Peer indicated use of the one preconfigured key by KID.");
                    #[expect(
                        clippy::clone_on_copy,
                        reason = "Lakers items are overly copy happy"
                    )]
                    return Some((credential.clone(), scope.clone()));
                }
            } else {
                // ad Ok: This is always the case for CCSs, but inapplicable eg. for PSKs.
                if credential.by_value() == Ok(id_cred_x) {
                    debug!("Peer indicated use of the one preconfigured credential by value.");
                    #[expect(
                        clippy::clone_on_copy,
                        reason = "Lakers items are overly copy happy"
                    )]
                    return Some((credential.clone(), scope.clone()));
                }
            }
        }

        if let Some(small_scope) = self.nosec_authorization() {
            trace!("Unauthenticated clients are generally accepted, evaluating credential.");
            if let Some(credential_by_value) = id_cred_x.get_ccs() {
                debug!("The unauthorized client provided a usable credential by value.");
                #[expect(clippy::clone_on_copy, reason = "Lakers items are overly copy happy")]
                return Some((credential_by_value.clone(), small_scope.clone()));
            }
        }

        None
    }

    fn render_not_allowed<M: coap_message::MutableWritableMessage>(
        &self,
        message: &mut M,
    ) -> Result<(), NotAllowedRenderingFailed> {
        use coap_message::Code;
        message.set_code(M::Code::new(coap_numbers::code::UNAUTHORIZED).map_err(|_| {
            error!("CoAP stack can not represent Unauthorized responses.");
            NotAllowedRenderingFailed
        })?);
        message
            .set_payload(self.request_creation_hints)
            .map_err(|_| {
                error!("Request creation hints do not fit in error message.");
                NotAllowedRenderingFailed
            })?;
        Ok(())
    }
}

impl Default for ConfigBuilder {
    fn default() -> Self {
        ConfigBuilder::new()
    }
}

impl ConfigBuilder {
    /// Creates an empty server security configuration.
    ///
    /// Without any additional building steps, this is equivalent to [`DenyAll`].
    pub fn new() -> Self {
        Self {
            as_key_31: None,
            as_key_neg7: None,
            unauthenticated_scope: None,
            known_edhoc_clients: None,
            own_edhoc_credential: None,
            request_creation_hints: &[],
        }
    }

    /// Sets a single Authorization Server recognized by a shared `AES-16-128-256` (COSE algorithm
    /// 31) key.
    ///
    /// Scopes are accepted as given by the AS using the AIF REST model as understood by
    /// [`crate::scope::AifValue`].
    ///
    /// # Caveats and evolution
    ///
    /// Currently, this type just supports a single AS; it should therefore only be called once,
    /// and the latest value overwrites any earlier. Building these in type state (as `[(&as_key);
    /// { N+1 }]` (once that is possible) or `(&as_key1, (&as_key2, ()))` will make sense on the
    /// long run, but is not implemented yet.
    ///
    /// Depending on whether the keys are already referenced in a long-lived location, when
    /// implementing that, it can also make sense to allow using any `AsRef<[u8; 32]>` types at
    /// that point.
    ///
    /// Currently, keys are taken as byte sequence. With the expected flexibilization of crypto
    /// backends, this may later allow a more generic type that reflects secure element key slots.
    pub fn with_aif_symmetric_as_aesccm256(self, key: [u8; 32]) -> Self {
        Self {
            as_key_31: Some(key),
            ..self
        }
    }

    /// Sets a single Authorization Server recignized by its `ES256` (COSE algorithm -7) signing
    /// key.
    ///
    /// An audience identifier is taken along with the key; signed tokens are only accepted if they
    /// have that audience.
    ///
    /// Scopes are accepted as given by the AS using the AIF REST model as understood by
    /// [`crate::scope::AifValue`].
    ///
    /// # Caveats and evolution
    ///
    /// Same from [`Self::with_aif_symmetric_as_aesccm256`] apply, minus the considerations for
    /// secure key storage.
    pub fn with_aif_asymmetric_es256(
        self,
        x: [u8; 32],
        y: [u8; 32],
        audience: heapless::String<MAX_AUD_SIZE>,
    ) -> Self {
        Self {
            as_key_neg7: Some((x, y, audience)),
            ..self
        }
    }

    /// Allow use of the server within the limits of the given scope by EDHOC clients provided they
    /// present the given credential.
    ///
    /// # Caveats and evolution
    ///
    /// Currently, this type just supports a single credential; it should therefore only be called
    /// once, and the latest value overwrites any earlier. (See
    /// [`Self::with_aif_symmetric_as_aesccm256`] for plans).
    pub fn with_known_edhoc_credential(
        self,
        credential: lakers::Credential,
        scope: crate::scope::UnionScope,
    ) -> Self {
        Self {
            known_edhoc_clients: Some((credential, scope)),
            ..self
        }
    }

    /// Configures an EDHOC credential and private key to be presented by this server.
    ///
    /// # Panics
    ///
    /// When debug assertions are enabled, this panics if an own credential has already been
    /// configured.
    pub fn with_own_edhoc_credential(
        self,
        credential: lakers::Credential,
        key: lakers::BytesP256ElemLen,
    ) -> Self {
        debug_assert!(
            self.own_edhoc_credential.is_none(),
            "Overwriting previously configured own credential scope"
        );
        Self {
            own_edhoc_credential: Some((credential, key)),
            ..self
        }
    }

    /// Allow use of the server by unauthenticated clients using the given scope.
    ///
    /// # Panics
    ///
    /// When debug assertions are enabled, this panics if an unauthenticated scope has already been
    /// configured.
    pub fn allow_unauthenticated(self, scope: crate::scope::UnionScope) -> Self {
        debug_assert!(
            self.unauthenticated_scope.is_none(),
            "Overwriting previously configured unauthenticated scope"
        );
        Self {
            unauthenticated_scope: Some(scope),
            ..self
        }
    }

    /// Sets the payload of the "Unauthorized" response.
    ///
    /// # Panics
    ///
    /// When debug assertions are enabled, this panics if an unauthenticated scope has already been
    /// configured.
    pub fn with_request_creation_hints(self, request_creation_hints: &'static [u8]) -> Self {
        debug_assert!(
            self.request_creation_hints.is_empty(),
            "Overwriting previously configured unauthenticated scope"
        );
        Self {
            request_creation_hints,
            ..self
        }
    }
}