Skip to main content

confium_composite/
cose.rs

1//! COSE_Sign1 — CBOR-encoded signature wrapper (RFC 8152).
2//!
3//! Wraps a single signature in the COSE_Sign1 structure:
4//!
5//! ```text
6//! COSE_Sign1 = [
7//!     protected : bstr .cbor {1: algorithm},
8//!     unprotected : {},
9//!     payload : bstr,
10//!     signature : bstr
11//! ]
12//! ```
13//!
14//! Used in IoT and edge computing for compact binary signatures.
15//! The implementation uses a minimal CBOR encoder for the specific
16//! COSE_Sign1 structure — no external CBOR dependency.
17
18use serde::{Deserialize, Serialize};
19
20/// CBOR tag for COSE_Sign1 (RFC 8152 §4.1).
21pub const COSE_SIGN1_TAG: u64 = 18;
22
23/// Standard COSE algorithm parameter key.
24pub const COSE_ALG_PARAM: i32 = 1;
25
26/// Algorithm identifiers (subset of IANA COSE Algorithms registry).
27pub mod alg {
28    /// EdDSA (Ed25519).
29    pub const EDDSA: i32 = -8;
30    /// ECDSA with SHA-256 over P-256.
31    pub const ES256: i32 = -7;
32    /// Ed25519 signature.
33    pub const ED25519: i32 = -19;
34}
35
36/// A COSE_Sign1 structure.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub struct CoseSign1 {
39    /// Protected header (CBOR-encoded, raw bytes).
40    pub protected_bytes: Vec<u8>,
41    /// Unprotected header (raw bytes, usually empty).
42    pub unprotected_bytes: Vec<u8>,
43    /// Payload (the message being signed).
44    pub payload: Vec<u8>,
45    /// Signature bytes.
46    pub signature: Vec<u8>,
47}
48
49/// Errors during COSE operations.
50#[derive(Debug, thiserror::Error)]
51pub enum CoseError {
52    /// Encoding error.
53    #[error("cbor encoding error: {0}")]
54    Encode(String),
55    /// Decoding error.
56    #[error("cbor decoding error: {0}")]
57    Decode(String),
58}
59
60impl CoseSign1 {
61    /// Create a new COSE_Sign1 with a protected header containing
62    /// the algorithm identifier.
63    pub fn new(algorithm: i32, payload: &[u8], signature: &[u8]) -> Result<Self, CoseError> {
64        let protected = encode_protected_header(algorithm)?;
65        Ok(Self {
66            protected_bytes: protected,
67            unprotected_bytes: Vec::new(),
68            payload: payload.to_vec(),
69            signature: signature.to_vec(),
70        })
71    }
72
73    /// Encode to CBOR bytes (tagged with COSE_SIGN1_TAG).
74    pub fn encode(&self) -> Result<Vec<u8>, CoseError> {
75        encode_cose_sign1(self)
76    }
77
78    /// Decode from CBOR bytes.
79    pub fn decode(bytes: &[u8]) -> Result<Self, CoseError> {
80        decode_cose_sign1(bytes)
81    }
82
83    /// Extract the algorithm from the protected header.
84    pub fn algorithm(&self) -> Result<i32, CoseError> {
85        decode_algorithm(&self.protected_bytes)
86    }
87}
88
89// Minimal CBOR encoder
90
91const CBOR_MAJOR_UNSIGNED: u8 = 0;
92const CBOR_MAJOR_BYTE_STRING: u8 = 2;
93const CBOR_MAJOR_ARRAY: u8 = 4;
94const CBOR_MAJOR_MAP: u8 = 5;
95
96fn cbor_unsigned_int(n: u64) -> Vec<u8> {
97    if n <= 23 {
98        vec![(CBOR_MAJOR_UNSIGNED << 5) | n as u8]
99    } else if n <= u8::MAX as u64 {
100        let mut v = vec![(CBOR_MAJOR_UNSIGNED << 5) | 24];
101        v.push(n as u8);
102        v
103    } else if n <= u16::MAX as u64 {
104        let mut v = vec![(CBOR_MAJOR_UNSIGNED << 5) | 25];
105        v.extend_from_slice(&(n as u16).to_be_bytes());
106        v
107    } else if n <= u32::MAX as u64 {
108        let mut v = vec![(CBOR_MAJOR_UNSIGNED << 5) | 26];
109        v.extend_from_slice(&(n as u32).to_be_bytes());
110        v
111    } else {
112        let mut v = vec![(CBOR_MAJOR_UNSIGNED << 5) | 27];
113        v.extend_from_slice(&n.to_be_bytes());
114        v
115    }
116}
117
118fn cbor_negative_int(n: i64) -> Vec<u8> {
119    // CBOR negative integer: value = -(1 + unsigned).
120    // Encoded with major type 1, same info/length scheme as unsigned.
121    let unsigned = (-(n as i128) - 1) as u64;
122    if unsigned <= 23 {
123        vec![(1u8 << 5) | unsigned as u8]
124    } else if unsigned <= u8::MAX as u64 {
125        let mut v = vec![(1u8 << 5) | 24];
126        v.push(unsigned as u8);
127        v
128    } else if unsigned <= u16::MAX as u64 {
129        let mut v = vec![(1u8 << 5) | 25];
130        v.extend_from_slice(&(unsigned as u16).to_be_bytes());
131        v
132    } else if unsigned <= u32::MAX as u64 {
133        let mut v = vec![(1u8 << 5) | 26];
134        v.extend_from_slice(&(unsigned as u32).to_be_bytes());
135        v
136    } else {
137        let mut v = vec![(1u8 << 5) | 27];
138        v.extend_from_slice(&unsigned.to_be_bytes());
139        v
140    }
141}
142
143fn cbor_byte_string(bytes: &[u8]) -> Vec<u8> {
144    let len = bytes.len();
145    let mut v = vec![(CBOR_MAJOR_BYTE_STRING << 5)];
146    if len <= 23 {
147        v[0] |= len as u8;
148        v.extend_from_slice(bytes);
149    } else if len <= u8::MAX as usize {
150        v[0] |= 24;
151        v.push(len as u8);
152        v.extend_from_slice(bytes);
153    } else if len <= u16::MAX as usize {
154        v[0] |= 25;
155        v.extend_from_slice(&(len as u16).to_be_bytes());
156        v.extend_from_slice(bytes);
157    } else {
158        v[0] |= 26;
159        v.extend_from_slice(&(len as u32).to_be_bytes());
160        v.extend_from_slice(bytes);
161    }
162    v
163}
164
165fn cbor_array_header(count: usize) -> Vec<u8> {
166    let mut v = vec![(CBOR_MAJOR_ARRAY << 5)];
167    if count <= 23 {
168        v[0] |= count as u8;
169    } else if count <= u8::MAX as usize {
170        v[0] |= 24;
171        v.push(count as u8);
172    } else {
173        v[0] |= 25;
174        v.extend_from_slice(&(count as u16).to_be_bytes());
175    }
176    v
177}
178
179fn cbor_map_header(count: usize) -> Vec<u8> {
180    let mut v = vec![(CBOR_MAJOR_MAP << 5)];
181    if count <= 23 {
182        v[0] |= count as u8;
183    } else {
184        v[0] |= 24;
185        v.push(count as u8);
186    }
187    v
188}
189
190fn encode_protected_header(alg: i32) -> Result<Vec<u8>, CoseError> {
191    let mut buf = cbor_map_header(1);
192    buf.extend(cbor_unsigned_int(COSE_ALG_PARAM as u64));
193    if alg < 0 {
194        buf.extend(cbor_negative_int(alg as i64));
195    } else {
196        buf.extend(cbor_unsigned_int(alg as u64));
197    }
198    Ok(buf)
199}
200
201fn encode_cose_sign1(cose: &CoseSign1) -> Result<Vec<u8>, CoseError> {
202    let mut out = vec![];
203    // Tag 18 as CBOR major type 6 (tag).
204    // Major type 6 = 0xC0. For tag 18 (≤ 23): 0xC0 | 18 = 0xD2.
205    out.push(0xC0 | COSE_SIGN1_TAG as u8);
206
207    // Array of 4 elements.
208    out.extend(cbor_array_header(4));
209
210    // [0] protected header (bstr containing CBOR map)
211    out.extend(cbor_byte_string(&cose.protected_bytes));
212    // [1] unprotected header (bstr, usually empty map)
213    out.extend(cbor_byte_string(&cose.unprotected_bytes));
214    // [2] payload
215    out.extend(cbor_byte_string(&cose.payload));
216    // [3] signature
217    out.extend(cbor_byte_string(&cose.signature));
218
219    Ok(out)
220}
221
222// Minimal CBOR decoder
223
224struct CborReader<'a> {
225    bytes: &'a [u8],
226    pos: usize,
227}
228
229impl<'a> CborReader<'a> {
230    fn new(bytes: &'a [u8]) -> Self {
231        Self { bytes, pos: 0 }
232    }
233
234    fn read_u8(&mut self) -> Option<u8> {
235        self.bytes.get(self.pos).map(|&b| {
236            self.pos += 1;
237            b
238        })
239    }
240
241    fn read_bytes(&mut self, n: usize) -> Option<&'a [u8]> {
242        // checked_add: an adversarial CBOR length near usize::MAX must
243        // surface as a parse error, not an overflowing comparison that
244        // wraps and slices out of bounds.
245        let end = self.pos.checked_add(n)?;
246        if end > self.bytes.len() {
247            return None;
248        }
249        let result = &self.bytes[self.pos..end];
250        self.pos = end;
251        Some(result)
252    }
253
254    /// Sanity-check a declared array/map count against the remaining
255    /// input: every CBOR item needs at least one byte, so a count
256    /// larger than the bytes left can never be satisfied. Bounds the
257    /// Vec allocation to the input size instead of trusting an
258    /// adversarial length header (u64::MAX → capacity-overflow panic).
259    fn check_count(&self, count: usize) -> Option<()> {
260        if count > self.bytes.len() - self.pos {
261            None
262        } else {
263            Some(())
264        }
265    }
266
267    fn read(&mut self) -> Option<CborValue<'a>> {
268        let initial = self.read_u8()?;
269        let major = (initial & 0xE0) >> 5;
270        let info = initial & 0x1F;
271
272        match major {
273            0 => {
274                let n = self.read_uint(info)?;
275                Some(CborValue::Unsigned(n))
276            }
277            1 => {
278                let n = self.read_uint(info)?;
279                Some(CborValue::Negative(n))
280            }
281            2 => {
282                let len = self.read_uint(info)? as usize;
283                let bytes = self.read_bytes(len)?;
284                Some(CborValue::Bytes(bytes))
285            }
286            4 => {
287                let count = self.read_uint(info)? as usize;
288                self.check_count(count)?;
289                let mut items = Vec::with_capacity(count);
290                for _ in 0..count {
291                    items.push(self.read()?);
292                }
293                Some(CborValue::Array(items))
294            }
295            5 => {
296                let count = self.read_uint(info)? as usize;
297                self.check_count(count)?;
298                let mut entries = Vec::with_capacity(count);
299                for _ in 0..count {
300                    let k = self.read()?;
301                    let v = self.read()?;
302                    entries.push((k, v));
303                }
304                Some(CborValue::Map(entries))
305            }
306            6 => {
307                // Tag: read the tag value, then the tagged item.
308                let _tag = self.read_uint(info)?;
309                self.read()
310            }
311            _ => None,
312        }
313    }
314
315    fn read_uint(&mut self, info: u8) -> Option<u64> {
316        match info {
317            n if n <= 23 => Some(n as u64),
318            24 => Some(self.read_u8()? as u64),
319            25 => {
320                let bytes = self.read_bytes(2)?;
321                Some(u16::from_be_bytes(bytes.try_into().ok()?) as u64)
322            }
323            26 => {
324                let bytes = self.read_bytes(4)?;
325                Some(u32::from_be_bytes(bytes.try_into().ok()?) as u64)
326            }
327            27 => {
328                let bytes = self.read_bytes(8)?;
329                Some(u64::from_be_bytes(bytes.try_into().ok()?))
330            }
331            _ => None,
332        }
333    }
334}
335
336#[derive(Debug, Clone)]
337enum CborValue<'a> {
338    Unsigned(u64),
339    Negative(u64),
340    Bytes(&'a [u8]),
341    Array(Vec<CborValue<'a>>),
342    Map(Vec<(CborValue<'a>, CborValue<'a>)>),
343}
344
345fn decode_cose_sign1(bytes: &[u8]) -> Result<CoseSign1, CoseError> {
346    // The CBOR reader transparently handles tags (major type 6):
347    // read() returns the tagged item directly.
348    let mut reader = CborReader::new(bytes);
349    let array_value = reader
350        .read()
351        .ok_or_else(|| CoseError::Decode("empty".into()))?;
352    decode_cose_sign1_array(&array_value, bytes)
353}
354
355fn decode_cose_sign1_array(value: &CborValue<'_>, _bytes: &[u8]) -> Result<CoseSign1, CoseError> {
356    let items = match value {
357        CborValue::Array(v) => v,
358        _ => return Err(CoseError::Decode("expected array".into())),
359    };
360    if items.len() != 4 {
361        return Err(CoseError::Decode("expected 4 elements".into()));
362    }
363    let protected = match &items[0] {
364        CborValue::Bytes(b) => b.to_vec(),
365        _ => return Err(CoseError::Decode("protected must be bstr".into())),
366    };
367    let unprotected = match &items[1] {
368        CborValue::Bytes(b) => b.to_vec(),
369        _ => return Err(CoseError::Decode("unprotected must be bstr".into())),
370    };
371    let payload = match &items[2] {
372        CborValue::Bytes(b) => b.to_vec(),
373        _ => return Err(CoseError::Decode("payload must be bstr".into())),
374    };
375    let signature = match &items[3] {
376        CborValue::Bytes(b) => b.to_vec(),
377        _ => return Err(CoseError::Decode("signature must be bstr".into())),
378    };
379    Ok(CoseSign1 {
380        protected_bytes: protected,
381        unprotected_bytes: unprotected,
382        payload,
383        signature,
384    })
385}
386
387fn decode_algorithm(protected_bytes: &[u8]) -> Result<i32, CoseError> {
388    let mut reader = CborReader::new(protected_bytes);
389    let map = reader
390        .read()
391        .ok_or_else(|| CoseError::Decode("empty protected header".into()))?;
392    let entries = match map {
393        CborValue::Map(e) => e,
394        _ => return Err(CoseError::Decode("protected header must be a map".into())),
395    };
396    for (k, v) in entries {
397        if let CborValue::Unsigned(key) = k {
398            if key == COSE_ALG_PARAM as u64 {
399                match v {
400                    CborValue::Unsigned(n) => return Ok(n as i32),
401                    CborValue::Negative(n) => {
402                        // CBOR negative: -(1 + n)
403                        let neg = -((n as i64) + 1);
404                        return Ok(neg as i32);
405                    }
406                    _ => {}
407                }
408            }
409        }
410    }
411    Err(CoseError::Decode("algorithm not found".into()))
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn adversarial_u64_max_length_is_error_not_panic() {
420        // Byte-string header (major 2) with 8-byte length = u64::MAX.
421        // The old `pos + n` comparison overflowed, wrapped, and sliced
422        // out of bounds — a panic on adversarial input.
423        let evil = [0x5B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
424        let result = std::panic::catch_unwind(|| CoseSign1::decode(&evil));
425        assert!(
426            result.is_ok(),
427            "decoder must not panic on adversarial length"
428        );
429        assert!(result.unwrap().is_err(), "u64::MAX length must not decode");
430    }
431
432    #[test]
433    fn adversarial_huge_length_at_offset_is_error_not_panic() {
434        // Same, but positioned after some valid bytes so pos > 0 when
435        // the overflowing length is read.
436        let mut evil = vec![0x83, 0x01]; // array(3), unsigned(1)
437        evil.extend_from_slice(&[0x5B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE]);
438        let result = std::panic::catch_unwind(|| CoseSign1::decode(&evil));
439        assert!(result.is_ok(), "decoder must not panic");
440        assert!(result.unwrap().is_err());
441    }
442
443    #[test]
444    fn truncated_input_is_error_not_panic() {
445        for len in 0..8 {
446            let truncated = &b"\xD2\x84\x43\x01\x02\x03\x04\xA0"[..len];
447            assert!(CoseSign1::decode(truncated).is_err());
448        }
449    }
450
451    #[test]
452    fn create_and_extract_algorithm() {
453        let cose = CoseSign1::new(alg::ES256, b"payload", b"signature").unwrap();
454        assert_eq!(cose.algorithm().unwrap(), alg::ES256);
455    }
456
457    #[test]
458    fn round_trip_preserves_payload_and_signature() {
459        let original = CoseSign1::new(alg::ED25519, b"my payload", b"my sig").unwrap();
460        let encoded = original.encode().unwrap();
461        let decoded = CoseSign1::decode(&encoded).unwrap();
462        assert_eq!(decoded.payload, b"my payload");
463        assert_eq!(decoded.signature, b"my sig");
464    }
465
466    #[test]
467    fn cbor_unsigned_encoding_zero() {
468        assert_eq!(cbor_unsigned_int(0), vec![0x00]);
469    }
470
471    #[test]
472    fn cbor_unsigned_encoding_small() {
473        assert_eq!(cbor_unsigned_int(23), vec![23]);
474    }
475
476    #[test]
477    fn cbor_unsigned_encoding_one_byte() {
478        assert_eq!(cbor_unsigned_int(200), vec![24, 200]);
479    }
480
481    #[test]
482    fn cbor_byte_string_empty() {
483        assert_eq!(cbor_byte_string(b""), vec![0x40]);
484    }
485
486    #[test]
487    fn cbor_byte_string_short() {
488        assert_eq!(cbor_byte_string(b"abc"), vec![0x43, b'a', b'b', b'c']);
489    }
490
491    #[test]
492    fn negative_algorithm_encoding() {
493        let cose = CoseSign1::new(alg::EDDSA, b"", b"").unwrap();
494        assert_eq!(cose.algorithm().unwrap(), alg::EDDSA);
495    }
496
497    #[test]
498    fn decode_empty_payload() {
499        let cose = CoseSign1::new(alg::ES256, b"", b"sig").unwrap();
500        let encoded = cose.encode().unwrap();
501        let decoded = CoseSign1::decode(&encoded).unwrap();
502        assert!(decoded.payload.is_empty());
503    }
504
505    #[test]
506    fn decode_large_signature() {
507        let sig = vec![0xAA; 256];
508        let cose = CoseSign1::new(alg::ES256, b"msg", &sig).unwrap();
509        let encoded = cose.encode().unwrap();
510        let decoded = CoseSign1::decode(&encoded).unwrap();
511        assert_eq!(decoded.signature, sig);
512    }
513}