diff --git a/CODEOWNERS b/CODEOWNERS index b6f8e2e7277e..41ce1145ab13 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 @@ -520,6 +523,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 @@ -641,6 +645,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/others/app_jwt.rst b/doc/nrf/libraries/others/app_jwt.rst new file mode 100644 index 000000000000..33791c2d9a60 --- /dev/null +++ b/doc/nrf/libraries/others/app_jwt.rst @@ -0,0 +1,84 @@ +.. _lib_app_jwt: + +Application JWT +############### + +.. contents:: +:local: +:depth: 2 + +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` + +#. Generate a Signing key pair if you don't want to use the IAK Key. +#. 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:: + If a timestamp is needed and there is an error getting the time from the clock source (or the returned time in seconds is 0), "iat" field will contain the value :kconfig:option:`CONFIG_APP_JWT_DEFAULT_TIMESTAMP`. + +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 ``kid`` claim. + Use ``false`` if you want to leave out this field. + If filled with the value ``true``, the claim ``kid`` will contain the SHA256 of the DER of the public part of the signing key. +* :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 ``false`` if you want to leave out this field. + If filled with the value ``true``, the claim ``iat`` will be filled with the current timestamp in seconds. +* :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/include/app_jwt.h b/include/app_jwt.h new file mode 100644 index 000000000000..39ea372a07f3 --- /dev/null +++ b/include/app_jwt.h @@ -0,0 +1,154 @@ +/* + * 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 an arbitrary timestamp. + * + * @param[in,out] jwt Pointer to struct containing JWT parameters and result. + * + * @retval 0 If the operation was successful. + * @retval -errno Negative errno for other failures. + * + */ +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. + * @retval -errno Negative errno for other failures. + * + */ +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..e0ff1aba62af --- /dev/null +++ b/lib/app_jwt/Kconfig @@ -0,0 +1,34 @@ +# +# 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_DEFAULT_TIMESTAMP + int "Default timestamp to use in case time value is 0" + default 1735682400 + +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..68f3db57f392 --- /dev/null +++ b/lib/app_jwt/app_jwt.c @@ -0,0 +1,721 @@ +/* + * 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_message 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; + } + + if (jwt->sec_tag != 0) { + /** + * Using sec_tag as key_id, you should be sure the provided + * sec_tag is a valid key_id + */ + LOG_WRN("sec_tag provided, value = 0x%08X", jwt->sec_tag); + key_id = jwt->sec_tag; + } else { + key_id = DEFAULT_IAK_APPLICATION; + } + + /* 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; + } + + 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; + } + + if (jwt->add_keyid_to_header) { + 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 = CONFIG_APP_JWT_DEFAULT_TIMESTAMP; + + 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 DEFAULT_TIMESTAMP value */ + LOG_WRN("clock_gettime error, use %d", CONFIG_APP_JWT_DEFAULT_TIMESTAMP); + } else { + issue_time = tp.tv_sec; + if (issue_time == 0) { + LOG_WRN("invalid time value, use %d", CONFIG_APP_JWT_DEFAULT_TIMESTAMP); + } + } + + 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 = 0; + + char *unsigned_jwt; + + uint8_t jwt_sig[B64_SIG_SZ]; + + if (jwt->sec_tag != 0) { + /** + * Using sec_tag as key_id, you should be sure the provided + * sec_tag is a valid key_id + */ + LOG_WRN("sec_tag provided, value = 0x%08X", jwt->sec_tag); + key_id = jwt->sec_tag; + } else { + key_id = DEFAULT_IAK_APPLICATION; + } + + /* 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 cleanup PSA Crypto, Error : %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..2627960efaff --- /dev/null +++ b/samples/jwt/README.rst @@ -0,0 +1,109 @@ +.. _jwt_application: + +JWT generator +############# + +.. contents:: + :local: + :depth: 2 + +This sample demonstrates how Application core can generate a signed JWT. + +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 +******** + +This code will generate two separate JWT : simplified (less fields) signed with a user generated key, and a complete JWT signed with the IAK key. +The sample goes through the following steps to generate a JWTs: + +1. Fill in the fields for the JWT. + + For the purposes of this Sample, an ECDSA keypair is created and fed to the Secure Domain using PSA Crypto interfaces, the returned key_id is used as a `sec_tag` for signing the JWT. + + The key has to be of type ECDSA secp256r1 and the usage needs to be for message signing and message signature verification. + + The simplified JWT version, only the `sub` claim and the `exp` claim fields are filled. + + The full version, `sub`, `aud`, `iss`, `iat` and `exp` are filled and `kid` is added to the header. + +2. Generates a 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 signing key. + +Export public part of the signing key +===================================== + +You might be interrested in the DER formatted 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. +The DER key can be turned into PEM format by encoding it in base64 and adding the PEM markers `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----`. + +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: Application JWT sample (nrf54h20dk) + jwt_sample: JWT simplified token : + app_jwt: sec_tag provided, value = 0x7FFFFFE2 + app_jwt: sec_tag provided, value = 0x7FFFFFE2 + app_jwt: pubkey_der (91) = 3059301306072a8648ce3d020106082a8648ce3d0301070342000469fa6c852bfc0b749838708b8c15292a78f1c26050cb6c32d53b69eb6c3589e4581477c417cb7c1eabc74ea2addb979a38ea3f5810b4be2eb5c4a98fc6ae2f4c + app_jwt: issue_time is 0, will use value "1735682400" + jwt_sample: JWT(218) : + jwt_sample: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJucmY1NGgyMGRrLjJiNzZmNjA2NWEyYTgyODcxYTIxOGYwMTVhMDhkZmM4IiwiZXhwIjoxNzM2Mjg3MjAwfQ.jQSl_p8LE1VCBxb1gZWbu0AyuTOslDxY5Oue1jX4UZyfKbcdS2GHnGn4VD2q1SNfeC_Ncpzh0Y1c4L3s-4uYSg + jwt_sample: JWT full token : + app_jwt: pubkey_der (91) = 3059301306072a8648ce3d020106082a8648ce3d03010703420004017e627cc237c5a37d9142d0cba1530a5653c4f41e6ba6e06d3b74fdf5c308b09afffd761d99946d5deb4dd97dbd0dbcba62c3d9ba518fc9e43be88b780b1484 + app_jwt: issue_time is 0, will use value "1735682400" + jwt_sample: JWT(505) : + jwt_sample: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjRkMmM2NjdmNTYzMzExY2Q4OWE2N2M2ZWQ4ZDQwZDBmYWIyMzAyNTc3MjIyNjNjNDNkMThiNTM4YzZjN2JjZTkifQ.eyJqdGkiOiJucmY1NGgyMGRrLmI2ZjNkOGI1YjQzNmY5MDhkNjExYzYzZTE1ZTI1OTA1IiwiaXNzIjoibnJmNTRoMjBkay4zNzU4ZTE5NC03MjgyLTExZWYtOTMyYi03YjFmMDcwY2NlN2EiLCJzdWIiOiIzNzU4ZTE5NC03MjgyLTExZWYtOTMyYi03YjFmMDcwY2NlN2EiLCJhdWQiOiJKU09OIHdlYiB0b2tlbiBmb3IgZGVtb25zdHJhdGlvbiIsImV4cCI6MTczNjI4NzIwMH0.9CM8z1tdmDjqmrBd32EqmCWzCNj8wrnCusC7qFqXloYoU9GtowGNhliwt3ENqrAc2Rur2-szqazWWVrKQw5uMA + + If an error occurs, the sample prints an error message. + +.. 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..312668ae01b4 --- /dev/null +++ b/samples/jwt/src/main.c @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA. + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#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" + +#define RANDOM_STR_MAX_SIZE 33 + +static int generate_ecdsa_keypair(uint32_t *user_keypair_id) +{ + int err = -1; + + if (user_keypair_id != NULL) { + psa_key_attributes_t key_attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_status_t status; + + /* Initialize PSA Crypto */ + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + LOG_INF("psa_crypto_init failed! (Error: %d)", status); + return -1; + } + + /* Configure the key attributes */ + psa_set_key_usage_flags(&key_attributes, + PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_lifetime(&key_attributes, PSA_KEY_LIFETIME_VOLATILE); + psa_set_key_algorithm(&key_attributes, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + psa_set_key_type(&key_attributes, + PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&key_attributes, 256); + + /** + * Generate a random keypair. The keypair is not exposed to the application, + * we can use it to sign messages. + */ + status = psa_generate_key(&key_attributes, user_keypair_id); + if (status != PSA_SUCCESS) { + LOG_INF("psa_generate_key failed! (Error: %d)", status); + return status; + } + + psa_reset_key_attributes(&key_attributes); + + err = 0; + } + return err; +} + +static int get_random_bytes_str(uint8_t *output_str, const size_t output_str_length) +{ + int err = -ENODATA; + + if ((NULL == output_str) || (output_str_length > (RANDOM_STR_MAX_SIZE))) { + /* Bad parameter */ + return -EINVAL; + } + + uint8_t tmp_output_buffer[(RANDOM_STR_MAX_SIZE - 1) / 2] = {0}; + + psa_status_t psa_status = psa_generate_random(tmp_output_buffer, sizeof(tmp_output_buffer)); + + if (PSA_SUCCESS != psa_status) { + LOG_ERR("psa_generate_random failed! = %d", psa_status); + err = -ENOMEM; + } else { + err = 0; + } + + int printed_bytes = 0; + + for (uint32_t i = 0; i < ((RANDOM_STR_MAX_SIZE - 1) / 8); i++) { + printed_bytes += snprintf((output_str + printed_bytes), 9, "%08x", + (uint32_t)*((uint32_t *)tmp_output_buffer + i)); + } + output_str[printed_bytes + 1] = '\0'; + + return err; +} + +static int jwt_generate_simplified_token(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; + + int user_key_id = 0; + + /** + * Simplified token requies using a propriatary key for signing. + * The key needs to be generated before using its id for signing. + * Key needs to be valid for signing messages and verifying message + * signatures. It needs to be an ECDSA with a secp256r1 curve. + */ + err = generate_ecdsa_keypair(&user_key_id); + if (err != 0) { + LOG_ERR("generate_ecdsa_keypair failed! (err: %d)", err); + return -ENOMEM; + } + + struct app_jwt_data jwt = {.sec_tag = user_key_id, + .key_type = JWT_KEY_TYPE_CLIENT_PRIV, + .alg = JWT_ALG_TYPE_ES256, + /** + * Simplified token doesn't need + * a `kid` claim in its header + */ + .add_keyid_to_header = false, + .jwt_buf = out_buffer, + .jwt_sz = out_buffer_size}; + + /* Simplified token requies using an expiration claim */ + 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; + } + + /* Simplified token requies using a subject claim */ + /* Subject: format: .<16-random_bytes> */ + char token_subject_str[APP_JWT_CLAIM_MAX_SIZE] = {0}; + + char subject_random_str[RANDOM_STR_MAX_SIZE] = {0}; + + if (0 == get_random_bytes_str(subject_random_str, RANDOM_STR_MAX_SIZE)) { + snprintf(token_subject_str, APP_JWT_CLAIM_MAX_SIZE, "%s.%s", CONFIG_BOARD, + subject_random_str); + token_subject_str[APP_JWT_CLAIM_MAX_SIZE - 1] = '\0'; + } + + jwt.subject = token_subject_str; + + /* JWT Claims that are not requiered are marked with a `0` */ + jwt.json_token_id = 0; + jwt.audience = 0; + jwt.issuer = 0; + + /* Simplified token doesn't need a `iat` claim in its header */ + jwt.add_timestamp = false, + + /* Call app_jwt API */ + err = app_jwt_generate(&jwt); + + return err; +} + +static int jwt_generate_full_token(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; + + /* Full token used IAK key for signing */ + struct app_jwt_data jwt = {.sec_tag = 0, + .key_type = 0, + .alg = JWT_ALG_TYPE_ES256, + /* Full token requieres `kid` claim to its header */ + .add_keyid_to_header = true, + /* Full token requieres `iat` claim */ + .add_timestamp = true, + .jwt_buf = out_buffer, + .jwt_sz = out_buffer_size}; + + /* Full token requieres `exp` claim */ + 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; + } + + /* Full token requieres `sub`, `iss`, `jti` and `aud` claims */ + /* 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}; + + char jti_random_str[RANDOM_STR_MAX_SIZE] = {0}; + + if (0 == get_random_bytes_str(jti_random_str, RANDOM_STR_MAX_SIZE)) { + snprintf(json_token_id_str, APP_JWT_CLAIM_MAX_SIZE, "%s.%s", CONFIG_BOARD, + jti_random_str); + 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; + + /* Call app_jwt API */ + 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}; + + LOG_INF("JWT simplified token :"); + int ret = jwt_generate_simplified_token(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_simplified_token Failed! (ret: %d)", ret); + } + + memset(jwt_str, 0, APP_JWT_STR_MAX_LEN); + LOG_INF("JWT full token :"); + ret = jwt_generate_full_token(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_full_token Failed! (ret: %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