From 9e12dcccf95092545a179535e4fc45e54f2386b6 Mon Sep 17 00:00:00 2001 From: Laukik Hase Date: Tue, 31 Oct 2023 17:33:07 +0530 Subject: [PATCH] Add `l8w8jwt` component --- .build-test-rules.yml | 5 + .github/ISSUE_TEMPLATE/bug-report.yml | 1 + .github/workflows/upload_component.yml | 1 + .gitmodules | 3 + l8w8jwt/CMakeLists.txt | 15 + l8w8jwt/LICENSE | 1 + l8w8jwt/README.md | 1 + l8w8jwt/examples/es256/CMakeLists.txt | 6 + l8w8jwt/examples/es256/main/CMakeLists.txt | 2 + l8w8jwt/examples/es256/main/idf_component.yml | 8 + l8w8jwt/examples/es256/main/main.c | 166 +++++++++ l8w8jwt/idf_component.yml | 7 + l8w8jwt/l8w8jwt | 1 + l8w8jwt/port/private_include/checknum.h | 153 +++++++++ l8w8jwt/port/private_include/chillbuff.h | 314 ++++++++++++++++++ l8w8jwt/test/CMakeLists.txt | 4 + l8w8jwt/test/test.c | 144 ++++++++ test_app/CMakeLists.txt | 2 +- 18 files changed, 833 insertions(+), 1 deletion(-) create mode 100644 l8w8jwt/CMakeLists.txt create mode 120000 l8w8jwt/LICENSE create mode 120000 l8w8jwt/README.md create mode 100644 l8w8jwt/examples/es256/CMakeLists.txt create mode 100644 l8w8jwt/examples/es256/main/CMakeLists.txt create mode 100644 l8w8jwt/examples/es256/main/idf_component.yml create mode 100644 l8w8jwt/examples/es256/main/main.c create mode 100644 l8w8jwt/idf_component.yml create mode 160000 l8w8jwt/l8w8jwt create mode 100644 l8w8jwt/port/private_include/checknum.h create mode 100644 l8w8jwt/port/private_include/chillbuff.h create mode 100644 l8w8jwt/test/CMakeLists.txt create mode 100644 l8w8jwt/test/test.c diff --git a/.build-test-rules.yml b/.build-test-rules.yml index 7ef74d5652..90c1e8c80e 100644 --- a/.build-test-rules.yml +++ b/.build-test-rules.yml @@ -42,3 +42,8 @@ catch2/examples/catch2-console: disable: - if: IDF_VERSION_MAJOR < 5 reason: Example relies on WHOLE_ARCHIVE component property which was introduced in IDF v5.0 + +l8w8jwt/examples/es256: + enable: + - if: IDF_VERSION_MAJOR > 4 + reason: Supported only on IDF versions with Mbed TLS v3.x diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index c84cfc5b8f..0f9375b151 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -36,6 +36,7 @@ body: - jsmn - json_generator - json_parser + - l8w8jwt - led_strip - libsodium - nghttp diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index 362e698f80..962ef6239a 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -51,6 +51,7 @@ jobs: json_generator; json_parser; led_strip; + l8w8jwt; libsodium; nghttp; onewire_bus; diff --git a/.gitmodules b/.gitmodules index df609fc429..6d365d66c0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -57,3 +57,6 @@ [submodule "catch2/Catch2"] path = catch2/Catch2 url = https://github.com/catchorg/Catch2.git +[submodule "l8w8jwt/l8w8jwt"] + path = l8w8jwt/l8w8jwt + url = https://github.com/GlitchedPolygons/l8w8jwt.git diff --git a/l8w8jwt/CMakeLists.txt b/l8w8jwt/CMakeLists.txt new file mode 100644 index 0000000000..53a4006c0f --- /dev/null +++ b/l8w8jwt/CMakeLists.txt @@ -0,0 +1,15 @@ +idf_component_register(SRCS "l8w8jwt/src/base64.c" + "l8w8jwt/src/claim.c" + "l8w8jwt/src/decode.c" + "l8w8jwt/src/encode.c" + "l8w8jwt/src/util.c" + "l8w8jwt/src/version.c" + INCLUDE_DIRS "l8w8jwt/include" + PRIV_INCLUDE_DIRS "port/private_include" + PRIV_REQUIRES "mbedtls" +) + +set_source_files_properties("l8w8jwt/src/encode.c" "l8w8jwt/src/decode.c" + PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized") + +target_compile_options(${COMPONENT_LIB} PRIVATE "-DL8W8JWT_SMALL_STACK=1" "-DL8W8JWT_ENABLE_EDDSA=0") diff --git a/l8w8jwt/LICENSE b/l8w8jwt/LICENSE new file mode 120000 index 0000000000..6fcdebc403 --- /dev/null +++ b/l8w8jwt/LICENSE @@ -0,0 +1 @@ +l8w8jwt/LICENSE \ No newline at end of file diff --git a/l8w8jwt/README.md b/l8w8jwt/README.md new file mode 120000 index 0000000000..1624a78eeb --- /dev/null +++ b/l8w8jwt/README.md @@ -0,0 +1 @@ +l8w8jwt/README.md \ No newline at end of file diff --git a/l8w8jwt/examples/es256/CMakeLists.txt b/l8w8jwt/examples/es256/CMakeLists.txt new file mode 100644 index 0000000000..eecff3d6de --- /dev/null +++ b/l8w8jwt/examples/es256/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(es256) diff --git a/l8w8jwt/examples/es256/main/CMakeLists.txt b/l8w8jwt/examples/es256/main/CMakeLists.txt new file mode 100644 index 0000000000..8a3ab69279 --- /dev/null +++ b/l8w8jwt/examples/es256/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS "") diff --git a/l8w8jwt/examples/es256/main/idf_component.yml b/l8w8jwt/examples/es256/main/idf_component.yml new file mode 100644 index 0000000000..8ab243f8b2 --- /dev/null +++ b/l8w8jwt/examples/es256/main/idf_component.yml @@ -0,0 +1,8 @@ +## IDF Component Manager Manifest File +version: "1.0.0" +description: l8w8jwt Example +dependencies: + idf: ">=5.0" + espressif/l8w8jwt: + version: '>=2.2.1' + override_path: '../../../' diff --git a/l8w8jwt/examples/es256/main/main.c b/l8w8jwt/examples/es256/main/main.c new file mode 100644 index 0000000000..d36070e50a --- /dev/null +++ b/l8w8jwt/examples/es256/main/main.c @@ -0,0 +1,166 @@ +/* + Copyright 2020 Raphael Beck + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include "l8w8jwt/encode.h" +#include "l8w8jwt/decode.h" + +/* + * This keypair was generated using the following command: + * openssl ecparam -name prime256v1 -genkey -noout -out private.pem && openssl ec -in private.pem -pubout -out public.pem + */ + +static const char ECDSA_PRIVATE_KEY[] = "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEILvM6E7mLOdndALDyFc3sOgUTb6iVjgwRBtBwYZngSuwoAoGCCqGSM49\n" + "AwEHoUQDQgAEMlFGAIxe+/zLanxz4bOxTI6daFBkNGyQ+P4bc/RmNEq1NpsogiMB\n" + "5eXC7jUcD/XqxP9HCIhdRBcQHx7aOo3ayQ==\n" + "-----END EC PRIVATE KEY-----"; + +static const char ECDSA_PUBLIC_KEY[] = "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMlFGAIxe+/zLanxz4bOxTI6daFBk\n" + "NGyQ+P4bc/RmNEq1NpsogiMB5eXC7jUcD/XqxP9HCIhdRBcQHx7aOo3ayQ==\n" + "-----END PUBLIC KEY-----"; + +int example_jwt_decode(char *jwt) +{ + struct l8w8jwt_decoding_params params; + l8w8jwt_decoding_params_init(¶ms); + + params.alg = L8W8JWT_ALG_ES256; + + params.jwt = jwt; + params.jwt_length = strlen(jwt); + + params.verification_key = (unsigned char *)ECDSA_PUBLIC_KEY; + params.verification_key_length = strlen(ECDSA_PUBLIC_KEY); + + params.validate_iss = "Black Mesa"; + params.validate_iss_length = strlen(params.validate_iss); + + params.validate_sub = "Gordon Freeman"; + params.validate_sub_length = strlen(params.validate_sub); + + params.validate_exp = 1; + params.exp_tolerance_seconds = 60; + + params.validate_iat = 1; + params.iat_tolerance_seconds = 60; + + enum l8w8jwt_validation_result validation_result; + int r = l8w8jwt_decode(¶ms, &validation_result, NULL, NULL); + + printf("\nl8w8jwt_decode_es256 function returned %s (code %d).\n\nValidation result: \n%d\n", r == L8W8JWT_SUCCESS ? "successfully" : "", r, validation_result); + return r; +} + +char *example_jwt_encode(void) +{ + char *jwt; + size_t jwt_length; + + struct l8w8jwt_claim header_claims[] = { + { + .key = "kid", + .key_length = 3, + .value = "some-key-id-here-012345", + .value_length = strlen("some-key-id-here-012345"), + .type = L8W8JWT_CLAIM_TYPE_STRING + } + }; + + struct l8w8jwt_claim payload_claims[] = { + { + .key = "ctx", + .key_length = 3, + .value = "Unforseen Consequences", + .value_length = strlen("Unforseen Consequences"), + .type = L8W8JWT_CLAIM_TYPE_STRING + }, + { + .key = "age", + .key_length = 3, + .value = "27", + .value_length = strlen("27"), + .type = L8W8JWT_CLAIM_TYPE_INTEGER + }, + { + .key = "size", + .key_length = strlen("size"), + .value = "1.85", + .value_length = strlen("1.85"), + .type = L8W8JWT_CLAIM_TYPE_NUMBER + }, + { + .key = "alive", + .key_length = strlen("alive"), + .value = "true", + .value_length = strlen("true"), + .type = L8W8JWT_CLAIM_TYPE_BOOLEAN + }, + { + .key = "nulltest", + .key_length = strlen("nulltest"), + .value = "null", + .value_length = strlen("null"), + .type = L8W8JWT_CLAIM_TYPE_NULL + } + }; + + struct l8w8jwt_encoding_params params; + l8w8jwt_encoding_params_init(¶ms); + + params.alg = L8W8JWT_ALG_ES256; + + params.sub = "Gordon Freeman"; + params.sub_length = strlen("Gordon Freeman"); + + params.iss = "Black Mesa"; + params.iss_length = strlen("Black Mesa"); + + params.aud = "Administrator"; + params.aud_length = strlen("Administrator"); + + params.iat = time(NULL); + params.exp = time(NULL) + 600; // Set to expire after 10 minutes (600 seconds). + + params.additional_header_claims = header_claims; + params.additional_header_claims_count = sizeof(header_claims) / sizeof(struct l8w8jwt_claim); + + params.additional_payload_claims = payload_claims; + params.additional_payload_claims_count = sizeof(payload_claims) / sizeof(struct l8w8jwt_claim); + + params.secret_key = (unsigned char *)ECDSA_PRIVATE_KEY; + params.secret_key_length = strlen(ECDSA_PRIVATE_KEY); + + params.out = &jwt; + params.out_length = &jwt_length; + + int r = l8w8jwt_encode(¶ms); + printf("\nl8w8jwt_encode_es256 function returned %s (code %d).\n\nCreated token: \n%s\n", r == L8W8JWT_SUCCESS ? "successfully" : "", r, jwt); + + return jwt; +} + +void app_main(void) +{ + printf("=== JWT Example on ESP32 ===\n"); + char *jwt = example_jwt_encode(); + int ret = example_jwt_decode(jwt); + l8w8jwt_free(jwt); /* Never forget to free the jwt string! */ + printf("JWT generation and decoding: %s\n", ret == L8W8JWT_SUCCESS ? "success" : "failed"); + return; +} diff --git a/l8w8jwt/idf_component.yml b/l8w8jwt/idf_component.yml new file mode 100644 index 0000000000..aa87690874 --- /dev/null +++ b/l8w8jwt/idf_component.yml @@ -0,0 +1,7 @@ +version: "2.2.1" +description: "Minimal, OpenSSL-less and super lightweight JWT library written in C" +url: https://github.com/espressif/idf-extra-components/tree/master/l8w8jwt +dependencies: + idf: ">=5.0" + espressif/jsmn: + version: "^1.1.0" diff --git a/l8w8jwt/l8w8jwt b/l8w8jwt/l8w8jwt new file mode 160000 index 0000000000..824bb52799 --- /dev/null +++ b/l8w8jwt/l8w8jwt @@ -0,0 +1 @@ +Subproject commit 824bb52799a8ea55a12c75af9280ed9c6f73ebf4 diff --git a/l8w8jwt/port/private_include/checknum.h b/l8w8jwt/port/private_include/checknum.h new file mode 100644 index 0000000000..6a7327f0bf --- /dev/null +++ b/l8w8jwt/port/private_include/checknum.h @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: 2020 Raphael Beck + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2023 Espressif Systems (Shanghai) CO LTD + */ +/* + Copyright 2020 Raphael Beck + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * @file checknum.h + * @author Raphael Beck + * @brief Check whether a given string contains an integer or floating point number. + */ + +/* https://github.com/GlitchedPolygons/checknum */ + +#ifndef CHECKNUM_H +#define CHECKNUM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CHECKNUM_STATIC +#define CHECKNUM_API static +#else +#define CHECKNUM_API extern +#endif + +#include +#include +#include +#include + +/** + * Checks whether a given string contains a valid integer or floating point number.

