coapcore/
seccontext.rs

1//! The main workhorse module of this crate.
2#![expect(
3    clippy::redundant_closure_for_method_calls,
4    reason = "all occurrences of this make the code strictly less obvious to understand"
5)]
6
7use core::marker::PhantomData;
8
9use coap_message::{
10    Code, MessageOption, MinimalWritableMessage, MutableWritableMessage, ReadableMessage,
11    error::RenderableOnMinimal,
12};
13use coap_message_utils::{Error as CoAPError, OptionsExt as _};
14use defmt_or_log::{Debug2Format, debug, error, trace};
15
16use crate::generalclaims::{self, GeneralClaims as _};
17use crate::helpers::COwn;
18use crate::scope::Scope;
19use crate::seccfg::ServerSecurityConfig;
20
21use crate::time::TimeProvider;
22
23const MAX_CONTEXTS: usize = 4;
24const _MAX_CONTEXTS_CHECK: () = assert!(MAX_CONTEXTS <= COwn::GENERATABLE_VALUES);
25
26/// Helper for cutting branches that can not be reached; could be a provided function of the
27/// [`ServerSecurityConfig`], but we need it const.
28const fn has_oscore<SSC: ServerSecurityConfig>() -> bool {
29    SSC::HAS_EDHOC || SSC::PARSES_TOKENS
30}
31
32/// Space allocated for the message into which an EDHOC request is copied to remove EDHOC option
33/// and payload.
34///
35/// embedded-nal-coap uses this max size, and our messages are same size or smaller,
36/// so it's a guaranteed fit.
37///
38/// # FIXME: Having a buffer here should just go away
39///
40/// Until liboscore can work on an arbitrary message, in particular a
41/// `StrippingTheEdhocOptionAndPayloadPart<M>`, we have to create a copy to remove the EDHOC option
42/// and payload. (Conveniently, that also sidesteps the need to `downcast_from` to a type libOSCORE
43/// knows, but that's not why we do it, that's what downcasting would be for.)
44///
45/// Furthermore, we need mutable access (something we can't easily gain by just downcasting).
46const EDHOC_COPY_BUFFER_SIZE: usize = 1152;
47
48/// A pool of security contexts shareable by several users inside a thread.
49type SecContextPool<Crypto, Claims> =
50    crate::oluru::OrderedPool<SecContextState<Crypto, Claims>, MAX_CONTEXTS, LEVEL_COUNT>;
51
52/// Copy of the OSCORE option
53type OscoreOption = heapless::Vec<u8, 16>;
54
55struct SecContextState<Crypto: lakers::Crypto, GeneralClaims: generalclaims::GeneralClaims> {
56    // FIXME: Updating this should also check the timeout.
57
58    // This is Some(...) unless the stage is unusable.
59    authorization: Option<GeneralClaims>,
60    protocol_stage: SecContextStage<Crypto>,
61}
62
63impl<Crypto: lakers::Crypto, GeneralClaims: generalclaims::GeneralClaims> Default
64    for SecContextState<Crypto, GeneralClaims>
65{
66    fn default() -> Self {
67        Self {
68            authorization: None,
69            protocol_stage: SecContextStage::Empty,
70        }
71    }
72}
73
74#[derive(Debug)]
75#[expect(
76    clippy::large_enum_variant,
77    reason = "requiring more memory during connection setup is expected, but the complexity of an inhmogenous pool is currently impractical"
78)]
79enum SecContextStage<Crypto: lakers::Crypto> {
80    Empty,
81
82    // if we have time to spare, we can have empty-but-prepared-with-single-use-random-key entries
83    // :-)
84
85    // actionable in response building
86    EdhocResponderProcessedM1 {
87        responder: lakers::EdhocResponderProcessedM1<Crypto>,
88        // May be removed if lakers keeps access to those around if they are set at this point at
89        // all
90        c_r: COwn,
91        c_i: lakers::ConnId,
92        requested_cred_by_value: bool,
93    },
94    //
95    EdhocResponderSentM2 {
96        responder: lakers::EdhocResponderWaitM3<Crypto>,
97        c_r: COwn,
98        c_i: lakers::ConnId,
99    },
100
101    // FIXME: Also needs a flag for whether M4 was received; if not, it's GC'able
102    Oscore(liboscore::PrimitiveContext),
103}
104
105const LEVEL_ADMIN: usize = 0;
106const LEVEL_AUTHENTICATED: usize = 1;
107const LEVEL_ONGOING: usize = 2;
108const LEVEL_EMPTY: usize = 3;
109// FIXME introduce a level for expired states; they're probably the least priority.
110const LEVEL_COUNT: usize = 4;
111
112impl<Crypto: lakers::Crypto, GeneralClaims: generalclaims::GeneralClaims>
113    crate::oluru::PriorityLevel for SecContextState<Crypto, GeneralClaims>
114{
115    fn level(&self) -> usize {
116        match &self.protocol_stage {
117            SecContextStage::Empty => LEVEL_EMPTY,
118            SecContextStage::EdhocResponderProcessedM1 { .. } => {
119                // If this is ever tested, means we're outbound message limited, so let's try to
120                // get one through rather than pointlessly sending errors
121                LEVEL_ONGOING
122            }
123            SecContextStage::EdhocResponderSentM2 { .. } => {
124                // So far, the peer didn't prove they have anything other than entropy (maybe not
125                // even that)
126                LEVEL_ONGOING
127            }
128            SecContextStage::Oscore(_) => {
129                if self
130                    .authorization
131                    .as_ref()
132                    .is_some_and(|a| a.is_important())
133                {
134                    LEVEL_ADMIN
135                } else {
136                    LEVEL_AUTHENTICATED
137                }
138            }
139        }
140    }
141}
142
143impl<Crypto: lakers::Crypto, GeneralClaims: generalclaims::GeneralClaims>
144    SecContextState<Crypto, GeneralClaims>
145{
146    fn corresponding_cown(&self) -> Option<COwn> {
147        match &self.protocol_stage {
148            SecContextStage::Empty => None,
149            // We're keeping a c_r in there assigned early so that we can find the context when
150            // building the response; nothing in the responder is tied to c_r yet.
151            SecContextStage::EdhocResponderProcessedM1 { c_r, .. }
152            | SecContextStage::EdhocResponderSentM2 { c_r, .. } => Some(*c_r),
153            SecContextStage::Oscore(ctx) => COwn::from_kid(ctx.recipient_id()),
154        }
155    }
156}
157
158/// A CoAP handler wrapping inner resources, and adding EDHOC, OSCORE and ACE support.
159///
160/// While the ACE (authz-info) and EDHOC parts could be implemented as a handler that is to be
161/// added into the tree, the OSCORE part needs to wrap the inner handler anyway, and EDHOC and
162/// OSCORE are intertwined rather strongly in processing the EDHOC option.
163pub struct OscoreEdhocHandler<
164    H: coap_handler::Handler,
165    Crypto: lakers::Crypto,
166    CryptoFactory: Fn() -> Crypto,
167    SSC: ServerSecurityConfig,
168    RNG: rand_core::RngCore + rand_core::CryptoRng,
169    TP: TimeProvider,
170> {
171    // It'd be tempted to have sharing among multiple handlers for multiple CoAP stacks, but
172    // locks for such sharing could still be acquired in a factory (at which point it may make
173    // sense to make this a &mut).
174    pool: SecContextPool<Crypto, SSC::GeneralClaims>,
175
176    authorities: SSC,
177
178    // FIXME: This currently bakes in the assumption that there is a single tree both for
179    // unencrypted and encrypted resources. We may later generalize this by making this a factory,
180    // or a single item that has two AsMut<impl Handler> accessors for separate encrypted and
181    // unencrypted tree.
182
183    // FIXME That assumption could be easily violated by code changes that don't take the big
184    // picture into account. It might make sense to wrap the inner into some
185    // zero-cost/build-time-only wrapper that verifies that either request_is_allowed() has been
186    // called, or an AuthorizationChecked::Allowed is around.
187    inner: H,
188
189    time: TP,
190
191    crypto_factory: CryptoFactory,
192    rng: RNG,
193}
194
195impl<
196    H: coap_handler::Handler,
197    Crypto: lakers::Crypto,
198    CryptoFactory: Fn() -> Crypto,
199    SSC: ServerSecurityConfig,
200    RNG: rand_core::RngCore + rand_core::CryptoRng,
201    TP: TimeProvider,
202> OscoreEdhocHandler<H, Crypto, CryptoFactory, SSC, RNG, TP>
203{
204    /// Creates a new CoAP server implementation (a [Handler][coap_handler::Handler]), wrapping an
205    /// inner (application) handler.
206    ///
207    /// The main configuration is passed in as `authorities`; the [`seccfg`][crate::seccfg] module
208    /// has suitable implementations.
209    ///
210    /// The time provider is used to evaluate any time limited tokens leniently; choosing a "bad"
211    /// time source here (in particular [`crate::time::TimeUnknown`]) leads to acceptance of expired
212    /// tokens.
213    ///
214    /// `rng` and `crypto_factory` are used to pass in platform specific implementations of what
215    /// may be accelerated by hardware or reuse operating system infrastructure. Any CSPRNG is
216    /// suitable for `rng` (Ariel OS picks `rand_chacha::ChaCha20Rng` at the time of writing); the
217    /// crypto factory can come from the `lakers_crypto_rustcrypto::Crypto` or any more specialized
218    /// hardware based implementation.
219    pub fn new(
220        inner: H,
221        authorities: SSC,
222        crypto_factory: CryptoFactory,
223        rng: RNG,
224        time: TP,
225    ) -> Self {
226        Self {
227            pool: crate::oluru::OrderedPool::new(),
228            inner,
229            crypto_factory,
230            authorities,
231            rng,
232            time,
233        }
234    }
235
236    /// Produces a [`COwn`] (as a recipient identifier) that is both available and not equal to the
237    /// peer's recipient identifier.
238    fn cown_but_not(&self, c_peer: &[u8]) -> COwn {
239        // Let's pick one now already: this allows us to use the identifier in our
240        // request data.
241        COwn::not_in_iter(
242            self.pool
243                .iter()
244                .filter_map(|entry| entry.corresponding_cown())
245                // C_R does not only need to be unique, it also must not be identical
246                // to C_I. If it is not expressible as a COwn (as_slice gives []),
247                // that's fine and we don't have to consider it.
248                .chain(COwn::from_kid(c_peer).as_slice().iter().copied()),
249        )
250    }
251
252    /// Processes a CoAP request containing a message sent to /.well-known/edhoc.
253    ///
254    /// The caller has already checked Uri-Path and all other critical options, and that the
255    /// request was a POST.
256    ///
257    /// # Errors
258    ///
259    /// This produces errors if the input (which is typically received from the network) is
260    /// malformed or contains unsupported items.
261    #[allow(
262        clippy::type_complexity,
263        reason = "Type is subset of RequestData that has no alias in the type"
264    )]
265    fn extract_edhoc<M: ReadableMessage>(
266        &mut self,
267        request: &M,
268    ) -> Result<OwnRequestData<Result<H::RequestData, H::ExtractRequestError>>, CoAPError> {
269        let own_identity = self
270            .authorities
271            .own_edhoc_credential()
272            // 4.04 Not Found does not precisely capture it when we later support reverse flow, but
273            // until then, "there is no EDHOC" is a good rendition of lack of own key.
274            .ok_or_else(CoAPError::not_found)?;
275
276        let (first_byte, edhoc_m1) = request.payload().split_first().ok_or_else(|| {
277            error!("Empty EDHOC requests (reverse flow) not supported yet.");
278            CoAPError::bad_request()
279        })?;
280        let starts_with_true = first_byte == &0xf5;
281
282        if starts_with_true {
283            trace!("Processing incoming EDHOC message 1");
284            let message_1 =
285                &lakers::EdhocMessageBuffer::new_from_slice(edhoc_m1).map_err(too_small)?;
286
287            let mut requested_cred_by_value = false;
288
289            let (responder, c_i, ead_1) = lakers::EdhocResponder::new(
290                (self.crypto_factory)(),
291                lakers::EDHOCMethod::StatStat,
292                own_identity.1,
293                own_identity.0,
294            )
295            .process_message_1(message_1)
296            .map_err(render_error)?;
297
298            if let Some(ead_1) = ead_1 {
299                if ead_1.label == crate::iana::edhoc_ead::CRED_BY_VALUE {
300                    requested_cred_by_value = true;
301                } else if ead_1.is_critical {
302                    error!("Critical EAD1 item received, aborting");
303                    // FIXME: send error message
304                    return Err(CoAPError::bad_request());
305                }
306            }
307
308            let c_r = self.cown_but_not(c_i.as_slice());
309
310            let _evicted = self.pool.force_insert(SecContextState {
311                protocol_stage: SecContextStage::EdhocResponderProcessedM1 {
312                    c_r,
313                    c_i,
314                    responder,
315                    requested_cred_by_value,
316                },
317                authorization: self.authorities.nosec_authorization(),
318            });
319
320            Ok(OwnRequestData::EdhocOkSend2(c_r))
321        } else {
322            // for the time being we'll only take the EDHOC option
323            error!(
324                "Sending EDHOC message 3 to the /.well-known/edhoc resource is not supported yet"
325            );
326            Err(CoAPError::bad_request())
327        }
328    }
329
330    /// Builds an EDHOC response message 2 after successful processing of a request in
331    /// [`Self::extract_edhoc()`]
332    ///
333    /// # Errors
334    ///
335    /// This produces errors if the input (which is typically received from the network) is
336    /// malformed or contains unsupported items.
337    fn build_edhoc_message_2<M: MutableWritableMessage>(
338        &mut self,
339        response: &mut M,
340        c_r: COwn,
341    ) -> Result<(), Result<CoAPError, M::UnionError>> {
342        let message_2 = self.pool.lookup(
343            |c| c.corresponding_cown() == Some(c_r),
344            |matched| -> Result<_, lakers::EDHOCError> {
345                // temporary default will not live long (and may be only constructed if
346                // prepare_message_2 fails)
347                let taken = core::mem::take(matched);
348                let SecContextState {
349                    protocol_stage:
350                        SecContextStage::EdhocResponderProcessedM1 {
351                            c_r: matched_c_r,
352                            c_i,
353                            responder: taken,
354                            requested_cred_by_value,
355                        },
356                    authorization,
357                } = taken
358                else {
359                    todo!();
360                };
361                debug_assert_eq!(
362                    matched_c_r, c_r,
363                    "The first lookup function ensured this property"
364                );
365                let (responder, message_2) = taken
366                    // We're sending our ID by reference: we have a CCS and don't expect anyone to
367                    // run EDHOC with us who can not verify who we are (and from the CCS there is
368                    // no better way). Also, conveniently, this covers our privacy well.
369                    // (Sending ByValue would still work)
370                    .prepare_message_2(
371                        if requested_cred_by_value {
372                            lakers::CredentialTransfer::ByValue
373                        } else {
374                            lakers::CredentialTransfer::ByReference
375                        },
376                        Some(c_r.into()),
377                        &None,
378                    )?;
379                *matched = SecContextState {
380                    protocol_stage: SecContextStage::EdhocResponderSentM2 {
381                        responder,
382                        c_i,
383                        c_r,
384                    },
385                    authorization,
386                };
387                Ok(message_2)
388            },
389        );
390
391        let message_2 = match message_2 {
392            Some(Ok(m)) => m,
393            Some(Err(e)) => {
394                render_error(e).render(response).map_err(Err)?;
395                return Ok(());
396            }
397            // Can't happen with the current CoAP stack, but might happen when there is some
398            // possibly possible concurrency.
399            None => {
400                response.set_code(
401                    M::Code::new(coap_numbers::code::INTERNAL_SERVER_ERROR)
402                        .map_err(|x| Err(x.into()))?,
403                );
404                return Ok(());
405            }
406        };
407
408        // FIXME: Why does the From<O> not do the map_err?
409        response.set_code(M::Code::new(coap_numbers::code::CHANGED).map_err(|x| Err(x.into()))?);
410
411        response
412            .set_payload(message_2.as_slice())
413            .map_err(|x| Err(x.into()))?;
414
415        Ok(())
416    }
417
418    /// Processes a CoAP request containing an OSCORE option and possibly an EDHOC option.
419    ///
420    /// # Errors
421    ///
422    /// This produces errors if the input (which is typically received from the network) is
423    /// malformed, contains unsupported items, or is too large for the allocated buffers.
424    #[allow(
425        clippy::type_complexity,
426        reason = "type is subset of RequestData that has no alias in the type"
427    )]
428    fn extract_oscore_edhoc<M: ReadableMessage>(
429        &mut self,
430        request: &M,
431        oscore_option: &OscoreOption,
432        with_edhoc: bool,
433    ) -> Result<OwnRequestData<Result<H::RequestData, H::ExtractRequestError>>, CoAPError> {
434        let payload = request.payload();
435
436        // We know this to not fail b/c we only got here due to its presence
437        let oscore_option = liboscore::OscoreOption::parse(oscore_option).map_err(|_| {
438            error!("OSCORE option could not be parsed");
439            CoAPError::bad_option(coap_numbers::option::OSCORE)
440        })?;
441
442        let kid = COwn::from_kid(oscore_option.kid().ok_or_else(|| {
443            error!("OSCORE KID is not in our value space");
444            CoAPError::bad_option(coap_numbers::option::OSCORE)
445        })?)
446        // same as if it's not found in the pool
447        .ok_or_else(CoAPError::bad_request)?;
448        // If we don't make progress, we're dropping it altogether. Unless we use the
449        // responder we might legally continue (because we didn't send data to EDHOC), but
450        // once we've received something that (as we now know) looks like a message 3 and
451        // isn't processable, it's unlikely that another one would come up and be.
452        let taken = self
453            .pool
454            .lookup(|c| c.corresponding_cown() == Some(kid), core::mem::take)
455            // following RFC8613 Section 8.2 item 2.2
456            .ok_or_else(|| {
457                error!("No security context with this KID.");
458                // FIXME unauthorized (unreleased in coap-message-utils)
459                CoAPError::bad_request()
460            })?;
461
462        let (taken, front_trim_payload) = if with_edhoc {
463            if !SSC::HAS_EDHOC {
464                unreachable!(
465                    "In this variant, that option is not consumed so the argument is always false"
466                );
467            }
468            self.process_edhoc_in_payload(payload, taken)?
469        } else {
470            (taken, 0)
471        };
472
473        let SecContextState {
474            protocol_stage: SecContextStage::Oscore(mut oscore_context),
475            authorization: Some(authorization),
476        } = taken
477        else {
478            // FIXME: How'd we even get there? Should this be unreachable?
479            error!("Found empty security context.");
480            return Err(CoAPError::bad_request());
481        };
482
483        if !authorization
484            .time_constraint()
485            .is_valid_with(&mut self.time)
486        {
487            // Token expired.
488            //
489            // By returning early after having taken the context, we discard it completely.
490            //
491            // FIXME: Find out whether there is any merit in retaining the security context without
492            // authorization at all -- it may be that for the purpose of time series it is useful
493            // to retain the authorization (if there is some kind of renewal tokens / token
494            // series).
495            debug!("Discarding expired context");
496            return Err(CoAPError::bad_request());
497        }
498
499        // See comment on EDHOC_COPY_BUFFER_SIZE
500        let mut read_copy = [0u8; EDHOC_COPY_BUFFER_SIZE];
501        let mut code_copy = 0;
502        let mut copied_message = coap_message_implementations::inmemory_write::Message::new(
503            &mut code_copy,
504            &mut read_copy[..],
505        );
506        // We could also do
507        //     copied_message.set_from_message(request);
508        // if we specified a "hiding EDHOC" message view.
509        copied_message.set_code(request.code().into());
510        // This may panic in theory on options being added in the wrong sequence; as we
511        // don't downcast, we don't get the information on whether the underlying
512        // implementation produces the options in the right sequence. Practically
513        // (typically, and concretely in Ariel OS), it is given. (And it's not like we have
514        // a fallback: inmemory_write has no more expensive option for reshuffling).
515        for opt in request.options() {
516            if opt.number() == coap_numbers::option::EDHOC {
517                continue;
518            }
519            copied_message
520                .add_option(opt.number(), opt.value())
521                .map_err(|_| {
522                    error!("Options produced in unexpected sequence.");
523                    CoAPError::internal_server_error()
524                })?;
525        }
526        #[allow(clippy::indexing_slicing, reason = "slice fits by construction")]
527        copied_message
528            .set_payload(&payload[front_trim_payload..])
529            .map_err(|_| {
530                error!("Unexpectedly large EDHOC-less message");
531                CoAPError::internal_server_error()
532            })?;
533
534        let decrypted = liboscore::unprotect_request(
535            &mut copied_message,
536            oscore_option,
537            &mut oscore_context,
538            |request| {
539                if authorization.scope().request_is_allowed(request) {
540                    AuthorizationChecked::Allowed(self.inner.extract_request_data(request))
541                } else {
542                    AuthorizationChecked::NotAllowed
543                }
544            },
545        );
546
547        // With any luck, this never moves out.
548        //
549        // Storing it even on decryption failure to avoid DoS from the first message (but
550        // FIXME, should we increment an error count and lower priority?)
551        #[allow(clippy::used_underscore_binding, reason = "used only in debug asserts")]
552        let _evicted = self.pool.force_insert(SecContextState {
553            protocol_stage: SecContextStage::Oscore(oscore_context),
554            authorization: Some(authorization),
555        });
556        debug_assert!(
557            matches!(
558                _evicted,
559                Some(SecContextState {
560                    protocol_stage: SecContextStage::Empty,
561                    ..
562                }) | None
563            ),
564            "A Default (Empty) was placed when an item was taken, which should have the lowest priority"
565        );
566
567        let Ok((correlation, extracted)) = decrypted else {
568            // FIXME is that the right code?
569            error!("Decryption failure");
570            return Err(CoAPError::unauthorized());
571        };
572
573        Ok(OwnRequestData::EdhocOscoreRequest {
574            kid,
575            correlation,
576            extracted,
577        })
578    }
579
580    /// Processes an EDHOC message 3 at the beginning of a payload, and returns the number of bytes
581    /// that were in the message.
582    ///
583    /// # Errors
584    ///
585    /// This produces errors if the input (which is typically received from the network) is
586    /// malformed or contains unsupported items.
587    ///
588    /// # Panics
589    ///
590    /// This panics if cipher suite negotiation passed for a suite whose algorithms are unsupported
591    /// in libOSCORE.
592    fn process_edhoc_in_payload(
593        &self,
594        payload: &[u8],
595        sec_context_state: SecContextState<Crypto, SSC::GeneralClaims>,
596    ) -> Result<(SecContextState<Crypto, SSC::GeneralClaims>, usize), CoAPError> {
597        // We're not supporting block-wise here -- but could later, to the extent we support
598        // outer block-wise.
599
600        // Workaround for https://github.com/openwsn-berkeley/lakers/issues/255
601        let mut decoder = minicbor::decode::Decoder::new(payload);
602        let _ = decoder
603            .decode::<&minicbor::bytes::ByteSlice>()
604            .map_err(|_| {
605                error!("EDHOC request is not prefixed with valid CBOR.");
606                CoAPError::bad_request()
607            })?;
608        let cutoff = decoder.position();
609
610        let sec_context_state = if let SecContextState {
611            protocol_stage:
612                SecContextStage::EdhocResponderSentM2 {
613                    responder,
614                    c_r,
615                    c_i,
616                },
617            .. // Discarding original authorization
618        } = sec_context_state
619        {
620            #[allow(clippy::indexing_slicing, reason = "slice fits by construction")]
621            let msg_3 = lakers::EdhocMessageBuffer::new_from_slice(&payload[..cutoff])
622                .map_err(too_small)?;
623
624            let (responder, id_cred_i, mut ead_3) =
625                responder.parse_message_3(&msg_3).map_err(render_error)?;
626
627            let mut cred_i_and_authorization = None;
628
629            if let Some(lakers::EADItem { label: crate::iana::edhoc_ead::ACETOKEN, value: Some(value), .. }) = ead_3.take() {
630                match crate::ace::process_edhoc_token(value.as_slice(), &self.authorities) {
631                    Ok(ci_and_a) => cred_i_and_authorization = Some(ci_and_a),
632                    Err(e) => {
633                        error!("Received unprocessable token {}, error: {:?}", defmt_or_log::wrappers::Cbor(value.as_slice()), Debug2Format(&e));
634                    }
635                }
636            }
637
638            if cred_i_and_authorization.is_none() {
639                cred_i_and_authorization = self
640                    .authorities
641                    .expand_id_cred_x(id_cred_i);
642            }
643
644            let Some((cred_i, authorization)) = cred_i_and_authorization else {
645                // FIXME: send better message; how much variability should we allow?
646                error!("Peer's ID_CRED_I could not be resolved into CRED_I.");
647                return Err(CoAPError::bad_request());
648            };
649
650            if let Some(ead_3) = ead_3 && ead_3.is_critical {
651                error!("Critical EAD3 item received, aborting");
652                // FIXME: send error message
653                return Err(CoAPError::bad_request());
654            }
655
656            let (responder, _prk_out) =
657                responder.verify_message_3(cred_i).map_err(render_error)?;
658
659            let mut responder = responder.completed_without_message_4().map_err(render_error)?;
660
661            // Once this gets updated beyond Lakers 0.7.2 (likely to 0.8), this will be needed:
662            // let mut responder = responder.completed_without_message_4()
663            //     .map_err(render_error)?;
664
665            let oscore_secret = responder.edhoc_exporter(0u8, &[], 16); // label is 0
666            let oscore_salt = responder.edhoc_exporter(1u8, &[], 8); // label is 1
667            let oscore_secret = &oscore_secret[..16];
668            let oscore_salt = &oscore_salt[..8];
669
670            let sender_id = c_i.as_slice();
671            let recipient_id = c_r.as_slice();
672
673            // FIXME probe cipher suite
674            let hkdf = liboscore::HkdfAlg::from_number(crate::iana::cose_alg::HKDF_HMAC256256).unwrap();
675            let aead = liboscore::AeadAlg::from_number(crate::iana::cose_alg::AES_CCM_16_64_128).unwrap();
676
677            let immutables = liboscore::PrimitiveImmutables::derive(
678                hkdf,
679                oscore_secret,
680                oscore_salt,
681                None,
682                aead,
683                sender_id,
684                recipient_id,
685            )
686            // FIXME convert error
687            .unwrap();
688
689            let context = liboscore::PrimitiveContext::new_from_fresh_material(immutables);
690
691            SecContextState {
692                protocol_stage: SecContextStage::Oscore(context),
693                authorization: Some(authorization),
694            }
695        } else {
696            // Return the state. Best bet is that it was already advanced to an OSCORE
697            // state, and the peer sent message 3 with multiple concurrent in-flight
698            // messages. We're ignoring the EDHOC value and continue with OSCORE
699            // processing.
700            sec_context_state
701        };
702
703        debug!(
704            "Processing {} bytes at start of message into new EDHOC Message 3.",
705            cutoff
706        );
707
708        Ok((sec_context_state, cutoff))
709    }
710
711    /// Builds an OSCORE response message after successful processing of a request in
712    /// [`Self::extract_oscore_edhoc()`].
713    ///
714    /// # Errors
715    ///
716    /// This produces errors if requests are processed in unexpected out-of-order ways.
717    ///
718    /// # Panics
719    ///
720    /// Panics if the writable message is not a
721    /// [`coap_message_implementations::inmemory_write::Message`]. See module level documentation
722    /// for details.
723    fn build_oscore_response<M: MutableWritableMessage>(
724        &mut self,
725        response: &mut M,
726        kid: COwn,
727        mut correlation: liboscore::raw::oscore_requestid_t,
728        extracted: AuthorizationChecked<Result<H::RequestData, H::ExtractRequestError>>,
729    ) -> Result<(), Result<CoAPError, M::UnionError>> {
730        response.set_code(M::Code::new(coap_numbers::code::CHANGED).map_err(|x| Err(x.into()))?);
731
732        // BIG FIXME: We have currently no way to rewind through a message once we've started
733        // building it.
734        //
735        // We *could* to some extent rewind if we sent things out in an error, but that error would
736        // need to have a clone of the correlation data, and that means that all our errors would
737        // become much larger than needed, because they all consume own sequence numbers.
738        //
739        // Putting this aside for the moment and accepting that in some few cases there will be
740        // unexpected options from the first attempt to render in the eventual message (in theory
741        // even panics when a payload is already set and then the error adds options), but the
742        // easiest path there is to wait for the next iteration of handler where everything is
743        // async and the handler has a method to start writing to the message (which kind'a
744        // implies rewinding)
745
746        self.pool
747                    .lookup(|c| c.corresponding_cown() == Some(kid), |matched| {
748                        // Not checking authorization any more: we don't even have access to the
749                        // request any more, that check was done.
750                        let SecContextState { protocol_stage: SecContextStage::Oscore(oscore_context), .. } = matched else {
751                            // State vanished before response was built.
752                            //
753                            // As it is, depending on the CoAP stack, there may be DoS if a peer
754                            // can send many requests before the server starts rendering responses.
755                            error!("State vanished before response was built.");
756                            return Err(CoAPError::internal_server_error());
757                        };
758
759                        let response = coap_message_implementations::inmemory_write::Message::downcast_from(response)
760                            .expect("OSCORE handler currently requires a response message implementation that is of fixed type");
761
762                        response.set_code(coap_numbers::code::CHANGED);
763
764                        if liboscore::protect_response(
765                            response,
766                            // SECURITY BIG FIXME: How do we make sure that our correlation is really for
767                            // what we find in the pool and not for what wound up there by the time we send
768                            // the response? (Can't happen with the current stack, but conceptually there
769                            // should be a tie; carry the OSCORE context in an owned way?).
770                            oscore_context,
771                            &mut correlation,
772                            |response| match extracted {
773                                AuthorizationChecked::Allowed(Ok(extracted)) => match self.inner.build_response(response, extracted) {
774                                    Ok(()) => {
775                                        // All fine, response was built
776                                    },
777                                    // One attempt to render rendering errors
778                                    // FIXME rewind message
779                                    Err(e) => {
780                                        error!("Rendering successful extraction failed with {:?}", Debug2Format(&e));
781                                        match e.render(response) {
782                                            Ok(()) => {
783                                                error!("Error rendered.");
784                                            },
785                                            Err(e2) => {
786                                                error!("Error could not be rendered: {:?}.", Debug2Format(&e2));
787                                                // FIXME rewind message
788                                                response.set_code(coap_numbers::code::INTERNAL_SERVER_ERROR);
789                                            }
790                                        }
791                                    },
792                                },
793                                AuthorizationChecked::Allowed(Err(inner_request_error)) => {
794                                    error!("Extraction failed with {:?}.", Debug2Format(&inner_request_error));
795                                    match inner_request_error.render(response) {
796                                        Ok(()) => {
797                                            error!("Original error rendered successfully.");
798                                        },
799                                        Err(e) => {
800                                            error!("Original error could not be rendered due to {:?}:", Debug2Format(&e));
801                                            // Two attempts to render extraction errors
802                                            // FIXME rewind message
803                                            match e.render(response) {
804                                                Ok(()) => {
805                                                    error!("Error was rendered fine.");
806                                                },
807                                                Err(e2) => {
808                                                    error!("Rendering error caused {:?}.", Debug2Format(&e2));
809                                                    // FIXME rewind message
810                                                    response.set_code(
811                                                        coap_numbers::code::INTERNAL_SERVER_ERROR,
812                                                    );
813                                                }
814                                            }
815                                        }
816                                    }
817                                }
818                                AuthorizationChecked::NotAllowed => {
819                                    if self.authorities.render_not_allowed(response).is_err() {
820                                        // FIXME rewind message
821                                        response.set_code(coap_numbers::code::UNAUTHORIZED);
822                                    }
823                                }
824                            },
825                        )
826                        .is_err()
827                        {
828                            error!("Oups, responding with weird state");
829                            // todo!("Thanks to the protect API we've lost access to our response");
830                        }
831                        Ok(())
832                    })
833                .transpose().map_err(Ok)?;
834        Ok(())
835    }
836
837    /// Processes a CoAP request containing an ACE token for /authz-info.
838    ///
839    /// This assumes that the content format was pre-checked to be application/ace+cbor, both in
840    /// Content-Format and Accept (absence is fine too), no other critical options are present,
841    /// and the code was POST.
842    ///
843    /// # Errors
844    ///
845    /// This produces errors if the input (which is typically received from the network) is
846    /// malformed or contains unsupported items.
847    fn extract_token(
848        &mut self,
849        payload: &[u8],
850    ) -> Result<crate::ace::AceCborAuthzInfoResponse, CoAPError> {
851        let mut nonce2 = [0; crate::ace::OWN_NONCE_LEN];
852        self.rng.fill_bytes(&mut nonce2);
853
854        let (response, oscore, generalclaims) =
855            crate::ace::process_acecbor_authz_info(payload, &self.authorities, nonce2, |nonce1| {
856                // This preferably (even exclusively) produces EDHOC-ideal recipient IDs, but as long
857                // as we're having more of those than slots, no point in not reusing the code.
858                self.cown_but_not(nonce1)
859            })
860            .map_err(|e| {
861                error!("Sending out error:");
862                error!("{:?}", Debug2Format(&e));
863                e.position
864                    // FIXME: Could also come from processing inner
865                    .map_or(CoAPError::bad_request(), CoAPError::bad_request_with_rbep)
866            })?;
867
868        debug!(
869            "Established OSCORE context with recipient ID {:?} and authorization {:?} through ACE-OSCORE",
870            oscore.recipient_id(),
871            Debug2Format(&generalclaims)
872        );
873        // FIXME: This should be flagged as "unconfirmed" for rapid eviction, as it could be part
874        // of a replay.
875        let _evicted = self.pool.force_insert(SecContextState {
876            protocol_stage: SecContextStage::Oscore(oscore),
877            authorization: Some(generalclaims),
878        });
879
880        Ok(response)
881    }
882}
883
884/// A wrapper around for a handler's inner RequestData used by [`OscoreEdhocHandler`] both for
885/// OSCORE and plain text requests.
886///
887/// Other crates should not rely on this (but making it an enum wrapped in a struct for privacy is
888/// considered excessive at this point).
889#[doc(hidden)]
890pub enum AuthorizationChecked<I> {
891    /// Middleware checks were successful, data was extracted
892    Allowed(I),
893    /// Middleware checks failed, return a 4.01 Unauthorized
894    NotAllowed,
895}
896
897/// Request state created by an [`OscoreEdhocHandler`] for successful non-plaintext cases.
898///
899/// Other crates should not rely on this (but making it an enum wrapped in a struct for privacy is
900/// considered excessive at this point).
901#[doc(hidden)]
902pub enum OwnRequestData<I> {
903    // Taking a small state here: We already have a slot in the pool, storing the big data there
904    #[expect(private_interfaces, reason = "should be addressed eventually")]
905    EdhocOkSend2(COwn),
906    // Could have a state Message3Processed -- but do we really want to implement that? (like, just
907    // use the EDHOC option)
908    EdhocOscoreRequest {
909        #[expect(private_interfaces, reason = "should be addressed eventually")]
910        kid: COwn,
911        correlation: liboscore::raw::oscore_requestid_t,
912        extracted: AuthorizationChecked<I>,
913    },
914    ProcessedToken(crate::ace::AceCborAuthzInfoResponse),
915}
916
917// FIXME: It'd be tempting to implement Drop for Response to set the slot back to Empty -- but
918// that'd be easier if we could avoid the Drop during enum destructuring, which AIU is currently
919// not supported in match or let destructuring. (But our is_gc_eligible should be good enough
920// anyway).
921
922/// Renders a [`lakers::MessageBufferError`] into the common Error type.
923///
924/// It is yet to be determined whether anything more informative should be returned (likely it
925/// should; maybe Request Entity Too Large or some error code about unusable credential.
926///
927/// Places using this function may be simplified if From/Into is specified (possibly after
928/// enlarging the Error type)
929#[track_caller]
930#[expect(
931    clippy::needless_pass_by_value,
932    reason = "ergonomics at the call sites need this"
933)]
934fn too_small(e: lakers::MessageBufferError) -> CoAPError {
935    #[allow(
936        clippy::match_same_arms,
937        reason = "https://github.com/rust-lang/rust-clippy/issues/13522"
938    )]
939    match e {
940        lakers::MessageBufferError::BufferAlreadyFull => {
941            error!("Lakers buffer size exceeded: Buffer full.");
942        }
943        lakers::MessageBufferError::SliceTooLong => {
944            error!("Lakers buffer size exceeded: Slice too long.");
945        }
946    }
947    CoAPError::bad_request()
948}
949
950/// Renders a [`lakers::EDHOCError`] into the common Error type.
951///
952/// It is yet to be decided based on the EDHOC specification which
953/// [`EDHOCError`][lakers::EDHOCError] values would be reported with precise data, and which should
954/// rather produce a generic response.
955///
956/// Places using this function may be simplified if From/Into is specified (possibly after
957/// enlarging the Error type)
958#[track_caller]
959#[expect(
960    clippy::needless_pass_by_value,
961    reason = "ergonomics at the call sites need this"
962)]
963fn render_error(e: lakers::EDHOCError) -> CoAPError {
964    #[allow(
965        clippy::match_same_arms,
966        reason = "https://github.com/rust-lang/rust-clippy/issues/13522"
967    )]
968    match e {
969        lakers::EDHOCError::UnexpectedCredential => error!("Lakers error: UnexpectedCredential"),
970        lakers::EDHOCError::MissingIdentity => error!("Lakers error: MissingIdentity"),
971        lakers::EDHOCError::IdentityAlreadySet => error!("Lakers error: IdentityAlreadySet"),
972        lakers::EDHOCError::MacVerificationFailed => error!("Lakers error: MacVerificationFailed"),
973        lakers::EDHOCError::UnsupportedMethod => error!("Lakers error: UnsupportedMethod"),
974        lakers::EDHOCError::UnsupportedCipherSuite => {
975            error!("Lakers error: UnsupportedCipherSuite");
976        }
977        lakers::EDHOCError::ParsingError => error!("Lakers error: ParsingError"),
978        lakers::EDHOCError::EncodingError => error!("Lakers error: EncodingError"),
979        lakers::EDHOCError::CredentialTooLongError => {
980            error!("Lakers error: CredentialTooLongError");
981        }
982        lakers::EDHOCError::EadLabelTooLongError => error!("Lakers error: EadLabelTooLongError"),
983        lakers::EDHOCError::EadTooLongError => error!("Lakers error: EadTooLongError"),
984        lakers::EDHOCError::EADUnprocessable => error!("Lakers error: EADUnprocessable"),
985        lakers::EDHOCError::AccessDenied => error!("Lakers error: AccessDenied"),
986        _ => error!("Lakers error (unknown)"),
987    }
988    CoAPError::bad_request()
989}
990
991/// An Either-style type used internally by [`OscoreEdhocHandler`].
992///
993/// Other crates should not rely on this (but making it an enum wrapped in a struct for privacy is
994/// considered excessive at this point).
995#[doc(hidden)]
996#[derive(Debug)]
997pub enum OrInner<O, I> {
998    Own(O),
999    Inner(I),
1000}
1001
1002impl<O, I> From<O> for OrInner<O, I> {
1003    fn from(own: O) -> Self {
1004        OrInner::Own(own)
1005    }
1006}
1007
1008impl<O: RenderableOnMinimal, I: RenderableOnMinimal> RenderableOnMinimal for OrInner<O, I> {
1009    type Error<IE>
1010        = OrInner<O::Error<IE>, I::Error<IE>>
1011    where
1012        IE: RenderableOnMinimal,
1013        IE: core::fmt::Debug;
1014    fn render<M: MinimalWritableMessage>(
1015        self,
1016        msg: &mut M,
1017    ) -> Result<(), Self::Error<M::UnionError>> {
1018        match self {
1019            OrInner::Own(own) => own.render(msg).map_err(OrInner::Own),
1020            OrInner::Inner(inner) => inner.render(msg).map_err(OrInner::Inner),
1021        }
1022    }
1023}
1024
1025impl<
1026    H: coap_handler::Handler,
1027    Crypto: lakers::Crypto,
1028    CryptoFactory: Fn() -> Crypto,
1029    SSC: ServerSecurityConfig,
1030    RNG: rand_core::RngCore + rand_core::CryptoRng,
1031    TP: TimeProvider,
1032> coap_handler::Handler for OscoreEdhocHandler<H, Crypto, CryptoFactory, SSC, RNG, TP>
1033{
1034    type RequestData = OrInner<
1035        OwnRequestData<Result<H::RequestData, H::ExtractRequestError>>,
1036        AuthorizationChecked<H::RequestData>,
1037    >;
1038
1039    type ExtractRequestError = OrInner<CoAPError, H::ExtractRequestError>;
1040    type BuildResponseError<M: MinimalWritableMessage> =
1041        OrInner<Result<CoAPError, M::UnionError>, H::BuildResponseError<M>>;
1042
1043    #[expect(clippy::too_many_lines, reason = "no good refactoring point known")]
1044    fn extract_request_data<M: ReadableMessage>(
1045        &mut self,
1046        request: &M,
1047    ) -> Result<Self::RequestData, Self::ExtractRequestError> {
1048        use OrInner::{Inner, Own};
1049
1050        #[derive(Default, Debug)]
1051        // SSC could be boolean AS_PARSES_TOKENS but not until feature(generic_const_exprs)
1052        enum Recognition<SSC: ServerSecurityConfig> {
1053            #[default]
1054            Start,
1055            /// Seen an OSCORE option
1056            Oscore { oscore: OscoreOption },
1057            /// Seen an OSCORE option and an EDHOC option
1058            Edhoc { oscore: OscoreOption },
1059            /// Seen path ".well-known" (after not having seen an OSCORE option)
1060            WellKnown,
1061            /// Seen path ".well-known" and "edhoc"
1062            WellKnownEdhoc,
1063            /// Seen path "authz-info"
1064            // FIXME: Should we allow arbitrary paths here?
1065            //
1066            // Also, in the !PARSES_TOKENS case, this would ideally be marked uninhabitable, but that's
1067            // hard to express in associated types and functions.
1068            //
1069            // Also, the PhantomData doesn't actually need to be precisely in here, but it needs to
1070            // be somewhere.
1071            AuthzInfo(PhantomData<SSC>),
1072            /// Seen anything else (where the request handler, or more likely the ACL filter, will
1073            /// trip over the critical options)
1074            Unencrypted,
1075        }
1076        #[allow(clippy::enum_glob_use, reason = "local use")]
1077        use Recognition::*;
1078
1079        impl<SSC: ServerSecurityConfig> Recognition<SSC> {
1080            /// Given a state and an option, produce the next state and whether the option should
1081            /// be counted as consumed for the purpose of assessing .well-known/edchoc's
1082            /// [`ignore_elective_others()`][coap_message_utils::option_processing::OptionsExt::ignore_elective_others].
1083            fn update(self, o: &impl MessageOption) -> (Self, bool) {
1084                use coap_numbers::option;
1085
1086                match (self, o.number(), o.value()) {
1087                    // FIXME: Store full value (but a single one is sufficient while we do EDHOC
1088                    // extraction)
1089                    (Start, option::OSCORE, optval) if has_oscore::<SSC>() => match optval.try_into() {
1090                        Ok(oscore) => (Oscore { oscore }, false),
1091                        _ => (Start, true),
1092                    },
1093                    (Start, option::URI_PATH, b".well-known") if SSC::HAS_EDHOC /* or anything else that lives in here */ => (WellKnown, false),
1094                    (Start, option::URI_PATH, b"authz-info") if SSC::PARSES_TOKENS => {
1095                        (AuthzInfo(PhantomData), false)
1096                    }
1097                    (Start, option::URI_PATH, _) => (Unencrypted, true /* doesn't matter */),
1098                    (Oscore { oscore }, option::EDHOC, b"") if SSC::HAS_EDHOC => {
1099                        (Edhoc { oscore }, true /* doesn't matter */)
1100                    }
1101                    (WellKnown, option::URI_PATH, b"edhoc") if SSC::HAS_EDHOC => (WellKnownEdhoc, false),
1102                    (AuthzInfo(ai), option::CONTENT_FORMAT, &[19]) if SSC::PARSES_TOKENS => {
1103                        (AuthzInfo(ai), false)
1104                    }
1105                    (AuthzInfo(ai), option::ACCEPT, &[19]) if SSC::PARSES_TOKENS => {
1106                        (AuthzInfo(ai), false)
1107                    }
1108                    (any, _, _) => (any, true),
1109                }
1110            }
1111
1112            /// Return true if the options in a request are only handled by this handler
1113            ///
1114            /// In all other cases, critical options are allowed to be passed on; the next-stage
1115            /// processor check on its own.
1116            fn errors_handled_here(&self) -> bool {
1117                match self {
1118                    WellKnownEdhoc | AuthzInfo(_) => true,
1119                    Start | Oscore { .. } | Edhoc { .. } | WellKnown | Unencrypted => false,
1120                }
1121            }
1122        }
1123
1124        // This will always be Some in practice, just taken while it is being updated.
1125        let mut state = Some(Recognition::<SSC>::Start);
1126
1127        // Some small potential for optimization by cutting iteration short on Edhoc, but probably
1128        // not worth it.
1129        let extra_options = request
1130            .options()
1131            .filter(|o| {
1132                let (new_state, filter) = state.take().unwrap().update(o);
1133                state = Some(new_state);
1134                filter
1135            })
1136            // FIXME: This aborts early on critical options, even when the result is later ignored
1137            .ignore_elective_others();
1138        let state = state.unwrap();
1139
1140        if state.errors_handled_here()
1141            && let Err(error) = extra_options
1142        {
1143            // Critical options in all other cases are handled by the Unencrypted or Oscore
1144            // handlers
1145            return Err(Own(error));
1146        }
1147
1148        let require_post = || {
1149            if coap_numbers::code::POST == request.code().into() {
1150                Ok(())
1151            } else {
1152                Err(CoAPError::method_not_allowed())
1153            }
1154        };
1155
1156        match state {
1157            Start | WellKnown | Unencrypted => {
1158                if self.authorities.nosec_authorization().is_some_and(|s| {
1159                    s.scope().request_is_allowed(request)
1160                        && s.time_constraint().is_valid_with(&mut self.time)
1161                }) {
1162                    self.inner
1163                        .extract_request_data(request)
1164                        .map(|extracted| Inner(AuthorizationChecked::Allowed(extracted)))
1165                        .map_err(Inner)
1166                } else {
1167                    Ok(Inner(AuthorizationChecked::NotAllowed))
1168                }
1169            }
1170            WellKnownEdhoc => {
1171                if !SSC::HAS_EDHOC {
1172                    unreachable!("State is not constructed");
1173                }
1174                require_post()?;
1175                self.extract_edhoc(&request).map(Own).map_err(Own)
1176            }
1177            AuthzInfo(_) => {
1178                if !SSC::PARSES_TOKENS {
1179                    // This makes extract_token and everything down the line effectively dead code on
1180                    // setups with empty SSC, without triggering clippy's nervous dead code warnings.
1181                    //
1182                    // The compiler should be able to eliminiate even this one statement based on
1183                    // this variant not being constructed under the same condition, but that
1184                    // property is not being tested.
1185                    unreachable!("State is not constructed");
1186                }
1187                require_post()?;
1188                self.extract_token(request.payload())
1189                    .map(|r| Own(OwnRequestData::ProcessedToken(r)))
1190                    .map_err(Own)
1191            }
1192            Edhoc { oscore } => {
1193                if !SSC::HAS_EDHOC {
1194                    // We wouldn't get that far in a non-EDHOC situation because the option is not processed,
1195                    // but the optimizer may not see that, and this is the place where a reviewer of
1196                    // extract_oscore_edhoc can convince themself that indeed the with_edhoc=true case is
1197                    // unreachable when HAS_EDHOC is not set.
1198                    unreachable!("State is not constructed");
1199                }
1200                self.extract_oscore_edhoc(&request, &oscore, true)
1201                    .map(Own)
1202                    .map_err(Own)
1203            }
1204            Oscore { oscore } => {
1205                if !has_oscore::<SSC>() {
1206                    unreachable!("State is not constructed");
1207                }
1208                self.extract_oscore_edhoc(&request, &oscore, false)
1209                    .map(Own)
1210                    .map_err(Own)
1211            }
1212        }
1213    }
1214    fn estimate_length(&mut self, req: &Self::RequestData) -> usize {
1215        match req {
1216            OrInner::Own(_) => 2 + lakers::MAX_BUFFER_LEN,
1217            OrInner::Inner(AuthorizationChecked::Allowed(i)) => self.inner.estimate_length(i),
1218            OrInner::Inner(AuthorizationChecked::NotAllowed) => 1,
1219        }
1220    }
1221    fn build_response<M: MutableWritableMessage>(
1222        &mut self,
1223        response: &mut M,
1224        req: Self::RequestData,
1225    ) -> Result<(), Self::BuildResponseError<M>> {
1226        use OrInner::{Inner, Own};
1227
1228        match req {
1229            Own(OwnRequestData::EdhocOkSend2(c_r)) => {
1230                if !SSC::HAS_EDHOC {
1231                    unreachable!("State is not constructed");
1232                }
1233                self.build_edhoc_message_2(response, c_r).map_err(Own)?;
1234            }
1235            Own(OwnRequestData::ProcessedToken(r)) => {
1236                if !SSC::PARSES_TOKENS {
1237                    unreachable!("State is not constructed");
1238                }
1239                r.render(response).map_err(|e| Own(Err(e)))?;
1240            }
1241            Own(OwnRequestData::EdhocOscoreRequest {
1242                kid,
1243                correlation,
1244                extracted,
1245            }) => {
1246                if !has_oscore::<SSC>() {
1247                    unreachable!("State is not constructed");
1248                }
1249                self.build_oscore_response(response, kid, correlation, extracted)
1250                    .map_err(Own)?;
1251            }
1252            Inner(AuthorizationChecked::Allowed(i)) => {
1253                self.inner.build_response(response, i).map_err(Inner)?;
1254            }
1255            Inner(AuthorizationChecked::NotAllowed) => {
1256                self.authorities
1257                    .render_not_allowed(response)
1258                    .map_err(|_| Own(Ok(CoAPError::unauthorized())))?;
1259            }
1260        }
1261        Ok(())
1262    }
1263}