From 91405932864cad6edc7493523570550f43c28b34 Mon Sep 17 00:00:00 2001 From: Aymen LAOUINI Date: Mon, 16 Dec 2024 15:43:49 +0200 Subject: [PATCH] lib: app_jwt: add an app core jwt generator and add a sample for usage Ref: NRFX-6688 Signed-off-by: Aymen LAOUINI --- CODEOWNERS | 5 + doc/nrf/libraries/app_jwt/app_jwt.rst | 80 ++ doc/nrf/libraries/app_jwt/index.rst | 11 + include/app_jwt.h | 155 ++++ lib/CMakeLists.txt | 1 + lib/Kconfig | 1 + lib/app_jwt/CMakeLists.txt | 11 + lib/app_jwt/Kconfig | 30 + lib/app_jwt/app_jwt.c | 706 ++++++++++++++++++ samples/jwt/CMakeLists.txt | 15 + samples/jwt/Kconfig | 20 + samples/jwt/README.rst | 96 +++ .../boards/nrf54h20dk_nrf54h20_cpuapp.conf | 46 ++ .../boards/nrf54h20dk_nrf54h20_cpuapp.overlay | 13 + samples/jwt/prj.conf | 33 + samples/jwt/sample.yaml | 28 + samples/jwt/src/main.c | 101 +++ samples/jwt/uart_logging.conf | 16 + 18 files changed, 1368 insertions(+) create mode 100644 doc/nrf/libraries/app_jwt/app_jwt.rst create mode 100644 doc/nrf/libraries/app_jwt/index.rst create mode 100644 include/app_jwt.h create mode 100644 lib/app_jwt/CMakeLists.txt create mode 100644 lib/app_jwt/Kconfig create mode 100644 lib/app_jwt/app_jwt.c create mode 100644 samples/jwt/CMakeLists.txt create mode 100644 samples/jwt/Kconfig create mode 100644 samples/jwt/README.rst create mode 100644 samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.conf create mode 100644 samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.overlay create mode 100644 samples/jwt/prj.conf create mode 100644 samples/jwt/sample.yaml create mode 100644 samples/jwt/src/main.c create mode 100644 samples/jwt/uart_logging.conf diff --git a/CODEOWNERS b/CODEOWNERS index f71e13955070..877bd9183364 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -123,6 +123,7 @@ /doc/nrf/includes/ @nrfconnect/ncs-doc-leads /doc/nrf/includes/boardname_tables/sample_boardnames.txt @nrfconnect/ncs-co-doc /doc/nrf/installation/ @nrfconnect/ncs-doc-leads @nrfconnect/ncs-vestavind-doc @nrfconnect/ncs-wayland-doc +/doc/nrf/libraries/app_jwt/ @nrfconnect/ncs-modem-doc @ayla-nordicsemi /doc/nrf/libraries/bin/ @nrfconnect/ncs-doc-leads /doc/nrf/libraries/bin/lwm2m_carrier/ @nrfconnect/ncs-carrier-doc /doc/nrf/libraries/bluetooth/ @nrfconnect/ncs-si-muffin-doc @nrfconnect/ncs-dragoon-doc @@ -315,6 +316,7 @@ /ext/iperf3/ @nrfconnect/ncs-code-owners @jhirsi # Include +/include/app_jwt.h @nrfconnect/ncs-modem @ayla-nordicsemi /include/audio/ @nrfconnect/ncs-audio /include/audio_module/ @nrfconnect/ncs-audio /include/bluetooth/ @nrfconnect/ncs-dragoon @@ -378,6 +380,7 @@ # Libraries /lib/adp536x/ @nrfconnect/ncs-cia +/lib/app_jwt/ @nrfconnect/ncs-modem @ayla-nordicsemi /lib/at_cmd_parser/ @nrfconnect/ncs-co-networking @nrfconnect/ncs-modem /lib/at_cmd_custom/ @nrfconnect/ncs-modem /lib/at_host/ @nrfconnect/ncs-co-networking @nrfconnect/ncs-modem @@ -519,6 +522,7 @@ /samples/gazell/ @leewkb4567 /samples/hw_id/ @nrfconnect/ncs-cia /samples/ipc/ipc_service/ @nrfconnect/ncs-si-muffin +/samples/jwt/ @nrfconnect/ncs-modem @ayla-nordicsemi /samples/keys/ @nrfconnect/ncs-aegir /samples/matter/ @nrfconnect/ncs-matter /samples/mpsl/ @nrfconnect/ncs-dragoon @@ -640,6 +644,7 @@ /samples/gazell/**/*.rst @nrfconnect/ncs-si-muffin-doc /samples/hw_id/*.rst @nrfconnect/ncs-cia-doc /samples/ipc/ipc_service/*.rst @nrfconnect/ncs-si-muffin-doc +/samples/jwt/*.rst @nrfconnect/ncs-modem-doc @ayla-nordicsemi /samples/keys/**/*.rst @nrfconnect/ncs-aegir-doc /samples/matter/**/*.rst @nrfconnect/ncs-matter-doc /samples/mpsl/**/*.rst @nrfconnect/ncs-dragoon-doc diff --git a/doc/nrf/libraries/app_jwt/app_jwt.rst b/doc/nrf/libraries/app_jwt/app_jwt.rst new file mode 100644 index 000000000000..f3283c797add --- /dev/null +++ b/doc/nrf/libraries/app_jwt/app_jwt.rst @@ -0,0 +1,80 @@ +.. _app_jwt: + +Application JWT +################### + +The Application JWT library provides access to the `JSON Web Token (JWT)`_ generation feature from application core using signing and identity services from secure core. + +Configuration +************* + +To use the library to request a JWT, complete the following steps: + +1. Set the following Kconfig options to enable the library: + + * :kconfig:option:`CONFIG_APP_JWT` + * :kconfig:option:`CONFIG_APP_JWT_VERIFY_SIGNATURE` + * :kconfig:option:`CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER` + * :kconfig:option:`CONFIG_NRF_SECURITY` + * :Kconfig:option:`CONFIG_SSF_PSA_CRYPTO_SERVICE_ENABLED` + * :Kconfig:option:`CONFIG_SSF_DEVICE_INFO_SERVICE_ENABLED` + +#. Read the device UUID (:c:func:`app_jwt_get_uuid`) +#. Populate the :c:struct:`app_jwt_data` structure with your desired values. + See `Possible structure values`_ for more information. +#. Pass the structure to the function that generates JWT (:c:func:`app_jwt_generate`). + +If the function executes successfully, :c:member:`app_jwt_data.jwt_buf` will contain the JSON Web Token. + +.. note:: + The library doesn't check the validity of the time source, it is up to the caller to make sure that the system has access to a valid one, otherwise "iat" field will containt the time since boot in seconds. + +.. note:: + If the time value in seconds is equal to ``0``, the claim ``iat`` will be left out of the JWT. + +Possible structure values +========================= + +You can configure the following values in the :c:struct:`app_jwt_data` structure: + +* :c:member:`app_jwt_data.sec_tag` - Optional, the ``sec_tag`` must contain a valid signing key. + If set to 0, the library will use the IAK for signing. +* :c:member:`app_jwt_data.key_type` - Required if ``sec_tag`` is not zero. + Defines the type of key in the sec tag. +* :c:member:`app_jwt_data.alg` - Required, always use the value JWT_ALG_TYPE_ES256. + Defines the JWT signing algorithm. Currently, only ECDSA 256 is supported. +* :c:member:`app_jwt_data.add_keyid_to_header` - Optional. + Corresponds to ``keyid`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.json_token_id` - Optional. + Corresponds to ``jti`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.subject` - Optional. + Corresponds to ``sub`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.audience` - Optional. + Corresponds to ``aud`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.issuer` - Optional. + Corresponds to ``iss`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.add_timestamp` - Optional. + Corresponds to ``iat`` claim. + Use ``0`` if you want to leave out this field. +* :c:member:`app_jwt_data.validity_s` - Optional. + Defines the expiration date for the JWT. + If set to 0, the field ``exp`` will be omitted from the generated JWT. +* :c:member:`app_jwt_data.jwt_buf` - Required. + Buffer for the generated, null-terminated, JWT string. + Buffer size has to be al least 600 bytes, at most 900 bytes. + The user has to provide a valid buffer, library doesn't do any allocation. +* :c:member:`app_jwt_data.jwt_sz` - Size of JWT buffer. + Required, has to be equal to the size of :c:member:`app_jwt_data.jwt_buf`. + +API documentation +***************** + +| Header file: :file:`include/app_jwt.h` +| Source file: :file:`lib/app_jwt/app_jwt.c` + +.. doxygengroup:: app_jwt diff --git a/doc/nrf/libraries/app_jwt/index.rst b/doc/nrf/libraries/app_jwt/index.rst new file mode 100644 index 000000000000..6cd7c9255587 --- /dev/null +++ b/doc/nrf/libraries/app_jwt/index.rst @@ -0,0 +1,11 @@ +.. _lib_app_jwt: + +Library Application JWT +######################## + +.. toctree:: + :maxdepth: 1 + :glob: + :caption: Subpages: + + * diff --git a/include/app_jwt.h b/include/app_jwt.h new file mode 100644 index 000000000000..cf5efede19ff --- /dev/null +++ b/include/app_jwt.h @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef _APP_JWT_H +#define _APP_JWT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file app_jwt.h + * + * @brief Generate a JWT with from application core. + * @defgroup app_jwt JWT generation + * @{ + * + */ + +#include +#include +#include + +/** @brief Maximum size of a JWT string, could be used to allocate JWT + * output buffer. + */ +#define APP_JWT_STR_MAX_LEN 900 + +/** @brief Maximum valid duration for JWTs generated by user application */ +#define APP_JWT_VALID_TIME_S_MAX (7 * 24 * 60 * 60) + +/** @brief Default valid duration for JWTs generated by user application */ +#define APP_JWT_VALID_TIME_S_DEF (10 * 60) + +/** @brief UUID size in bytes */ +#define APP_JWT_UUID_BYTE_SZ 16 + +/** @brief UUID v4 format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + '\0' */ +#define APP_JWT_UUID_V4_STR_LEN (((APP_JWT_UUID_BYTE_SZ * 2) + 4) + 1) + +/** @brief Size in bytes of each JWT String field */ +#define APP_JWT_CLAIM_MAX_SIZE 64 + +/** @brief The type of key to be used for signing the JWT. */ +enum app_jwt_key_type { + JWT_KEY_TYPE_CLIENT_PRIV = 2, + JWT_KEY_TYPE_ENDORSEMENT = 8, +}; + +/** @brief JWT signing algorithm */ +enum app_jwt_alg_type { + JWT_ALG_TYPE_ES256 = 0, +}; + +/** @brief JWT parameters required for JWT generation and pointer to generated JWT */ +struct app_jwt_data { + /** Sec tag to use for JWT signing */ + unsigned int sec_tag; + /** Key type in the specified sec tag */ + enum app_jwt_key_type key_type; + /** JWT signing algorithm */ + enum app_jwt_alg_type alg; + + /** + * Indicates if a 'kid' claim is requiered or not, if set to 1, 'kid' claim + * will contain sha256 of the signing key. + */ + bool add_keyid_to_header; + + /** + * NULL terminated 'jti' claim; Unique identifier; can be used to prevent the + * JWT from being replayed + */ + const char *json_token_id; + /** NULL terminated 'sub' claim; the principal that is the subject of the JWT */ + const char *subject; + /** NULL terminated 'aud' claim; intended recipient of the JWT */ + const char *audience; + /** NULL terminated 'iss' claim; Issuer of the JWT */ + const char *issuer; + + /** + * Indicates if an issue timestamp is requiered or not, if set to 1, 'exp' claim + * will be present. + */ + bool add_timestamp; + + /** + * Corresponds to 'exp' claim; Defines how long the JWT will be valid. + * If application has a valid time source, and the 'iat' claim is present, + * the timestamp in seconds will be added to this value. + */ + uint32_t validity_s; + + /** + * Buffer to which the NULL terminated JWT will be copied. + * It is the responsibility of the user to provide a valid buffer. + * The returned JWT could be as long as 900 bytes, use the + * defined size value APP_JWT_STR_MAX_LEN to create your supplied return buffer. + */ + char *jwt_buf; + /** Size of the user provided buffer. */ + size_t jwt_sz; +}; + +/** + * @brief Generates a JWT using the supplied parameters. If successful, + * the JWT string will be stored in the supplied struct. + * The user is responsible for providing a valid pointer to store the JWT. + * + * Subject and audience fields may be NULL in which case those fields are left out + * from generated JWT token. + * + * The API doesn't verify the time source validity, it is up to the caller to make sure + * that the system has access to a valid time source, otherwise "iat" field will + * contain the time since boot in seconds. + * + * JWT is signed with the application identity attestation key, no matter what + * value is supplied in the sec_tag. + * + * @param[in,out] jwt Pointer to struct containing JWT parameters and result. + * + * @retval 0 If the operation was successful. + * Otherwise, a (negative) error code is returned, check header errno.h for a full list. + */ +int app_jwt_generate(struct app_jwt_data *const jwt); + +/** + * @brief Gets the device UUID from the secure domain + * and returns it as a NULL terminated string in the supplied buffer. + * The device UUID can be used as a device identifier for cloud services and + * for secure device management using the nRF Cloud Identity Service. + * + * UUID v4 defined by ITU-T X.667 | ISO/IEC 9834-8 has a length of 35 bytes, add + * 1 byte for the atring termination character. User is expected to provide a buffer + * of at least 36 bytes. + * + * @param[out] uuid_buffer Pointer to buffer where the device UUID string will be written to. + * @param[in] uuid_buffer_size Size of the provided buffer. + * + * @retval 0 If the operation was successful. + * Otherwise, a (negative) error code is returned, check header errno.h for a full list. + */ +int app_jwt_get_uuid(char *uuid_buffer, const size_t uuid_buffer_size); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* _APP_JWT_H */ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index b74915671bec..705274168ab0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -33,6 +33,7 @@ add_subdirectory_ifdef(CONFIG_HW_ID_LIBRARY hw_id) add_subdirectory_ifdef(CONFIG_EDGE_IMPULSE edge_impulse) add_subdirectory_ifdef(CONFIG_WAVE_GEN_LIB wave_gen) add_subdirectory_ifdef(CONFIG_HW_UNIQUE_KEY_SRC hw_unique_key) +add_subdirectory_ifdef(CONFIG_APP_JWT app_jwt) add_subdirectory_ifdef(CONFIG_MODEM_JWT modem_jwt) add_subdirectory_ifdef(CONFIG_MODEM_SLM modem_slm) add_subdirectory_ifdef(CONFIG_MODEM_ATTEST_TOKEN modem_attest_token) diff --git a/lib/Kconfig b/lib/Kconfig index bb2f65d2f077..4f068905ef94 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -6,6 +6,7 @@ menu "Libraries" +rsource "app_jwt/Kconfig" rsource "bin/Kconfig" rsource "nrf_modem_lib/Kconfig" rsource "adp536x/Kconfig" diff --git a/lib/app_jwt/CMakeLists.txt b/lib/app_jwt/CMakeLists.txt new file mode 100644 index 000000000000..334b7f2a5b2b --- /dev/null +++ b/lib/app_jwt/CMakeLists.txt @@ -0,0 +1,11 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +zephyr_library() + +zephyr_library_sources( + app_jwt.c +) diff --git a/lib/app_jwt/Kconfig b/lib/app_jwt/Kconfig new file mode 100644 index 000000000000..49ef70a80e11 --- /dev/null +++ b/lib/app_jwt/Kconfig @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig APP_JWT + bool "Application JWT Library" + depends on SSF_CLIENT && SSF_PSA_CRYPTO_SERVICE_ENABLED && SSF_DEVICE_INFO_SERVICE_ENABLED + select BASE64 + # Needed for time and date + select POSIX_API + # Needed to print integer values in JSON + select CJSON_LIB + select CBPRINTF_FP_SUPPORT + +if APP_JWT + +config APP_JWT_VERIFY_SIGNATURE + bool "Verify signature after signing" + default y + +config APP_JWT_PRINT_EXPORTED_PUBKEY_DER + bool "Print to terminal the DER formatted public key" + +module=APP_JWT +module-str=User App JWT +source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config" + +endif # APP_JWT diff --git a/lib/app_jwt/app_jwt.c b/lib/app_jwt/app_jwt.c new file mode 100644 index 000000000000..2355f42c319e --- /dev/null +++ b/lib/app_jwt/app_jwt.c @@ -0,0 +1,706 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(app_jwt, CONFIG_APP_JWT_LOG_LEVEL); + +/* Size of a UUID in words */ +#define UUID_BINARY_WORD_SZ (4) + +/* Size of a UUID in bytes */ +#define UUID_BINARY_BYTES_SZ (16) + +/* String size of a binary word encoded in hexadecimal */ +#define BINARY_WORD_STR_SZ (9) + +/* Default signing key is IAK GEN1 */ +#define DEFAULT_IAK_APPLICATION IAK_APPLICATION_GEN1 + +/* Size of an ECDSA signature */ +#define ECDSA_SHA_256_SIG_SZ (64) + +/* Size of an SHA 256 hash */ +#define ECDSA_SHA_256_HASH_SZ (32) + +/* Size of a public ECDSA key in raw binary format */ +#define ECDSA_PUBLIC_KEY_SZ (65) + +/* Size of a public ECDSA key in DER format */ +#define ECDSA_PUBLIC_KEY_DER_SZ (91) + +/* Macro to determine the size of a data encoded in base64 */ +#define BASE64_ENCODE_SZ(n) (((4 * n / 3) + 3) & ~3) + +/* Size of ECDSA signature encoded in base64 */ +#define B64_SIG_SZ (BASE64_ENCODE_SZ(ECDSA_SHA_256_SIG_SZ) + 1) + +/* Maximum possible size for a JWT in a string format */ +#define JWT_STR_MAX_SZ (900) + +static void base64_url_format(char *const base64_string) +{ + if (base64_string == NULL) { + return; + } + + char *found = NULL; + + /* Replace '+' with "-" */ + for (found = base64_string; (found = strchr(found, '+'));) { + *found = '-'; + } + + /* Replace '/' with "_" */ + for (found = base64_string; (found = strchr(found, '/'));) { + *found = '_'; + } + + /* Remove padding '=' */ + found = strchr(base64_string, '='); + if (found) { + *found = '\0'; + } +} + +static int bytes_to_uuid_str(const uint32_t *uuid_words, const int32_t uuid_byte_len, + char *uuid_str_out, const int32_t uuid_str_out_size) +{ + + if ((NULL == uuid_words) || (uuid_byte_len < UUID_BINARY_WORD_SZ * 4) || + (NULL == uuid_str_out) || (uuid_str_out_size < APP_JWT_UUID_V4_STR_LEN)) { + /* Bad parameter */ + return -EINVAL; + } + /* This will hold 4 integer words in string format */ + /* (string Length 8 + 1) */ + char temp_str[UUID_BINARY_WORD_SZ][BINARY_WORD_STR_SZ] = {0}; + + /* Transform integers to strings */ + for (int i = 0; i < UUID_BINARY_WORD_SZ; i++) { + /* UUID byte endiannes is little endian first. */ + snprintf(temp_str[i], 9, "%08x", sys_cpu_to_be32(uuid_words[i])); + } + + /* UUID string format defined by ITU-T X.667 | ISO/IEC 9834-8 : */ + /* <8 char>-<4 char>-<4 char>-<4 char>-<12 char> */ + + snprintf(uuid_str_out, APP_JWT_UUID_V4_STR_LEN, "%.8s-%.4s-%.4s-%.4s-%.4s%.8s", + temp_str[0], temp_str[1] + 0, temp_str[1] + 4, temp_str[2] + 0, + temp_str[2] + 4, temp_str[3]); + + uuid_str_out[APP_JWT_UUID_V4_STR_LEN - 1] = '\0'; + + return 0; +} + +static int crypto_init(void) +{ + psa_status_t status; + + /* Initialize PSA Crypto */ + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_crypto_init failed! (Error: %d)", status); + return -ENOMEM; + } + + return 0; +} + +static int raw_ecc_pubkey_to_der(uint8_t *raw_pub_key, uint8_t *der_pub_key, + const size_t der_pub_key_buffer_size, size_t *der_pub_key_len) +{ + int err = -1; + + if (ECDSA_PUBLIC_KEY_DER_SZ <= der_pub_key_buffer_size) { + uint8_t der_pubkey_header[27] = { + /* Integer sequence of 89 bytes */ + 0x30, 0x59, + /* Integer sequence of 19 bytes */ + 0x30, 0x13, + /* ecPublicKey (ANSI X9.62 public key type) */ + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, + /* prime256v1 (ANSI X9.62 named elliptic curve) */ + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + /* Integer sequence of 66 bytes */ + 0x03, 0x42, + /* Header of uncompressed RAW public ECC key */ + 0x00, 0x04}; + + memcpy(der_pub_key, der_pubkey_header, sizeof(der_pubkey_header)); + memcpy((der_pub_key + 27), (raw_pub_key + 1), ECDSA_PUBLIC_KEY_SZ - 1); + + *der_pub_key_len = ECDSA_PUBLIC_KEY_DER_SZ; + err = 0; + } + + return err; +} + +static int export_public_key_hash(const uint32_t user_key_id, uint8_t *key_hash_out, + size_t key_hash_buffer_size, size_t *key_hash_Length) +{ + int err; + + psa_status_t status; + + size_t olen; + + uint8_t pub_key[ECDSA_PUBLIC_KEY_SZ]; + + /* Export the public key */ + status = psa_export_public_key(user_key_id, pub_key, sizeof(pub_key), &olen); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_export_public_key failed! (Error: %d)", status); + return -1; + } + + uint8_t pubkey_der[ECDSA_PUBLIC_KEY_DER_SZ] = {0}; + size_t pubkey_der_len = 0; + + err = raw_ecc_pubkey_to_der(pub_key, pubkey_der, sizeof(pubkey_der), &pubkey_der_len); + if (err != 0) { + LOG_ERR("raw_pubkey_to_der failed! (Error: %d)", err); + return -1; + } + +#if defined(CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER) + /* String size is double the binary size +1 for null termination */ + char pubkey_der_str[(ECDSA_PUBLIC_KEY_DER_SZ * 2) + 1] = {0}; + + size_t pubkey_der_str_len = 0; + + pubkey_der_str_len = + bin2hex(pubkey_der, pubkey_der_len, pubkey_der_str, sizeof(pubkey_der_str)); + LOG_INF("pubkey_der (%d) = %s", pubkey_der_len, pubkey_der_str); + +#endif /* CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER */ + + /* Compute the SHA256 hash of public key DER format */ + status = psa_hash_compute(PSA_ALG_SHA_256, pubkey_der, sizeof(pubkey_der), key_hash_out, + key_hash_buffer_size, key_hash_Length); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_hash_compute failed! (Error: %d)", status); + return -1; + } + + return status; +} + +static int sign_message(const uint32_t user_key_id, const uint8_t *input, size_t input_length, + uint8_t *signature, size_t signature_size, size_t *signature_length) +{ + psa_status_t status; + + /* Sign the hash */ + status = psa_sign_message(user_key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), input, input_length, + signature, signature_size, signature_length); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_sign_hash failed! (Error: %d)", status); + return -1; + } + + return 0; +} + +#if defined(CONFIG_APP_JWT_VERIFY_SIGNATURE) +static int verify_message_signature(const uint32_t user_key_id, const char *const message, + size_t message_size, uint8_t *signature, size_t signature_size) +{ + psa_status_t status; + + /* Verify the signature of the message */ + status = psa_verify_message(user_key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), message, + message_size, signature, signature_size); + if (status != PSA_SUCCESS) { + LOG_ERR("signature verification failed! (Error: %d)", status); + return -1; + } + + return 0; +} +#endif /* CONFIG_APP_JWT_VERIFY_SIGNATURE */ + +static int crypto_finish(const int key_id) +{ + psa_status_t status; + + /* Purge the key from memory */ + status = psa_purge_key(key_id); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_purge_key failed! (Error: %d)", status); + return -ENOMEM; + } + + return 0; +} + +static char *jwt_header_create(struct app_jwt_data *const jwt) +{ + if (jwt == NULL) { + return NULL; + } + + int err = 0; + + char *hdr_str = NULL; + + int key_id = DEFAULT_IAK_APPLICATION; + + cJSON *jwt_hdr = cJSON_CreateObject(); + + if (!jwt_hdr) { + LOG_ERR("cJSON_CreateObject failed!"); + return NULL; + } + + /* Type: format: always "JWT" */ + if (cJSON_AddStringToObjectCS(jwt_hdr, "typ", "JWT") == NULL) { + goto clean_exit; + } + + /* Algorithme: format: always "ES256" */ + if (jwt->alg == JWT_ALG_TYPE_ES256) { + if (cJSON_AddStringToObjectCS(jwt_hdr, "alg", "ES256") == NULL) { + goto clean_exit; + } + } else { + goto clean_exit; + } + + if (jwt->add_keyid_to_header) { + uint8_t pub_key_hash[ECDSA_SHA_256_HASH_SZ]; + + /* Kid: format: sha256 string */ + /* Get kid: sha256 over public key */ + size_t olen; + + err = export_public_key_hash(key_id, pub_key_hash, ECDSA_SHA_256_HASH_SZ, &olen); + if (err) { + LOG_ERR("Failed to export public key, error: %d", err); + goto clean_exit; + } + + char sha256_str[ECDSA_SHA_256_SIG_SZ + 1] = {0}; + + int32_t printed_bytes = 0; + + for (uint32_t i = 0; i < 8; i++) { + printed_bytes += + snprintf((sha256_str + printed_bytes), 9, "%08x", + sys_cpu_to_be32((uint32_t)*((uint32_t *)pub_key_hash + i))); + } + sha256_str[ECDSA_SHA_256_SIG_SZ] = '\0'; + + if (cJSON_AddStringToObjectCS(jwt_hdr, "kid", sha256_str) == NULL) { + err = -ENOMEM; + } + } + + if (err == 0) { + hdr_str = cJSON_PrintUnformatted(jwt_hdr); + } + +clean_exit: + cJSON_Delete(jwt_hdr); + jwt_hdr = NULL; + + return hdr_str; +} + +static char *convert_str_to_b64_url(const char *const str) +{ + if (str == NULL) { + return NULL; + } + + size_t str_len = strnlen(str, JWT_STR_MAX_SZ); + + size_t b64_sz = BASE64_ENCODE_SZ(str_len) + 1; + + char *const b64_out = calloc(b64_sz, 1); + + if (b64_out) { + int err = base64_encode(b64_out, b64_sz, &b64_sz, str, + strnlen(str, JWT_STR_MAX_SZ)); + + if (err) { + LOG_ERR("base64_encode failed, error: %d", err); + + free(b64_out); + return NULL; + } + } + + /* Convert to base64 URL */ + base64_url_format(b64_out); + + return b64_out; +} + +static char *jwt_header_payload_combine(const char *const hdr, const char *const pay) +{ + if (hdr == NULL || pay == NULL) { + return NULL; + } + /* Allocate buffer for the JWT header and payload to be signed */ + size_t msg_sz = strnlen(hdr, JWT_STR_MAX_SZ) + 1 + strnlen(pay, JWT_STR_MAX_SZ) + 1; + + char *msg_out = calloc(msg_sz, 1); + + /* Build the base64 URL JWT to sign: + * . + */ + if (msg_out) { + int ret = snprintk(msg_out, msg_sz, "%s.%s", hdr, pay); + + if ((ret < 0) || (ret >= msg_sz)) { + LOG_ERR("Could not format JWT to be signed"); + free(msg_out); + msg_out = NULL; + } + } + + return msg_out; +} + +static char *jwt_payload_create(struct app_jwt_data *const jwt) +{ + if (jwt == NULL) { + return NULL; + } + + int err = 0; + + struct timespec tp; + + uint64_t issue_time = 0; + + char *pay_str = NULL; + + cJSON *jwt_pay = cJSON_CreateObject(); + + if (!jwt_pay) { + LOG_ERR("cJSON_CreateObject failed!"); + return NULL; + } + + err = clock_gettime(CLOCK_REALTIME, &tp); + if (err) { + /* clock_gettime error, use 0 value */ + issue_time = 0; + } else { + issue_time = tp.tv_sec; + } + + if (jwt->add_timestamp && (issue_time != 0)) { + /* Issued at : timestamp is seconds */ + if (cJSON_AddNumberToObjectCS(jwt_pay, "iat", issue_time) == NULL) { + err = -ENOMEM; + } + } + + if (jwt->json_token_id != NULL) { + char claim_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(claim_str, APP_JWT_CLAIM_MAX_SIZE, "%s", jwt->json_token_id); + claim_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + if (cJSON_AddStringToObjectCS(jwt_pay, "jti", claim_str) == NULL) { + err = -ENOMEM; + } + } + + if (jwt->issuer != NULL) { + char claim_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(claim_str, APP_JWT_CLAIM_MAX_SIZE, "%s", jwt->issuer); + claim_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + if (cJSON_AddStringToObjectCS(jwt_pay, "iss", claim_str) == NULL) { + err = -ENOMEM; + } + } + + if (jwt->subject != NULL) { + char claim_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(claim_str, APP_JWT_CLAIM_MAX_SIZE, "%s", jwt->subject); + claim_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + if (cJSON_AddStringToObjectCS(jwt_pay, "sub", claim_str) == NULL) { + err = -ENOMEM; + } + } + + if (jwt->audience != NULL) { + char claim_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(claim_str, APP_JWT_CLAIM_MAX_SIZE, "%s", jwt->audience); + claim_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + if (cJSON_AddStringToObjectCS(jwt_pay, "aud", claim_str) == NULL) { + err = -ENOMEM; + } + } + + /* Expiration: format: time in seconds as integer + expiration */ + if (jwt->validity_s > 0) { + /* Add expiration (exp) claim */ + if (cJSON_AddNumberToObjectCS(jwt_pay, "exp", (jwt->validity_s + issue_time)) == + NULL) { + err = -ENOMEM; + } + } + + if (err == 0) { + pay_str = cJSON_PrintUnformatted(jwt_pay); + } + + cJSON_Delete(jwt_pay); + jwt_pay = NULL; + + return pay_str; +} + +static char *unsigned_jwt_create(struct app_jwt_data *const jwt) +{ + char *hdr_str, *pay_str; + + char *hdr_b64, *pay_b64; + + char *unsigned_jwt = NULL; + + if (jwt == NULL) { + return NULL; + } + + /* Create the header */ + hdr_str = jwt_header_create(jwt); + if (!hdr_str) { + LOG_ERR("Failed to create JWT JSON payload"); + return NULL; + } + + /* Convert header JSON string to base64 URL */ + hdr_b64 = convert_str_to_b64_url(hdr_str); + + cJSON_free(hdr_str); + hdr_str = NULL; + + if (!hdr_b64) { + LOG_ERR("Failed to convert header string to base64"); + return NULL; + } + + /* Create the payload */ + pay_str = jwt_payload_create(jwt); + if (!pay_str) { + LOG_ERR("Failed to create JWT JSON payload"); + goto clean_hdr_64_exit; + } + + /* Convert payload JSON string to base64 URL */ + pay_b64 = convert_str_to_b64_url(pay_str); + + cJSON_free(pay_str); + pay_str = NULL; + + if (!pay_b64) { + LOG_ERR("Failed to convert payload string to base64"); + goto clean_hdr_64_exit; + } + + /* Create the base64 URL data to be signed */ + unsigned_jwt = jwt_header_payload_combine(hdr_b64, pay_b64); + + free(pay_b64); + pay_b64 = NULL; + +clean_hdr_64_exit: + free(hdr_b64); + hdr_b64 = NULL; + + return unsigned_jwt; +} + +static int jwt_signature_get(const int key_id, const char *const jwt, char *const sig_buf, + size_t sig_sz) +{ + int err; + + size_t o_len; + + uint8_t sig_raw[ECDSA_SHA_256_SIG_SZ]; + + if (jwt == NULL || sig_buf == NULL) { + return -EINVAL; + } + + /* Use Application IAK key for signing the JWT */ + err = sign_message(key_id, jwt, strnlen(jwt, JWT_STR_MAX_SZ), sig_raw, + ECDSA_SHA_256_SIG_SZ, &o_len); + if (err) { + LOG_ERR("Failed to sign message : %d", err); + return -EACCES; + } + +#if defined(CONFIG_APP_JWT_VERIFY_SIGNATURE) + err = verify_message_signature(key_id, jwt, strnlen(jwt, JWT_STR_MAX_SZ), sig_raw, o_len); + if (err) { + LOG_ERR("Failed to verify message signature : %d", err); + return -EACCES; + } +#endif /* CONFIG_APP_JWT_VERIFY_SIGNATURE */ + + /* Convert signature to base64 URL */ + err = base64_encode(sig_buf, sig_sz, &o_len, sig_raw, sizeof(sig_raw)); + if (err) { + LOG_ERR("base64_encode failed, error: %d", err); + return -EIO; + } + + base64_url_format(sig_buf); + return 0; +} + +static int jwt_signature_append(const char *const unsigned_jwt, const char *const sig, + char *const jwt_buf, size_t jwt_sz) +{ + int err = 0; + + if (unsigned_jwt == NULL || sig == NULL) { + return -EINVAL; + } + + /* Get the size of the final, signed JWT: +1 for */ + /* '.' and null-terminator */ + size_t final_sz = strnlen(unsigned_jwt, JWT_STR_MAX_SZ) + 1 + + strnlen(sig, ECDSA_SHA_256_SIG_SZ) + 1; + + if (final_sz > jwt_sz) { + /* Provided buffer is too small */ + return -E2BIG; + } + + /* JWT final form: + * .. + */ + int ret = snprintk(jwt_buf, jwt_sz, "%s.%s", unsigned_jwt, sig); + + if ((ret < 0) || (ret >= jwt_sz)) { + err = -ETXTBSY; + } + + return err; +} + +int app_jwt_generate(struct app_jwt_data *const jwt) +{ + if (jwt == NULL) { + return -EINVAL; + } + + if ((jwt->jwt_buf == NULL) || (jwt->jwt_sz == 0)) { + return -EMSGSIZE; + } + + int err = 0; + + int key_id = DEFAULT_IAK_APPLICATION; + + char *unsigned_jwt; + + uint8_t jwt_sig[B64_SIG_SZ]; + + if (jwt->sec_tag) { + /** + * Using sec_tag as key_id, you should be sure the provided + * sec_tag is a valid key_id + */ + LOG_ERR("sec_tag provided, this is not supported"); + return -ENOTSUP; + } + + /* Init crypto services, required for the rest of the operations */ + err = crypto_init(); + if (err) { + LOG_ERR("Failed to initialize PSA Crypto, error: %d", err); + return err; + } + + /* Create the JWT to be signed */ + unsigned_jwt = unsigned_jwt_create(jwt); + if (!unsigned_jwt) { + LOG_ERR("Failed to create JWT to be signed"); + goto finish_crypto_exit; + } + + /* Get the signature of the unsigned JWT */ + err = jwt_signature_get(key_id, unsigned_jwt, jwt_sig, sizeof(jwt_sig)); + if (err) { + LOG_ERR("Failed to get JWT signature, error: %d", err); + goto clean_exit; + } + + /* Append the signature, creating the complete JWT */ + err = jwt_signature_append(unsigned_jwt, jwt_sig, jwt->jwt_buf, jwt->jwt_sz); + +clean_exit: + free(unsigned_jwt); + unsigned_jwt = NULL; + +finish_crypto_exit: + /* Crypto services not required anymore */ + err = crypto_finish(key_id); + if (err) { + LOG_ERR("Failed to sign message : %d", err); + return err; + } + + return err; +} + +int app_jwt_get_uuid(char *uuid_buffer, const size_t uuid_buffer_size) +{ + if ((NULL == uuid_buffer) || (uuid_buffer_size < APP_JWT_UUID_V4_STR_LEN)) { + /* Bad parameter */ + return -EINVAL; + } + + uint8_t uuid_bytes[UUID_BINARY_BYTES_SZ]; + + if (0 != ssf_device_info_get_uuid(uuid_bytes)) { + /* Couldn't read data */ + return -ENXIO; + } + + return bytes_to_uuid_str((uint32_t *)uuid_bytes, UUID_BINARY_BYTES_SZ, uuid_buffer, + uuid_buffer_size); +} diff --git a/samples/jwt/CMakeLists.txt b/samples/jwt/CMakeLists.txt new file mode 100644 index 000000000000..6193c8dd1822 --- /dev/null +++ b/samples/jwt/CMakeLists.txt @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.13.1) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(jwt_sample) + +# NORDIC SDK APP START +target_sources(app PRIVATE src/main.c) + +# NORDIC SDK APP END diff --git a/samples/jwt/Kconfig b/samples/jwt/Kconfig new file mode 100644 index 000000000000..fc1546509444 --- /dev/null +++ b/samples/jwt/Kconfig @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig JWT_SAMPLE + bool "Json Web Token Sample" + help + Enable sample-specific configurations. + +if JWT_SAMPLE + +module = JWT_SAMPLE +module-str = JWT Sample +source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config" + +endif # JWT_SAMPLE + +source "Kconfig.zephyr" diff --git a/samples/jwt/README.rst b/samples/jwt/README.rst new file mode 100644 index 000000000000..cf1649e4158d --- /dev/null +++ b/samples/jwt/README.rst @@ -0,0 +1,96 @@ +.. _jwt_application: + +jwt generator +################### + +.. contents:: + :local: + :depth: 2 + +This sample shows how Application core can generate a JWT signed with the IAK. + +Requirements +************ + +The sample supports the following development kits: + +.. list-table:: Supported development kits + :widths: 50 50 + :header-rows: 1 + + * - Board + - Support + * - nrf54h20dk/nrf54h20/cpuapp + - Yes + +Overview +******** + +The sample goes through the following steps to generate a JWT: + +1. Reads the device UUID. + + The returned UUID is compliant with UUID v4 defined by ITU-T X.667 | ISO/IEC 9834-8. + +2. Generates a JWT. + + Uses the user provided fields for audiance and expiration delta, will always use IAK key to sign the JWT. + The generated JWT is printed to serial terminal if project configuration allows it. + +Configuration +************* + +|config| + +LIB JWT APP +=========== + +As per provided on the project config, the used APIs requier the usage of lib::app_jwt. + +JWT signing verification +======================== + +Flag :kconfig:option:`CONFIG_APP_JWT_VERIFY_SIGNATURE` allow to verify the JWT signature against the IAK key. + +Export public IAK key +===================== + +User might be interrested in the DER formatted IAK key for later verifications of the generated JWT, the flag :kconfig:option:`CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER` allows printing the DER formatted key to debug terminal. + +Building and running +******************** + + .. code-block:: console + + west build -p -b nrf54h20dk/nrf54h20/cpuapp nrf/samples/jwt --build-dir build_cpuapp_nrf54h20dk_jwt_logging_serial/ -T samples.jwt.logging.uart + +Testing +======= + +|test_sample| + +1. |connect_terminal_specific| +#. Reset the kit. +#. Observe the following output (DER formatted public IAK key, and the JWT): + + .. code-block:: console + + jwt_sample: Appcation JWT sample (nrf54h20dk) + jwt_sample: App: pubkey_der (91) = 3059301306072a8648ce3d020106082a8648ce3d03010703420004017e627cc237c5a37d9142d0cba1530a5653c4f41e6ba6e06d3b74fdf5c308b09aff1d99946d5deb4dd97dbd0dbcba62c3d9ba518fc9e43be88b780b1484 + jwt_sample: JWT(500) : + jwt_sample: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRkMmM2NjdmNTYzMzExY2Q4OWE2N2M2ZWQ4ZDQwZDBmYWIyMzAyNTc3MjIyNjNjNDNkMThiNTM4YzZjN2JjZTkifQ.eyJqdGkiOiJucmY1NGgyMGRrLmZlMDI1ODlhYzA4NzVmY2IzMDk3Y2U5MmFmZGIyMTBlIiwiaXNzIjoibnJmNTRoMjBkay4zNzU4ZTE5NC03MjgyLTExZWYtOTMyYi03YjFmMDcwY2NlN2EiLCJzdWIiOiIzNzU4ZTE5NC03MjgyLTExZWYtOTMyYi03YjFmMDcwY2NlN2EiLCJhdWQiOiJKU09OIHdlYiB0b2tlbiBmb3IgZGVtb25zdHJhdGlvbiIsImV4cCI6NjA0ODAwfQ.w0Bf6QvXBLshvRn1vT9qcLjlFovgx9beOknHl848XeWgpaOWAdzmUZ6eB85-SMnQPZTglGZhcdWSp5mMMQABXg + + If an error occurs, the sample prints an error message. + +.. note:: + Due to the extra long strings that the terminal has to print, be sure to configure :kconfig:option:`CONFIG_LOG_BUFFER_SIZE` with enough size to avoid truncated strings on the output. + +.. note:: + Currently, the provided Sample doesn't support other boards than nrf54h20dk. + +Dependencies +************ + +This sample uses the following |NCS| libraries: + +* :ref:`lib_app_jwt` diff --git a/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.conf b/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.conf new file mode 100644 index 000000000000..4f6687ac7a27 --- /dev/null +++ b/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.conf @@ -0,0 +1,46 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# Disable Data Cache +CONFIG_DCACHE=n + +CONFIG_JWT_SAMPLE=y +CONFIG_JWT_SAMPLE_LOG_LEVEL_INF=y + +# JWT needs at least 900 bytes of stack +CONFIG_MAIN_STACK_SIZE=4096 + +# Multiple dynamic allocations are performed by cJSON Lib and for base64 encoding. +CONFIG_HEAP_MEM_POOL_SIZE=8192 + +# Use app jwt Library info log level +CONFIG_APP_JWT=y +CONFIG_APP_JWT_LOG_LEVEL_INF=y + +# Verify JWT signature after signing +CONFIG_APP_JWT_VERIFY_SIGNATURE=y + +# Optional : print the exported public key in DER format to logging terminal, +# requieres CONFIG_APP_JWT_LOG_LEVEL_INF=y to be made visible. +CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER=y + +# Enable nordic security backend and PSA APIs +CONFIG_NRF_SECURITY=y + +# Enable Cracen PSA crypto drivers +CONFIG_PSA_CRYPTO_DRIVER_CRACEN=y +CONFIG_PSA_CRYPTO_DRIVER_CC3XX=n +CONFIG_PSA_CRYPTO_DRIVER_OBERON=n + +# Enable PSA crypto from SSF client +CONFIG_SSF_PSA_CRYPTO_SERVICE_ENABLED=y + +# Enable Device Info service +CONFIG_SSF_DEVICE_INFO_SERVICE_ENABLED=y + +# Enable SUIT bundling +CONFIG_SUIT=y +CONFIG_ZCBOR_CANONICAL=y diff --git a/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.overlay b/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.overlay new file mode 100644 index 000000000000..ee0bfcad083a --- /dev/null +++ b/samples/jwt/boards/nrf54h20dk_nrf54h20_cpuapp.overlay @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +&cpusec_cpuapp_ipc { + status = "okay"; +}; + +&cpusec_bellboard { + status = "okay"; +}; diff --git a/samples/jwt/prj.conf b/samples/jwt/prj.conf new file mode 100644 index 000000000000..289995d235c8 --- /dev/null +++ b/samples/jwt/prj.conf @@ -0,0 +1,33 @@ +CONFIG_JWT_SAMPLE=y +CONFIG_JWT_SAMPLE_LOG_LEVEL_INF=y + +# JWT needs at least 900 bytes of stack +CONFIG_MAIN_STACK_SIZE=4096 + +# Multiple dynamic allocations are performed by cJSON Lib and for base64 encoding. +CONFIG_HEAP_MEM_POOL_SIZE=8192 + +# Use app jwt Library info log level +CONFIG_APP_JWT=y +CONFIG_APP_JWT_LOG_LEVEL_INF=y + +# Verify JWT signature after signing +CONFIG_APP_JWT_VERIFY_SIGNATURE=y + +# Optional : print the exported public key in DER format to logging terminal, +# requieres CONFIG_APP_JWT_LOG_LEVEL_INF=y to be made visible. +CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER=y + +# Enable nordic security backend and PSA APIs +CONFIG_NRF_SECURITY=y + +# Enable Cracen PSA crypto drivers +CONFIG_PSA_CRYPTO_DRIVER_CRACEN=y +CONFIG_PSA_CRYPTO_DRIVER_CC3XX=n +CONFIG_PSA_CRYPTO_DRIVER_OBERON=n + +# Enable PSA crypto from SSF client +CONFIG_SSF_PSA_CRYPTO_SERVICE_ENABLED=y + +# Enable Device Info service +CONFIG_SSF_DEVICE_INFO_SERVICE_ENABLED=y diff --git a/samples/jwt/sample.yaml b/samples/jwt/sample.yaml new file mode 100644 index 000000000000..b6308c8390bc --- /dev/null +++ b/samples/jwt/sample.yaml @@ -0,0 +1,28 @@ +sample: + name: JWT Sample + description: | + Sample demonstrating how to generate a JSON web token (JWT). + +common: + build_only: true + tags: ci_build ci_samples_jwt + +tests: + samples.jwt.logging.uart: + sysbuild: true + tags: sysbuild ci_samples_jwt + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + integration_platforms: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - EXTRA_CONF_FILE=uart_logging.conf + - SB_CONFIG_SUIT_ENVELOPE=y + extra_configs: + - CONFIG_LOG_BUFFER_SIZE=8196 + - CONFIG_SUIT=y + - CONFIG_ZCBOR=y + - CONFIG_ZCBOR_CANONICAL=y + - CONFIG_SUIT_ENVELOPE_TARGET="application" + - CONFIG_SUIT_ENVELOPE_TEMPLATE_FILENAME="app_envelope.yaml.jinja2" + - CONFIG_SUIT_LOCAL_ENVELOPE_GENERATE=y diff --git a/samples/jwt/src/main.c b/samples/jwt/src/main.c new file mode 100644 index 000000000000..7a326c51f346 --- /dev/null +++ b/samples/jwt/src/main.c @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA. + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include + +#include + +#include +#include + +LOG_MODULE_REGISTER(jwt_sample, CONFIG_JWT_SAMPLE_LOG_LEVEL); + +#define JWT_AUDIENCE_STR "JSON web token for demonstration" + +static int jwt_generate(uint32_t exp_time_s, char *out_buffer, const size_t out_buffer_size) +{ + if (!out_buffer || !out_buffer_size) { + return -EINVAL; + } + + int err = -EINVAL; + + struct app_jwt_data jwt = {.sec_tag = 0, + .key_type = 0, + .alg = JWT_ALG_TYPE_ES256, + .add_keyid_to_header = true, + .add_timestamp = true, + .jwt_buf = out_buffer, + .jwt_sz = out_buffer_size}; + + if (exp_time_s > APP_JWT_VALID_TIME_S_MAX) { + jwt.validity_s = APP_JWT_VALID_TIME_S_MAX; + } else if (exp_time_s == 0) { + jwt.validity_s = APP_JWT_VALID_TIME_S_DEF; + } else { + jwt.validity_s = exp_time_s; + } + + /* Subject: format: "user_defined_string" , we use uuid as subject */ + char device_uuid_str[APP_JWT_UUID_V4_STR_LEN] = {0}; + + if (0 != app_jwt_get_uuid(device_uuid_str, APP_JWT_UUID_V4_STR_LEN)) { + return -ENXIO; + } + + jwt.subject = device_uuid_str; + + /* Issuer: format: . */ + char token_issuer_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(token_issuer_str, APP_JWT_CLAIM_MAX_SIZE, "%s.%s", CONFIG_BOARD, jwt.subject); + token_issuer_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + jwt.issuer = token_issuer_str; + + /* Json Token ID: format: .<16-random_bytes> */ + char json_token_id_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(json_token_id_str, APP_JWT_CLAIM_MAX_SIZE, "%s.%s", CONFIG_BOARD, + "fe02589ac0875fcb3097ce92afdb210e"); + json_token_id_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + jwt.json_token_id = json_token_id_str; + + /* Audience: format: "user_defined_string" */ + char audience_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + snprintf(audience_str, APP_JWT_CLAIM_MAX_SIZE, "%s", JWT_AUDIENCE_STR); + audience_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + + jwt.audience = audience_str; + + err = app_jwt_generate(&jwt); + + return err; +} + +int main(void) +{ + LOG_INF("Application JWT sample (%s)", CONFIG_BOARD); + + char jwt_str[APP_JWT_STR_MAX_LEN] = {0}; + + int ret = jwt_generate(APP_JWT_VALID_TIME_S_MAX, jwt_str, APP_JWT_STR_MAX_LEN); + + if (ret == 0) { + LOG_INF("JWT(%d) :", strlen(jwt_str)); + LOG_INF("%s", jwt_str); + + } else { + LOG_ERR("jwt_generate error : %d", ret); + } + + return 0; +} diff --git a/samples/jwt/uart_logging.conf b/samples/jwt/uart_logging.conf new file mode 100644 index 000000000000..2d8ae3bca9fb --- /dev/null +++ b/samples/jwt/uart_logging.conf @@ -0,0 +1,16 @@ +# Enable serial printing via UART +CONFIG_SERIAL=y +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +CONFIG_LOG=y +CONFIG_LOG_BACKEND_UART=y +CONFIG_LOG_PRINTK=y +CONFIG_LOG_BUFFER_SIZE=8196 +CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 + +# Show function names for errors only +CONFIG_LOG_FUNC_NAME_PREFIX_ERR=y +CONFIG_LOG_FUNC_NAME_PREFIX_WRN=n +CONFIG_LOG_FUNC_NAME_PREFIX_INF=n +CONFIG_LOG_FUNC_NAME_PREFIX_DBG=n