Skip to content

Commit 80618f1

Browse files
committed
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 <maximilian.deubel@nordicsemi.no>
1 parent 831d14c commit 80618f1

File tree

5 files changed

+254
-0
lines changed

5 files changed

+254
-0
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,7 @@
878878
/tests/lib/sms/ @nrfconnect/ncs-modem-tre
879879
/tests/lib/tone/ @nrfconnect/ncs-audio
880880
/tests/lib/uicc_lwm2m/ @stig-bjorlykke
881+
/tests/lib/app_jwt/ @nrfconnect/ncs-modem @ayla-nordicsemi @maxd-nordic
881882
/tests/mocks/nrf_rpc/ @nrfconnect/ncs-protocols-serialization
882883
/tests/modules/lib/zcbor/ @oyvindronningstad
883884
/tests/modules/mcuboot/direct_xip/ @nrfconnect/ncs-pluto

tests/lib/app_jwt/CMakeLists.txt

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
${ZEPHYR_NRF_MODULE_DIR}/../modules/crypto/tinycrypt/lib/include/
25+
)
26+
27+
target_compile_options(app
28+
PRIVATE
29+
-DCONFIG_APP_JWT_LOG_LEVEL=4
30+
-DCONFIG_APP_JWT_DEFAULT_TIMESTAMP=1735682400
31+
)

tests/lib/app_jwt/prj.conf

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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_PSA_CRYPTO_C=y
16+
CONFIG_MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG=y
17+
CONFIG_PSA_CRYPTO_ENABLE_ALL=y
18+
CONFIG_BASE64=y
19+
20+
CONFIG_ENTROPY_GENERATOR=y
21+
CONFIG_TEST_RANDOM_GENERATOR=y

