Skip to main content

scll_core/
tlv.rs

1//! BER-TLV parse/encode (PDD §3.5). Pure, fuzzable (§10.5 target #2).
2//!
3//! Used by the card-response parsers: CRD `'66'`, CCI `'67'`, key-info template
4//! `'00E0'`, and `GET STATUS` `'E3'`. Property obligation (§10.4):
5//! `parse ∘ encode == identity`.
6//!
7//! `no_std`, zero-copy: [`Tlv`] borrows its `value` from the input rather than
8//! owning it (no per-object heap/inline buffer; nested templates recurse on
9//! `tlv.value` for free). [`parse`] returns a bounded `heapless::Vec` of
10//! borrows; [`encode`] writes into a caller buffer. Both are total — malformed
11//! or oversized input yields a typed [`TlvError`], never a panic (§10.5).
12//!
13//! Tag/length encoding follows ISO/IEC 7816-4:2020 §5.2.2 (BER-TLV, the same
14//! basic encoding rules as ITU-T X.690). The tag is stored as the raw, big-
15//! endian concatenation of its identifier octets in a `u32` (e.g. `'9F70'` →
16//! `0x0000_9F70`, `'E0'` → `0x0000_00E0`), which is how GP templates are keyed.
17//! Tags wider than four octets are out of range for GP and rejected. The parser
18//! is a lenient BER reader (it accepts non-minimal long-form lengths, which BER
19//! permits); the encoder always emits the minimal canonical form, so its output
20//! always re-parses.
21
22use heapless::Vec;
23
24use crate::limits::MAX_TLVS;
25
26/// A parsed tag-length-value triple (tag may be multi-byte, e.g. `'9F70'`).
27/// `value` borrows into the slice passed to [`parse`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Tlv<'a> {
30    pub tag: u32,
31    pub value: &'a [u8],
32}
33
34/// Low 5 bits of the leading tag octet all set ⇒ the tag continues into further
35/// octets (ISO/IEC 7816-4 §5.2.2.1 / X.690 §8.1.2.4).
36const TAG_MULTIBYTE: u8 = 0x1F;
37/// b8 of a subsequent tag octet set ⇒ another tag octet follows.
38const TAG_MORE: u8 = 0x80;
39/// Largest tag width we represent (a `u32` holds four identifier octets).
40const TAG_MAX_OCTETS: usize = 4;
41/// b8 of the leading length octet set ⇒ long form; low 7 bits give the count.
42const LEN_LONG: u8 = 0x80;
43
44/// Parse a sequence of top-level BER-TLV objects from a byte slice. Values
45/// borrow from `input`. Returns `TlvError::TooMany` if more than [`MAX_TLVS`]
46/// objects are present (rather than allocating or panicking).
47///
48/// # Errors
49/// Returns [`TlvError::Truncated`] / [`TlvError::BadLength`] for malformed
50/// input, or [`TlvError::TooMany`] if more than [`MAX_TLVS`] top-level objects
51/// are present.
52pub fn parse(input: &[u8]) -> Result<Vec<Tlv<'_>, MAX_TLVS>, TlvError> {
53    let mut out = Vec::new();
54    let mut pos = 0usize;
55    while pos < input.len() {
56        let tag = parse_tag(input, &mut pos)?;
57        let len = parse_len(input, &mut pos)?;
58        let end = pos.checked_add(len).ok_or(TlvError::Truncated)?;
59        if end > input.len() {
60            return Err(TlvError::Truncated);
61        }
62        out.push(Tlv {
63            tag,
64            value: &input[pos..end],
65        })
66        .map_err(|_| TlvError::TooMany)?;
67        pos = end;
68    }
69    Ok(out)
70}
71
72/// Encode a sequence of BER-TLV objects into `out`, returning the number of
73/// bytes written. `TlvError::Overflow` if `out` is too small (the caller sizes
74/// the scratch — typically a command data field ≤ 255 B).
75///
76/// # Errors
77/// Returns [`TlvError::Overflow`] if `out` is too small to hold the encoded
78/// objects.
79pub fn encode(items: &[Tlv<'_>], out: &mut [u8]) -> Result<usize, TlvError> {
80    let mut pos = 0usize;
81    for item in items {
82        pos = write_tag(item.tag, out, pos)?;
83        pos = write_len(item.value.len(), out, pos)?;
84        pos = write_bytes(out, pos, item.value)?;
85    }
86    Ok(pos)
87}
88
89/// Read one (possibly multi-octet) tag, advancing `*pos`.
90fn parse_tag(input: &[u8], pos: &mut usize) -> Result<u32, TlvError> {
91    let b0 = *input.get(*pos).ok_or(TlvError::Truncated)?;
92    *pos += 1;
93    let mut tag = u32::from(b0);
94    if b0 & TAG_MULTIBYTE == TAG_MULTIBYTE {
95        let mut octets = 1usize;
96        loop {
97            let b = *input.get(*pos).ok_or(TlvError::Truncated)?;
98            *pos += 1;
99            octets += 1;
100            if octets > TAG_MAX_OCTETS {
101                return Err(TlvError::BadLength);
102            }
103            tag = (tag << 8) | u32::from(b);
104            if b & TAG_MORE == 0 {
105                break;
106            }
107        }
108    }
109    Ok(tag)
110}
111
112/// Read a definite BER length (short or long form), advancing `*pos`.
113fn parse_len(input: &[u8], pos: &mut usize) -> Result<usize, TlvError> {
114    let b0 = *input.get(*pos).ok_or(TlvError::Truncated)?;
115    *pos += 1;
116    if b0 & LEN_LONG == 0 {
117        return Ok(usize::from(b0));
118    }
119    let count = usize::from(b0 & !LEN_LONG);
120    // `0x80` = indefinite form (not permitted in DER and unused by GP); a count
121    // wider than a `usize` cannot index the buffer. Either ⇒ malformed.
122    if count == 0 || count > core::mem::size_of::<usize>() {
123        return Err(TlvError::BadLength);
124    }
125    let mut len = 0usize;
126    for _ in 0..count {
127        let b = *input.get(*pos).ok_or(TlvError::Truncated)?;
128        *pos += 1;
129        len = (len << 8) | usize::from(b);
130    }
131    Ok(len)
132}
133
134/// Write a tag as its minimal big-endian identifier octets.
135fn write_tag(tag: u32, out: &mut [u8], pos: usize) -> Result<usize, TlvError> {
136    let bytes = tag.to_be_bytes();
137    // First significant octet; for `tag == 0` emit the single octet `0x00`.
138    let start = bytes
139        .iter()
140        .position(|&b| b != 0)
141        .unwrap_or(bytes.len() - 1);
142    write_bytes(out, pos, &bytes[start..])
143}
144
145/// Write a definite BER length in its minimal canonical form.
146fn write_len(len: usize, out: &mut [u8], pos: usize) -> Result<usize, TlvError> {
147    if len < usize::from(LEN_LONG) {
148        // `len < 0x80`, so the conversion cannot fail.
149        let b = u8::try_from(len).map_err(|_| TlvError::Overflow)?;
150        return write_bytes(out, pos, &[b]);
151    }
152    let bytes = len.to_be_bytes();
153    let start = bytes
154        .iter()
155        .position(|&b| b != 0)
156        .unwrap_or(bytes.len() - 1);
157    let body = &bytes[start..];
158    // `body.len() <= size_of::<usize>() <= 8`, so it fits the low 7 bits.
159    let count = u8::try_from(body.len()).map_err(|_| TlvError::Overflow)?;
160    let pos = write_bytes(out, pos, &[LEN_LONG | count])?;
161    write_bytes(out, pos, body)
162}
163
164/// Copy `src` into `out` at `pos`, bounds-checked; returns the new position.
165fn write_bytes(out: &mut [u8], pos: usize, src: &[u8]) -> Result<usize, TlvError> {
166    let end = pos.checked_add(src.len()).ok_or(TlvError::Overflow)?;
167    if end > out.len() {
168        return Err(TlvError::Overflow);
169    }
170    out[pos..end].copy_from_slice(src);
171    Ok(end)
172}
173
174/// TLV parse/encode failure.
175#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum TlvError {
178    /// Length field runs past the end of the buffer.
179    #[error("TLV value runs past end of buffer")]
180    Truncated,
181    /// Malformed length encoding (e.g. reserved long-form, non-minimal).
182    #[error("malformed TLV length encoding")]
183    BadLength,
184    /// More than `MAX_TLVS` top-level objects in the input.
185    #[error("more than MAX_TLVS top-level TLV objects")]
186    TooMany,
187    /// `encode` output buffer too small.
188    #[error("encode output buffer too small")]
189    Overflow,
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use proptest::prelude::*;
196    use scll_test_util::HexSlice;
197
198    // ---- Concrete GP-shaped vectors --------------------------------------
199
200    #[test]
201    fn parses_two_single_byte_tag_objects() {
202        // '4F' (AID) len 2 + '9F70' (life-cycle) len 1.
203        let input = [0x4F, 0x02, 0xA0, 0x00, 0x9F, 0x70, 0x01, 0x07];
204        let tlvs = parse(&input).unwrap();
205        assert_eq!(tlvs.len(), 2);
206        assert_eq!(tlvs[0].tag, 0x4F);
207        assert_eq!(HexSlice(tlvs[0].value), HexSlice(&[0xA0, 0x00]));
208        assert_eq!(tlvs[1].tag, 0x9F70);
209        assert_eq!(HexSlice(tlvs[1].value), HexSlice(&[0x07]));
210    }
211
212    #[test]
213    fn parses_long_form_length() {
214        // '66' with a 130-byte value via long form 0x81 0x82.
215        let mut input = heapless::Vec::<u8, 300>::new();
216        input.extend_from_slice(&[0x66, 0x81, 0x82]).unwrap();
217        input.extend_from_slice(&[0xAB; 0x82]).unwrap();
218        let tlvs = parse(&input).unwrap();
219        assert_eq!(tlvs.len(), 1);
220        assert_eq!(tlvs[0].tag, 0x66);
221        assert_eq!(tlvs[0].value.len(), 0x82);
222    }
223
224    #[test]
225    fn empty_input_yields_no_objects() {
226        assert_eq!(parse(&[]).unwrap().len(), 0);
227    }
228
229    // ---- Malformed input is rejected, never panics (§10.5) ---------------
230
231    #[test]
232    fn truncated_value_is_rejected() {
233        assert_eq!(parse(&[0x4F, 0x05, 0x01, 0x02]), Err(TlvError::Truncated));
234    }
235
236    #[test]
237    fn truncated_tag_is_rejected() {
238        // Leading octet signals continuation, but the buffer ends.
239        assert_eq!(parse(&[0x9F]), Err(TlvError::Truncated));
240    }
241
242    #[test]
243    fn missing_length_octet_is_rejected() {
244        assert_eq!(parse(&[0x4F]), Err(TlvError::Truncated));
245    }
246
247    #[test]
248    fn indefinite_length_is_rejected() {
249        assert_eq!(parse(&[0x4F, 0x80, 0x01]), Err(TlvError::BadLength));
250    }
251
252    #[test]
253    fn oversized_tag_is_rejected() {
254        // Five continuation octets cannot fit a u32 tag.
255        assert_eq!(
256            parse(&[0x1F, 0x81, 0x81, 0x81, 0x81, 0x01]),
257            Err(TlvError::BadLength)
258        );
259    }
260
261    #[test]
262    fn long_form_length_octets_exceeding_usize_rejected() {
263        // 0x80 | 9 ⇒ nine length octets, wider than any usize.
264        assert_eq!(parse(&[0x4F, 0x89]), Err(TlvError::BadLength));
265    }
266
267    #[test]
268    fn too_many_objects_is_rejected() {
269        // (MAX_TLVS + 1) minimal one-byte-value objects.
270        let mut input = heapless::Vec::<u8, { (MAX_TLVS + 1) * 3 }>::new();
271        for _ in 0..=MAX_TLVS {
272            input.extend_from_slice(&[0x80, 0x01, 0x00]).unwrap();
273        }
274        assert_eq!(parse(&input), Err(TlvError::TooMany));
275    }
276
277    // ---- encode ----------------------------------------------------------
278
279    #[test]
280    fn encode_emits_canonical_long_form() {
281        let value = [0xAB; 0x82];
282        let items = [Tlv {
283            tag: 0x66,
284            value: &value,
285        }];
286        let mut out = [0u8; 300];
287        let n = encode(&items, &mut out).unwrap();
288        assert_eq!(HexSlice(&out[..3]), HexSlice(&[0x66, 0x81, 0x82]));
289        assert_eq!(n, 3 + 0x82);
290    }
291
292    #[test]
293    fn encode_overflow_is_reported_not_panicked() {
294        let items = [Tlv {
295            tag: 0x4F,
296            value: &[1, 2, 3, 4],
297        }];
298        let mut out = [0u8; 3]; // too small for tag+len+4
299        assert_eq!(encode(&items, &mut out), Err(TlvError::Overflow));
300    }
301
302    // ---- Properties (§10.4) ---------------------------------------------
303
304    /// A round-trippable tag: either a single octet that does not signal
305    /// continuation, or a two-octet tag (`0x9Fxx`, `xx` terminating).
306    fn tag_strategy() -> impl Strategy<Value = u32> {
307        prop_oneof![
308            (0u32..=0xFF).prop_filter("not a continuation tag", |t| t & 0x1F != 0x1F),
309            (0u32..=0x7F).prop_map(|lo| 0x9F00 | lo),
310        ]
311    }
312
313    fn tlv_items() -> impl Strategy<Value = std::vec::Vec<(u32, std::vec::Vec<u8>)>> {
314        proptest::collection::vec(
315            (
316                tag_strategy(),
317                proptest::collection::vec(any::<u8>(), 0..=64),
318            ),
319            0..=MAX_TLVS,
320        )
321    }
322
323    proptest! {
324        /// `parse ∘ encode == identity` (§10.4).
325        #[test]
326        fn parse_after_encode_is_identity(items in tlv_items()) {
327            let tlvs: std::vec::Vec<Tlv> =
328                items.iter().map(|(t, v)| Tlv { tag: *t, value: v }).collect();
329            let mut out = [0u8; 64 * 70];
330            let n = encode(&tlvs, &mut out).unwrap();
331            let parsed = parse(&out[..n]).unwrap();
332            prop_assert_eq!(parsed.len(), tlvs.len());
333            for (got, want) in parsed.iter().zip(tlvs.iter()) {
334                prop_assert_eq!(got.tag, want.tag);
335                prop_assert_eq!(got.value, want.value);
336            }
337        }
338
339        /// The encoder never emits a byte string the parser rejects.
340        #[test]
341        fn encoder_output_always_parses(items in tlv_items()) {
342            let tlvs: std::vec::Vec<Tlv> =
343                items.iter().map(|(t, v)| Tlv { tag: *t, value: v }).collect();
344            let mut out = [0u8; 64 * 70];
345            let n = encode(&tlvs, &mut out).unwrap();
346            prop_assert!(parse(&out[..n]).is_ok());
347        }
348
349        /// Parsing arbitrary bytes never panics: always Ok or a typed error.
350        #[test]
351        fn parse_arbitrary_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..512)) {
352            let _ = parse(&bytes);
353        }
354    }
355}