Skip to content

Commit ef79dff

Browse files
maxd-nordicrlubos
authored andcommitted
tests: add jwt library test
This patch adds a unit test for the JWT library to confirm that the results can be used with nRF Cloud. Signed-off-by: Maximilian Deubel <[email protected]>
1 parent a92f67b commit ef79dff

File tree

6 files changed

+266
-1
lines changed

6 files changed

+266
-1
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@
880880
/tests/lib/sms/ @nrfconnect/ncs-modem-tre
881881
/tests/lib/tone/ @nrfconnect/ncs-audio
882882
/tests/lib/uicc_lwm2m/ @stig-bjorlykke
883+
/tests/lib/app_jwt/ @nrfconnect/ncs-modem
883884
/tests/mocks/nrf_rpc/ @nrfconnect/ncs-protocols-serialization
884885
/tests/modules/lib/zcbor/ @oyvindronningstad
885886
/tests/modules/mcuboot/direct_xip/ @nrfconnect/ncs-pluto

lib/app_jwt/app_jwt.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
55
*/
66

7+
#ifdef _POSIX_C_SOURCE
8+
#undef _POSIX_C_SOURCE
9+
#endif
10+
/* Define _POSIX_C_SOURCE before including <string.h> in order to use `strtok_r`. */
11+
#define _POSIX_C_SOURCE 200809L
12+
713
#include <string.h>
814
#include <stdlib.h>
915
#include <stdio.h>
@@ -14,13 +20,17 @@
1420
#include <date_time.h>
1521
#include <psa/crypto.h>
1622
#include <psa/crypto_extra.h>
23+
24+
#if defined(CONFIG_BOARD_NATIVE_SIM)
25+
#define IAK_APPLICATION_GEN1 0x41020100
26+
#else
1727
#include <psa/nrf_platform_key_ids.h>
28+
#endif
1829

1930
#include <cJSON.h>
2031

2132
#include <app_jwt.h>
2233

23-
#include <zephyr/posix/time.h>
2434
#include <zephyr/sys/base64.h>
2535
#include <zephyr/sys/byteorder.h>
2636