+ * If it's an integer, 1 is returned.

+ * If it's a float or a double, 2 is returned.

+ * If the string doesn't contain a valid number at all, 0 is returned. + */ +CHECKNUM_API int checknum(char *string, size_t string_length) +{ + if (string == NULL) { + return 0; + } + + if (string_length == 0) { + string_length = strlen(string); + } + + char *c = string; + + while (*c == ' ' && c < string + string_length) { + c++; + } + + while (*(string + string_length - 1) == ' ' && c < string + string_length) { + string_length--; + } + + switch (*c) { + case '+': + case '-': + if (++c >= string + string_length) { + return 0; + } + default: + break; + } + + unsigned int type = 0; + + if (*c == '0') { + type |= 1 << 0; + if (*++c != '.' && c < string + string_length) { + return 0; + } + } + + for (; c < string + string_length; c++) { + switch (*c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + type |= 1 << 0; + continue; + case '-': + if (type & 1 << 1 || (*(c - 1) != 'E' && *(c - 1) != 'e')) { + return 0; + } + type |= 1 << 1; + continue; + case '.': + if (type & 1 << 2 || type & 1 << 3) { + return 0; + } + type |= 1 << 2; + continue; + case 'E': + case 'e': + if (!(type & 1 << 0) || type & 1 << 3 || c + 1 >= string + string_length) { + return 0; + } + type |= 1 << 3; + continue; + case '+': + if (type & 1 << 4 || (*(c - 1) != 'E' && *(c - 1) != 'e')) { + return 0; + } + type |= 1 << 4; + continue; + default: + return 0; + } + } + + switch (type) { + case 0: + return 0; + case 1 << 0: + return 1; + default: + return type & 1 << 0 ? 2 : 0; + } +} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // CHECKNUM_H diff --git a/l8w8jwt/port/private_include/chillbuff.h b/l8w8jwt/port/private_include/chillbuff.h new file mode 100644 index 0000000000..83528fe7ab --- /dev/null +++ b/l8w8jwt/port/private_include/chillbuff.h @@ -0,0 +1,314 @@ +/* + * SPDX-FileCopyrightText: 2019 Raphael Beck + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2023 Espressif Systems (Shanghai) CO LTD + */ +/* + Copyright 2019 Raphael Beck + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * @file chillbuff.h + * @author Raphael Beck + * @date 27. December 2019 + * @brief Array. Dynamic size. Push back 'n' chill. Buffer stuff. Dynamic stuff that's buff. Dynamically reallocating buff.. Yeah! + * @see https://github.com/GlitchedPolygons/chillbuff + */ + +#ifndef CHILLBUFF_H +#define CHILLBUFF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +/* The following are the chillbuff exit codes returned from the various chillbuff functions. */ + +/** + * Returned from a chillbuff function when everything went smooth 'n' chill. Time to get Schwifty! + */ +#define CHILLBUFF_SUCCESS 0 + +/** + * Chill time is over, you're out of memory... Time to reconsider memory usage. + */ +#define CHILLBUFF_OUT_OF_MEM 100 + +/** + * Error code returned by a chillbuff function if you passed a NULL argument that shouldn't have been NULL. + */ +#define CHILLBUFF_NULL_ARG 200 + +/** + * This error code is returned by a chillbuff function if you passed an invalid parameter into it. + */ +#define CHILLBUFF_INVALID_ARG 300 + +/** + * Not good... + */ +#define CHILLBUFF_OVERFLOW 400 + +/** @private */ +static void (*_chillbuff_error_callback)(const char *) = NULL; + +/** + * How should the chillbuff's underlying array grow in size + * once its maximum capacity is reached during a push_back? + */ +typedef enum chillbuff_growth_method { + /** + * Double the capacity. + */ + CHILLBUFF_GROW_DUPLICATIVE = 0, + + /** + * Triple the capacity. + */ + CHILLBUFF_GROW_TRIPLICATIVE = 1, + + /** + * Grow by the same capacity every time the buffer is full. + */ + CHILLBUFF_GROW_LINEAR = 2, + + /** + * Multiplies the capacity by itself. Not the greatest idea... Use carefully! + */ + CHILLBUFF_GROW_EXPONENTIAL = 3 +} chillbuff_growth_method; + +/** + * Self-reallocating dynamic size array of no strictly defined type. + * Easy 'n' "chill" (hope you like segmentation fault errors). + */ +typedef struct chillbuff { + /** + * The buffer's underlying array that stores the data. + */ + void *array; + + /** + * The current amount of elements stored in the chillbuff. DO NOT touch this yourself, only read! + */ + size_t length; + + /** + * The current buffer capacity. This grows dynamically according to the specified {@link #chillbuff_growth_method}. + */ + size_t capacity; + + /** + * The size of each stored element. DO NOT CHANGE THIS! Only read (if necessary)... + */ + size_t element_size; + + /** + * The way the buffer's capacity is increased when it's full. + */ + chillbuff_growth_method growth_method; +} chillbuff; + +/** @private */ +static inline void _chillbuff_printerr(const char *error, const char *origin) +{ + const size_t error_length = 64 + strlen(error) + strlen(origin); + char *error_msg = (char *)malloc(error_length * sizeof(char)); // cast malloc because of compat with C++ D: + if (error_msg != NULL) { + snprintf(error_msg, error_length, "\nCHILLBUFF ERROR: (%s) %s\n", origin, error); + if (_chillbuff_error_callback != NULL) { + _chillbuff_error_callback(error_msg); + } + free(error_msg); + } +} + +/** + * Sets the chillbuff error callback.

