]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add cbor encoder and decoder
authorAlan T. DeKok <aland@freeradius.org>
Fri, 8 Nov 2024 16:21:09 +0000 (16:21 +0000)
committerAlan T. DeKok <aland@freeradius.org>
Mon, 11 Nov 2024 00:19:01 +0000 (19:19 -0500)
src/lib/util/cbor.c [new file with mode: 0644]
src/lib/util/cbor.h [new file with mode: 0644]
src/lib/util/libfreeradius-util.mk

diff --git a/src/lib/util/cbor.c b/src/lib/util/cbor.c
new file mode 100644 (file)
index 0000000..ce68a75
--- /dev/null
@@ -0,0 +1,1007 @@
+/*
+ *   This library is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU Lesser General Public
+ *   License as published by the Free Software Foundation; either
+ *   version 2.1 of the License, or (at your option) any later version.
+ *
+ *   This library is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ *   Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU Lesser General Public
+ *   License along with this library; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/** CBPR encoding and decoding
+ *
+ * @file src/lib/util/cbor.c
+ *
+ * @copyright 2024 Network RADIUS SAS (legal@networkradius.com)
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/util/cbor.h>
+
+#define CBOR_INTEGER   (0)
+#define CBOR_NEGATIVE  (1)
+#define CBOR_STRING    (2)
+#define CBOR_OCTETS    (3)
+#define CBOR_ARRAY     (4)
+#define CBOR_MAP       (5)
+#define CBOR_TAG       (6)
+#define CBOR_FLOAT     (7)
+
+#define CBOR_1_BYTE ((uint8_t) 24)
+#define CBOR_2_BYTE ((uint8_t) 25)
+#define CBOR_4_BYTE ((uint8_t) 26)
+#define CBOR_8_BYTE ((uint8_t) 27)
+
+/*
+ *     Some of our data types need tags.
+ *
+ *     We don't have a tag to data type array.  When decoding, we should usually have the enclosing pair
+ *     number, which includes our data type.  If the tag type doesn't match the value here, then something is
+ *     wrong.
+ */
+static const uint64_t cbor_type_to_tag[FR_TYPE_MAX] = {
+       [FR_TYPE_DATE] = 1,
+       [FR_TYPE_ETHERNET] = 48,
+       [FR_TYPE_IPV4_ADDR] = 52,
+       [FR_TYPE_IPV4_PREFIX] = 52,
+       [FR_TYPE_IPV6_ADDR] = 54,
+       [FR_TYPE_IPV6_PREFIX] = 54,
+};
+
+static ssize_t cbor_encode_integer(fr_dbuff_t *dbuff, uint8_t type, uint64_t data)
+{
+       fr_dbuff_t      work_dbuff = FR_DBUFF(dbuff);
+
+       type <<= 5;
+
+       if (data < 24) {
+               data |= type;
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) (data & 0xff));
+               goto done;
+       }
+
+       if (data < (((uint64_t) 1) << 8)) {
+               uint8_t value = data;
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) (type | CBOR_1_BYTE));
+               FR_DBUFF_IN_RETURN(&work_dbuff, value);
+               goto done;
+       }
+
+       if (data < (((uint64_t) 1) << 16)) {
+               uint16_t value = data;
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) (type | CBOR_2_BYTE));
+               FR_DBUFF_IN_RETURN(&work_dbuff, value);
+               goto done;
+       }
+
+       if (data < (((uint64_t) 1) << 32)) {
+               uint16_t value = data;
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) (type | CBOR_4_BYTE));
+               FR_DBUFF_IN_RETURN(&work_dbuff, value);
+               goto done;
+       }
+
+       /*
+        *      Has to be 8 bytes.
+        */
+       FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, type | CBOR_8_BYTE);
+       FR_DBUFF_IN_RETURN(&work_dbuff, data);
+
+done:
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+#define cbor_encode_array(_dbuff, _size) cbor_encode_integer(_dbuff, CBOR_ARRAY, _size);
+
+#define cbor_encode_tag(_dbuff, _tag) cbor_encode_integer(_dbuff, CBOR_TAG, _tag);
+
+/*
+ *     Octets is length + data
+ */
+static ssize_t cbor_encode_octets(fr_dbuff_t *dbuff, uint8_t const *data, size_t data_len)
+{
+       fr_dbuff_t      work_dbuff = FR_DBUFF(dbuff);
+       ssize_t slen;
+
+       slen = cbor_encode_integer(&work_dbuff, CBOR_OCTETS, data_len);
+       if (slen <= 0) return slen;
+
+       FR_DBUFF_IN_MEMCPY_RETURN(&work_dbuff, data, data_len);
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+/** Encode CBOR
+ *
+ *  Values 0..23 can be encoded in place.  Other values can be encoded using the closest smallest integer
+ */
+ssize_t fr_cbor_encode_value_box(fr_dbuff_t *dbuff, fr_value_box_t *vb)
+{
+       fr_dbuff_t      work_dbuff = FR_DBUFF(dbuff);
+       uint8_t         type = CBOR_INTEGER;
+       uint64_t        data;
+       int64_t         neg;
+       ssize_t         slen;
+
+       switch (vb->type) {
+       case FR_TYPE_BOOL:
+               data = vb->vb_bool;
+               goto encode_int;
+
+       case FR_TYPE_UINT8:
+               data = vb->vb_uint8;
+               goto encode_int;
+
+       case FR_TYPE_UINT16:
+               data = vb->vb_uint16;
+               goto encode_int;
+
+       case FR_TYPE_UINT32:
+               data = vb->vb_uint64;
+               goto encode_int;
+
+       case FR_TYPE_UINT64:
+               data = vb->vb_uint64;
+               goto encode_int;
+
+       /*
+        *      Negative numbers.
+        */
+       case FR_TYPE_INT8:
+               neg = vb->vb_int8;
+               goto encode_neg;
+
+       case FR_TYPE_INT16:
+               neg = vb->vb_int16;
+               goto encode_neg;
+
+       case FR_TYPE_INT32:
+               neg = vb->vb_int64;
+               goto encode_neg;
+
+       case FR_TYPE_INT64:
+               neg = vb->vb_int64;
+       encode_neg:
+               if (neg >= 0) {
+                       type = CBOR_NEGATIVE;
+                       data = neg;
+                       goto encode_int;
+               }
+
+               /*
+                *      convert -1..-2^63 to 0..-(2^63-1)
+                *      and then it fits into a positive integer.
+                */
+               neg++;
+               data = -neg;
+
+       encode_int:
+               return cbor_encode_integer(dbuff, type, data);
+
+       case FR_TYPE_OCTETS:
+               return cbor_encode_octets(dbuff, vb->vb_octets, vb->vb_length);
+
+       case FR_TYPE_STRING:
+               slen = cbor_encode_integer(&work_dbuff, CBOR_STRING, vb->vb_length);
+               if (slen <= 0) return slen;
+
+               FR_DBUFF_IN_MEMCPY_RETURN(&work_dbuff, vb->vb_strvalue, vb->vb_length);
+               break;
+
+               /*
+                *      More complex data types are represented by type "tag", followed by a tag number.  The
+                *      actual data is then encoded as the next item after the tag.
+                */
+       case FR_TYPE_ETHERNET:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_octets(&work_dbuff, vb->vb_ether, sizeof(vb->vb_ether));
+               if (slen <= 0) return slen;
+               break;
+
+       case FR_TYPE_DATE:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               neg = fr_unix_time_to_sec(vb->vb_date);
+               if (neg >= 0) {
+                       slen = cbor_encode_integer(&work_dbuff, CBOR_INTEGER, neg);
+               } else {
+                       neg++;
+                       data = -neg;
+                       slen = cbor_encode_integer(&work_dbuff, CBOR_NEGATIVE, data);
+               }
+               if (slen <= 0) return slen;
+               break;
+
+               /*
+                *      RFC 9164, Section 3.3
+                *
+                *      tag=IPv4 + address
+                */
+       case FR_TYPE_IPV4_ADDR:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_ip.addr.v4.s_addr, 4);
+               if (slen <= 0) return slen;
+               break;
+
+               /*
+                *      RFC 9164, Section 3.2
+                *
+                *      tag=IPv6 + address
+                */
+       case FR_TYPE_IPV6_ADDR:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_ip.addr.v6.s6_addr, 16);
+               if (slen <= 0) return slen;
+               break;
+
+               /*
+                *      RFC 9164, Section 3.3
+                *
+                *      tag=IPv4 + array(prefix-length, address)
+                */
+       case FR_TYPE_IPV4_PREFIX:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_array(&work_dbuff, 2);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_integer(&work_dbuff, CBOR_INTEGER, vb->vb_ip.prefix);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_ip.addr.v4.s_addr, 4);
+               if (slen <= 0) return slen;
+               break;
+
+               /*
+                *      RFC 9164, Section 3.2
+                *
+                *      tag=IPv6 + array(prefix-length, address)
+                */
+       case FR_TYPE_IPV6_PREFIX:
+               slen = cbor_encode_tag(&work_dbuff, cbor_type_to_tag[vb->type]);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_array(&work_dbuff, 2);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_integer(&work_dbuff, CBOR_INTEGER, vb->vb_ip.prefix);
+               if (slen <= 0) return slen;
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_ip.addr.v6.s6_addr, 16);
+               if (slen <= 0) return slen;
+
+               break;
+
+       case FR_TYPE_FLOAT32:
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) ((CBOR_FLOAT << 5) | CBOR_4_BYTE));
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_float32, 4);
+               if (slen <= 0) return slen;
+               break;
+
+       case FR_TYPE_FLOAT64:
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) ((CBOR_FLOAT << 5) | CBOR_8_BYTE));
+
+               slen = cbor_encode_octets(&work_dbuff, (uint8_t const *) &vb->vb_float64, 8);
+               if (slen <= 0) return slen;
+               break;
+
+       case FR_TYPE_GROUP:
+               /*
+                *      The value is array(children)
+                */
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) ((CBOR_ARRAY << 5) | 31)); /* indefinite array */
+
+               fr_value_box_list_foreach(&vb->vb_group, child) {
+                       slen = fr_cbor_encode_value_box(&work_dbuff, child);
+                       if (slen <= 0) return slen; /* @todo - dbuff want more room? */
+               }
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) 0xff); /* break */
+               break;
+
+
+       default:
+               fr_strerror_printf("Invalid data type %s for cbor encoding", fr_type_to_str(vb->type));
+               return -1;
+       }
+
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+
+static ssize_t cbor_decode_integer(uint64_t *out, uint8_t info, fr_dbuff_t *dbuff)
+{
+       fr_dbuff_t work_dbuff = FR_DBUFF(dbuff);
+
+       if (info < 24) {
+               *out = info;
+               return 0;
+       }
+
+       if (info == CBOR_1_BYTE) {
+               uint8_t value;
+
+               FR_DBUFF_OUT_RETURN(&value, &work_dbuff);
+               *out = value;
+               goto done;
+       }
+
+       if (info == CBOR_2_BYTE) {
+               uint16_t value;
+
+               FR_DBUFF_OUT_RETURN(&value, &work_dbuff);
+               *out = value;
+               goto done;
+       }
+
+       if (info == CBOR_4_BYTE) {
+               uint32_t value;
+
+               FR_DBUFF_OUT_RETURN(&value, &work_dbuff);
+               *out = value;
+               goto done;
+       }
+
+       if (info == CBOR_8_BYTE) {
+               uint64_t value;
+
+               FR_DBUFF_OUT_RETURN(&value, &work_dbuff);
+               *out = value;
+               goto done;
+       }
+
+       /*
+        *      28 and greater are invalid according to the RFCs.
+        */
+
+done:
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+/*
+ *     @todo - fr_cbor_encode_pair_list().  And then if we have da->flags.array, we encode the _value_ as an
+ *     array of indeterminate length.  This is a little bit of a special case, but not terrible.
+ */
+ssize_t fr_cbor_decode_value_box(TALLOC_CTX *ctx, fr_value_box_t *vb, fr_dbuff_t *dbuff, fr_type_t type, bool tainted)
+{
+       fr_dbuff_t work_dbuff = FR_DBUFF(dbuff);
+       bool indefinite;
+       uint8_t major, info;
+       ssize_t slen;
+       int64_t neg;
+       uint64_t value;
+       uint8_t *ptr;
+
+       FR_DBUFF_OUT_RETURN(&major, &work_dbuff);
+
+       if (type != FR_TYPE_NULL) {
+               fr_value_box_init(vb, type, NULL, tainted);
+       }
+
+       info = major & 0x1f;
+       major >>= 5;
+
+       /*
+        *      Invalid combinations.
+        */
+       if (((info >= 28) && (info <= 30)) ||
+           ((info == 31) && ((major == 0) || (major == 1) || (major == 6)))) {
+               fr_strerror_const("Invalid cbor data - input is not 'well formed'");
+               return -1;
+       }
+
+       switch (major) {
+       case CBOR_STRING:
+               if (major != CBOR_STRING) {
+               mismatch:
+                       fr_strerror_printf("cbor data contains invalid content %d for expected data type %s",
+                                          major, fr_type_to_str(type));
+                       return -1;
+               }
+
+               fr_assert(info != 31);
+
+               /*
+                *      @todo - undefinite length strings.  Which are really "chunked" strings.
+                */
+               slen = cbor_decode_integer(&value, info, &work_dbuff);
+               if (slen < 0) return slen;
+
+               /*
+                *      A little bit of sanity check.
+                */
+               if (value > (1 << 20)) {
+                       fr_strerror_printf("cbor data string is too long (%llu)", value);
+                       return -1;
+               }
+
+               ptr = talloc_array(ctx, uint8_t, value + 1);
+               if (!ptr) {
+                       fr_strerror_const("Out of memory");
+                       return -1;
+               }
+               talloc_set_type(ptr, char);
+               ptr[value] = '\0';
+
+               if (type == FR_TYPE_NULL) fr_value_box_init(vb, FR_TYPE_STRING, NULL, tainted);
+               fr_value_box_strdup_shallow(vb, NULL, (char const *) ptr, false); /* tainted? */
+
+               FR_DBUFF_OUT_MEMCPY_RETURN(ptr, &work_dbuff, value);
+               break;
+
+       case CBOR_OCTETS:
+               if (major != CBOR_OCTETS) goto mismatch;
+
+               fr_assert(info != 31);
+
+               /*
+                *      @todo - undefinite length octet strings.  Which are really "chunked" octet strings.
+                */
+               slen = cbor_decode_integer(&value, info, &work_dbuff);
+               if (slen < 0) return slen;
+
+               /*
+                *      A little bit of sanity check.
+                */
+               if (value > (1 << 20)) {
+                       fr_strerror_printf("cbor data string is too long (%llu)", value);
+                       return -1;
+               }
+
+               ptr = talloc_array(ctx, uint8_t, value);
+               if (!ptr) {
+                       fr_strerror_const("Out of memory");
+                       return -1;
+               }
+
+               if (type == FR_TYPE_NULL) fr_value_box_init(vb, FR_TYPE_OCTETS, NULL, tainted);
+               fr_value_box_memdup_shallow(vb, NULL, (uint8_t const *) ptr, value, false); /* tainted? */
+
+               FR_DBUFF_OUT_MEMCPY_RETURN(ptr, &work_dbuff, value);
+               break;
+
+       case CBOR_INTEGER:
+               slen = cbor_decode_integer(&value, info, &work_dbuff);
+               if (slen < 0) return slen;
+
+               switch (type) {
+               case FR_TYPE_UINT8:
+                       if (value > UINT8_MAX) {
+                       invalid:
+                               fr_strerror_printf("cbor value is too large for output data type %s",
+                                                  fr_type_to_str(type));
+                               return -1;
+                       }
+                       vb->vb_uint8 = value;
+                       break;
+
+               case FR_TYPE_UINT16:
+                       if (value > UINT16_MAX) goto invalid;
+                       vb->vb_uint16 = value;
+                       break;
+
+               case FR_TYPE_UINT32:
+                       if (value > UINT32_MAX) goto invalid;
+                       vb->vb_uint32 = value;
+                       break;
+
+               case FR_TYPE_NULL:
+                       fr_value_box_init(vb, FR_TYPE_UINT64, NULL, tainted);
+                       FALL_THROUGH;
+
+               case FR_TYPE_UINT64:
+                       vb->vb_uint64 = value;
+                       break;
+
+               case FR_TYPE_INT8:
+                       if (value > INT8_MAX) goto invalid;
+                       vb->vb_int8 = value;
+                       break;
+
+               case FR_TYPE_INT16:
+                       if (value > INT16_MAX) goto invalid;
+                       vb->vb_int16 = value;
+                       break;
+
+               case FR_TYPE_INT32:
+                       if (value > INT32_MAX) goto invalid;
+                       vb->vb_int32 = value;
+                       break;
+
+               case FR_TYPE_INT64:
+                       if (value > INT64_MAX) goto invalid;
+                       vb->vb_int64 = value;
+                       break;
+
+               default:
+               integer_type_mismatch:
+                       fr_strerror_printf("Unexpected cbor type 'integer' when decoding data type %s",
+                                          fr_type_to_str(type));
+                       return -1;
+               }
+               break;
+
+       case CBOR_NEGATIVE:
+               slen = cbor_decode_integer(&value, info, &work_dbuff);
+               if (slen < 0) return slen;
+
+               /*
+                *      Signed numbers only go down to -2^63
+                */
+               if (value > ((uint64_t) 1) << 63) goto invalid;
+
+               /*
+                *      Convert 0..(2^63-1) into -0..-(2^63-1)
+                *      then conver to -1..-(2^63)
+                */
+               neg = -value;
+               neg--;
+
+               switch (type) {
+               case FR_TYPE_INT8:
+                       if (neg < INT8_MIN) goto invalid;
+                       vb->vb_int8 = neg;
+                       break;
+
+               case FR_TYPE_INT16:
+                       if (neg < INT16_MIN) goto invalid;
+                       vb->vb_int16 = neg;
+                       break;
+
+               case FR_TYPE_INT32:
+                       if (neg < INT32_MIN) goto invalid;
+                       vb->vb_int32 = neg;
+                       break;
+
+               case FR_TYPE_NULL:
+                       fr_value_box_init(vb, FR_TYPE_INT64, NULL, tainted);
+                       FALL_THROUGH;
+
+               case FR_TYPE_INT64:
+                       vb->vb_int64 = neg;
+                       break;
+
+               default:
+                       goto integer_type_mismatch;
+               }
+               break;
+
+       case CBOR_FLOAT:
+               /*
+                *      Floats can be encoded as integers.
+                */
+               if (info < 24) {
+                       switch (type) {
+                       case FR_TYPE_FLOAT32:
+                               vb->vb_float32 = info;
+                               break;
+
+                       case FR_TYPE_NULL:
+                               fr_value_box_init(vb, FR_TYPE_FLOAT64, NULL, tainted);
+                               FALL_THROUGH;
+
+                       case FR_TYPE_FLOAT64:
+                               vb->vb_float64 = info;
+                               break;
+
+                       default:
+                       float_type_mismatch:
+                               fr_strerror_printf("Unexpected cbor type 'float' when decoding data type %s",
+                                                  fr_type_to_str(type));
+                               return -1;
+                       }
+
+                       break;
+               }
+
+               /*
+                *      Or as one-byte integers.
+                */
+               if (info == CBOR_1_BYTE) {
+                       uint8_t data;
+
+                       FR_DBUFF_OUT_RETURN(&data, &work_dbuff);
+
+                       switch (type) {
+                       case FR_TYPE_FLOAT32:
+                               vb->vb_float32 = data;
+                               break;
+
+                       case FR_TYPE_NULL:
+                               fr_value_box_init(vb, FR_TYPE_FLOAT64, NULL, tainted);
+                               FALL_THROUGH;
+
+                       case FR_TYPE_FLOAT64:
+                               vb->vb_float64 = data;
+                               break;
+
+                       default:
+                               goto float_type_mismatch;
+                       }
+
+                       break;
+               }
+
+               /*
+                *      We don't support float16
+                */
+
+               if (info == CBOR_4_BYTE) {
+                       uint8_t buffer[4];
+                       double data;
+
+                       FR_DBUFF_OUT_MEMCPY_RETURN(&buffer[0], &work_dbuff, sizeof(buffer));
+                       memcpy(&data, &buffer[0], sizeof(data));
+
+                       switch (type) {
+                       case FR_TYPE_FLOAT32:
+                               vb->vb_float32 = data;
+                               break;
+
+                       case FR_TYPE_NULL:
+                               fr_value_box_init(vb, FR_TYPE_FLOAT64, NULL, tainted);
+                               FALL_THROUGH;
+
+                       case FR_TYPE_FLOAT64:
+                               vb->vb_float64 = data;
+                               break;
+
+                       default:
+                               goto float_type_mismatch;
+                       }
+
+                       break;
+               }
+
+               if (info == CBOR_8_BYTE) {
+                       uint8_t buffer[8];
+                       double data;
+
+                       FR_DBUFF_OUT_MEMCPY_RETURN(&buffer[0], &work_dbuff, sizeof(buffer));
+                       memcpy(&data, &buffer[0], sizeof(data));
+
+                       switch (type) {
+                       case FR_TYPE_FLOAT32:
+                               vb->vb_float32 = data; /* maybe loses precision? */
+                               break;
+
+                       case FR_TYPE_NULL:
+                               fr_value_box_init(vb, FR_TYPE_FLOAT64, NULL, tainted);
+                               FALL_THROUGH;
+
+                       case FR_TYPE_FLOAT64:
+                               vb->vb_float64 = data;
+                               break;
+
+                       default:
+                               goto float_type_mismatch;
+                       }
+
+                       break;
+               }
+
+               /*
+                *      24 is FLOAT16, which we don't support.
+                *      31 is BREAK, which the caller should have checked for.
+                */
+               goto float_type_mismatch;
+
+       case CBOR_TAG:
+               /*
+                *      We only support a limited number of tags.
+                */
+               fr_assert(0);
+               break;
+
+       case CBOR_ARRAY:
+               if (type == FR_TYPE_NULL) {
+                       type = FR_TYPE_GROUP;
+                       fr_value_box_init(vb, type, NULL, tainted);
+               }
+
+               if (type != FR_TYPE_GROUP) goto invalid_type;
+
+               /*
+                *      Loop until done.
+                */
+               if (info == 31) {
+                       value = ~0;
+                       indefinite = true;
+
+               } else {
+                       slen = cbor_decode_integer(&value, info, &work_dbuff);
+                       if (slen < 0) return slen;
+
+                       indefinite = false;
+               }
+
+               /*
+                *      Loop until we decode everything.  For simplicity, we handle indefinite and definite
+                *      length arrays in the same loop.
+                */
+               for (/* nothing */; value > 0; value--) {
+                       uint8_t header;
+                       fr_value_box_t *child;
+
+                       /*
+                        *      Require at least one byte in the buffer.
+                        */
+                       if (fr_dbuff_extend_lowat(NULL, &work_dbuff, 1) == 0) return -fr_dbuff_used(&work_dbuff);
+
+                       /*
+                        *      Peek ahead for a break.
+                        */
+                       header = *fr_dbuff_current(&work_dbuff);
+                       if (header == 0xff) {
+                               if (!indefinite) {
+                                       fr_strerror_const("Unexpected 'break' found in cbor data");
+                                       return -fr_dbuff_used(&work_dbuff);
+                               }
+
+                               /*
+                                *      Done!
+                                */
+                               fr_dbuff_advance(&work_dbuff, 1);
+                               break;
+                       }
+
+                       child = fr_value_box_alloc(ctx, FR_TYPE_NULL, NULL);
+                       if (!child) {
+                               fr_strerror_const("Out of memory");
+                               return -fr_dbuff_used(&work_dbuff);
+                       }
+
+                       /*
+                        *      We have to decode at least one value.
+                        */
+                       slen = fr_cbor_decode_value_box(child, child, &work_dbuff, FR_TYPE_NULL, tainted);
+                       if (slen <= 0) return slen - fr_dbuff_used(&work_dbuff);
+
+                       fr_value_box_list_insert_tail(&vb->vb_group, child);
+               }
+
+               break;
+
+               /*
+                *      These are not value-box types.
+                */
+       case CBOR_MAP:
+       invalid_type:
+               fr_strerror_printf("Invalid data type %s for cbor to value-box", fr_type_to_str(type));
+               return -1;
+       }
+
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+/** Encode a pair
+ *
+ */
+ssize_t fr_cbor_encode_pair(fr_dbuff_t *dbuff, fr_pair_t *vp)
+{
+       fr_dbuff_t      work_dbuff = FR_DBUFF(dbuff);
+       ssize_t         slen;
+
+       FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) ((CBOR_MAP << 5) | 1)); /* map of 1 item */
+
+       /*
+        *      Key is the attribute number.
+        */
+       slen = cbor_encode_integer(&work_dbuff, CBOR_INTEGER, vp->da->attr);
+       if (slen <= 0) return slen;
+
+       /*
+        *      Value is the actual value of the leaf, or the array of children.
+        */
+       switch (vp->vp_type) {
+       case FR_TYPE_LEAF:
+               slen = fr_cbor_encode_value_box(&work_dbuff, &vp->data);
+               if (slen <= 0) return slen;
+               break;
+
+               /*
+                *      The only difference between TLV and VSA is that the children of VSA are all VENDORs.
+                */
+       case FR_TYPE_VENDOR:
+       case FR_TYPE_VSA:
+       case FR_TYPE_TLV:
+               /*
+                *      The value is array(children)
+                */
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) ((CBOR_ARRAY << 5) | 31)); /* indefinite array */
+
+               fr_pair_list_foreach(&vp->vp_group, child) {
+                       /*
+                        *      We don't allow changing dictionaries here.
+                        */
+                       if (child->da->parent != vp->da) continue;
+
+                       slen = fr_cbor_encode_pair(&work_dbuff, child);
+                       if (slen <= 0) return slen; /* @todo - dbuff want more room? */
+               }
+
+               FR_DBUFF_IN_BYTES_RETURN(&work_dbuff, (uint8_t) 0xff); /* break */
+               break;
+
+       /*
+        *      @todo - struct, except if we hit the end of the struct, check if the next child is the child
+        *      of the key?  That makes it all more annoying :(
+        */
+
+       default:
+               fr_strerror_printf("Invalid data type %s for cbor encoding", fr_type_to_str(vp->vp_type));
+               return -1;
+       }
+
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
+
+ssize_t fr_cbor_decode_pair(TALLOC_CTX *ctx, fr_pair_list_t *out, fr_dbuff_t *dbuff,
+                           fr_dict_attr_t const *parent, bool tainted)
+{
+       fr_dbuff_t work_dbuff = FR_DBUFF(dbuff);
+       uint8_t header, major, info;
+       bool indefinite;
+       ssize_t slen;
+       fr_pair_t *vp;
+       uint64_t value;
+       fr_dict_attr_t const *da;
+
+       FR_DBUFF_OUT_RETURN(&header, &work_dbuff);
+
+       /*
+        *      We require a 2-element array(attribute number, value)
+        */
+       if (header != (((CBOR_MAP) << 5) | 1)) {
+               fr_strerror_printf("Invalid cbor header - expected map of 1 elements, got %02x", header);
+               return -1;
+       }
+
+       /*
+        *      This should be a CBOR_INTEGER.
+        */
+       FR_DBUFF_OUT_RETURN(&major, &work_dbuff);
+
+       info = major & 0x1f;
+       major >>= 5;
+
+       if (major != CBOR_INTEGER) {
+               fr_strerror_printf("Invalid cbor - expected 'integer', got major type %d",
+                                  major);
+               return -1;
+       }
+
+       slen = cbor_decode_integer(&value, info, &work_dbuff);
+       if (slen < 0) {
+               return slen - fr_dbuff_used(&work_dbuff);
+       }
+
+       da = fr_dict_attr_child_by_num(parent, value);
+       if (!da) {
+               fr_assert(0);   /* @todo - yup! */
+       }
+
+       vp = fr_pair_afrom_da(ctx, da);
+       if (!vp) {
+               fr_strerror_const("Out of memory");
+               return -fr_dbuff_used(&work_dbuff);
+       }
+
+       /*
+        *      Leaf values are easy.
+        */
+       if (fr_type_is_leaf(da->type)) {
+               slen = fr_cbor_decode_value_box(vp, &vp->data, &work_dbuff, da->type, tainted);
+               if (slen <= 0) {
+                       talloc_free(vp);
+                       return slen - fr_dbuff_used(&work_dbuff);
+               }
+
+               goto done;
+       }
+
+       /*
+        *      All of these are essentially the same.
+        */
+       switch (da->type) {
+       case FR_TYPE_VENDOR:
+       case FR_TYPE_VSA:
+       case FR_TYPE_TLV:
+               break;
+
+       default:
+               talloc_free(vp);
+               fr_strerror_printf("Invalid data type %s for child %s of %s",
+                                  fr_type_to_str(da->type), vp->da->name, parent->name);
+               return -1;
+       }
+
+       /*
+        *      This should be a CBOR_ARRAY.
+        */
+       FR_DBUFF_OUT_RETURN(&major, &work_dbuff);
+
+       info = major & 0x1f;
+       major >>= 5;
+
+       if (major != CBOR_ARRAY) {
+               talloc_free(vp);
+               fr_strerror_printf("Invalid cbor - expected 'array', got major type %d",
+                                  major);
+               return -1;
+       }
+
+       if (info == 31) {
+               value = ~0;
+               indefinite = true;
+
+       } else {
+               slen = cbor_decode_integer(value, info, &work_dbuff);
+               if (slen < 0) {
+                       talloc_free(vp);
+                       return slen - fr_dbuff_used(&work_dbuff);
+               }
+
+               indefinite = false;
+       }
+
+       /*
+        *      Loop until we decode everything.  For simplicity, we handle indefinite and definite
+        *      length arrays in the same loop.
+        */
+       for (/* nothing */; value > 0; value--) {
+               /*
+                *      Require at least one byte in the buffer.
+                */
+               if (fr_dbuff_extend_lowat(NULL, &work_dbuff, 1) == 0) {
+                       talloc_free(vp);
+                       return -fr_dbuff_used(&work_dbuff);
+               }
+
+               /*
+                *      Peek ahead for a break.
+                */
+               header = *fr_dbuff_current(&work_dbuff);
+               if (header == 0xff) {
+                       if (!indefinite) {
+                               talloc_free(vp);
+                               fr_strerror_const("Unexpected 'break' found in cbor data");
+                               return -fr_dbuff_used(&work_dbuff);
+                       }
+
+                       /*
+                        *      Done!
+                        */
+                       fr_dbuff_advance(&work_dbuff, 1);
+                       break;
+               }
+
+               slen = fr_cbor_decode_pair(vp, &vp->vp_group, &work_dbuff, vp->da, tainted);
+               if (slen <= 0) {
+                       talloc_free(vp);
+                       return slen - fr_dbuff_used(&work_dbuff);
+               }
+       }
+
+done:
+       fr_pair_append(out, vp);
+       return fr_dbuff_set(dbuff, &work_dbuff);
+}
diff --git a/src/lib/util/cbor.h b/src/lib/util/cbor.h
new file mode 100644 (file)
index 0000000..7bdb289
--- /dev/null
@@ -0,0 +1,45 @@
+#pragma once
+/*
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/** CBOR encoder and decoder
+ *
+ * @file src/lib/util/cbor.h
+ *
+ * @copyright 2024 Network RADIUS SAS (legal@networkradius.com)
+ */
+RCSIDH(cbor_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <freeradius-devel/util/dbuff.h>
+#include <freeradius-devel/util/pair.h>
+
+ssize_t fr_cbor_encode_value_box(fr_dbuff_t *dbuff, fr_value_box_t *vb) CC_HINT(nonnull);
+
+ssize_t fr_cbor_decode_value_box(TALLOC_CTX *ctx, fr_value_box_t *vb, fr_dbuff_t *dbuff, fr_type_t hint, bool taint) CC_HINT(nonnull);
+
+ssize_t fr_cbor_encode_pair(fr_dbuff_t *dbuff, fr_pair_t *vp) CC_HINT(nonnull);
+
+ssize_t fr_cbor_decode_pair(TALLOC_CTX *ctx, fr_pair_list_t *out, fr_dbuff_t *dbuff,
+                           fr_dict_attr_t const *parent, bool tainted) CC_HINT(nonnull);
+
+
+#ifdef __cplusplus
+}
+#endif
index 359067860cfaa22df2f9b6cb44aaef86f8accce5..b2c8cf127ada65ad864db55c93a8bbdae6f49511 100644 (file)
@@ -18,6 +18,7 @@ SOURCES               := \
                   calc.c \
                   cap.c \
                   chap.c \
+                  cbor.c \
                   dbuff.c \
                   debug.c \
                   decode.c \