tests/lib/app_jwt/CMakeLists.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# Copyright (c) 2025 Nordic Semiconductor
3+
#
4+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
5+
#
6+
7+
cmake_minimum_required(VERSION 3.20.0)
8+
9+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
10+
project(app_jwt_test)
11+
12+
13+
FILE(GLOB app_sources src/*.c)
14+
target_sources(app PRIVATE ${app_sources})
15+
16+
target_sources(app
17+
PRIVATE
18+
${ZEPHYR_NRF_MODULE_DIR}/lib/app_jwt/app_jwt.c
19+
)
20+
21+
target_include_directories(app
22+
PRIVATE
23+
${ZEPHYR_NRF_MODULE_DIR}/subsys/net/lib/nrf_cloud/include/
24+
)
25+
26+
target_compile_options(app
27+
PRIVATE
28+
-DCONFIG_APP_JWT_LOG_LEVEL=4
29+
-DCONFIG_APP_JWT_DEFAULT_TIMESTAMP=1735682400
30+
)

tests/lib/app_jwt/prj.conf

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#
2+
# Copyright (c) 2025 Nordic Semiconductor
3+
#
4+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
5+
#
6+
CONFIG_ZTEST=y
7+
8+
CONFIG_MAIN_STACK_SIZE=4096
9+
CONFIG_CJSON_LIB=y
10+
CONFIG_NEWLIB_LIBC=y
11+
CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y
12+
CONFIG_HEAP_MEM_POOL_SIZE=4096
13+
14+
CONFIG_MBEDTLS=y
15+
CONFIG_MBEDTLS_BUILTIN=y
16+
CONFIG_MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG=y
17+
CONFIG_BASE64=y
18+
19+
CONFIG_ENTROPY_GENERATOR=y
20+
CONFIG_TEST_RANDOM_GENERATOR=y
21+
22+
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
23+
24+
CONFIG_PSA_WANT_ALG_ECDSA=y
25+
CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT=y
26+
CONFIG_PSA_WANT_ECC_SECP_R1_256=y
27+
CONFIG_PSA_WANT_ALG_SHA_256=y

tests/lib/app_jwt/src/main.c

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright (c) 2025 Nordic Semiconductor ASA
3+
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
*/
5+
6+
#include <string.h>
7+
#include <stdint.h>
8+
#include <zephyr/kernel.h>
9+
#include <zephyr/ztest.h>
10+
#include <zephyr/logging/log.h>
11+
#include <zephyr/sys/base64.h>
12+
13+
#include <psa/crypto.h>
14+
15+
#include <app_jwt.h>
16+
17+
LOG_MODULE_REGISTER(test_main, LOG_LEVEL_DBG);
18+
19+
int date_time_now(int64_t *timestamp)
20+
{
21+
*timestamp = 1739881289000;
22+
return 0;
23+
}
24+
25+
static psa_key_id_t kid;
26+
27+
#define EC_PUB_KEY_RAW_LEN 65
28+
#define EC_PUB_KEY_HEADER_LEN 26
29+
#define EC_PUB_KEY_CONTENT_LEN (EC_PUB_KEY_RAW_LEN + EC_PUB_KEY_HEADER_LEN)
30+
#define PEM_LINE_LENGTH 64
31+
32+
#define EXPECTED_JWT_MIN \
33+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9." \
34+
"eyJzdWIiOiJucmYtMzU4Mjk5ODQwMTIzNDU2IiwiZXhwIjoxNzM5ODgxODg5fQ.cYGNV8-" \
35+
"DxTKyV3MlsSALKlva3Aarx3u0Owj8cxAH5tm3WRPLC8-xZtTW0djJp-j3V8sU1Mt_xjm3OIzMx4RNcw"
36+
37+
#define EXPECTED_JWT_MIN_UUID \
38+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9." \
39+
"eyJzdWIiOiI2YzBmMTIyNi1mNTA2LTExZWYtYTViYi05Mzc2M2I0YmRmMTEiLCJleHAiOjE3Mzk4ODE4ODl9." \
40+
"oIDg_ptVr7EP0Bg4qxmj5jh53d30FaBtzHOdeQJXyakz9_WQALqPXIrPl4immMvYhgcQD12KWxFiHh4YPjhBaw"
41+
42+
#define EXPECTED_JWT_NRF91 \
43+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjJhNTNkMjBmODcxYjFkZWVhYjUzYTFiZmNlMDJhZTVk" \
44+
"NWVmYzEyZjFlMGY0YjVjZTY2YzRhMWMyOGRhZDExMmMifQ." \
45+
"eyJpYXQiOjE3Mzk4ODEyODksImp0aSI6Im5SRjkxNjAuNTY0YWIzNTYtZjUwNS0xMWVmLWIyODctYjc2OTAyMWQ1" \
46+
"ZmQ2LjlmZjE3NTI0OWFmZTQ4OTQxMiIsImlzcyI6Im5SRjkxNjAuNTY0YWIzNTYtZjUwNS0xMWVmLWIyODctYjc2" \
47+
"OTAyMWQ1ZmQ2Iiwic3ViIjoibnJmLTM1ODI5OTg0MDEyMzQ1NiIsImV4cCI6MTczOTg4MTg4OX0." \
48+
"i3xPhJ1dQw2bb_2OgaAZ8w-IP_EpLiDgt97agpAdIsWDSdzCWd2iwwCpV937mAyqXWe-eoc-VjnBlWfXk5nXqQ"
49+
50+
#define EC_PRV_KEY \
51+
"-----BEGIN PRIVATE KEY-----\n" \
52+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyw3wVT7Uo05WHT4t\n" \
53+
"w8AgAo6eXMm0Pj8jqnAoABVtflmhRANCAAQ2ofrLZw+Fve9PzQ4z9COJE8IFtFnH\n" \
54+
"V0ey+tw/yXcqWROTXMauO4bJ4TC7CrmpM8IBXkKQm/gEHdQQCZXYAn68\n" \
55+
"-----END PRIVATE KEY-----\n"
56+
57+
#define EC_PUB_KEY \
58+
"-----BEGIN PUBLIC KEY-----\n" \
59+
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENqH6y2cPhb3vT80OM/QjiRPCBbRZ\n" \
60+
"x1dHsvrcP8l3KlkTk1zGrjuGyeEwuwq5qTPCAV5CkJv4BB3UEAmV2AJ+vA==\n" \
61+
"-----END PUBLIC KEY-----\n"
62+
63+
static int import_ecdsa_keypair(uint32_t *user_keypair_id)
64+
{
65+
int err = 0;
66+
const uint8_t priv_key[] = {0xcb, 0x0d, 0xf0, 0x55, 0x3e, 0xd4, 0xa3, 0x4e,
67+
0x56, 0x1d, 0x3e, 0x2d, 0xc3, 0xc0, 0x20, 0x02,
68+
0x8e, 0x9e, 0x5c, 0xc9, 0xb4, 0x3e, 0x3f, 0x23,
69+
0xaa, 0x70, 0x28, 0x00, 0x15, 0x6d, 0x7e, 0x59};
70+
71+
if (user_keypair_id != NULL) {
72+
psa_key_attributes_t key_attributes = PSA_KEY_ATTRIBUTES_INIT;
73+
74+
/* Initialize PSA Crypto */
75+
err = psa_crypto_init();
76+
if (err != PSA_SUCCESS) {
77+
printk("psa_crypto_init failed! (Error: %d)\n", err);
78+
return -1;
79+
}
80+
81+
psa_set_key_usage_flags(&key_attributes,
82+
PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE);
83+
psa_set_key_lifetime(&key_attributes, PSA_KEY_LIFETIME_VOLATILE);
84+
psa_set_key_algorithm(&key_attributes, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
85+
psa_set_key_type(&key_attributes,
86+
PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
87+
psa_set_key_bits(&key_attributes, 256);
88+
89+
/* Import key into PSA */
90+
err = psa_import_key(&key_attributes, priv_key, sizeof(priv_key), &kid);
91+
if (err != PSA_SUCCESS) {
92+
LOG_ERR("psa_import_key failed! (Error: %d)", err);
93+
return err;
94+
}
95+
96+
LOG_INF("Public key in PEM format:");
97+
printk(EC_PUB_KEY "\n");
98+
}
99+
return err;
100+
}
101+
102+
/* Construct minimal JWT that still contains enough information to be accepted by nRF Cloud. */
103+
ZTEST(app_jwt, test_minimal)
104+
{
105+
uint8_t buf[APP_JWT_STR_MAX_LEN];
106+
int err = 0;
107+
const char *expected_jwt = EXPECTED_JWT_MIN;
108+
109+
struct app_jwt_data jwt = {
110+
.sec_tag = kid,
111+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
112+
.alg = JWT_ALG_TYPE_ES256,
113+
.add_keyid_to_header = false,
114+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
115+
.jwt_buf = buf,
116+
.jwt_sz = sizeof(buf),
117+
.subject = "nrf-358299840123456",
118+
};
119+
120+
err = app_jwt_generate(&jwt);
121+
zassert_equal(err, 0, "err: %d", err);
122+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
123+
LOG_INF("minimal JWT: %s", jwt.jwt_buf);
124+
}
125+
126+
/* Construct minimal JWT using a UUID as subject. */
127+
ZTEST(app_jwt, test_minimal_uuid)
128+
{
129+
uint8_t buf[APP_JWT_STR_MAX_LEN];
130+
int err = 0;
131+
const char *expected_jwt = EXPECTED_JWT_MIN_UUID;
132+
133+
struct app_jwt_data jwt = {
134+
.sec_tag = kid,
135+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
136+
.alg = JWT_ALG_TYPE_ES256,
137+
.add_keyid_to_header = false,
138+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
139+
.jwt_buf = buf,
140+
.jwt_sz = sizeof(buf),
141+
.subject = "6c0f1226-f506-11ef-a5bb-93763b4bdf11",
142+
};
143+
144+
err = app_jwt_generate(&jwt);
145+
zassert_equal(err, 0, "err: %d", err);
146+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
147+
LOG_INF("minimal JWT using UUID: %s", jwt.jwt_buf);
148+
}
149+
150+
/* Construct JWT similar to how the nRF91 modem makes them. */
151+
ZTEST(app_jwt, test_nrf91_like)
152+
{
153+
uint8_t buf[APP_JWT_STR_MAX_LEN];
154+
int err = 0;
155+
const char *expected_jwt = EXPECTED_JWT_NRF91;
156+
157+
struct app_jwt_data jwt = {
158+
.sec_tag = kid,
159+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
160+
.alg = JWT_ALG_TYPE_ES256,
161+
.add_keyid_to_header = true,
162+
.add_timestamp = true,
163+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
164+
.jwt_buf = buf,
165+
.jwt_sz = sizeof(buf),
166+
.issuer = "nRF9160.564ab356-f505-11ef-b287-b769021d5fd6",
167+
.json_token_id = "nRF9160.564ab356-f505-11ef-b287-b769021d5fd6.9ff175249afe489412",
168+
.subject = "nrf-358299840123456",
169+
};
170+
171+
err = app_jwt_generate(&jwt);
172+
zassert_equal(err, 0, "err: %d", err);
173+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
174+
LOG_INF("nrf91-like JWT: %s", jwt.jwt_buf);
175+
}
176+
177+
static void *setup(void)
178+
{
179+
int err = import_ecdsa_keypair(&kid);
180+
181+
zassert_equal(err, 0, "err: %d", err);
182+
183+
return NULL;
184+
}
185+
186+
ZTEST_SUITE(app_jwt, NULL, setup, NULL, NULL, NULL);

tests/lib/app_jwt/testcase.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
tests:
2+
net.lib.app_jwt:
3+
sysbuild: true
4+
platform_allow:
5+
- native_sim
6+
integration_platforms:
7+
- native_sim
8+
tags:
9+
- fota
10+
- sysbuild
11+
- ci_tests_lib_app_jwt

0 commit comments

Comments
 (0)