+ * If errors occur, they'll be passed as a string into the provided callback function. + * @param error_callback The function to call when errors occur. + * @return Whether the callback was set up correctly or not (chillbuff exit code, see top of chillbuff.h file for more details). + */ +static inline int chillbuff_set_error_callback(void (*error_callback)(const char *)) +{ + if (error_callback == NULL) { + _chillbuff_printerr("The passed error callback is empty; Operation cancelled!", __func__); + return CHILLBUFF_NULL_ARG; + } + + _chillbuff_error_callback = error_callback; + return CHILLBUFF_SUCCESS; +} + +/** + * Clears the chillbuff error callback (errors won't be printed anymore). + */ +static inline void chillbuff_unset_error_callback(void) +{ + _chillbuff_error_callback = NULL; +} + +/** + * Initializes a chillbuff instance and makes it ready to accept data. + * @param buff The chillbuff instance to init (or rather, a pointer to it). + * @param initial_capacity The initial capacity of the underlying array. If you pass 0 here, 16 is used by default. + * @param element_size How big should every array element be? E.g. if you're storing int you should pass sizeof(int). + * @param growth_method How should the buffer grow once its maximum capacity is reached? @see chillbuff_growth_method + * @return Chillbuff exit code as defined at the top of the chillbuff.h header file. 0 means success. + */ +static inline int chillbuff_init(chillbuff *buff, const size_t initial_capacity, const size_t element_size, const chillbuff_growth_method growth_method) +{ + if (buff == NULL) { + _chillbuff_printerr("Tried to init a NULL chillbuff instance; wouldn't end well. Cancelled...", __func__); + return CHILLBUFF_NULL_ARG; + } + + if (element_size == 0) { + _chillbuff_printerr("Storing elements of size \"0\" makes no sense...", __func__); + return CHILLBUFF_INVALID_ARG; + } + + if (growth_method < 0 || growth_method > 3) { + _chillbuff_printerr("Invalid grow method! Please use the appropriate chillbuff_growth_method enum!", __func__); + return CHILLBUFF_INVALID_ARG; + } + + buff->length = 0; + buff->element_size = element_size; + buff->growth_method = growth_method; + buff->capacity = initial_capacity == 0 ? 16 : initial_capacity; + buff->array = calloc(buff->capacity, buff->element_size); + + if (buff->array == NULL) { + _chillbuff_printerr("OUT OF MEMORY!", __func__); + return CHILLBUFF_OUT_OF_MEM; + } + + return CHILLBUFF_SUCCESS; +} + +/** + * Frees a chillbuff instance. + * @param buff The chillbuff to deallocate. If this is NULL, nothing happens at all. + */ +static inline void chillbuff_free(chillbuff *buff) +{ + if (buff == NULL) { + return; + } + + memset(buff->array, '\0', buff->length); + free(buff->array); + buff->array = NULL; + buff->length = buff->capacity = buff->element_size = 0; +} + +/** + * Clears a chillbuff's data.

+ * Deletes all of the underlying array's elements and resets the length to 0.

+ * Leaves the array allocated at the current capacity. + * @param buff The chillbuff to clear. If this is NULL, nothing happens at all. + */ +static inline void chillbuff_clear(chillbuff *buff) +{ + if (buff == NULL) { + return; + } + + memset(buff->array, '\0', buff->capacity); + buff->length = 0; +} + +/** + * Appends one or more elements to the buffer. + * If the buffer is full, it will be expanded automatically. + * @param buff The buffer into which to insert the elements. + * @param elements The array of elements to insert (pointer to the first element). + * @param elements_count Amount of elements to add (for example: if your buffer stores the type uint32_t, you'd pass sizeof(elements_to_add) / sizeof(uint32_t) here). If you're only adding a single element, pass 1. + * @return Chillbuff exit code that describes the insertion's outcome. + */ +static int chillbuff_push_back(chillbuff *buff, const void *elements, const size_t elements_count) +{ + if (buff == NULL) { + _chillbuff_printerr("Tried to append to a NULL chillbuff instance!", __func__); + return CHILLBUFF_NULL_ARG; + } + + if (elements == NULL) { + _chillbuff_printerr("Tried to append NULL element(s) to a chillbuff instance!", __func__); + return CHILLBUFF_NULL_ARG; + } + + if (elements_count == 0) { + _chillbuff_printerr("The passed \"elements_count\" argument is zero; nothing to append!", __func__); + return CHILLBUFF_INVALID_ARG; + } + + for (size_t i = 0; i < elements_count; i++) { + if (buff->length == buff->capacity) { + size_t new_capacity; + + switch (buff->growth_method) { + default: + _chillbuff_printerr("Invalid grow method! Please use the appropriate chillbuff_growth_method enum!", __func__); + return CHILLBUFF_INVALID_ARG; + case CHILLBUFF_GROW_DUPLICATIVE: + new_capacity = (buff->capacity * 2); + break; + case CHILLBUFF_GROW_TRIPLICATIVE: + new_capacity = (buff->capacity * 3); + break; + case CHILLBUFF_GROW_LINEAR: + new_capacity = (buff->capacity + buff->element_size); + break; + case CHILLBUFF_GROW_EXPONENTIAL: + new_capacity = (buff->capacity * buff->capacity); + break; + } + + if (new_capacity <= buff->capacity || new_capacity >= UINT64_MAX / buff->element_size) { + _chillbuff_printerr("Couldn't push back due to buffer OVERFLOW!", __func__); + return CHILLBUFF_OVERFLOW; + } + + void *new_array = realloc(buff->array, new_capacity * buff->element_size); + if (new_array == NULL) { + _chillbuff_printerr("Couldn't resize chillbuff underlying array; OUT OF MEMORY!", __func__); + return CHILLBUFF_OUT_OF_MEM; + } + + memset((char *)new_array + (buff->element_size * buff->length), '\0', (new_capacity - buff->length) * buff->element_size); + buff->array = new_array; + buff->capacity = new_capacity; + } + + memcpy((char *)buff->array + (buff->element_size * buff->length++), (char *)elements + (i * buff->element_size), buff->element_size); + } + + return CHILLBUFF_SUCCESS; +} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // CHILLBUFF_H diff --git a/l8w8jwt/test/CMakeLists.txt b/l8w8jwt/test/CMakeLists.txt new file mode 100644 index 0000000000..1666e72dd3 --- /dev/null +++ b/l8w8jwt/test/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + REQUIRES unity + PRIV_REQUIRES cmock l8w8jwt mbedtls) diff --git a/l8w8jwt/test/test.c b/l8w8jwt/test/test.c new file mode 100644 index 0000000000..f0605575c1 --- /dev/null +++ b/l8w8jwt/test/test.c @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include + +#include "esp_err.h" +#include "esp_log.h" + +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/ecdh.h" +#include "mbedtls/ecdsa.h" +#include "mbedtls/error.h" +#include "mbedtls/pk.h" + +#include "freertos/FreeRTOS.h" + +#include "unity.h" +#include "l8w8jwt/encode.h" +#include "l8w8jwt/decode.h" + +#define TEST_ASSERT_MBEDTLS_OK(X) TEST_ASSERT_EQUAL_HEX32(0, -(X)) + +#define ECDSA_KEYS_BUF_SIZE (256) + +static void ecdsa_256_genkey(unsigned char *pvtkey, size_t pvtkey_size, unsigned char *pubkey, size_t pubkey_size) +{ + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context random; + + mbedtls_pk_context key; + const char *p_str = "myecdsa"; + + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&random); + mbedtls_pk_init(&key); + + TEST_ASSERT_MBEDTLS_OK(mbedtls_ctr_drbg_seed(&random, mbedtls_entropy_func, &entropy, (const unsigned char *) p_str, strlen(p_str))); + + TEST_ASSERT_MBEDTLS_OK(mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY))); + + TEST_ASSERT_MBEDTLS_OK(mbedtls_ecdsa_genkey(mbedtls_pk_ec(key), MBEDTLS_ECP_DP_SECP256R1, mbedtls_ctr_drbg_random, &random)); + + TEST_ASSERT_MBEDTLS_OK(mbedtls_pk_write_key_pem(&key, pvtkey, pvtkey_size)); + + TEST_ASSERT_MBEDTLS_OK(mbedtls_pk_write_pubkey_pem(&key, pubkey, pubkey_size)); + + mbedtls_pk_free(&key); + mbedtls_ctr_drbg_free(&random); + mbedtls_entropy_free(&entropy); +} + +int example_jwt_decode(char *jwt, const unsigned char *pubkey, enum l8w8jwt_validation_result *validation_result) +{ + struct l8w8jwt_decoding_params params; + l8w8jwt_decoding_params_init(¶ms); + + params.alg = L8W8JWT_ALG_ES256; + + params.jwt = jwt; + params.jwt_length = strlen(jwt); + + params.verification_key = (unsigned char *)pubkey; + params.verification_key_length = strlen((const char *)pubkey); + + params.validate_iss = "Black Mesa"; + params.validate_iss_length = strlen(params.validate_iss); + + params.validate_sub = "Gordon Freeman"; + params.validate_sub_length = strlen(params.validate_sub); + + params.validate_exp = 1; + params.exp_tolerance_seconds = 60; + + params.validate_iat = 1; + params.iat_tolerance_seconds = 0; + + int ret = l8w8jwt_decode(¶ms, validation_result, NULL, NULL); + printf("%d\n", *validation_result); + return ret; +} + +int example_jwt_encode(const unsigned char *pvtkey, char **jwt) +{ + size_t jwt_length; + + struct l8w8jwt_claim payload_claims[] = { + { + .key = "ctx", + .key_length = 3, + .value = "Unforseen Consequences", + .value_length = strlen("Unforseen Consequences"), + .type = L8W8JWT_CLAIM_TYPE_STRING + }, + }; + + struct l8w8jwt_encoding_params params; + l8w8jwt_encoding_params_init(¶ms); + + params.alg = L8W8JWT_ALG_ES256; + + params.sub = "Gordon Freeman"; + params.sub_length = strlen("Gordon Freeman"); + + params.iss = "Black Mesa"; + params.iss_length = strlen("Black Mesa"); + + params.aud = "Administrator"; + params.aud_length = strlen("Administrator"); + + params.iat = time(NULL); + params.exp = time(NULL) + 600; // Set to expire after 10 minutes (600 seconds). + + params.additional_payload_claims = payload_claims; + params.additional_payload_claims_count = sizeof(payload_claims) / sizeof(struct l8w8jwt_claim); + + params.secret_key = (unsigned char *)pvtkey; + params.secret_key_length = strlen((const char *)pvtkey); + + params.out = jwt; + params.out_length = &jwt_length; + + return l8w8jwt_encode(¶ms); +} + +TEST_CASE("Verify encoded and signed JWT", "[l8w8jwt]") +{ + static unsigned char pvtkey[ECDSA_KEYS_BUF_SIZE], pubkey[ECDSA_KEYS_BUF_SIZE]; + ecdsa_256_genkey(pvtkey, sizeof(pvtkey), pubkey, sizeof(pubkey)); + + char *jwt = NULL; + TEST_ASSERT_EQUAL_HEX32(L8W8JWT_SUCCESS, example_jwt_encode(pvtkey, &jwt)); + TEST_ASSERT_NOT_NULL(jwt); + + enum l8w8jwt_validation_result validation_result = L8W8JWT_NBF_FAILURE; + TEST_ASSERT_EQUAL_HEX32(L8W8JWT_SUCCESS, example_jwt_decode(jwt, pubkey, &validation_result)); + TEST_ASSERT_EQUAL_HEX32(L8W8JWT_VALID, validation_result); + + /* Never forget to free the jwt string! */ + l8w8jwt_free(jwt); +} diff --git a/test_app/CMakeLists.txt b/test_app/CMakeLists.txt index b27297b994..a1c846ea11 100644 --- a/test_app/CMakeLists.txt +++ b/test_app/CMakeLists.txt @@ -14,7 +14,7 @@ endif() # 3. Add here if the component is compatible with IDF >= v5.0 if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0") - list(APPEND EXTRA_COMPONENT_DIRS ../bdc_motor ../led_strip ../sh2lib ../nghttp ../esp_serial_slave_link ../onewire_bus ../ccomp_timer ../catch2) + list(APPEND EXTRA_COMPONENT_DIRS ../bdc_motor ../l8w8jwt ../led_strip ../sh2lib ../nghttp ../esp_serial_slave_link ../onewire_bus ../ccomp_timer ../catch2) endif() # !This section should NOT be touched when adding new component!