#codec #cbor #encoder #deserialize #serialization #decoder #array

no-std tps_minicbor

A CBOR encoder and decoder suitable for no_std embedded targets

3 unstable releases

0.5.1 Mar 28, 2023
0.4.3 Dec 19, 2022
0.4.2 Dec 19, 2022
0.4.1 Dec 12, 2022

#812 in Encoding

MIT license

225KB
3.5K SLoC

TPS-MINICBOR

An implementation of CBOR in Rust which is aimed at relatively constrained embedded systems where the Serde implementation is not necessarily well suited.

License

tps_minicbor is MIT licensed. See LICENSE.

Features

  • Designed to constrained embedded environments requiring #[no_std] support.
  • High-level encoding and decoding APIs make serialization and deserialization flows easier to write correctly.
  • Supports most CBOR constructions (see limitations)
    • All primitive types (positive and negative integers, tstr, bstr, simple types, tags, floats (including 16 bit floats).
  • Arbitrary nesting of arrays and maps with automatic calculation of the correct number of items.
  • Supports a subset of standard tags (Date/Time and Unix Epoch). Note that these require an allocator to be available.
  • Conversions to/from Rust primitive types.
  • Automatic preferred serialization for integers and floats.
  • Iterators and indexing over arrays and maps when deserializing
  • Extensive test cases, including test cases for all supported features from RFC8949
    • Note that floating point +Infinity, NaN and -Infinity are always serialized as f16 format because this is the preferred representation. Deserialisation works for all cases.
  • Deserialization of non-preferred representations is supported.

Current Limitations

  • Does not support Canonical CBOR
  • Does not support preferred serialization for arrays and maps
  • Does not support indefinite length encoding
  • Does not directly support Bignum, DecFrac or BigFloat

Testing

You can run the test cases as follows:

cargo test --features=full

Note that a current limitation is that tests cannot be executed by cargo test alone as the featurization does not allow this.

A flavour of the APIs

CBOR Encoding

Despite the small memory footprint, the CBOR serialization API is quite high-level, supporting arbitrary nesting of arrays and maps.

The example below is an implementation of Simple TEE Attestation from draft 14 of the Entity Attestation Token specification under development at the IETF.

In CBOR diagnostic format, this is displayed as:

{
    / nonce /           10: h'948f8860d13a463e',
    / UEID /           256: h'0198f50a4ff6c05861c8860d13a638ea',
    / OEMID /          258: 64242, / Private Enterprise Number /
    / security-level / 261: 3, / hardware level security /
    / secure-boot /    262: true,
    / debug-status /   263: 3, / disabled-permanently /
    / HW version /     260: [ "3.1", 1 ] / Type is multipartnumeric /
}

This is encoded in tps_minicbor as:

fn encode_tee_eat() -> Result<(), CBORError> {
    // Encode-decode round trip test
    println!("<========================== encode_tee_eat =========================>");
    let mut bytes = [0u8; 1024];
    let nonce: &[u8] = &[0x94, 0x8f, 0x88, 0x60, 0xd1, 0x3a, 0x46, 0x3e];
    let ueid: &[u8] = &[
        0x01, 0x98, 0xf5, 0x0a, 0x4f, 0xf6, 0xc0, 0x58, 0x61, 0xc8, 0x86, 0x0d, 0x13,
        0xa6, 0x38, 0xea,
    ];

    let mut encoded_cbor = CBORBuilder::new(&mut bytes);
    encoded_cbor.insert(&map(|buff| {
        buff.insert_key_value(&10, &nonce)?
            .insert_key_value(&256, &ueid)?
            .insert_key_value(&258, &64242)?
            .insert_key_value(&261, &3)?
            .insert_key_value(&262, &true)?
            .insert_key_value(&263, &3)?
            .insert_key_value(&260, &array(|buf| buf.insert(&"3.1")?.insert(&1)))
    }))?;

    // do_something_with(encoded_cbor.encoded()?);
    Ok(())
}

The only work to do 'by hand' is turning the bstr values into suitable references.

CBOR Decoding

The example below shows one way to decode the payload generated above.

fn decode_tee_eat() -> Result<(), CBORError> {
    let mut input: &[u8] = &[
        167, 10, 72, 148, 143, 136, 96, 209, 58, 70, 62, 25, 1, 0, 80, 1, 152, 245,
        10, 79, 246, 192, 88, 97, 200, 134, 13, 19, 166, 56, 234, 25, 1, 2, 25, 250,
        242, 25, 1, 5, 3, 25, 1, 6,  245, 25, 1, 7, 3, 25, 1, 4, 130, 99, 51, 46,
        49, 1,
    ];
    let mut nonce = None;
    let mut ueid = None;
    let mut oemid = None;
    let mut sec_level = None;
    let mut sec_boot = None;
    let mut debug_state = None;
    let mut hw_ver_int = None;

    let mut decoder = CBORDecoder::from_slice(&mut input);
    decoder.decode_with(is_map(), |cbor| {
        if let CBOR::Map(map) = cbor {
            nonce = map.get_int(10);
            ueid = map.get_int(256);
            oemid = map.get_int(258);
            sec_level = map.get_int(261);
            sec_boot = map.get_int(262);
            debug_state = map.get_int(263);
            if let Some(CBOR::Array(ab)) = map.get_int(260) {
                hw_ver_int = match ab.index(1) {
                    None => None,
                    Some(CBOR::UInt(vi)) => Some(vi.clone()),
                    _ => None
                };
            }
        }
        Ok(())
    })?;
 Ok(())
}

Examples

decode

The decode example is a very short sample of the use of the low-level decode API.

To run the example, from the top directory of the tps_minicbor crate:

cargo run --example decode --features=full

The expected output is:

v1 = Ok(1000), v2 = Ok(1000), v3 = Ok(1000), v4 = Err(OutOfRange)
r1 = UInt(1000), e = Some(Eof)
Value: UInt(1000)

trivial_cose

The trivial_cose example is an implementation of the COSE_Sign1 single signer example in RFC9052 Appendix C.2.1. Keys, the message to be signed and other aspects of the cryptographic configuration are hard-coded to the values in the Appendix.

While the example is called trivial_cose as it implements only the very simplest COSE example, it does stand as a good example of how to encode and decode moderately complex CBOR structures. All of the inputs and outputs are bit-exact against the example, thanks to the use of deterministic ECDSA in the signature.

The code also serves as a simplistic example of how to do ECDSA using the Rust crypto traits - something for which there is a dearth of realistic examples.

Note: The p256 crate used for ECDSA has not been audited. Please see the warning on the p256 crate, and perform your own due diligence before use in production.

To run the example, from the top directory of the tps_minicbor crate:

cargo run --example trivial_cose --features=full

The expected output is:

To be signed 846a5369676e61747572653143a101264054546869732069732074686520636f6e74656e742e
Signature 8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36
Output d28443a10126a10242313154546869732069732074686520636f6e74656e742e58408eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36
 18(     [
   h'a10126' ,
   {
      2 :  h'3131' ,
   }
,
   h'546869732069732074686520636f6e74656e742e' ,
   h'8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36' ,
 ],
 )
To be verified 846a5369676e61747572653143a101264054546869732069732074686520636f6e74656e742e
Verification succeeded: message content [84, 104, 105, 115, 32, 105, 115, 32, 116, 104, 101, 32, 99, 111, 110, 116, 101, 110, 116, 46]

Dependencies

~4MB
~91K SLoC