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}