From c00402e21ed76d9b9b97c711cba2a32974a1616f Mon Sep 17 00:00:00 2001 From: Janis Danisevskis Date: Mon, 15 Jun 2026 16:11:04 -0700 Subject: [PATCH 1/4] Add indefinite lenght support to cbor read functions. --- include/nat20/cbor.h | 73 +++++- src/core/cbor.c | 146 ++++++++++-- src/core/test/cbor.cpp | 433 +++++++++++++++++++++++++++++++++- src/service/test/messages.cpp | 6 +- 4 files changed, 635 insertions(+), 23 deletions(-) diff --git a/include/nat20/cbor.h b/include/nat20/cbor.h index b79a9484..48dc8424 100644 --- a/include/nat20/cbor.h +++ b/include/nat20/cbor.h @@ -53,6 +53,15 @@ extern "C" { * data items as specified in RFC 8949. Each type corresponds to a specific kind of data * that can be encoded in CBOR. * + * In addition to the CBOR major types, this enumeration defines a set of + * synthetic types that represent indefinite length items. These are not CBOR + * major types. They encode the indefinite length variant of a major type by + * setting the 0x100 bit, i.e. their value is `(major_type | 0x100)`. The read + * and write functions use this bit to convert between the synthetic type and + * its wire encoding, which uses the additional info value 31. The break stop + * code that terminates indefinite length items is likewise represented as a + * synthetic type (@ref n20_cbor_type_indefinite_break_e). + * * @sa https://tools.ietf.org/html/rfc8949 */ typedef enum n20_cbor_type_s { @@ -112,6 +121,41 @@ typedef enum n20_cbor_type_s { * Represents simple values (e.g., true, false, null) or floating-point numbers. */ n20_cbor_type_simple_float_e = 7, + /** + * @brief Indefinite length byte string. + * + * Synthetic type for the byte string major type with the indefinite + * length bit (0x100) set. + */ + n20_cbor_type_indefinite_bytes_e = 0x102, + /** + * @brief Indefinite length text string. + * + * Synthetic type for the text string major type with the indefinite + * length bit (0x100) set. + */ + n20_cbor_type_indefinite_string_e = 0x103, + /** + * @brief Indefinite length array. + * + * Synthetic type for the array major type with the indefinite length + * bit (0x100) set. + */ + n20_cbor_type_indefinite_array_e = 0x104, + /** + * @brief Indefinite length map. + * + * Synthetic type for the map major type with the indefinite length + * bit (0x100) set. + */ + n20_cbor_type_indefinite_map_e = 0x105, + /** + * @brief Break stop code for indefinite length items. + * + * Synthetic type for the simple/float major type with the indefinite + * length bit (0x100) set. It marks the end of an indefinite length item. + */ + n20_cbor_type_indefinite_break_e = 0x107, } n20_cbor_type_t; /** @@ -142,6 +186,12 @@ typedef enum n20_cbor_type_s { * it writes the special value 0xf7 to the stream, and @p value is ignored. * 0xf7 is the encoding of the special value "undefined" in CBOR. * + * If @p type is one of the indefinite length types + * (@ref n20_cbor_type_indefinite_bytes_e, @ref n20_cbor_type_indefinite_string_e, + * @ref n20_cbor_type_indefinite_array_e, @ref n20_cbor_type_indefinite_map_e) + * or @ref n20_cbor_type_indefinite_break_e, an indefinite length header + * (additional info value 31) is written and @p value is ignored. + * * @param s The stream to write to. * @param type The CBOR type (see @ref n20_cbor_type_t). * @param value The value associated with the CBOR type. @@ -249,6 +299,16 @@ extern void n20_cbor_write_map_header(n20_stream_t *s, size_t size); * * This function reads the CBOR header for a given type and value from the stream. * + * Indefinite length headers (additional info value 31) for byte strings, text + * strings, arrays, and maps are reported via the corresponding synthetic types + * (@ref n20_cbor_type_indefinite_bytes_e, @ref n20_cbor_type_indefinite_string_e, + * @ref n20_cbor_type_indefinite_array_e, @ref n20_cbor_type_indefinite_map_e) + * with @p n set to 0. The break stop code is reported as + * @ref n20_cbor_type_indefinite_break_e. Additional info value 31 with any + * other major type (unsigned integer, negative integer, or tag) is rejected + * and the function returns false, as are the reserved additional info values + * 28 to 30. + * * @param s The stream to read from. * @param type The CBOR type (see @ref n20_cbor_type_t). * @param n The value associated with the CBOR type. @@ -263,8 +323,17 @@ extern bool n20_cbor_read_header(n20_istream_t *s, n20_cbor_type_t *type, uint64 * past the item. If the item has tags or has a nested structure, like * an array or map, it will also advance past those structures. * - * This function will skip past any CBOR structure, however, it does not - * support indefinite length items. + * This function will skip past any CBOR structure, including indefinite length + * byte strings, text strings, arrays, and maps. For indefinite length items it + * consumes the contained chunks or elements up to and including the break stop + * code. + * + * A break stop code encountered where a data item is expected (for example as + * the top-level item, in the value position of a map, or in place of a chunk + * of an indefinite length string) is treated as an error and the function + * returns false. The chunks of an indefinite length byte or text string must + * be definite length byte or text strings respectively; anything else is an + * error. * * @param s The stream to read from. * @return true if the item was skipped successfully, false otherwise. diff --git a/src/core/cbor.c b/src/core/cbor.c index 5be29e35..60796fe1 100644 --- a/src/core/cbor.c +++ b/src/core/cbor.c @@ -41,6 +41,11 @@ #include void n20_cbor_write_header(n20_stream_t *const s, n20_cbor_type_t cbor_type, uint64_t n) { + bool indefinite_length = (cbor_type & 0x100) != 0; + if (indefinite_length) { + /* Indefinite length encoding is encoded in the ninth bit of the type. */ + cbor_type = (n20_cbor_type_t)(cbor_type - 0x100); + } if ((unsigned int)cbor_type > 7) { /* 0xf7 is the encoding of the special value "undefined". */ cbor_type = n20_cbor_type_simple_float_e; @@ -50,7 +55,12 @@ void n20_cbor_write_header(n20_stream_t *const s, n20_cbor_type_t cbor_type, uin size_t value_size = 0; - if (n < 24) { + if (indefinite_length) { + /* Indefinite length encoding is denoted by additional info value 31. */ + header |= 31; + n20_stream_prepend(s, &header, /*src_len=*/1); + return; + } else if (n < 24) { header |= (uint8_t)n; n20_stream_prepend(s, &header, /*src_len=*/1); return; @@ -137,9 +147,28 @@ bool n20_cbor_read_header(n20_istream_t *const s, n20_cbor_type_t *const type, u *type = (n20_cbor_type_t)(header >> 5); uint8_t additional_info = header & 0x1f; + if (additional_info == 31) { + switch (*type) { + case n20_cbor_type_array_e: + case n20_cbor_type_map_e: + case n20_cbor_type_bytes_e: + case n20_cbor_type_string_e: + case n20_cbor_type_simple_float_e: + /* Indefinite length encoding is encoded in the ninth bit of the type. */ + *type = (n20_cbor_type_t)(*type + 0x100); + *n = 0; + return true; + default: + /* Additional info 31 is only valid for arrays, maps, byte strings, and text + * strings. and in the simple/float type denoting the end of indefinite length + * items. */ + return false; + } + } + if (additional_info > 27) { - /* Reserved additional info value. And this code does not - * support indefinite length encoding (31). */ + /* Reserved additional info values (28-30). Indefinite length + * encoding (31) is handled above. */ return false; } @@ -163,36 +192,81 @@ bool n20_cbor_read_header(n20_istream_t *const s, n20_cbor_type_t *const type, u return true; } -bool n20_cbor_read_skip_item(n20_istream_t *const s) { +typedef enum n20_cbor_read_skip_item_result_s { + n20_cbor_read_skip_item_ok_e, + n20_cbor_read_skip_item_error_e, + n20_cbor_read_skip_item_break_e, +} n20_cbor_read_skip_item_result_t; + +static n20_cbor_read_skip_item_result_t n20_cbor_read_skip_item_internal(n20_istream_t *const s); + +static n20_cbor_read_skip_item_result_t n20_cbor_read_skip_item_map_element_internal( + n20_istream_t *const s) { + n20_cbor_read_skip_item_result_t result = n20_cbor_read_skip_item_internal(s); + if (result != n20_cbor_read_skip_item_ok_e) { + return result; + } + if (n20_cbor_read_skip_item_internal(s) != n20_cbor_read_skip_item_ok_e) { + /* If the second item is a terminator or if we ran out of buffer + * we consider it an error. */ + return n20_cbor_read_skip_item_error_e; + } + return n20_cbor_read_skip_item_ok_e; +} + +static n20_cbor_read_skip_item_result_t n20_cbor_read_skip_item_stringish_chunk_internal( + n20_istream_t *const s, bool string) { + n20_cbor_type_t type; + uint64_t n; + if (!n20_cbor_read_header(s, &type, &n)) { + return n20_cbor_read_skip_item_error_e; + } + if (type == n20_cbor_type_indefinite_break_e) { + return n20_cbor_read_skip_item_break_e; + } + if ((string && type != n20_cbor_type_string_e) || (!string && type != n20_cbor_type_bytes_e)) { + return n20_cbor_read_skip_item_error_e; /* Not a valid expected stringish chunk. */ + } + if (n > SIZE_MAX) { + /* Prevent uncaught truncation. */ + return n20_cbor_read_skip_item_error_e; + } + if (!n20_istream_get_slice(s, NULL, n)) { + return n20_cbor_read_skip_item_error_e; + } + return n20_cbor_read_skip_item_ok_e; +} + +static n20_cbor_read_skip_item_result_t n20_cbor_read_skip_item_internal(n20_istream_t *const s) { n20_cbor_type_t type = n20_cbor_type_none_e; uint64_t n = 0; if (!n20_cbor_read_header(s, &type, &n)) { - return false; + return n20_cbor_read_skip_item_error_e; } switch (type) { case n20_cbor_type_array_e: if (n > SIZE_MAX) { /* Prevent overflow in the loop counter. */ - return false; + return n20_cbor_read_skip_item_error_e; } for (size_t i = 0; i < n; i++) { - if (!n20_cbor_read_skip_item(s)) { - return false; + if (n20_cbor_read_skip_item_internal(s) != n20_cbor_read_skip_item_ok_e) { + return n20_cbor_read_skip_item_error_e; } } break; case n20_cbor_type_map_e: if (n > SIZE_MAX) { /* Prevent overflow in the loop counter. */ - return false; + return n20_cbor_read_skip_item_error_e; } for (size_t i = 0; i < n; i++) { - if (!n20_cbor_read_skip_item(s)) { - return false; + if (n20_cbor_read_skip_item_internal(s) != n20_cbor_read_skip_item_ok_e) { + return n20_cbor_read_skip_item_error_e; } - if (!n20_cbor_read_skip_item(s)) { - return false; + if (n20_cbor_read_skip_item_internal(s) != n20_cbor_read_skip_item_ok_e) { + return n20_cbor_read_skip_item_error_e; } } break; @@ -200,20 +274,58 @@ bool n20_cbor_read_skip_item(n20_istream_t *const s) { case n20_cbor_type_string_e: { if (n > SIZE_MAX) { /* Prevent uncaught truncation. */ - return false; + return n20_cbor_read_skip_item_error_e; } if (!n20_istream_get_slice(s, NULL, n)) { - return false; + return n20_cbor_read_skip_item_error_e; } break; } case n20_cbor_type_tag_e: /* Skip the tag and the item it refers to. */ - return n20_cbor_read_skip_item(s); + return n20_cbor_read_skip_item_internal(s); + case n20_cbor_type_indefinite_bytes_e: + case n20_cbor_type_indefinite_string_e: { + n20_cbor_read_skip_item_result_t result; + do { + result = n20_cbor_read_skip_item_stringish_chunk_internal( + s, type == n20_cbor_type_indefinite_string_e); + if (result == n20_cbor_read_skip_item_error_e) { + return n20_cbor_read_skip_item_error_e; + } + } while (result != n20_cbor_read_skip_item_break_e); + break; + } + case n20_cbor_type_indefinite_array_e: { + n20_cbor_read_skip_item_result_t result; + do { + result = n20_cbor_read_skip_item_internal(s); + if (result == n20_cbor_read_skip_item_error_e) { + return n20_cbor_read_skip_item_error_e; + } + } while (result != n20_cbor_read_skip_item_break_e); + break; + } + case n20_cbor_type_indefinite_map_e: { + n20_cbor_read_skip_item_result_t result; + do { + result = n20_cbor_read_skip_item_map_element_internal(s); + if (result == n20_cbor_read_skip_item_error_e) { + return n20_cbor_read_skip_item_error_e; + } + } while (result != n20_cbor_read_skip_item_break_e); + break; + } + case n20_cbor_type_indefinite_break_e: + return n20_cbor_read_skip_item_break_e; default: /* Simple values and integers have no additional data to skip. */ break; } - return true; + return n20_cbor_read_skip_item_ok_e; +} + +bool n20_cbor_read_skip_item(n20_istream_t *const s) { + return n20_cbor_read_skip_item_internal(s) == n20_cbor_read_skip_item_ok_e; } diff --git a/src/core/test/cbor.cpp b/src/core/test/cbor.cpp index d11d3d30..80983462 100644 --- a/src/core/test/cbor.cpp +++ b/src/core/test/cbor.cpp @@ -55,6 +55,8 @@ INSTANTIATE_TEST_CASE_P( std::tuple(n20_cbor_type_map_e, UINT64_C(1), std::vector{0xa1}), std::tuple(n20_cbor_type_map_e, UINT64_C(23), std::vector{0xb7}), std::tuple(n20_cbor_type_map_e, UINT64_C(24), std::vector{0xb8, 0x18}), + /* Check that 31 is not treated as indefinite length. */ + std::tuple(n20_cbor_type_map_e, UINT64_C(31), std::vector{0xb8, 0x1F}), std::tuple(n20_cbor_type_map_e, UINT64_C(255), std::vector{0xb8, 0xff}), std::tuple(n20_cbor_type_map_e, UINT64_C(256), std::vector{0xb9, 0x01, 0x00}), std::tuple(n20_cbor_type_map_e, UINT64_C(0xffff), std::vector{0xb9, 0xff, 0xff}), @@ -90,6 +92,77 @@ TEST_P(CborHeaderTestFixture, CborHeaderTest) { ASSERT_EQ(got_encoding, encoding); } +// Tests for writing indefinite length headers. +// +// An indefinite length header is the major type in the top 3 bits ORed with +// the additional info value 31 (0x1f) in the low 5 bits. The value argument +// is ignored for these types. +// - indefinite byte string -> 0x5f +// - indefinite text string -> 0x7f +// - indefinite array -> 0x9f +// - indefinite map -> 0xbf +// - break stop code -> 0xff +class CborIndefiniteHeaderTestFixture + : public testing::TestWithParam>> {}; + +INSTANTIATE_TEST_CASE_P( + CborIndefiniteHeaderTestInstance, + CborIndefiniteHeaderTestFixture, + testing::Values(std::tuple(n20_cbor_type_indefinite_bytes_e, std::vector{0x5f}), + std::tuple(n20_cbor_type_indefinite_string_e, std::vector{0x7f}), + std::tuple(n20_cbor_type_indefinite_array_e, std::vector{0x9f}), + std::tuple(n20_cbor_type_indefinite_map_e, std::vector{0xbf}), + std::tuple(n20_cbor_type_indefinite_break_e, std::vector{0xff}))); + +TEST_P(CborIndefiniteHeaderTestFixture, CborWriteIndefiniteHeader) { + auto [type, encoding] = GetParam(); + + uint8_t buffer[20]; + + n20_stream_t s; + n20_stream_init(&s, &buffer[0], sizeof(buffer)); + + // The value argument must be ignored for indefinite length headers. + n20_cbor_write_header(&s, type, 12345); + + ASSERT_FALSE(n20_stream_has_buffer_overflow(&s)); + size_t bytes_written = n20_stream_byte_count(&s); + auto got_encoding = std::vector(n20_stream_data(&s), n20_stream_data(&s) + bytes_written); + ASSERT_EQ(got_encoding, encoding); +} + +// An indefinite length header written by n20_cbor_write_header must be read +// back by n20_cbor_read_header as the corresponding indefinite type. +TEST(CborTests, CborIndefiniteHeaderRoundTrip) { + struct { + n20_cbor_type_t write_type; + n20_cbor_type_t read_type; + } const cases[] = { + {n20_cbor_type_indefinite_bytes_e, n20_cbor_type_indefinite_bytes_e}, + {n20_cbor_type_indefinite_string_e, n20_cbor_type_indefinite_string_e}, + {n20_cbor_type_indefinite_array_e, n20_cbor_type_indefinite_array_e}, + {n20_cbor_type_indefinite_map_e, n20_cbor_type_indefinite_map_e}, + {n20_cbor_type_indefinite_break_e, n20_cbor_type_indefinite_break_e}, + }; + + for (auto const& c : cases) { + uint8_t buffer[20]; + n20_stream_t s; + n20_stream_init(&s, &buffer[0], sizeof(buffer)); + n20_cbor_write_header(&s, c.write_type, 0); + ASSERT_FALSE(n20_stream_has_buffer_overflow(&s)); + + n20_istream_t is; + n20_istream_init(&is, n20_stream_data(&s), n20_stream_byte_count(&s)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&is, &type, &value)); + EXPECT_EQ(type, c.read_type); + EXPECT_EQ(value, 0u); + } +} + class CborIntegerTestFixture : public testing::TestWithParam< std::tuple, std::vector>> {}; @@ -889,6 +962,364 @@ TEST_F(CborReadTest, SkipZeroLengthStrings) { EXPECT_EQ(value, 1); } +// Tests for indefinite length encoding in n20_cbor_read_header. +// +// The header byte for an indefinite length item is the major type shifted +// left by 5 ORed with the additional info value 31 (0x1f). +// - 0x5f: indefinite byte string (major type 2) +// - 0x7f: indefinite text string (major type 3) +// - 0x9f: indefinite array (major type 4) +// - 0xbf: indefinite map (major type 5) +// - 0xff: break stop code (major type 7) +// The reported value @c n is always 0 for these headers. +TEST_F(CborReadTest, ReadHeaderIndefiniteByteString) { + WriteCborData({0x5f}); + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_indefinite_bytes_e); + EXPECT_EQ(value, 0); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteTextString) { + WriteCborData({0x7f}); + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_indefinite_string_e); + EXPECT_EQ(value, 0); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteArray) { + WriteCborData({0x9f}); + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_indefinite_array_e); + EXPECT_EQ(value, 0); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteMap) { + WriteCborData({0xbf}); + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_indefinite_map_e); + EXPECT_EQ(value, 0); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteBreak) { + WriteCborData({0xff}); + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_indefinite_break_e); + EXPECT_EQ(value, 0); +} + +// Additional info 31 is only valid for byte strings, text strings, arrays, +// maps, and the simple/float major type (the break code). It must be +// rejected for unsigned integers, negative integers, and tags. +TEST_F(CborReadTest, ReadHeaderIndefiniteUintFails) { + WriteCborData({0x1f}); // major type 0 (uint) with additional info 31 + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_FALSE(n20_cbor_read_header(&stream, &type, &value)); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteNintFails) { + WriteCborData({0x3f}); // major type 1 (nint) with additional info 31 + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_FALSE(n20_cbor_read_header(&stream, &type, &value)); +} + +TEST_F(CborReadTest, ReadHeaderIndefiniteTagFails) { + WriteCborData({0xdf}); // major type 6 (tag) with additional info 31 + CreateStream(); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_FALSE(n20_cbor_read_header(&stream, &type, &value)); +} + +// Tests for indefinite length encoding in n20_cbor_read_skip_item. +TEST_F(CborReadTest, SkipIndefiniteByteString) { + // Indefinite byte string with two chunks "hi" and "bye", then uint 1. + WriteCborData({0x5f, 0x42, 'h', 'i', 0x43, 'b', 'y', 'e', 0xff, 0x01}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + // Should be positioned at uint 1. + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipIndefiniteTextString) { + // Indefinite text string with two chunks "hi" and "bye", then uint 2. + WriteCborData({0x7f, 0x62, 'h', 'i', 0x63, 'b', 'y', 'e', 0xff, 0x02}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + // Should be positioned at uint 2. + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 2); +} + +TEST_F(CborReadTest, SkipEmptyIndefiniteByteString) { + // Indefinite byte string with no chunks (immediate break), then uint 1. + WriteCborData({0x5f, 0xff, 0x01}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringWithZeroLengthChunk) { + // Indefinite byte string containing an empty chunk and a non-empty chunk. + WriteCborData({0x5f, 0x40, 0x42, 'h', 'i', 0xff, 0x01}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipIndefiniteArray) { + // Indefinite array [1, 2, 3], then uint 4. + WriteCborData({0x9f, 0x01, 0x02, 0x03, 0xff, 0x04}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 4); +} + +TEST_F(CborReadTest, SkipEmptyIndefiniteArray) { + WriteCborData({0x9f, 0xff, 0x01}); // empty indefinite array, uint 1 + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipIndefiniteMap) { + // Indefinite map {1: 2, 3: 4}, then uint 5. + WriteCborData({0xbf, 0x01, 0x02, 0x03, 0x04, 0xff, 0x05}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 5); +} + +TEST_F(CborReadTest, SkipEmptyIndefiniteMap) { + WriteCborData({0xbf, 0xff, 0x01}); // empty indefinite map, uint 1 + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipNestedIndefiniteArray) { + // Indefinite array containing an indefinite array: [[1], 2], then uint 3. + // The inner break must not terminate the outer array. + WriteCborData({0x9f, 0x9f, 0x01, 0xff, 0x02, 0xff, 0x03}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 3); +} + +TEST_F(CborReadTest, SkipIndefiniteArrayWithEmptyInnerIndefiniteArray) { + // Outer indefinite array whose first element is an empty indefinite array. + // Verifies the inner break does not prematurely end the outer array. + WriteCborData({0x9f, 0x9f, 0xff, 0x01, 0xff, 0x02}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 2); +} + +TEST_F(CborReadTest, SkipIndefiniteMapWithIndefiniteValue) { + // Indefinite map {1: [2, 3]} where the value is an indefinite array, + // then uint 4. + WriteCborData({0xbf, 0x01, 0x9f, 0x02, 0x03, 0xff, 0xff, 0x04}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 4); +} + +TEST_F(CborReadTest, SkipTaggedIndefiniteArray) { + // Tag 1 applied to an indefinite array [1], then uint 2. + WriteCborData({0xc1, 0x9f, 0x01, 0xff, 0x02}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 2); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringInsideDefiniteArray) { + // Definite array of 2: [indefinite byte string "hi", uint 9], then uint 1. + WriteCborData({0x82, 0x5f, 0x42, 'h', 'i', 0xff, 0x09, 0x01}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +// Failure cases for indefinite length encoding. + +TEST_F(CborReadTest, SkipBareBreakFails) { + // A break stop code on its own is not a valid item to skip. + WriteCborData({0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringUnterminated) { + // Indefinite byte string with a chunk but no break stop code. + WriteCborData({0x5f, 0x42, 'h', 'i'}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringWithWrongChunkType) { + // The chunks of an indefinite byte string must themselves be definite + // byte strings. A uint chunk is invalid. + WriteCborData({0x5f, 0x01, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringWithNestedIndefiniteChunk) { + // Chunks must be definite byte strings, not nested indefinite ones. + WriteCborData({0x5f, 0x5f, 0xff, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteTextStringWithByteStringChunk) { + // The chunks of an indefinite text string must be definite text strings, + // not byte strings. + WriteCborData({0x7f, 0x42, 'h', 'i', 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteByteStringWithTruncatedChunk) { + // The chunk claims 5 bytes but only 2 are present. + WriteCborData({0x5f, 0x45, 'h', 'i'}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteArrayUnterminated) { + // Indefinite array with elements but no break stop code. + WriteCborData({0x9f, 0x01, 0x02}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteMapUnterminated) { + // Indefinite map with one complete pair but no break stop code. + WriteCborData({0xbf, 0x01, 0x02}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipIndefiniteMapWithBreakInValuePosition) { + // A break after a key (in the value position) is invalid: a map element + // must always have both a key and a value. + WriteCborData({0xbf, 0x01, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + class CborInvalidHeaderTestFixture : public CborReadTest, public testing::WithParamInterface> {}; @@ -904,7 +1335,7 @@ INSTANTIATE_TEST_SUITE_P( n20_cbor_type_map_e, n20_cbor_type_tag_e, n20_cbor_type_simple_float_e), - testing::Values(28, 29, 30, 31)), + testing::Values(28, 29, 30)), [](testing::TestParamInfo const& info) { return std::to_string(std::get<0>(info.param)) + "_" + std::to_string(std::get<1>(info.param)); diff --git a/src/service/test/messages.cpp b/src/service/test/messages.cpp index 7b215570..1e470433 100644 --- a/src/service/test/messages.cpp +++ b/src/service/test/messages.cpp @@ -1089,7 +1089,7 @@ TEST_F(MessagesTest, MalformedIssueCertResponseHandling) { WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_ok_e, n20_msg_issue_cert_response_read(&response, test_slice)); - cbor_data = {0xA1, 0x17, 0xFF}; + cbor_data = {0xA1, 0x17, 0xFE}; // Unknown field key. Not a valid CBOR item. WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_unexpected_message_structure_e, @@ -1155,7 +1155,7 @@ TEST_F(MessagesTest, MalformedErrorResponseReadHandling) { WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_ok_e, n20_msg_error_response_read(&response, test_slice)); - cbor_data = {0xA1, 0x17, 0xFF}; + cbor_data = {0xA1, 0x17, 0xFE}; // Unknown field key. Not a valid CBOR item. WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_unexpected_message_structure_e, @@ -1229,7 +1229,7 @@ TEST_F(MessagesTest, MalformedEcaEeSignResponseHandling) { WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_ok_e, n20_msg_eca_ee_sign_response_read(&response, test_slice)); - cbor_data = {0xA1, 0x17, 0xFF}; + cbor_data = {0xA1, 0x17, 0xFE}; // Unknown field key. Not a valid CBOR item. WriteTestCborMessage(cbor_data); EXPECT_EQ(n20_error_unexpected_message_structure_e, From 9a447582712ffe63baa028ce892c8d145972016b Mon Sep 17 00:00:00 2001 From: Janis Danisevskis Date: Tue, 16 Jun 2026 10:35:38 -0700 Subject: [PATCH 2/4] Fix missing handling of undefined types. --- src/core/cbor.c | 42 +++++++++++++++++++++++++++--------------- src/core/test/cbor.cpp | 10 +++++++++- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/core/cbor.c b/src/core/cbor.c index 60796fe1..4aaf70ce 100644 --- a/src/core/cbor.c +++ b/src/core/cbor.c @@ -41,26 +41,38 @@ #include void n20_cbor_write_header(n20_stream_t *const s, n20_cbor_type_t cbor_type, uint64_t n) { - bool indefinite_length = (cbor_type & 0x100) != 0; - if (indefinite_length) { - /* Indefinite length encoding is encoded in the ninth bit of the type. */ - cbor_type = (n20_cbor_type_t)(cbor_type - 0x100); - } - if ((unsigned int)cbor_type > 7) { - /* 0xf7 is the encoding of the special value "undefined". */ - cbor_type = n20_cbor_type_simple_float_e; - n = N20_SIMPLE_UNDEFINED; + switch (cbor_type) { + case n20_cbor_type_uint_e: + case n20_cbor_type_nint_e: + case n20_cbor_type_bytes_e: + case n20_cbor_type_string_e: + case n20_cbor_type_array_e: + case n20_cbor_type_map_e: + case n20_cbor_type_tag_e: + case n20_cbor_type_simple_float_e: + break; + case n20_cbor_type_indefinite_bytes_e: + case n20_cbor_type_indefinite_string_e: + case n20_cbor_type_indefinite_array_e: + case n20_cbor_type_indefinite_map_e: + case n20_cbor_type_indefinite_break_e: { + cbor_type = (n20_cbor_type_t)(cbor_type - 0x100); + uint8_t header = (uint8_t)(cbor_type << 5) | 31; + n20_stream_prepend(s, &header, /*src_len=*/1); + return; + } + default: + /* Invalid types are encoded as "undefined". */ + cbor_type = n20_cbor_type_simple_float_e; + n = N20_SIMPLE_UNDEFINED; + break; } + uint8_t header = (uint8_t)(cbor_type << 5); size_t value_size = 0; - if (indefinite_length) { - /* Indefinite length encoding is denoted by additional info value 31. */ - header |= 31; - n20_stream_prepend(s, &header, /*src_len=*/1); - return; - } else if (n < 24) { + if (n < 24) { header |= (uint8_t)n; n20_stream_prepend(s, &header, /*src_len=*/1); return; diff --git a/src/core/test/cbor.cpp b/src/core/test/cbor.cpp index 80983462..bf3f2558 100644 --- a/src/core/test/cbor.cpp +++ b/src/core/test/cbor.cpp @@ -74,7 +74,15 @@ INSTANTIATE_TEST_CASE_P( std::vector{0xbb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}), /* Invalid types map to "undefined" CBOR type (0xf7). */ std::tuple(n20_cbor_type_none_e, UINT64_C(0), std::vector{0xf7}), - std::tuple((n20_cbor_type_t)8, UINT64_C(0), std::vector{0xf7}))); + std::tuple((n20_cbor_type_t)8, UINT64_C(0), std::vector{0xf7}), + /* An invalid type that has the indefinite length bit set must also be mapped to "undefined" + CBOR type (0xf7). */ + std::tuple((n20_cbor_type_t)0xFFF, UINT64_C(0), std::vector{0xf7}), + /* Int, nint, and tag with the indefinite flag set must also be mapped to "undefined" + CBOR type (0xf7). */ + std::tuple((n20_cbor_type_t)0x100, UINT64_C(0), std::vector{0xf7}), + std::tuple((n20_cbor_type_t)0x101, UINT64_C(0), std::vector{0xf7}), + std::tuple((n20_cbor_type_t)0x106, UINT64_C(0), std::vector{0xf7}))); TEST_P(CborHeaderTestFixture, CborHeaderTest) { auto [type, integer, encoding] = GetParam(); From 2dcc2ab200b8d7b46fe3ddf83f8f3c3387710a2a Mon Sep 17 00:00:00 2001 From: Janis Danisevskis Date: Tue, 16 Jun 2026 10:49:41 -0700 Subject: [PATCH 3/4] Address comments --- src/core/cbor.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/cbor.c b/src/core/cbor.c index 4aaf70ce..2f27de03 100644 --- a/src/core/cbor.c +++ b/src/core/cbor.c @@ -172,7 +172,7 @@ bool n20_cbor_read_header(n20_istream_t *const s, n20_cbor_type_t *const type, u return true; default: /* Additional info 31 is only valid for arrays, maps, byte strings, and text - * strings. and in the simple/float type denoting the end of indefinite length + * strings, and in the simple/float type denoting the end of indefinite length * items. */ return false; } @@ -295,7 +295,8 @@ static n20_cbor_read_skip_item_result_t n20_cbor_read_skip_item_internal(n20_ist } case n20_cbor_type_tag_e: /* Skip the tag and the item it refers to. */ - return n20_cbor_read_skip_item_internal(s); + return n20_cbor_read_skip_item(s) ? n20_cbor_read_skip_item_ok_e + : n20_cbor_read_skip_item_error_e; case n20_cbor_type_indefinite_bytes_e: case n20_cbor_type_indefinite_string_e: { n20_cbor_read_skip_item_result_t result; From c9cfdfa8b5f4d9cec9a2040a294d8521bc31560f Mon Sep 17 00:00:00 2001 From: Janis Danisevskis Date: Tue, 16 Jun 2026 15:20:07 -0700 Subject: [PATCH 4/4] Add nested indefinite types tests --- src/core/test/cbor.cpp | 172 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/src/core/test/cbor.cpp b/src/core/test/cbor.cpp index bf3f2558..7fce4b23 100644 --- a/src/core/test/cbor.cpp +++ b/src/core/test/cbor.cpp @@ -1328,6 +1328,178 @@ TEST_F(CborReadTest, SkipIndefiniteMapWithBreakInValuePosition) { EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); } +// Indefinite length items nested as the keys and values of an indefinite +// length map. +TEST_F(CborReadTest, SkipIndefiniteMapWithIndefiniteKeyAndValue) { + // Indefinite map { (indefinite text "hi"): (indefinite array [1]) }, + // then uint 7. + WriteCborData({0xbf, // indefinite map + 0x7f, + 0x62, + 'h', + 'i', + 0xff, // key: indefinite text string "hi" + 0x9f, + 0x01, + 0xff, // value: indefinite array [1] + 0xff, // map break + 0x07}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 7); +} + +TEST_F(CborReadTest, SkipIndefiniteMapWithIndefiniteByteStringKey) { + // Indefinite map { (indefinite byte string "ab"): 9 }, then uint 1. + WriteCborData({0xbf, // indefinite map + 0x5f, + 0x42, + 'a', + 'b', + 0xff, // key: indefinite byte string "ab" + 0x09, // value: uint 9 + 0xff, // map break + 0x01}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 1); +} + +TEST_F(CborReadTest, SkipIndefiniteMapWithIndefiniteMapValue) { + // Indefinite map { 1: { 2: 3 } } where the value is itself an indefinite + // map, then uint 8. + WriteCborData({0xbf, // outer indefinite map + 0x01, // key: uint 1 + 0xbf, + 0x02, + 0x03, + 0xff, // value: indefinite map { 2: 3 } + 0xff, // outer map break + 0x08}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 8); +} + +// Indefinite length items nested as the elements of an indefinite length +// array. +TEST_F(CborReadTest, SkipIndefiniteArrayOfIndefiniteItems) { + // Indefinite array [ (indefinite text "x"), (indefinite map {1: 2}), + // (indefinite byte string {0x00}) ], then uint 4. + WriteCborData({0x9f, // indefinite array + 0x7f, + 0x61, + 'x', + 0xff, // indefinite text string "x" + 0xbf, + 0x01, + 0x02, + 0xff, // indefinite map {1: 2} + 0x5f, + 0x41, + 0x00, + 0xff, // indefinite byte string {0x00} + 0xff, // array break + 0x04}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 4); +} + +TEST_F(CborReadTest, SkipDeeplyNestedIndefiniteArrays) { + // Three levels of empty indefinite arrays: [[[]]], then uint 5. Verifies + // that each break terminates exactly one level. + WriteCborData({0x9f, 0x9f, 0x9f, 0xff, 0xff, 0xff, 0x05}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 5); +} + +TEST_F(CborReadTest, SkipIndefiniteArrayWithTaggedElements) { + // Indefinite array [ tag1(1), 2 ], then uint 3. Confirms that a tag inside + // an indefinite array consumes its tagged item without disturbing the + // recognition of the array's break stop code. + WriteCborData({0x9f, 0xc1, 0x01, 0x02, 0xff, 0x03}); + CreateStream(); + + EXPECT_TRUE(n20_cbor_read_skip_item(&stream)); + + n20_cbor_type_t type; + uint64_t value; + EXPECT_TRUE(n20_cbor_read_header(&stream, &type, &value)); + EXPECT_EQ(type, n20_cbor_type_uint_e); + EXPECT_EQ(value, 3); +} + +// A break stop code may only terminate an indefinite length item. A tag must +// be followed by a data item, and a break is not a data item. A "tagged break" +// must therefore be reported as an error rather than being mistaken for the +// break of an enclosing indefinite length item. +TEST_F(CborReadTest, SkipTaggedBreakFails) { + // Tag 1 followed by a bare break stop code. + WriteCborData({0xc1, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipTaggedBreakInIndefiniteArrayFails) { + // Indefinite array whose first element is a tag followed by a break. + // The tagged break must error, not prematurely terminate the array (which + // would leave the trailing break unconsumed and wrongly report success). + WriteCborData({0x9f, 0xc1, 0xff, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipTaggedBreakInIndefiniteMapFails) { + // Indefinite map whose first key is a tag followed by a break. The tagged + // break must error, not be mistaken for the map's break stop code. + WriteCborData({0xbf, 0xc1, 0xff, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + +TEST_F(CborReadTest, SkipTaggedBreakInDefiniteArrayFails) { + // Definite array of one element that is a tag followed by a break. + WriteCborData({0x81, 0xc1, 0xff}); + CreateStream(); + + EXPECT_FALSE(n20_cbor_read_skip_item(&stream)); +} + class CborInvalidHeaderTestFixture : public CborReadTest, public testing::WithParamInterface> {};