Repository: openwsn-berkeley/lakers (language: python)

Issue:
C_x: Represent using own ConnectionIdentifier newtype
This makes the APIs clearer and paves the way for adding support for longer C_x.

Contributes-To: https://github.com/openwsn-berkeley/lakers/issues/258

Marked as a draft while waiting for CI checks (I only did `cargo test` which usually misses some)
C_x only support u8
We currently only support C_x as u8. Given that the peer may choose a longer one, this is an interoperability hazard.

As far as I have found, there is no limit to their length in EDHOC, so the implementation will need to pick a limit, which can be reasonably 1, but right now this is not configurable.


Discussion / hints:

As a first step, I'll start wrapping the u8 in a newtype that'll allow changing this later.

Fix patch:
diff --git a/examples/coap/src/bin/coapclient.rs b/examples/coap/src/bin/coapclient.rs
index c04bd695..930e54b7 100644
--- a/examples/coap/src/bin/coapclient.rs
+++ b/examples/coap/src/bin/coapclient.rs
@@ -51,7 +51,7 @@ fn client_handshake() -> Result<(), EDHOCError> {
     let valid_cred_r = credential_check_or_fetch(Some(cred_r), id_cred_r).unwrap();
     let initiator = initiator.verify_message_2(&I, cred_i, valid_cred_r)?;
 
-    let mut msg_3 = Vec::from([c_r]);
+    let mut msg_3 = Vec::from(c_r.as_cbor());
     let (mut initiator, message_3, prk_out) =
         initiator.prepare_message_3(CredentialTransfer::ByReference, &None)?;
     msg_3.extend_from_slice(message_3.as_slice());
diff --git a/examples/coap/src/bin/coapserver-coaphandler.rs b/examples/coap/src/bin/coapserver-coaphandler.rs
index 65641a01..9705ff3c 100644
--- a/examples/coap/src/bin/coapserver-coaphandler.rs
+++ b/examples/coap/src/bin/coapserver-coaphandler.rs
@@ -21,7 +21,7 @@ const W_TV: &[u8] = &hex!("4E5E15AB35008C15B89E91F9F329164D4AACD53D9923672CE0019
 
 #[derive(Debug)]
 struct EdhocHandler {
-    connections: Vec<(u8, EdhocResponderWaitM3<Crypto>)>,
+    connections: Vec<(ConnId, EdhocResponderWaitM3<Crypto>)>,
     mock_server: ZeroTouchServer,
 }
 
@@ -48,7 +48,7 @@ fn render_error(_e: EDHOCError) -> Error {
 }
 
 impl EdhocHandler {
-    fn take_connection_by_c_r(&mut self, c_r: u8) -> Option<EdhocResponderWaitM3<Crypto>> {
+    fn take_connection_by_c_r(&mut self, c_r: ConnId) -> Option<EdhocResponderWaitM3<Crypto>> {
         let index = self
             .connections
             .iter()
@@ -58,7 +58,7 @@ impl EdhocHandler {
         Some(self.connections.pop().unwrap().1)
     }
 
-    fn new_c_r(&self) -> u8 {
+    fn new_c_r(&self) -> ConnId {
         // FIXME: We'll need to do better, but a) that'll be more practical when we can do more
         // than u8, and b) that'll best be coordinated with a storage that not only stores EDHOC
         // contexts but also OSCORE ones.
@@ -66,7 +66,7 @@ impl EdhocHandler {
         if result >= 24 {
             panic!("Contexts exceeded");
         }
-        result as _
+        ConnId::from_int_raw(result as _)
     }
 }
 
@@ -74,7 +74,7 @@ enum EdhocResponse {
     // We could also store the responder in the Vec (once we're done rendering the response, we'll
     // take up a slot there anyway) if we make it an enum.
     OkSend2 {
-        c_r: u8,
+        c_r: ConnId,
         responder: EdhocResponderProcessedM1<'static, Crypto>,
         // FIXME: Is the ead_2 the most practical data to store here? An easy alternative is the
         // voucher_response; ideal would be the voucher, bu that is only internal to prepare_ead_2.
@@ -113,7 +113,7 @@ impl coap_handler::Handler for EdhocHandler {
             let message_1 =
                 &EdhocMessageBuffer::new_from_slice(&request.payload()[1..]).map_err(too_small)?;
 
-            let (responder, ead_1) =
+            let (responder, _c_i, ead_1) =
                 EdhocResponder::new(lakers_crypto::default_crypto(), &R, cred_r)
                     .process_message_1(message_1)
                     .map_err(render_error)?;
@@ -149,15 +149,19 @@ impl coap_handler::Handler for EdhocHandler {
         } else {
             // potentially message 3
             //
-            // We know our short C_R lengths
+            // FIXME: Work with longer IDs as well (https://github.com/openwsn-berkeley/lakers/issues/258)
             let (c_r_rcvd, message_3) = request
                 .payload()
                 .split_first()
                 // FIXME: Being way too short, is that an EDHOC error or a CoAP error?
                 .ok_or_else(Error::bad_request)?;
 
+            // FIXME: This panics or creates an in valid ConnId (but once we fix
+            // working with longer IDs, there will be a function that has proper error handling)
+            let c_r_rcvd = ConnId::from_int_raw(*c_r_rcvd);
+
             let responder = self
-                .take_connection_by_c_r(*c_r_rcvd)
+                .take_connection_by_c_r(c_r_rcvd)
                 // FIXME: Produce proper error
                 .ok_or_else(Error::bad_request)?;
 
diff --git a/examples/coap/src/bin/coapserver.rs b/examples/coap/src/bin/coapserver.rs
index a058ff0a..c429d3c5 100644
--- a/examples/coap/src/bin/coapserver.rs
+++ b/examples/coap/src/bin/coapserver.rs
@@ -48,7 +48,7 @@ fn main() {
                     .expect("wrong length");
                 let result = responder.process_message_1(&message_1);
 
-                if let Ok((responder, ead_1)) = result {
+                if let Ok((responder, _c_i, ead_1)) = result {
                     let c_r =
                         generate_connection_identifier_cbor(&mut lakers_crypto::default_crypto());
                     let ead_2 = if let Some(ead_1) = ead_1 {
@@ -83,7 +83,7 @@ fn main() {
             } else {
                 // potentially message 3
                 println!("Received message 3");
-                let c_r_rcvd = request.message.payload[0];
+                let c_r_rcvd = ConnId::from_int_raw(request.message.payload[0]);
                 // FIXME let's better not *panic here
                 let responder = take_state(c_r_rcvd, &mut edhoc_connections).unwrap();
 
@@ -141,8 +141,8 @@ fn main() {
 }
 
 fn take_state<R>(
-    c_r_rcvd: u8,
-    edhoc_protocol_states: &mut Vec<(u8, R)>,
+    c_r_rcvd: ConnId,
+    edhoc_protocol_states: &mut Vec<(ConnId, R)>,
 ) -> Result<R, &'static str> {
     for (i, element) in edhoc_protocol_states.iter().enumerate() {
         let (c_r, _responder) = element;
diff --git a/examples/lakers-no_std/src/main.rs b/examples/lakers-no_std/src/main.rs
index b6aa8936..bf859276 100644
--- a/examples/lakers-no_std/src/main.rs
+++ b/examples/lakers-no_std/src/main.rs
@@ -83,8 +83,8 @@ fn main() -> ! {
     fn test_prepare_message_1() {
         let mut initiator = EdhocInitiator::new(lakers_crypto::default_crypto());
 
-        let c_i: u8 =
-            generate_connection_identifier_cbor(&mut lakers_crypto::default_crypto()).into();
+        let c_i =
+            generate_connection_identifier_cbor(&mut lakers_crypto::default_crypto()).as_slice();
         let message_1 = initiator.prepare_message_1(None, &None);
         assert!(message_1.is_ok());
     }
@@ -101,7 +101,7 @@ fn main() -> ! {
 
         let (initiator, message_1) = initiator.prepare_message_1(None, &None).unwrap();
 
-        let (responder, _ead_1) = responder.process_message_1(&message_1).unwrap();
+        let (responder, _c_i, _ead_1) = responder.process_message_1(&message_1).unwrap();
         let (responder, message_2) = responder
             .prepare_message_2(CredentialTransfer::ByReference, None, &None)
             .unwrap();
diff --git a/lakers-c/src/initiator.rs b/lakers-c/src/initiator.rs
index 4d9a1a27..4f07cc09 100644
--- a/lakers-c/src/initiator.rs
+++ b/lakers-c/src/initiator.rs
@@ -56,7 +56,7 @@ pub unsafe extern "C" fn initiator_prepare_message_1(
     let c_i = if c_i.is_null() {
         generate_connection_identifier_cbor(crypto)
     } else {
-        *c_i
+        ConnId::from_int_raw(*c_i)
     };
 
     let ead_1 = if ead_1_c.is_null() {
@@ -108,7 +108,9 @@ pub unsafe extern "C" fn initiator_parse_message_2(
     let result = match i_parse_message_2(&state, crypto, &(*message_2)) {
         Ok((state, c_r, id_cred_r, ead_2)) => {
             ProcessingM2C::copy_into_c(state, &mut (*initiator_c).processing_m2);
-            *c_r_out = c_r;
+            let c_r = c_r.as_slice();
+            assert_eq!(c_r.len(), 1, "C API only supports short C_R");
+            *c_r_out = c_r[0];
             *id_cred_r_out = id_cred_r;
             if let Some(ead_2) = ead_2 {
                 EADItemC::copy_into_c(ead_2, ead_2_c_out);
diff --git a/lakers-c/src/lib.rs b/lakers-c/src/lib.rs
index af7c67e8..1476e30c 100644
--- a/lakers-c/src/lib.rs
+++ b/lakers-c/src/lib.rs
@@ -86,7 +86,7 @@ impl ProcessingM2C {
             x: self.x,
             g_y: self.g_y,
             plaintext_2: self.plaintext_2,
-            c_r: self.c_r,
+            c_r: ConnId::from_int_raw(self.c_r),
             ead_2: if self.ead_2.is_null() {
                 None
             } else {
@@ -107,7 +107,9 @@ impl ProcessingM2C {
         (*processing_m2_c).x = processing_m2.x;
         (*processing_m2_c).g_y = processing_m2.g_y;
         (*processing_m2_c).plaintext_2 = processing_m2.plaintext_2;
-        (*processing_m2_c).c_r = processing_m2.c_r;
+        let c_r = processing_m2.c_r.as_slice();
+        assert_eq!(c_r.len(), 1, "C API only supports short C_R");
+        (*processing_m2_c).c_r = c_r[0];
     }
 }
 
diff --git a/lakers-python/src/initiator.rs b/lakers-python/src/initiator.rs
index 1519aca4..0edaaf63 100644
--- a/lakers-python/src/initiator.rs
+++ b/lakers-python/src/initiator.rs
@@ -40,11 +40,13 @@ impl PyEdhocInitiator {
     fn prepare_message_1<'a>(
         &mut self,
         py: Python<'a>,
-        c_i: Option<u8>,
+        c_i: Option<Vec<u8>>,
         ead_1: Option<EADItem>,
     ) -> PyResult<&'a PyBytes> {
         let c_i = match c_i {
-            Some(c_i) => c_i,
+            Some(c_i) => ConnId::from_slice(c_i.as_slice()).ok_or(
+                pyo3::exceptions::PyValueError::new_err("Connection identifier out of range"),
+            )?,
             None => generate_connection_identifier_cbor(&mut default_crypto()),
         };
 
@@ -61,7 +63,7 @@ impl PyEdhocInitiator {
         &mut self,
         py: Python<'a>,
         message_2: Vec<u8>,
-    ) -> PyResult<(u8, &'a PyBytes, Option<EADItem>)> {
+    ) -> PyResult<(&'a PyBytes, &'a PyBytes, Option<EADItem>)> {
         let message_2 = EdhocMessageBuffer::new_from_slice(message_2.as_slice())?;
 
         match i_parse_message_2(&self.wait_m2, &mut default_crypto(), &message_2) {
@@ -72,6 +74,7 @@ impl PyEdhocInitiator {
                 } else {
                     PyBytes::new(py, id_cred_r.value.as_slice())
                 };
+                let c_r = PyBytes::new(py, c_r.as_slice());
                 Ok((c_r, id_cred_r, ead_2))
             }
             Err(error) => Err(error.into()),
diff --git a/lakers-python/src/responder.rs b/lakers-python/src/responder.rs
index ee254a62..20fe93a6 100644
--- a/lakers-python/src/responder.rs
+++ b/lakers-python/src/responder.rs
@@ -33,23 +33,31 @@ impl PyEdhocResponder {
         }
     }
 
-    fn process_message_1(&mut self, message_1: Vec<u8>) -> PyResult<Option<EADItem>> {
+    fn process_message_1<'a>(
+        &mut self,
+        py: Python<'a>,
+        message_1: Vec<u8>,
+    ) -> PyResult<(&'a PyBytes, Option<EADItem>)> {
         let message_1 = EdhocMessageBuffer::new_from_slice(message_1.as_slice())?;
-        let (state, ead_1) = r_process_message_1(&self.start, &mut default_crypto(), &message_1)?;
+        let (state, c_i, ead_1) =
+            r_process_message_1(&self.start, &mut default_crypto(), &message_1)?;
         self.processing_m1 = state;
+        let c_i = PyBytes::new(py, c_i.as_slice());
 
-        Ok(ead_1)
+        Ok((c_i, ead_1))
     }
 
     fn prepare_message_2<'a>(
         &mut self,
         py: Python<'a>,
         cred_transfer: CredentialTransfer,
-        c_r: Option<u8>,
+        c_r: Option<Vec<u8>>,
         ead_2: Option<EADItem>,
     ) -> PyResult<&'a PyBytes> {
         let c_r = match c_r {
-            Some(c_r) => c_r,
+            Some(c_r) => ConnId::from_slice(c_r.as_slice()).ok_or(
+                pyo3::exceptions::PyValueError::new_err("Connection identifier out of range"),
+            )?,
             None => generate_connection_identifier_cbor(&mut default_crypto()),
         };
         let mut r = BytesP256ElemLen::default();
diff --git a/lib/src/edhoc.rs b/lib/src/edhoc.rs
index d3622b67..f565770c 100644
--- a/lib/src/edhoc.rs
+++ b/lib/src/edhoc.rs
@@ -57,7 +57,7 @@ pub fn r_process_message_1(
     state: &ResponderStart,
     crypto: &mut impl CryptoTrait,
     message_1: &BufferMessage1,
-) -> Result<(ProcessingM1, Option<EADItem>), EDHOCError> {
+) -> Result<(ProcessingM1, ConnId, Option<EADItem>), EDHOCError> {
     // Step 1: decode message_1
     // g_x will be saved to the state
     if let Ok((method, suites_i, suites_i_len, g_x, c_i, ead_1)) = parse_message_1(message_1) {
@@ -78,6 +78,7 @@ pub fn r_process_message_1(
                         g_x,
                         h_message_1,
                     },
+                    c_i,
                     ead_1,
                 ))
             } else {
@@ -96,7 +97,7 @@ pub fn r_prepare_message_2(
     crypto: &mut impl CryptoTrait,
     cred_r: CredentialRPK,
     r: &BytesP256ElemLen, // R's static private DH key
-    c_r: u8,
+    c_r: ConnId,
     cred_transfer: CredentialTransfer,
     ead_2: &Option<EADItem>,
 ) -> Result<(WaitM3, BufferMessage2), EDHOCError> {
@@ -269,7 +270,7 @@ pub fn r_verify_message_3(
 pub fn i_prepare_message_1(
     state: &InitiatorStart,
     crypto: &mut impl CryptoTrait,
-    c_i: u8,
+    c_i: ConnId,
     ead_1: &Option<EADItem>, // FIXME: make it a list of EADItem
 ) -> Result<(WaitM2, BufferMessage1), EDHOCError> {
     // Encode message_1 as a sequence of CBOR encoded data items as specified in Section 5.2.1
@@ -302,7 +303,7 @@ pub fn i_parse_message_2<'a>(
     state: &WaitM2,
     crypto: &mut impl CryptoTrait,
     message_2: &BufferMessage2,
-) -> Result<(ProcessingM2, u8, CredentialRPK, Option<EADItem>), EDHOCError> {
+) -> Result<(ProcessingM2, ConnId, CredentialRPK, Option<EADItem>), EDHOCError> {
     let res = parse_message_2(message_2);
     if let Ok((g_y, ciphertext_2)) = res {
         let th_2 = compute_th_2(crypto, &g_y, &state.h_message_1);
@@ -497,7 +498,7 @@ fn encode_message_1(
     suites: &BytesSuites,
     suites_len: usize,
     g_x: &BytesP256ElemLen,
-    c_i: u8,
+    c_i: ConnId,
     ead_1: &Option<EADItem>,
 ) -> Result<BufferMessage1, EDHOCError> {
     let mut output = BufferMessage1::new();
@@ -535,8 +536,9 @@ fn encode_message_1(
     output.content[2 + raw_suites_len] = P256_ELEM_LEN as u8; // length of the byte string
     output.content[3 + raw_suites_len..3 + raw_suites_len + P256_ELEM_LEN]
         .copy_from_slice(&g_x[..]);
-    output.content[3 + raw_suites_len + P256_ELEM_LEN] = c_i;
-    output.len = 3 + raw_suites_len + P256_ELEM_LEN + 1;
+    let c_i = c_i.as_slice();
+    output.len = 3 + raw_suites_len + P256_ELEM_LEN + c_i.len();
+    output.content[3 + raw_suites_len + P256_ELEM_LEN..][..c_i.len()].copy_from_slice(c_i);
 
     if let Some(ead_1) = ead_1 {
         match encode_ead_item(ead_1) {
@@ -757,7 +759,7 @@ fn decrypt_message_3(
 
 // output must hold id_cred.len() + cred.len()
 fn encode_kdf_context(
-    c_r: Option<u8>, // only present for MAC_2
+    c_r: Option<ConnId>, // only present for MAC_2
     id_cred: &BytesIdCred,
     th: &BytesHashLen,
     cred: &[u8],
@@ -768,8 +770,9 @@ fn encode_kdf_context(
     let mut output: BytesMaxContextBuffer = [0x00; MAX_KDF_CONTEXT_LEN];
 
     let mut output_len = if let Some(c_r) = c_r {
-        output[0] = c_r;
-        1 // size of u8
+        let c_r = c_r.as_slice();
+        output[..c_r.len()].copy_from_slice(c_r);
+        c_r.len()
     } else {
         0 // no u8 encoded
     };
@@ -824,7 +827,7 @@ fn compute_mac_3(
 fn compute_mac_2(
     crypto: &mut impl CryptoTrait,
     prk_3e2m: &BytesHashLen,
-    c_r: u8,
+    c_r: ConnId,
     id_cred_r: &BytesIdCred,
     cred_r: &[u8],
     th_2: &BytesHashLen,
@@ -843,24 +846,25 @@ fn compute_mac_2(
 }
 
 fn encode_plaintext_2(
-    c_r: u8,
+    c_r: ConnId,
     id_cred_r: &IdCred,
     mac_2: &BytesMac2,
     ead_2: &Option<EADItem>,
 ) -> Result<BufferPlaintext2, EDHOCError> {
     let mut plaintext_2: BufferPlaintext2 = BufferPlaintext2::new();
-    plaintext_2.content[0] = c_r;
+    let c_r = c_r.as_slice();
+    plaintext_2.content[..c_r.len()].copy_from_slice(c_r);
 
     let offset_cred = match id_cred_r {
         IdCred::CompactKid(kid) => {
-            plaintext_2.content[1] = *kid;
-            2
+            plaintext_2.content[c_r.len()] = *kid;
+            c_r.len() + 1
         }
         IdCred::FullCredential(cred) => {
-            plaintext_2.content[1] = CBOR_BYTE_STRING;
-            plaintext_2.content[2] = cred.len() as u8;
-            plaintext_2.content[3..3 + cred.len()].copy_from_slice(cred);
-            3 + cred.len()
+            plaintext_2.content[c_r.len()] = CBOR_BYTE_STRING;
+            plaintext_2.content[c_r.len() + 1] = cred.len() as u8;
+            plaintext_2.content[c_r.len() + 2..][..cred.len()].copy_from_slice(cred);
+            c_r.len() + 2 + cred.len()
         }
     };
 
@@ -1006,7 +1010,7 @@ mod tests {
     const SUITES_I_TV_FIRST_TIME: BytesSuites = hex!("060000000000000000");
     const G_X_TV_FIRST_TIME: BytesP256ElemLen =
         hex!("741a13d7ba048fbb615e94386aa3b61bea5b3d8f65f32620b749bee8d278efa9");
-    const C_I_TV_FIRST_TIME: u8 = 0x0e;
+    const C_I_TV_FIRST_TIME: ConnId = ConnId::from_int_raw(0x0e);
     const MESSAGE_1_TV_FIRST_TIME: &str =
         "03065820741a13d7ba048fbb615e94386aa3b61bea5b3d8f65f32620b749bee8d278efa90e";
 
@@ -1016,7 +1020,7 @@ mod tests {
     const SUITES_I_TV: BytesSuites = hex!("060200000000000000");
     const G_X_TV: BytesP256ElemLen =
         hex!("8af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b6");
-    const C_I_TV: u8 = 0x37;
+    const C_I_TV: ConnId = ConnId::from_int_raw(0x37);
     const MESSAGE_1_TV: &str =
         "0382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b637";
     // below are a few truncated messages for the purpose of testing cipher suites
@@ -1039,7 +1043,7 @@ mod tests {
         "0382060258208af6f430ebe18d34184017a9a11bf511c8dff8f834730b96c1b7c8dbca2fc3b63720cccccc";
     const G_Y_TV: BytesP256ElemLen =
         hex!("419701d7f00a26c2dc587a36dd752549f33763c893422c8ea0f955a13a4ff5d5");
-    const C_R_TV: u8 = 0x27;
+    const C_R_TV: ConnId = ConnId::from_int_raw(0x27);
     const MESSAGE_2_TV: &str = "582b419701d7f00a26c2dc587a36dd752549f33763c893422c8ea0f955a13a4ff5d59862a1eef9e0e7e1886fcd";
     const CIPHERTEXT_2_TV: &str = "9862a1eef9e0e7e1886fcd";
     const H_MESSAGE_1_TV: BytesHashLen =
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
index 20859a7a..c28e1a57 100644
--- a/lib/src/lib.rs
+++ b/lib/src/lib.rs
@@ -107,8 +107,15 @@ impl<'a, Crypto: CryptoTrait> EdhocResponder<'a, Crypto> {
     pub fn process_message_1(
         mut self,
         message_1: &BufferMessage1,
-    ) -> Result<(EdhocResponderProcessedM1<'a, Crypto>, Option<EADItem>), EDHOCError> {
-        let (state, ead_1) = r_process_message_1(&self.state, &mut self.crypto, message_1)?;
+    ) -> Result<
+        (
+            EdhocResponderProcessedM1<'a, Crypto>,
+            ConnId,
+            Option<EADItem>,
+        ),
+        EDHOCError,
+    > {
+        let (state, c_i, ead_1) = r_process_message_1(&self.state, &mut self.crypto, message_1)?;
 
         Ok((
             EdhocResponderProcessedM1 {
@@ -117,6 +124,7 @@ impl<'a, Crypto: CryptoTrait> EdhocResponder<'a, Crypto> {
                 cred_r: self.cred_r,
                 crypto: self.crypto,
             },
+            c_i,
             ead_1,
         ))
     }
@@ -126,7 +134,7 @@ impl<'a, Crypto: CryptoTrait> EdhocResponderProcessedM1<'a, Crypto> {
     pub fn prepare_message_2(
         mut self,
         cred_transfer: CredentialTransfer,
-        c_r: Option<u8>,
+        c_r: Option<ConnId>,
         ead_2: &Option<EADItem>,
     ) -> Result<(EdhocResponderWaitM3<Crypto>, BufferMessage2), EDHOCError> {
         let c_r = match c_r {
@@ -253,7 +261,7 @@ impl<'a, Crypto: CryptoTrait> EdhocInitiator<Crypto> {
 
     pub fn prepare_message_1(
         mut self,
-        c_i: Option<u8>,
+        c_i: Option<ConnId>,
         ead_1: &Option<EADItem>,
     ) -> Result<(EdhocInitiatorWaitM2<Crypto>, EdhocMessageBuffer), EDHOCError> {
         let c_i = match c_i {
@@ -289,7 +297,7 @@ impl<'a, Crypto: CryptoTrait> EdhocInitiatorWaitM2<Crypto> {
     ) -> Result<
         (
             EdhocInitiatorProcessingM2<Crypto>,
-            u8,
+            ConnId,
             CredentialRPK,
             Option<EADItem>,
         ),
@@ -399,16 +407,16 @@ impl<Crypto: CryptoTrait> EdhocInitiatorDone<Crypto> {
     }
 }
 
-pub fn generate_connection_identifier_cbor<Crypto: CryptoTrait>(crypto: &mut Crypto) -> u8 {
+pub fn generate_connection_identifier_cbor<Crypto: CryptoTrait>(crypto: &mut Crypto) -> ConnId {
     let c_i = generate_connection_identifier(crypto);
-    if c_i >= 0 && c_i <= 23 {
+    ConnId::from_int_raw(if c_i >= 0 && c_i <= 23 {
         c_i as u8 // verbatim encoding of single byte integer
     } else if c_i < 0 && c_i >= -24 {
         // negative single byte integer encoding
         CBOR_NEG_INT_1BYTE_START - 1 + c_i.unsigned_abs()
     } else {
         0
-    }
+    })
 }
 
 /// generates an identifier that can be serialized as a single CBOR integer, i.e. -24 <= x <= 23
@@ -567,7 +575,7 @@ mod test {
         // ---- end initiator handling
 
         // ---- begin responder handling
-        let (responder, _ead_1) = responder.process_message_1(&message_1).unwrap();
+        let (responder, _c_i, _ead_1) = responder.process_message_1(&message_1).unwrap();
         // if ead_1: process ead_1
         // if needed: prepare ead_2
         let (responder, message_2) = responder
@@ -681,7 +689,7 @@ mod test_authz {
         let (initiator, message_1) = initiator.prepare_message_1(None, &Some(ead_1)).unwrap();
         device.set_h_message_1(initiator.state.h_message_1.clone());
 
-        let (responder, ead_1) = responder.process_message_1(&message_1).unwrap();
+        let (responder, _c_i, ead_1) = responder.process_message_1(&message_1).unwrap();
         let ead_2 = if let Some(ead_1) = ead_1 {
             let (authenticator, _loc_w, voucher_request) =
                 authenticator.process_ead_1(&ead_1, &message_1).unwrap();
diff --git a/shared/src/lib.rs b/shared/src/lib.rs
index 0e4e63b2..222ac402 100644
--- a/shared/src/lib.rs
+++ b/shared/src/lib.rs
@@ -102,6 +102,58 @@ pub type BytesMac = [u8; MAC_LENGTH];
 pub type BytesEncodedVoucher = [u8; ENCODED_VOUCHER_LEN];
 pub type EADMessageBuffer = EdhocMessageBuffer; // TODO: make it of size MAX_EAD_SIZE_LEN
 
+/// Value of C_R or C_I, as chosen by ourself or the peer.
+///
+/// Semantically, this is a byte string of some length.
+///
+/// This currently only supports numeric values (see
+/// https://github.com/openwsn-berkeley/lakers/issues/258), but may support larger values in the
+/// future.
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+// TODO: This should not be needed, there is nothing special about the value 0.
+#[derive(Default)]
+pub struct ConnId(u8);
+
+impl ConnId {
+    /// Construct a ConnId from the result of [`cbor_decoder::int_raw`], which is a
+    /// byte that represents a single positive or negative CBOR integer encoded in the 5 bits minor
+    /// type.
+    ///
+    /// Evolving from u8-only values, this could later interact with the decoder directly.
+    pub const fn from_int_raw(raw: u8) -> Self {
+        debug_assert!(raw >> 5 <= 1, "Major type is not an integer");
+        debug_assert!(raw & 0x1f < 24, "Value is not immediate");
+        Self(raw)
+    }
+
+    /// The bytes that form the identifier (an arbitrary byte string)
+    pub fn as_slice(&self) -> &[u8] {
+        core::slice::from_ref(&self.0)
+    }
+
+    /// The CBOR encoding of the identifier.
+    pub fn as_cbor(&self) -> &[u8] {
+        core::slice::from_ref(&self.0)
+    }
+
+    /// Try to construct a ConnId from a slice.
+    ///
+    /// This is the inverse of [Self::as_slice], and returns None if the identifier is too long
+    /// (or, if only the compact 48 values are supported, outside of that range).
+    pub fn from_slice(input: &[u8]) -> Option<Self> {
+        if input.len() != 1 {
+            return None;
+        }
+        if input[0] >> 5 <= 1 {
+            return None;
+        }
+        if input[0] & 0x1f < 24 {
+            return None;
+        }
+        Some(Self(input[0]))
+    }
+}
+
 #[derive(PartialEq, Debug)]
 #[non_exhaustive]
 pub enum EDHOCError {
@@ -202,7 +254,7 @@ pub struct ResponderStart {
 pub struct ProcessingM1 {
     pub y: BytesP256ElemLen,
     pub g_y: BytesP256ElemLen,
-    pub c_i: u8,
+    pub c_i: ConnId,
     pub g_x: BytesP256ElemLen, // ephemeral public key of the initiator
     pub h_message_1: BytesHashLen,
 }
@@ -230,7 +282,7 @@ pub struct ProcessingM2 {
     pub x: BytesP256ElemLen,
     pub g_y: BytesP256ElemLen,
     pub plaintext_2: EdhocMessageBuffer,
-    pub c_r: u8,
+    pub c_r: ConnId,
     pub ead_2: Option<EADItem>,
 }
 
@@ -532,7 +584,7 @@ mod edhoc_parser {
             BytesSuites,
             usize,
             BytesP256ElemLen,
-            u8,
+            ConnId,
             Option<EADItem>,
         ),
         EDHOCError,
@@ -545,7 +597,7 @@ mod edhoc_parser {
             g_x.copy_from_slice(decoder.bytes_sized(P256_ELEM_LEN)?);
 
             // consume c_i encoded as single-byte int (we still do not support bstr encoding)
-            let c_i = decoder.int_raw()?;
+            let c_i = ConnId::from_int_raw(decoder.int_raw()?);
 
             // if there is still more to parse, the rest will be the EAD_1
             if rcvd_message_1.len > decoder.position() {
@@ -600,12 +652,12 @@ mod edhoc_parser {
 
     pub fn decode_plaintext_2(
         plaintext_2: &BufferCiphertext2,
-    ) -> Result<(u8, IdCred, BytesMac2, Option<EADItem>), EDHOCError> {
+    ) -> Result<(ConnId, IdCred, BytesMac2, Option<EADItem>), EDHOCError> {
         let mut mac_2: BytesMac2 = [0x00; MAC_LENGTH_2];
 
         let mut decoder = CBORDecoder::new(plaintext_2.as_slice());
 
-        let c_r = decoder.int_raw()?;
+        let c_r = ConnId::from_int_raw(decoder.int_raw()?);
 
         // NOTE: if len of bstr is 1, it is a compact kid and therefore should have been encoded as int
         let id_cred_r = if CBOR_MAJOR_BYTE_STRING == CBORDecoder::type_of(decoder.current()?)


Test patch:
diff --git a/lakers-python/test/test_ead_authz.py b/lakers-python/test/test_ead_authz.py
index 4b17c0e3..491c2819 100644
--- a/lakers-python/test/test_ead_authz.py
+++ b/lakers-python/test/test_ead_authz.py
@@ -68,7 +68,7 @@ def test_handshake_with_authz():
     device.set_h_message_1(initiator.get_h_message_1())
 
     # responder
-    ead_1 = responder.process_message_1(message_1)
+    _c_i, ead_1 = responder.process_message_1(message_1)
     loc_w, voucher_request = authenticator.process_ead_1(ead_1, message_1)
     voucher_response = enrollment_server.handle_voucher_request(voucher_request)
     ead_2 = authenticator.prepare_ead_2(voucher_response)
diff --git a/lakers-python/test/test_lakers.py b/lakers-python/test/test_lakers.py
index 162ee920..e8e665e5 100644
--- a/lakers-python/test/test_lakers.py
+++ b/lakers-python/test/test_lakers.py
@@ -29,7 +29,7 @@ def test_handshake():
     message_1 = initiator.prepare_message_1(c_i=None, ead_1=None)
 
     # responder
-    ead_1 = responder.process_message_1(message_1)
+    _c_i, ead_1 = responder.process_message_1(message_1)
     assert ead_1 == None
     message_2 = responder.prepare_message_2(lakers.CredentialTransfer.ByReference, None, ead_1)
     assert type(message_2) == bytes