tests/lib/app_jwt/src/main.c

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
static int generate_ecdsa_keypair(uint32_t *user_keypair_id)
33+
{
34+
int err = 0;
35+
uint8_t pub_key[EC_PUB_KEY_CONTENT_LEN] = {
36+
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
37+
0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00};
38+
char pub_key_base64[125];
39+
size_t olen;
40+
41+
if (user_keypair_id != NULL) {
42+
psa_key_attributes_t key_attributes = PSA_KEY_ATTRIBUTES_INIT;
43+
44+
/* Initialize PSA Crypto */
45+
err = psa_crypto_init();
46+
if (err != PSA_SUCCESS) {
47+
printk("psa_crypto_init failed! (Error: %d)\n", err);
48+
return -1;
49+
}
50+
51+
/* Configure the key attributes */
52+
psa_set_key_usage_flags(&key_attributes,
53+
PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE);
54+
psa_set_key_lifetime(&key_attributes, PSA_KEY_LIFETIME_VOLATILE);
55+
psa_set_key_algorithm(&key_attributes, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
56+
psa_set_key_type(&key_attributes,
57+
PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
58+
psa_set_key_bits(&key_attributes, 256);
59+
60+
/**
61+
* Generate a random keypair. The keypair is not exposed to the application,
62+
* we can use it to sign messages.
63+
*/
64+
err = psa_generate_key(&key_attributes, user_keypair_id);
65+
if (err != PSA_SUCCESS) {
66+
printk("psa_generate_key failed! (Error: %d)\n", err);
67+
return err;
68+
}
69+
70+
/* Export pub key in raw format */
71+
err = psa_export_public_key(*user_keypair_id, pub_key + EC_PUB_KEY_HEADER_LEN,
72+
EC_PUB_KEY_RAW_LEN, &olen);
73+
74+
if ((err != PSA_SUCCESS) || (olen != EC_PUB_KEY_RAW_LEN)) {
75+
LOG_INF("psa_export_public_key failed! (Error: %d). Exiting!", err);
76+
return err;
77+
}
78+
79+
/* base64-encode and print pub key in PEM format */
80+
err = base64_encode(pub_key_base64, sizeof(pub_key_base64), &olen, pub_key,
81+
sizeof(pub_key));
82+
if (err) {
83+
LOG_INF("base64_encode failed, error: %d", err);
84+
return err;
85+
}
86+
LOG_INF("Public key in PEM format:");
87+
printk("-----BEGIN PUBLIC KEY-----\n");
88+
for (size_t i = 0; i < sizeof(pub_key_base64); i += PEM_LINE_LENGTH) {
89+
printk("%.*s\n", PEM_LINE_LENGTH, pub_key_base64 + i);
90+
}
91+
printk("-----END PUBLIC KEY-----\n\n");
92+
}
93+
return err;
94+
}
95+
96+
ZTEST(jwt_app_test, minimal)
97+
{
98+
uint8_t buf[APP_JWT_STR_MAX_LEN];
99+
int err = 0;
100+
const char *expected_jwt =
101+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9." \
102+
"eyJzdWIiOiJucmYtMzU4Mjk5ODQwMTIzNDU2IiwiZXhwIjoxNzM5ODgxODg5fQ.QrHn_" \
103+
"M9POqkjKIXl8s0pfwiN7WFSsYjuliZEGvRnum3KzSwfRZTChd_o1TyAZC4NYhR4W2D2Rum4oFFf2gjRew";
104+
105+
struct app_jwt_data jwt = {
106+
.sec_tag = kid,
107+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
108+
.alg = JWT_ALG_TYPE_ES256,
109+
.add_keyid_to_header = false,
110+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
111+
.jwt_buf = buf,
112+
.jwt_sz = sizeof(buf),
113+
.subject = "nrf-358299840123456",
114+
};
115+
116+
err = app_jwt_generate(&jwt);
117+
zassert_equal(err, 0, "err: %d", err);
118+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
119+
LOG_INF("minimal JWT: %s", jwt.jwt_buf);
120+
}
121+
122+
ZTEST(jwt_app_test, minimal_uuid)
123+
{
124+
uint8_t buf[APP_JWT_STR_MAX_LEN];
125+
int err = 0;
126+
const char *expected_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9." \
127+
"eyJzdWIiOiI2YzBmMTIyNi1mNTA2LTExZWYtYTViYi05Mzc2M2I0YmRmMTEiL" \
128+
"CJleHAiOjE3Mzk4ODE4ODl9.Yjaj765W363ImhelkCwzucwWU6R90PA-fTrQR" \
129+
"YDT-vp7vioZ_132ukuY6JvfCi7naeC8ZkOYdK6wqnjFbgVqHg";
130+
131+
struct app_jwt_data jwt = {
132+
.sec_tag = kid,
133+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
134+
.alg = JWT_ALG_TYPE_ES256,
135+
.add_keyid_to_header = false,
136+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
137+
.jwt_buf = buf,
138+
.jwt_sz = sizeof(buf),
139+
.subject = "6c0f1226-f506-11ef-a5bb-93763b4bdf11",
140+
};
141+
142+
err = app_jwt_generate(&jwt);
143+
zassert_equal(err, 0, "err: %d", err);
144+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
145+
LOG_INF("minimal JWT using UUID: %s", jwt.jwt_buf);
146+
}
147+
148+
ZTEST(jwt_app_test, nrf91_like)
149+
{
150+
uint8_t buf[APP_JWT_STR_MAX_LEN];
151+
int err = 0;
152+
const char *expected_jwt =
153+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjU1NTFhYzBjOGIwYWExMjNhOGI4MTExNTJk" \
154+
"YjE0OWRiNzFlZDA5N2MyZWRhNzM1YWJjNzYxNjkwMGEzNDJhNTAifQ." \
155+
"eyJpYXQiOjE3Mzk4ODEyODksImp0aSI6Im5SRjkxNjAuNTY0YWIzNTYtZjUwNS0xMWVmLWIyODctYjc2" \
156+
"OTAyMWQ1ZmQ2LjlmZjE3NTI0OWFmZTQ4OTQxMiIsImlzcyI6Im5SRjkxNjAuNTY0YWIzNTYtZjUwNS0x" \
157+
"MWVmLWIyODctYjc2OTAyMWQ1ZmQ2Iiwic3ViIjoibnJmLTM1ODI5OTg0MDEyMzQ1NiIsImV4cCI6MTcz" \
158+
"OTg4MTg4OX0.hbyKcSdJ6A08vScTD2nufWufZD3GuuhEj9kz0yKKZuknDQFMXX25NYsuLO2ibDCs06u2" \
159+
"n-Hql3CfF0OM4X94sQ";
160+
161+
struct app_jwt_data jwt = {
162+
.sec_tag = kid,
163+
.key_type = JWT_KEY_TYPE_CLIENT_PRIV,
164+
.alg = JWT_ALG_TYPE_ES256,
165+
.add_keyid_to_header = true,
166+
.add_timestamp = true,
167+
.validity_s = APP_JWT_VALID_TIME_S_DEF,
168+
.jwt_buf = buf,
169+
.jwt_sz = sizeof(buf),
170+
.issuer = "nRF9160.564ab356-f505-11ef-b287-b769021d5fd6",
171+
.json_token_id = "nRF9160.564ab356-f505-11ef-b287-b769021d5fd6.9ff175249afe489412",
172+
.subject = "nrf-358299840123456",
173+
};
174+
175+
err = app_jwt_generate(&jwt);
176+
zassert_equal(err, 0, "err: %d", err);
177+
zassert_equal(strcmp(jwt.jwt_buf, expected_jwt), 0, "unexpected JWT: %s", jwt.jwt_buf);
178+
LOG_INF("nrf91-like JWT: %s", jwt.jwt_buf);
179+
}
180+
181+
static void *setup(void)
182+
{
183+
int err = generate_ecdsa_keypair(&kid);
184+
185+
zassert_equal(err, 0, "err: %d", err);
186+
187+
return NULL;
188+
}
189+
190+
ZTEST_SUITE(jwt_app_test, NULL, setup, NULL, NULL, NULL);

tests/lib/app_jwt/testcase.yaml

+11
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_subsys_net

0 commit comments

Comments
 (0)