Skip to main content

tf_types/
cbor.rs

1//! In-house CBOR codec (RFC 8949) — TrustForge owns its codec layer; see
2//! `docs/dependency-audit.md`. Mirror of `tools/tf-types-ts/src/core/cbor.ts`.
3//!
4//! Scope: exactly what the TrustForge wire formats need.
5//!
6//! * **Encoding is deterministic**: smallest-width integer/length headers,
7//!   definite lengths only, floats always emitted as 8-byte doubles.
8//!   Map entries are emitted **in the order provided** — canonical key
9//!   ordering is the caller's contract (`binary_format` sorts JSON keys
10//!   lexicographically before building the tree; see the determinism
11//!   notes there). This module must stay byte-parity with the TS encoder
12//!   over `conformance/binary-format-vectors.yaml`.
13//! * **Decoding is hardened** for externally produced CBOR (WebAuthn
14//!   attestation objects, COSE keys): depth-limited, length headers are
15//!   validated against remaining input before any allocation, and
16//!   indefinite-length items are accepted (CTAP1-era encoders emit them)
17//!   but bounded by the same limits. Trailing bytes after the first
18//!   value are ignored, matching the previous decoder.
19
20use std::fmt;
21
22/// Maximum nesting depth accepted by the decoder. Deep enough for any
23/// real packet or attestation; shallow enough that a hostile input
24/// cannot blow the stack.
25const MAX_DEPTH: usize = 128;
26
27#[derive(Debug, Clone, PartialEq)]
28pub enum Value {
29    /// Whole CBOR integer range: unsigned 0..=u64::MAX and negative
30    /// -1..=-(u64::MAX+1) both fit in i128.
31    Integer(i128),
32    Bytes(Vec<u8>),
33    Text(String),
34    Array(Vec<Value>),
35    /// Entries keep insertion order; the encoder does not sort.
36    Map(Vec<(Value, Value)>),
37    Tag(u64, Box<Value>),
38    Bool(bool),
39    Null,
40    /// Also used for CBOR `undefined` (0xf7) on decode.
41    Float(f64),
42}
43
44impl Value {
45    /// Convenience: look up a text key in a map.
46    pub fn map_get(&self, key: &str) -> Option<&Value> {
47        match self {
48            Value::Map(entries) => entries.iter().find_map(|(k, v)| match k {
49                Value::Text(t) if t == key => Some(v),
50                _ => None,
51            }),
52            _ => None,
53        }
54    }
55
56    /// Convenience: look up an integer key in a map (COSE-style).
57    pub fn map_get_int(&self, key: i128) -> Option<&Value> {
58        match self {
59            Value::Map(entries) => entries.iter().find_map(|(k, v)| match k {
60                Value::Integer(i) if *i == key => Some(v),
61                _ => None,
62            }),
63            _ => None,
64        }
65    }
66
67    pub fn as_bytes(&self) -> Option<&[u8]> {
68        match self {
69            Value::Bytes(b) => Some(b),
70            _ => None,
71        }
72    }
73
74    pub fn as_text(&self) -> Option<&str> {
75        match self {
76            Value::Text(t) => Some(t),
77            _ => None,
78        }
79    }
80
81    pub fn as_integer(&self) -> Option<i128> {
82        match self {
83            Value::Integer(i) => Some(*i),
84            _ => None,
85        }
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum CborError {
91    /// Input ended before the value did.
92    Truncated,
93    /// Structurally invalid or unsupported input byte.
94    Invalid(&'static str),
95    /// Nesting exceeded [`MAX_DEPTH`].
96    TooDeep,
97    /// A value cannot be represented (encode side).
98    Unrepresentable(&'static str),
99}
100
101impl fmt::Display for CborError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            CborError::Truncated => write!(f, "truncated CBOR input"),
105            CborError::Invalid(what) => write!(f, "invalid CBOR: {what}"),
106            CborError::TooDeep => write!(f, "CBOR nesting exceeds depth limit"),
107            CborError::Unrepresentable(what) => write!(f, "unrepresentable in CBOR: {what}"),
108        }
109    }
110}
111
112impl std::error::Error for CborError {}
113
114/* ------------------------------------------------------------------ */
115/*  Encoding                                                           */
116/* ------------------------------------------------------------------ */
117
118fn put_header(out: &mut Vec<u8>, major: u8, arg: u64) {
119    let mt = major << 5;
120    if arg < 24 {
121        out.push(mt | arg as u8);
122    } else if arg <= u8::MAX as u64 {
123        out.push(mt | 24);
124        out.push(arg as u8);
125    } else if arg <= u16::MAX as u64 {
126        out.push(mt | 25);
127        out.extend_from_slice(&(arg as u16).to_be_bytes());
128    } else if arg <= u32::MAX as u64 {
129        out.push(mt | 26);
130        out.extend_from_slice(&(arg as u32).to_be_bytes());
131    } else {
132        out.push(mt | 27);
133        out.extend_from_slice(&arg.to_be_bytes());
134    }
135}
136
137fn encode_into(value: &Value, out: &mut Vec<u8>) -> Result<(), CborError> {
138    match value {
139        Value::Integer(i) => {
140            if *i >= 0 {
141                let u = u64::try_from(*i)
142                    .map_err(|_| CborError::Unrepresentable("integer above u64::MAX"))?;
143                put_header(out, 0, u);
144            } else {
145                let magnitude = i
146                    .checked_neg()
147                    .and_then(|m| m.checked_sub(1))
148                    .and_then(|m| u64::try_from(m).ok())
149                    .ok_or(CborError::Unrepresentable("integer below -2^64"))?;
150                put_header(out, 1, magnitude);
151            }
152        }
153        Value::Bytes(b) => {
154            put_header(out, 2, b.len() as u64);
155            out.extend_from_slice(b);
156        }
157        Value::Text(t) => {
158            put_header(out, 3, t.len() as u64);
159            out.extend_from_slice(t.as_bytes());
160        }
161        Value::Array(items) => {
162            put_header(out, 4, items.len() as u64);
163            for item in items {
164                encode_into(item, out)?;
165            }
166        }
167        Value::Map(entries) => {
168            put_header(out, 5, entries.len() as u64);
169            for (k, v) in entries {
170                encode_into(k, out)?;
171                encode_into(v, out)?;
172            }
173        }
174        Value::Tag(tag, inner) => {
175            put_header(out, 6, *tag);
176            encode_into(inner, out)?;
177        }
178        Value::Bool(b) => out.push(if *b { 0xf5 } else { 0xf4 }),
179        Value::Null => out.push(0xf6),
180        Value::Float(f) => {
181            // Always 8-byte doubles: parity with the TS encoder
182            // (cbor-x `useFloat32: 0`).
183            out.push(0xfb);
184            out.extend_from_slice(&f.to_be_bytes());
185        }
186    }
187    Ok(())
188}
189
190pub fn encode(value: &Value) -> Result<Vec<u8>, CborError> {
191    let mut out = Vec::new();
192    encode_into(value, &mut out)?;
193    Ok(out)
194}
195
196/* ------------------------------------------------------------------ */
197/*  Decoding                                                           */
198/* ------------------------------------------------------------------ */
199
200struct Decoder<'a> {
201    buf: &'a [u8],
202    pos: usize,
203}
204
205impl<'a> Decoder<'a> {
206    fn byte(&mut self) -> Result<u8, CborError> {
207        let b = *self.buf.get(self.pos).ok_or(CborError::Truncated)?;
208        self.pos += 1;
209        Ok(b)
210    }
211
212    fn take(&mut self, n: usize) -> Result<&'a [u8], CborError> {
213        // `n` was validated against remaining input by `arg_as_len`, but
214        // guard again so this helper is safe standalone.
215        let end = self.pos.checked_add(n).ok_or(CborError::Truncated)?;
216        if end > self.buf.len() {
217            return Err(CborError::Truncated);
218        }
219        let s = &self.buf[self.pos..end];
220        self.pos = end;
221        Ok(s)
222    }
223
224    fn arg(&mut self, info: u8) -> Result<u64, CborError> {
225        match info {
226            0..=23 => Ok(info as u64),
227            24 => Ok(self.byte()? as u64),
228            25 => Ok(u16::from_be_bytes(self.take(2)?.try_into().unwrap()) as u64),
229            26 => Ok(u32::from_be_bytes(self.take(4)?.try_into().unwrap()) as u64),
230            27 => Ok(u64::from_be_bytes(self.take(8)?.try_into().unwrap())),
231            _ => Err(CborError::Invalid("reserved additional-info value")),
232        }
233    }
234
235    /// Interpret a header argument as a byte/item length, rejecting
236    /// anything that cannot possibly fit in the remaining input — this
237    /// is the allocation guard for hostile length headers.
238    fn arg_as_len(&self, arg: u64) -> Result<usize, CborError> {
239        let remaining = self.buf.len() - self.pos;
240        if arg > remaining as u64 {
241            return Err(CborError::Truncated);
242        }
243        Ok(arg as usize)
244    }
245
246    fn value(&mut self, depth: usize) -> Result<Value, CborError> {
247        if depth > MAX_DEPTH {
248            return Err(CborError::TooDeep);
249        }
250        let initial = self.byte()?;
251        let major = initial >> 5;
252        let info = initial & 0x1f;
253        match major {
254            0 => Ok(Value::Integer(self.arg(info)? as i128)),
255            1 => Ok(Value::Integer(-1 - self.arg(info)? as i128)),
256            2 => {
257                if info == 31 {
258                    self.indefinite_string(depth, 2).map(Value::Bytes)
259                } else {
260                    let arg = self.arg(info)?;
261                    let len = self.arg_as_len(arg)?;
262                    Ok(Value::Bytes(self.take(len)?.to_vec()))
263                }
264            }
265            3 => {
266                let bytes = if info == 31 {
267                    self.indefinite_string(depth, 3)?
268                } else {
269                    let arg = self.arg(info)?;
270                    let len = self.arg_as_len(arg)?;
271                    self.take(len)?.to_vec()
272                };
273                String::from_utf8(bytes)
274                    .map(Value::Text)
275                    .map_err(|_| CborError::Invalid("text string is not UTF-8"))
276            }
277            4 => {
278                if info == 31 {
279                    let mut items = Vec::new();
280                    while !self.at_break()? {
281                        items.push(self.value(depth + 1)?);
282                    }
283                    Ok(Value::Array(items))
284                } else {
285                    // Each item is at least one byte, so the count is
286                    // bounded by the remaining input.
287                    let arg = self.arg(info)?;
288                    let len = self.arg_as_len(arg)?;
289                    let mut items = Vec::with_capacity(len);
290                    for _ in 0..len {
291                        items.push(self.value(depth + 1)?);
292                    }
293                    Ok(Value::Array(items))
294                }
295            }
296            5 => {
297                if info == 31 {
298                    let mut entries = Vec::new();
299                    while !self.at_break()? {
300                        let k = self.value(depth + 1)?;
301                        let v = self.value(depth + 1)?;
302                        entries.push((k, v));
303                    }
304                    Ok(Value::Map(entries))
305                } else {
306                    let count = self.arg(info)?;
307                    // Each entry is at least two bytes.
308                    if count > (self.buf.len() - self.pos) as u64 / 2 {
309                        return Err(CborError::Truncated);
310                    }
311                    let count = count as usize;
312                    let mut entries = Vec::with_capacity(count);
313                    for _ in 0..count {
314                        let k = self.value(depth + 1)?;
315                        let v = self.value(depth + 1)?;
316                        entries.push((k, v));
317                    }
318                    Ok(Value::Map(entries))
319                }
320            }
321            6 => {
322                let tag = self.arg(info)?;
323                Ok(Value::Tag(tag, Box::new(self.value(depth + 1)?)))
324            }
325            7 => match info {
326                20 => Ok(Value::Bool(false)),
327                21 => Ok(Value::Bool(true)),
328                22 => Ok(Value::Null),
329                23 => Ok(Value::Null), // undefined → null
330                24 => {
331                    let b = self.byte()?;
332                    if b < 32 {
333                        return Err(CborError::Invalid("non-minimal simple value"));
334                    }
335                    Ok(Value::Integer(b as i128)) // unassigned simple value
336                }
337                25 => {
338                    let raw = u16::from_be_bytes(self.take(2)?.try_into().unwrap());
339                    Ok(Value::Float(half_to_f64(raw)))
340                }
341                26 => {
342                    let raw = u32::from_be_bytes(self.take(4)?.try_into().unwrap());
343                    Ok(Value::Float(f32::from_bits(raw) as f64))
344                }
345                27 => {
346                    let raw = u64::from_be_bytes(self.take(8)?.try_into().unwrap());
347                    Ok(Value::Float(f64::from_bits(raw)))
348                }
349                31 => Err(CborError::Invalid("unexpected break")),
350                _ => Err(CborError::Invalid("reserved simple value")),
351            },
352            _ => unreachable!("major type is 3 bits"),
353        }
354    }
355
356    /// Indefinite-length byte/text string: a sequence of definite chunks
357    /// of the same major type terminated by 0xff.
358    fn indefinite_string(&mut self, depth: usize, major: u8) -> Result<Vec<u8>, CborError> {
359        if depth + 1 > MAX_DEPTH {
360            return Err(CborError::TooDeep);
361        }
362        let mut out = Vec::new();
363        loop {
364            let initial = self.byte()?;
365            if initial == 0xff {
366                return Ok(out);
367            }
368            if initial >> 5 != major || initial & 0x1f == 31 {
369                return Err(CborError::Invalid("bad chunk in indefinite string"));
370            }
371            let arg = self.arg(initial & 0x1f)?;
372            let len = self.arg_as_len(arg)?;
373            out.extend_from_slice(self.take(len)?);
374        }
375    }
376
377    fn at_break(&mut self) -> Result<bool, CborError> {
378        if *self.buf.get(self.pos).ok_or(CborError::Truncated)? == 0xff {
379            self.pos += 1;
380            Ok(true)
381        } else {
382            Ok(false)
383        }
384    }
385}
386
387fn half_to_f64(raw: u16) -> f64 {
388    // RFC 8949 appendix D reference algorithm.
389    let exp = (raw >> 10) & 0x1f;
390    let mant = (raw & 0x3ff) as f64;
391    let magnitude = match exp {
392        0 => mant * 2f64.powi(-24),
393        31 => {
394            if mant == 0.0 {
395                f64::INFINITY
396            } else {
397                f64::NAN
398            }
399        }
400        _ => (mant + 1024.0) * 2f64.powi(exp as i32 - 25),
401    };
402    if raw & 0x8000 != 0 {
403        -magnitude
404    } else {
405        magnitude
406    }
407}
408
409/// Decode the first CBOR value in `bytes`. Trailing bytes are ignored,
410/// matching the replaced decoder's behavior.
411pub fn decode(bytes: &[u8]) -> Result<Value, CborError> {
412    let mut d = Decoder { buf: bytes, pos: 0 };
413    d.value(0)
414}
415
416/* ------------------------------------------------------------------ */
417/*  serde_json::Value bridging                                         */
418/* ------------------------------------------------------------------ */
419
420/// Build a CBOR value from a JSON value. Object key order is preserved
421/// as-is — sort beforehand for canonical output.
422pub fn from_json(v: &serde_json::Value) -> Result<Value, CborError> {
423    use serde_json::Value as J;
424    Ok(match v {
425        J::Null => Value::Null,
426        J::Bool(b) => Value::Bool(*b),
427        J::Number(n) => {
428            if let Some(i) = n.as_i64() {
429                Value::Integer(i as i128)
430            } else if let Some(u) = n.as_u64() {
431                Value::Integer(u as i128)
432            } else {
433                Value::Float(n.as_f64().ok_or(CborError::Unrepresentable("number"))?)
434            }
435        }
436        J::String(s) => Value::Text(s.clone()),
437        J::Array(items) => Value::Array(
438            items
439                .iter()
440                .map(from_json)
441                .collect::<Result<Vec<_>, _>>()?,
442        ),
443        J::Object(map) => Value::Map(
444            map.iter()
445                .map(|(k, val)| Ok((Value::Text(k.clone()), from_json(val)?)))
446                .collect::<Result<Vec<_>, CborError>>()?,
447        ),
448    })
449}
450
451/// Convert a decoded CBOR value back to JSON. Fails on shapes JSON
452/// cannot express (byte strings, non-text map keys, tags) — the typed
453/// wire bodies never contain them.
454pub fn to_json(v: &Value) -> Result<serde_json::Value, CborError> {
455    use serde_json::Value as J;
456    Ok(match v {
457        Value::Null => J::Null,
458        Value::Bool(b) => J::Bool(*b),
459        Value::Integer(i) => {
460            if let Ok(n) = i64::try_from(*i) {
461                J::Number(n.into())
462            } else if let Ok(n) = u64::try_from(*i) {
463                J::Number(n.into())
464            } else {
465                return Err(CborError::Unrepresentable("integer outside JSON range"));
466            }
467        }
468        Value::Float(f) => serde_json::Number::from_f64(*f)
469            .map(J::Number)
470            .ok_or(CborError::Unrepresentable("non-finite float"))?,
471        Value::Text(t) => J::String(t.clone()),
472        Value::Array(items) => J::Array(
473            items
474                .iter()
475                .map(to_json)
476                .collect::<Result<Vec<_>, _>>()?,
477        ),
478        Value::Map(entries) => {
479            let mut out = serde_json::Map::with_capacity(entries.len());
480            for (k, val) in entries {
481                let Value::Text(key) = k else {
482                    return Err(CborError::Unrepresentable("non-text map key in JSON"));
483                };
484                out.insert(key.clone(), to_json(val)?);
485            }
486            J::Object(out)
487        }
488        Value::Tag(..) => return Err(CborError::Unrepresentable("tag in JSON")),
489        Value::Bytes(_) => return Err(CborError::Unrepresentable("byte string in JSON")),
490    })
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    fn hex(bytes: &[u8]) -> String {
498        bytes.iter().map(|b| format!("{b:02x}")).collect()
499    }
500
501    fn unhex(s: &str) -> Vec<u8> {
502        (0..s.len())
503            .step_by(2)
504            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
505            .collect()
506    }
507
508    #[test]
509    fn rfc8949_appendix_a_encodings() {
510        // (value, expected hex) pairs from RFC 8949 appendix A.
511        let cases: Vec<(Value, &str)> = vec![
512            (Value::Integer(0), "00"),
513            (Value::Integer(10), "0a"),
514            (Value::Integer(23), "17"),
515            (Value::Integer(24), "1818"),
516            (Value::Integer(100), "1864"),
517            (Value::Integer(1000), "1903e8"),
518            (Value::Integer(1000000), "1a000f4240"),
519            (Value::Integer(1000000000000), "1b000000e8d4a51000"),
520            (Value::Integer(u64::MAX as i128), "1bffffffffffffffff"),
521            (Value::Integer(-1), "20"),
522            (Value::Integer(-10), "29"),
523            (Value::Integer(-100), "3863"),
524            (Value::Integer(-1000), "3903e7"),
525            (Value::Integer(-(u64::MAX as i128) - 1), "3bffffffffffffffff"),
526            (Value::Bool(false), "f4"),
527            (Value::Bool(true), "f5"),
528            (Value::Null, "f6"),
529            (Value::Float(1.1), "fb3ff199999999999a"),
530            (Value::Float(-4.1), "fbc010666666666666"),
531            (Value::Bytes(vec![]), "40"),
532            (Value::Bytes(vec![1, 2, 3, 4]), "4401020304"),
533            (Value::Text(String::new()), "60"),
534            (Value::Text("IETF".into()), "6449455446"),
535            (Value::Text("\u{00fc}".into()), "62c3bc"),
536            (Value::Text("\u{6c34}".into()), "63e6b0b4"),
537            (Value::Array(vec![]), "80"),
538            (
539                Value::Array(vec![
540                    Value::Integer(1),
541                    Value::Integer(2),
542                    Value::Integer(3),
543                ]),
544                "83010203",
545            ),
546            (Value::Map(vec![]), "a0"),
547            (
548                Value::Map(vec![
549                    (Value::Text("a".into()), Value::Integer(1)),
550                    (
551                        Value::Text("b".into()),
552                        Value::Array(vec![Value::Integer(2), Value::Integer(3)]),
553                    ),
554                ]),
555                "a26161016162820203",
556            ),
557            (
558                Value::Tag(1, Box::new(Value::Integer(1363896240))),
559                "c11a514b67b0",
560            ),
561        ];
562        for (value, expected) in cases {
563            assert_eq!(hex(&encode(&value).unwrap()), expected, "{value:?}");
564            assert_eq!(decode(&unhex(expected)).unwrap(), value, "{expected}");
565        }
566    }
567
568    #[test]
569    fn float_widths_decode_to_f64() {
570        assert_eq!(decode(&unhex("f90000")).unwrap(), Value::Float(0.0));
571        assert_eq!(decode(&unhex("f93c00")).unwrap(), Value::Float(1.0));
572        assert_eq!(decode(&unhex("f97c00")).unwrap(), Value::Float(f64::INFINITY));
573        assert_eq!(decode(&unhex("fa47c35000")).unwrap(), Value::Float(100000.0));
574        // Half-precision subnormal.
575        assert_eq!(
576            decode(&unhex("f90001")).unwrap(),
577            Value::Float(5.960464477539063e-8)
578        );
579    }
580
581    #[test]
582    fn indefinite_lengths_accepted() {
583        // (_ h'0102', h'030405') from RFC 8949.
584        assert_eq!(
585            decode(&unhex("5f42010243030405ff")).unwrap(),
586            Value::Bytes(vec![1, 2, 3, 4, 5])
587        );
588        // ["a", {_ "b": "c"}]
589        assert_eq!(
590            decode(&unhex("826161bf61626163ff")).unwrap(),
591            Value::Array(vec![
592                Value::Text("a".into()),
593                Value::Map(vec![(Value::Text("b".into()), Value::Text("c".into()))]),
594            ])
595        );
596    }
597
598    #[test]
599    fn hostile_inputs_rejected_without_allocation() {
600        // Claims a 4 GiB byte string with 3 bytes of input.
601        assert_eq!(
602            decode(&unhex("5affffffff")).unwrap_err(),
603            CborError::Truncated
604        );
605        // Claims 2^64-1 array items.
606        assert_eq!(
607            decode(&unhex("9bffffffffffffffff")).unwrap_err(),
608            CborError::Truncated
609        );
610        // Map with absurd entry count.
611        assert_eq!(
612            decode(&unhex("bbffffffffffffffff")).unwrap_err(),
613            CborError::Truncated
614        );
615        // Empty input.
616        assert_eq!(decode(&[]).unwrap_err(), CborError::Truncated);
617        // Bare break byte.
618        assert!(matches!(
619            decode(&unhex("ff")).unwrap_err(),
620            CborError::Invalid(_)
621        ));
622        // Invalid UTF-8 text.
623        assert!(matches!(
624            decode(&unhex("61ff")).unwrap_err(),
625            CborError::Invalid(_)
626        ));
627    }
628
629    #[test]
630    fn depth_limit_enforced() {
631        // 200 nested single-item arrays.
632        let mut buf = vec![0x81u8; 200];
633        buf.push(0x00);
634        assert_eq!(decode(&buf).unwrap_err(), CborError::TooDeep);
635        // 100 nested arrays is fine.
636        let mut ok = vec![0x81u8; 100];
637        ok.push(0x00);
638        assert!(decode(&ok).is_ok());
639    }
640
641    #[test]
642    fn trailing_bytes_ignored() {
643        assert_eq!(decode(&unhex("01ffffff")).unwrap(), Value::Integer(1));
644    }
645
646    #[test]
647    fn json_round_trip() {
648        let json = serde_json::json!({
649            "z": "last",
650            "a": [1, 2.5, true, null, {"nested": "x"}],
651            "n": -42,
652            "big": u64::MAX,
653        });
654        let value = from_json(&json).unwrap();
655        let bytes = encode(&value).unwrap();
656        let back = to_json(&decode(&bytes).unwrap()).unwrap();
657        assert_eq!(back, json);
658    }
659}