diff --git a/CODEOWNERS b/CODEOWNERS index ddfe726966be..67028a531010 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -311,6 +311,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-si-muffin @nrfconnect/ncs-dragoon @@ -353,6 +354,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 @@ -493,6 +495,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 @@ -615,6 +618,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/include/app_jwt.h b/include/app_jwt.h new file mode 100644 index 000000000000..8c35a1922b15 --- /dev/null +++ b/include/app_jwt.h @@ -0,0 +1,127 @@ +/* + * 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 + +/** @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 */ +#define APP_JWT_UUID_V4_STR_LEN ((APP_JWT_UUID_BYTE_SZ * 2) + 4) + +/** @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; + /** JWT signing algorithm */ + enum app_jwt_alg_type alg; + + /** Defines how long the JWT will be valid; in seconds (from generation). + * The 'iat' and 'exp' claims will be populated only if the application has a + * valid date and time. + */ + uint32_t validity_s; + + /** 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; + + /** 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. + * + * 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. + */ +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. + */ +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..20c6a20961ea --- /dev/null +++ b/lib/app_jwt/Kconfig @@ -0,0 +1,29 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig APP_JWT + bool "Application JWT Library" + select BASE64 + select CJSON_LIB + # needed to print integer values in JSON + select CBPRINTF_FP_SUPPORT + select POSIX_API + +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" + default y + +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..ebbb00ba152c --- /dev/null +++ b/lib/app_jwt/app_jwt.c @@ -0,0 +1,767 @@ +/* + * 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 +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) + +/* 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) + +/* Length of random bits added to JTI field */ +#define JWT_NONCE_BITS (128) + +/* Length of JTI field, this should be at least UUIDv4 size + * + size of board name + **/ +#define JWT_CLAIM_FILED_STR_LENGTH (64) + +/* Length of uint64_t_max written as a string + 1 string + * termination character + **/ +#define JWT_EXPIRATION_STR_LENGTH (21) + +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. */ + sprintf(temp_str[i], "%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> */ + + /* for some reason, sprintf will not respect the amount */ + /* of characters to print that is specified in the */ + /* formatting string. use this cascade of sprintf to */ + /* overcome this issue. */ + /* 59fa2510\0 , 8952abc5\0 , a20cd981\0 , 2bf2e936\0 */ + /*'^ ' = "59fa2510" */ + sprintf(uuid_str_out, "%8s", temp_str[0]); + /* 59fa2510\0 , 8952abc5\0 , a20cd981\0 , 2bf2e936\0 */ + /* '^ ' = "8952" */ + sprintf(uuid_str_out + 8, "-%4s", temp_str[1] + 0); + /* 59fa2510\0 , 8952abc5\0 , a20cd981\0 , 2bf2e936\0 */ + /* '^ ' = "abc5" */ + sprintf(uuid_str_out + 8 + 5, "-%4s", temp_str[1] + 4); + /* 59fa2510\0 , 8952abc5\0 , a20cd981\0 , 2bf2e936\0 */ + /* '^ ' = "a20c" */ + sprintf(uuid_str_out + 8 + 5 + 5, "-%4s", temp_str[2] + 0); + /* 59fa2510\0 , 8952abc5\0 , a20cd981\0 , 2bf2e936\0 */ + /* '^ ' + '^ ' = "d9812bf2e936" */ + sprintf(uuid_str_out + 8 + 5 + 5 + 5, "-%4s%8s", temp_str[2] + 4, temp_str[3]); + uuid_str_out[APP_JWT_UUID_V4_STR_LEN] = '\0'; + + return 0; +} + +#if defined(CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER) + +static int bytes_to_str(const uint8_t *const bytes_buf, const size_t bytes_len, char *string_buff, + const size_t string_buff_size, size_t *string_len) +{ + int err = 0; + + if ((NULL != bytes_buf) && (0 != bytes_len) && (NULL != string_buff) && + (0 != string_buff_size) && (NULL != string_len)) { + uint32_t str_pos = 0; + + uint8_t byte_h, byte_l = 0; + + for (uint32_t count = 0; count < bytes_len; count++) { + byte_h = ((bytes_buf[count] >> 4) & 0xF); + byte_l = (bytes_buf[count] & 0x0F); + if (byte_h < 10) { + /* transpose from 0-9 integer numbers */ + /* to 0x30('0') - 0x31('9') ASCII characters */ + byte_h += '0'; + } else { + /* transpose from 10-15 integer numbers */ + /* to 0x61('a') - 0x66('f') ASCII characters */ + byte_h += ('a' - 10); + } + + if (byte_l < 10) { + byte_l += '0'; + } else { + byte_l += ('a' - 10); + } + + sprintf(string_buff + str_pos, "%c%c", (char)(byte_h), (char)(byte_l)); + str_pos += 2; + } + + string_buff[str_pos] = '\0'; + *string_len = str_pos + 1; + } else { + LOG_ERR("bytes_to_str : bad parameters"); + err = -1; + } + + return err; +} +#endif /* CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER */ + +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) + char pubkey_der_str[ECDSA_PUBLIC_KEY_DER_SZ * 2] = {0}; + + size_t pubkey_der_str_len = 0; + + err = bytes_to_str(pubkey_der, pubkey_der_len, pubkey_der_str, sizeof(pubkey_der_str), + &pubkey_der_str_len); + if (err == 0) { + /* Print DER formatted pubkey to traces */ + 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_INF("signature verification failed! (Error: %d)", status); + return -1; + } + + return 0; +} +#endif /* CONFIG_APP_JWT_VERIFY_SIGNATURE */ + +static int get_random_bytes(uint8_t *output_buffer, const size_t output_length) +{ + int err = -ENODATA; + + if ((NULL == output_buffer) || (output_length < 4)) { + /* bad parameter */ + return -EINVAL; + } + + psa_status_t psa_status = psa_generate_random(output_buffer, output_length); + + if (PSA_SUCCESS != psa_status) { + LOG_ERR("psa_generate_random failed! = %d", psa_status); + err = -ENOMEM; + } else { + err = 0; + } + + return err; +} + +static int crypto_finish(void) +{ + psa_status_t status; + + /* Purge the key from memory */ + status = psa_purge_key(IAK_APPLICATION_GEN1); + if (status != PSA_SUCCESS) { + LOG_ERR("psa_purge_key failed! (Error: %d)", status); + return -ENOMEM; + } + + return 0; +} + +static char *jwt_header_create(const uint32_t alg, const uint32_t keyid) +{ + int err = 0; + + uint8_t pub_key_hash[ECDSA_SHA_256_HASH_SZ]; + + char *hdr_str = NULL; + + 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) { + return NULL; + } + + /* Algorithme: format: always "ES256" */ + if (alg == JWT_ALG_TYPE_ES256) { + if (cJSON_AddStringToObjectCS(jwt_hdr, "alg", "ES256") == NULL) { + return NULL; + } + } else { + return NULL; + } + + /* Keyid: format: sha256 string */ + /* Get kid: sha256 over public key */ + size_t olen; + + err = export_public_key_hash(IAK_APPLICATION_GEN1, pub_key_hash, ECDSA_SHA_256_HASH_SZ, + &olen); + if (err) { + LOG_ERR("Failed to export public key, error: %d", err); + return NULL; + } + + 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 += + sprintf((sha256_str + printed_bytes), "%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); + } + + cJSON_Delete(jwt_hdr); + jwt_hdr = NULL; + + return hdr_str; +} + +static char *convert_str_to_b64_url(const char *const str) +{ + size_t str_len = strlen(str); + + 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, strlen(str)); + + if (err) { + LOG_ERR("base64_encode failed, error: %d", err); + } + } + + /* 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) +{ + /* Allocate buffer for the JWT header and payload to be signed */ + size_t msg_sz = strlen(hdr) + 1 + strlen(pay) + 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(const char *const sub, const char *const aud, uint64_t exp) +{ + int err = 0; + + struct timespec tp; + + uint64_t issue_time; + + 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; + } + + /* Issued at : timestamp is seconds */ + if (cJSON_AddNumberToObjectCS(jwt_pay, "iat", issue_time) == NULL) { + err = -ENOMEM; + } + + /* Json Token ID: format: .<16-random_bytes> */ + uint32_t nonce_words[JWT_NONCE_BITS / 32] = {0}; + + err = get_random_bytes((uint8_t *)nonce_words, JWT_NONCE_BITS / 8); + if (err) { + LOG_ERR("get_random_bytes failed! = %d", err); + err = -ENOMEM; + } else { + char nonce_uuid_str[APP_JWT_UUID_V4_STR_LEN] = {0}; + + bytes_to_uuid_str(nonce_words, UUID_BINARY_WORD_SZ * sizeof(uint32_t), + nonce_uuid_str, APP_JWT_UUID_V4_STR_LEN); + + char jti_str[JWT_CLAIM_FILED_STR_LENGTH] = {0}; + + int jti_str_length = sprintf(jti_str, "%s.%s", CONFIG_BOARD, nonce_uuid_str); + + jti_str[jti_str_length + 1] = '\0'; + if (cJSON_AddStringToObjectCS(jwt_pay, "jti", jti_str) == NULL) { + err = -ENOMEM; + } + } + + /* Issuer: format: . */ + char iss_str[JWT_CLAIM_FILED_STR_LENGTH] = {0}; + + int iss_str_length = sprintf(iss_str, "%s.%s", CONFIG_BOARD, sub); + + iss_str[iss_str_length + 1] = '\0'; + if (cJSON_AddStringToObjectCS(jwt_pay, "iss", iss_str) == NULL) { + err = -ENOMEM; + } + + /* Subject: format: "user_defined_string" */ + if (sub && (cJSON_AddStringToObjectCS(jwt_pay, "sub", sub) == NULL)) { + err = -ENOMEM; + } + + /* Audience: format: "user_defined_string" */ + if (aud && (cJSON_AddStringToObjectCS(jwt_pay, "aud", aud) == NULL)) { + err = -ENOMEM; + } + + /* Expiration: format: time in seconds as integer + expiration */ + if (exp > 0) { + /* Add expiration (exp) claim */ + if (cJSON_AddNumberToObjectCS(jwt_pay, "exp", exp) == 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) +{ + uint64_t exp_ts_s = 0; + + char *hdr_str, *pay_str; + + char *hdr_b64, *pay_b64; + + char *unsigned_jwt; + + struct timespec tp; + + /* Create the header */ + hdr_str = jwt_header_create(jwt->alg, jwt->key); + 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; + } + + /* Get expiration time stamp */ + if (jwt->validity_s > 0) { + int err = clock_gettime(CLOCK_REALTIME, &tp); + + if (err) { + /* clock_gettime error, use 0 value */ + exp_ts_s = 0; + } else { + exp_ts_s = tp.tv_sec; + } + + exp_ts_s += jwt->validity_s; + } + + /* Create the payload */ + pay_str = jwt_payload_create(jwt->subject, jwt->audience, exp_ts_s); + if (!pay_str) { + LOG_ERR("Failed to create JWT JSON payload"); + return NULL; + } + + /* 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"); + return NULL; + } + + /* Create the base64 URL data to be signed */ + unsigned_jwt = jwt_header_payload_combine(hdr_b64, pay_b64); + + free(hdr_b64); + hdr_b64 = NULL; + + free(pay_b64); + pay_b64 = NULL; + + return unsigned_jwt; +} + +static int jwt_signature_get(const uint32_t user_key_id, const int sec_tag, 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]; + + /* Use Application IAK key for signing the JWT */ + /* ignore the provided key_id and sec_tag */ + (void)sec_tag; + (void)user_key_id; + err = sign_message(IAK_APPLICATION_GEN1, jwt, strlen(jwt), 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(IAK_APPLICATION_GEN1, jwt, strlen(jwt), sig_raw, o_len); + if (err) { + LOG_ERR("Failed to verify message : %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; + + /* Get the size of the final, signed JWT: +1 for */ + /* '.' and null-terminator */ + size_t final_sz = strlen(unsigned_jwt) + 1 + strlen(sig) + 1; + + if (final_sz > jwt_sz) { + /* Provided buffer is too small */ + err = -E2BIG; + } + + /* JWT final form: + * .. + */ + if (err == 0) { + 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) && (jwt->jwt_sz == 0)) { + return -EMSGSIZE; + } + + int err = 0; + + char *unsigned_jwt; + + uint8_t jwt_sig[B64_SIG_SZ]; + + /* 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"); + return -EIO; + } + + /* Get the signature of the unsigned JWT */ + err = jwt_signature_get(jwt->key, jwt->sec_tag, unsigned_jwt, jwt_sig, sizeof(jwt_sig)); + if (err) { + LOG_ERR("Failed to get JWT signature, error: %d", err); + free(unsigned_jwt); + unsigned_jwt = NULL; + return -ENOEXEC; + } + + /* crypto services not required anymore */ + err = crypto_finish(); + if (err) { + LOG_ERR("Failed to sign message : %d", err); + return err; + } + + /* Append the signature, creating the complete JWT */ + err = jwt_signature_append(unsigned_jwt, jwt_sig, jwt->jwt_buf, jwt->jwt_sz); + + free(unsigned_jwt); + unsigned_jwt = NULL; + + 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..bcf458aac1ca --- /dev/null +++ b/samples/jwt/Kconfig @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +menuconfig JWT_SAMPLE + bool "Json Web Token Sample" + select APP_JWT + 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..19128616aec4 --- /dev/null +++ b/samples/jwt/README.rst @@ -0,0 +1,83 @@ +.. _jwt_usage: + +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: + +.. table-from-sample-yaml:: + +Overview +******** + +The sample goes through the following steps to generate a JWT: + +1. Reads the device UUID. + + The returned UUID is compiant 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 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 CONFIG_APP_JWT_PRINT_EXPORTED_PUBKEY_DER allows printing the DER formatted key to debug terminal. + +Building and running +******************** + +west build -p -b nrf9280pdk/nrf9280/cpuapp nrf/samples/jwt --build-dir build_cpuapp_nrf9280pdk_jwt_sample_logging_uart/ -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 custom JWT sample (nrf9280pdk) + user_app_jwt: pubkey_der (91) = 3059301306072a8648ce3d020106082a8648ce3d0301070342000491402ab677f6d49a0595a99a77156aa6e501b8f93efb23eccd41ee69e19c001f6e3da05925f953eff37ca9d7dba10fccfa747e6db28afbdc1f2be3d1867d3be1 + jwt_sample: jwt_generate generated json(517) : eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImQxOWU4YzY3Y2QzOTVkOGZiNTVkZTY5MmM1MmI1NjM3YWVkMWNiNTAzZDg0ZDI3MTExZjI3MmIwOWQwOWQxZTYifQ.eyJpYXQiOjQ4MzgsImp0aSI6Im5yZjkyODBwZGsuNjI3ZWI0ZmEtOGY4Yi1lYTI5LTJmMWQtMGQwOTg0OTg0ZWJlIiwiaXNzIjoibnJmOTI4MHBkay41MzEyNjhjZS0zYzA0LTExZWYtMzMwMS1mYmE5YzBmMGE0NzYiLCJzdWIiOiI1MzEyNjhjZS0zYzA0LTExZWYtMzMwMS1mYmE5YzBmMGE0NzYiLCJhdWQiOiJKU09OIHdlYiB0b2tlbiBmb3IgZGVtb25zdHJhdGlvbiIsImV4cCI6NTQzOH0.mFWn9Nj75KIzAGFdotB_PKXjTGr_L3uQiD9bMuwWxuRJiQ9vBt93gVK1ipukt9GTSAROvp7eBtY9RRqQTUiXbQ + + If an error occurs, the sample prints an error message. + +Dependencies +************ + +This sample uses the following libraries and services: + +* :ref:`app_jwt` +* :ref:`nrf_security` +* :ref:`psa_ssf_crypto_client` +* :ref:`ssf_device_info` diff --git a/samples/jwt/boards/nrf9280pdk_nrf9280_cpuapp.conf b/samples/jwt/boards/nrf9280pdk_nrf9280_cpuapp.conf new file mode 100644 index 000000000000..eafaa6a35c8a --- /dev/null +++ b/samples/jwt/boards/nrf9280pdk_nrf9280_cpuapp.conf @@ -0,0 +1,19 @@ +# +# Copyright (c) 2024 Nordic Semiconductor +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +# PSA configuration +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_PSA_SSF_CRYPTO_CLIENT=y + +# Disable Data Cache +CONFIG_DCACHE=n diff --git a/samples/jwt/boards/nrf9280pdk_nrf9280_cpuapp.overlay b/samples/jwt/boards/nrf9280pdk_nrf9280_cpuapp.overlay new file mode 100644 index 000000000000..ee0bfcad083a --- /dev/null +++ b/samples/jwt/boards/nrf9280pdk_nrf9280_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..49b06cd06281 --- /dev/null +++ b/samples/jwt/prj.conf @@ -0,0 +1,28 @@ +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 +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 PSA crypto from SSF client +CONFIG_PSA_SSF_CRYPTO_CLIENT=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..6b0ee3aa1306 --- /dev/null +++ b/samples/jwt/sample.yaml @@ -0,0 +1,18 @@ +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: + - nrf9280pdk/nrf9280/cpuapp + integration_platforms: + - nrf9280pdk/nrf9280/cpuapp + extra_args: EXTRA_CONF_FILE=uart_logging.conf diff --git a/samples/jwt/src/main.c b/samples/jwt/src/main.c new file mode 100644 index 000000000000..5a5fda7351aa --- /dev/null +++ b/samples/jwt/src/main.c @@ -0,0 +1,73 @@ +/* + * 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_DURATION_S (60 * 10) + +#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, /* Irrelevant since JWT will be signed with IAK */ + .key = 0, /* Irrelevant since JWT will be signed with IAK */ + .alg = JWT_ALG_TYPE_ES256, + .audience = JWT_AUDIENCE_STR, + .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; + } + + 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; + + err = app_jwt_generate(&jwt); + + return err; +} + +int main(void) +{ + LOG_INF("Application custom JWT sample (%s)", CONFIG_BOARD); + + char jwt_str[APP_JWT_STR_MAX_LEN] = {0}; + + int ret = jwt_generate(JWT_DURATION_S, jwt_str, APP_JWT_STR_MAX_LEN); + + if (ret == 0) { + LOG_INF("jwt_generate generated json(%d) : %s", strlen(jwt_str), 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..bdd802ffac2a --- /dev/null +++ b/samples/jwt/uart_logging.conf @@ -0,0 +1,14 @@ +# 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 + +# 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