From f8b838d404fd8e68a99ad41cc212d669c07c891b Mon Sep 17 00:00:00 2001 From: David Brown Date: Thu, 20 Sep 2018 12:15:20 -0700 Subject: [PATCH] jwt: Add JSON web token library This patch adds a JSON web token library that adds the capability to sign JSON tokens. This was located in subsys due to the dependency on MBEDTLS, which resides in /ext. Signed-off-by: David Brown Signed-off-by: Andy Gross --- doc/reference/misc/index.rst | 12 + include/zephyr/jwt.h | 96 ++++++ subsys/CMakeLists.txt | 1 + subsys/Kconfig | 2 + subsys/jwt/CMakeLists.txt | 4 + subsys/jwt/Kconfig | 32 ++ subsys/jwt/jwt.c | 320 ++++++++++++++++++ tests/subsys/jwt/CMakeLists.txt | 8 + tests/subsys/jwt/jwt-test-cert.pem | 18 + tests/subsys/jwt/jwt-test-private.der | Bin 0 -> 1216 bytes tests/subsys/jwt/jwt-test-private.pem | 28 ++ tests/subsys/jwt/prj.conf | 24 ++ tests/subsys/jwt/src/jwt-test-cert.c | 148 ++++++++ tests/subsys/jwt/src/jwt-test-private.c | 112 ++++++ tests/subsys/jwt/src/main.c | 67 ++++ tests/subsys/jwt/src/tls_config/user-tls.conf | 5 + tests/subsys/jwt/testcase.yaml | 5 + 17 files changed, 882 insertions(+) create mode 100644 include/zephyr/jwt.h create mode 100644 subsys/jwt/CMakeLists.txt create mode 100644 subsys/jwt/Kconfig create mode 100644 subsys/jwt/jwt.c create mode 100644 tests/subsys/jwt/CMakeLists.txt create mode 100644 tests/subsys/jwt/jwt-test-cert.pem create mode 100644 tests/subsys/jwt/jwt-test-private.der create mode 100644 tests/subsys/jwt/jwt-test-private.pem create mode 100644 tests/subsys/jwt/prj.conf create mode 100644 tests/subsys/jwt/src/jwt-test-cert.c create mode 100644 tests/subsys/jwt/src/jwt-test-private.c create mode 100644 tests/subsys/jwt/src/main.c create mode 100644 tests/subsys/jwt/src/tls_config/user-tls.conf create mode 100644 tests/subsys/jwt/testcase.yaml diff --git a/doc/reference/misc/index.rst b/doc/reference/misc/index.rst index 25f639642b2..21f931d2234 100644 --- a/doc/reference/misc/index.rst +++ b/doc/reference/misc/index.rst @@ -25,3 +25,15 @@ JSON .. doxygengroup:: json :project: Zephyr + +JWT +=== + +JSON Web Tokens (JWT) are an open, industry standard [RFC +7519](https://tools.ietf.org/html/rfc7519) method for representing +claims securely between two parties. Although JWT is fairly flexible, +this API is limited to creating the simplistic tokens needed to +authenticate with the Google Core IoT infrastructure. + +.. doxygengroup:: jwt + :project: Zephyr diff --git a/include/zephyr/jwt.h b/include/zephyr/jwt.h new file mode 100644 index 00000000000..db7712569e6 --- /dev/null +++ b/include/zephyr/jwt.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018 Linaro Ltd + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_JWT_H_ +#define ZEPHYR_INCLUDE_JWT_H_ + +#include +#include + +/** + * @brief JSON Web Token (JWT) + * @defgroup jwt JSON Web Token (JWT) + * @ingroup structured_data + * @{ + */ + +/** + * @brief JWT data tracking. + * + * JSON Web Tokens contain several sections, each encoded in base-64. + * This structure tracks the token as it is being built, including + * limits on the amount of available space. It should be initialized + * with jwt_init(). + */ +struct jwt_builder { + /** The base of the buffer we are writing to. */ + char *base; + + /** The place in this buffer where we are currently writing. + */ + char *buf; + + /** The length remaining to write. */ + size_t len; + + /** + * Flag that is set if we try to write past the end of the + * buffer. If set, the token is not valid. + */ + bool overflowed; + + /* Pending bytes yet to be converted to base64. */ + unsigned char wip[3]; + + /* Number of pending bytes. */ + int pending; +}; + +/** + * @brief Initialize the JWT builder. + * + * Initialize the given JWT builder for the creation of a fresh token. + * The buffer size should at least be as long as JWT_BUILDER_MAX_SIZE + * returns. + * + * @param builder The builder to initialize. + * @param buffer The buffer to write the token to. + * @param buffer_size The size of this buffer. The token will be NULL + * terminated, which needs to be allowed for in this size. + * + * @retval 0 Success + * @retval -ENOSPC Buffer is insufficient to initialize + */ +int jwt_init_builder(struct jwt_builder *builder, + char *buffer, + size_t buffer_size); + +/** + * @brief add JWT primary payload. + */ +int jwt_add_payload(struct jwt_builder *builder, + s32_t exp, + s32_t iat, + const char *aud); + +/** + * @brief Sign the JWT token. + */ +int jwt_sign(struct jwt_builder *builder, + const char *der_key, + size_t der_key_len); + + +static inline size_t jwt_payload_len(struct jwt_builder *builder) +{ + return (builder->buf - builder->base); +} + +/** + * @} + */ + +#endif diff --git a/subsys/CMakeLists.txt b/subsys/CMakeLists.txt index c4af21ff33a..3696f5adf50 100644 --- a/subsys/CMakeLists.txt +++ b/subsys/CMakeLists.txt @@ -17,3 +17,4 @@ add_subdirectory_ifdef(CONFIG_SETTINGS settings) add_subdirectory(fb) add_subdirectory(power) add_subdirectory(stats) +add_subdirectory_if_kconfig(jwt) diff --git a/subsys/Kconfig b/subsys/Kconfig index d51b582b232..df133c082ee 100644 --- a/subsys/Kconfig +++ b/subsys/Kconfig @@ -40,3 +40,5 @@ source "subsys/app_memory/Kconfig" source "subsys/power/Kconfig" source "subsys/fb/Kconfig" + +source "subsys/jwt/Kconfig" diff --git a/subsys/jwt/CMakeLists.txt b/subsys/jwt/CMakeLists.txt new file mode 100644 index 00000000000..e4893b6ff3b --- /dev/null +++ b/subsys/jwt/CMakeLists.txt @@ -0,0 +1,4 @@ +zephyr_link_interface_ifdef(CONFIG_MBEDTLS mbedTLS) +zephyr_library() +zephyr_library_sources(jwt.c) +zephyr_library_link_libraries_ifdef(CONFIG_MBEDTLS mbedTLS) diff --git a/subsys/jwt/Kconfig b/subsys/jwt/Kconfig new file mode 100644 index 00000000000..a71e64358f8 --- /dev/null +++ b/subsys/jwt/Kconfig @@ -0,0 +1,32 @@ +# +# Copyright (c) 2018 Linaro +# +# SPDX-License-Identifier: Apache-2.0 +# +menuconfig JWT + bool "Enable JSON Web Token generation" + select JSON_LIBRARY + help + Enable creation of JWT tokens + +if JWT +choice + prompt "JWT signature algorithm" + default JWT_SIGN_RSA + help + Select which algorithm to use for signing JWT tokens. + +config JWT_SIGN_RSA + bool "Use RSA signature (RS-256)" + select MBEDTLS + +config JWT_SIGN_ECDSA + bool "Use ECDSA signature (ES-256)" + select TINYCRYPT + select TINYCRYPT_SHA256 + select TINYCRYPT_ECC_DSA + select TINYCRYPT_CTR_PRNG + select TINYCRYPT_AES + +endchoice +endif diff --git a/subsys/jwt/jwt.c b/subsys/jwt/jwt.c new file mode 100644 index 00000000000..f6c7ea4b8e5 --- /dev/null +++ b/subsys/jwt/jwt.c @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 Linaro Ltd + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +#ifdef CONFIG_JWT_SIGN_RSA +#include +#include +#include +#endif + +#ifdef CONFIG_JWT_SIGN_ECDSA +#include +#include +#include +#include + +#include +#endif + +/* + * Base-64 encoding is typically done by lookup into a 64-byte static + * array. As an experiment, lets look at both code size and time for + * one that does the character encoding computationally. Like the + * array version, this doesn't do bounds checking, and assumes the + * passed value has been masked. + * + * On Cortex-M, this function is 34 bytes of code, which is only a + * little more than half of the size of the lookup table. + */ +#if 1 +static int base64_char(int value) +{ + if (value < 26) { + return value + 'A'; + } else if (value < 52) { + return value + 'a' - 26; + } else if (value < 62) { + return value + '0' - 52; + } else if (value == 62) { + return '-'; + } else { + return '_'; + } +} +#else +static const char b64_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +static inline int base64_char(int value) +{ + return b64_table[value]; +} +#endif + +/* + * Add a single character to the jwt buffer. Detects overflow, and + * always keeps the buffer null terminated. + */ +static void base64_outch(struct jwt_builder *st, char ch) +{ + if (st->overflowed) { + return; + } + + if (st->len < 2) { + st->overflowed = true; + return; + } + + *st->buf++ = ch; + st->len--; + *st->buf = 0; +} + +/* + * Flush any pending base64 character data out. If we have all three + * bytes are present, this will generate 4 characters, otherwise it + * may generate fewer. + */ +static void base64_flush(struct jwt_builder *st) +{ + if (st->pending < 1) { + return; + } + + base64_outch(st, base64_char(st->wip[0] >> 2)); + base64_outch(st, base64_char(((st->wip[0] & 0x03) << 4) | + (st->wip[1] >> 4))); + + if (st->pending >= 2) { + base64_outch(st, base64_char(((st->wip[1] & 0x0f) << 2) | + (st->wip[2] >> 6))); + } + if (st->pending >= 3) { + base64_outch(st, base64_char(st->wip[2] & 0x3f)); + } + + st->pending = 0; + memset(st->wip, 0, 3); +} + +static void base64_addbyte(struct jwt_builder *st, uint8_t byte) +{ + st->wip[st->pending++] = byte; + if (st->pending == 3) { + base64_flush(st); + } +} + +static int base64_append_bytes(const char *bytes, size_t len, + void *data) +{ + struct jwt_builder *st = data; + + while (len-- > 0) { + base64_addbyte(st, *bytes++); + } + + return 0; +} + +struct jwt_header { + char *typ; + char *alg; +}; + +static struct json_obj_descr jwt_header_desc[] = { + JSON_OBJ_DESCR_PRIM(struct jwt_header, alg, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct jwt_header, typ, JSON_TOK_STRING), +}; + +struct jwt_payload { + s32_t exp; + s32_t iat; + const char *aud; +}; + +static struct json_obj_descr jwt_payload_desc[] = { + JSON_OBJ_DESCR_PRIM(struct jwt_payload, aud, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct jwt_payload, exp, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM(struct jwt_payload, iat, JSON_TOK_NUMBER), +}; + +/* + * Add the JWT header to the buffer. + */ +static void jwt_add_header(struct jwt_builder *builder) +{ + static const struct jwt_header head = { + .typ = "JWT", +#ifdef CONFIG_JWT_SIGN_RSA + .alg = "RS256", +#endif +#ifdef CONFIG_JWT_SIGN_ECDSA + .alg = "ES256", +#endif + }; + + int res = json_obj_encode(jwt_header_desc, ARRAY_SIZE(jwt_header_desc), + &head, base64_append_bytes, builder); + if (res != 0) { + /* Log an error here. */ + return; + } + base64_flush(builder); +} + +int jwt_add_payload(struct jwt_builder *builder, + s32_t exp, + s32_t iat, + const char *aud) +{ + struct jwt_payload payload = { + .exp = exp, + .iat = iat, + .aud = aud, + }; + + base64_outch(builder, '.'); + int res = json_obj_encode(jwt_payload_desc, + ARRAY_SIZE(jwt_payload_desc), + &payload, base64_append_bytes, builder); + + base64_flush(builder); + return res; +} + +#ifdef CONFIG_JWT_SIGN_RSA +int jwt_sign(struct jwt_builder *builder, + const char *der_key, + size_t der_key_len) +{ + int res; + mbedtls_pk_context ctx; + + mbedtls_pk_init(&ctx); + + res = mbedtls_pk_parse_key(&ctx, der_key, der_key_len, + NULL, 0); + if (res != 0) { + return res; + } + + u8_t hash[32], sig[256]; + size_t sig_len = sizeof(sig); + + /* + * The '0' indicates to mbedtls to do a SHA256, instead of + * 224. + */ + mbedtls_sha256(builder->base, builder->buf - builder->base, + hash, 0); + + res = mbedtls_pk_sign(&ctx, MBEDTLS_MD_SHA256, + hash, sizeof(hash), + sig, &sig_len, + NULL, NULL); + if (res != 0) { + return res; + } + + base64_outch(builder, '.'); + base64_append_bytes(sig, sig_len, builder); + base64_flush(builder); + + return builder->overflowed ? -ENOMEM : 0; +} +#endif + +#ifdef CONFIG_JWT_SIGN_ECDSA +static TCCtrPrng_t prng_state; +static bool prng_init; + +static const char personalize[] = "zephyr:drivers/jwt/jwt.c"; + +static int setup_prng(void) +{ + if (prng_init) { + return 0; + } + prng_init = true; + + u8_t entropy[TC_AES_KEY_SIZE + TC_AES_BLOCK_SIZE]; + + for (int i = 0; i < sizeof(entropy); i += sizeof(u32_t)) { + u32_t rv = sys_rand32_get(); + + memcpy(entropy + i, &rv, sizeof(uint32_t)); + } + + int res = tc_ctr_prng_init(&prng_state, + (const uint8_t *) &entropy, sizeof(entropy), + personalize, + sizeof(personalize)); + + return res == TC_CRYPTO_SUCCESS ? 0 : -EINVAL; +} + +int default_CSPRNG(u8_t *dest, unsigned int size) +{ + int res = tc_ctr_prng_generate(&prng_state, NULL, 0, dest, size); + return res; +} + +int jwt_sign(struct jwt_builder *builder, + const char *der_key, + size_t der_key_len) +{ + struct tc_sha256_state_struct ctx; + u8_t hash[32], sig[64]; + int res; + + tc_sha256_init(&ctx); + tc_sha256_update(&ctx, builder->base, builder->buf - builder->base); + tc_sha256_final(hash, &ctx); + + res = setup_prng(); + + if (res != 0) { + return res; + } + uECC_set_rng(&default_CSPRNG); + + /* Note that tinycrypt only supports P-256. */ + res = uECC_sign(der_key, hash, sizeof(hash), + sig, &curve_secp256r1); + if (res != TC_CRYPTO_SUCCESS) { + return -EINVAL; + } + + base64_outch(builder, '.'); + base64_append_bytes(sig, sizeof(sig), builder); + base64_flush(builder); + + return 0; +} +#endif + +int jwt_init_builder(struct jwt_builder *builder, + char *buffer, + size_t buffer_size) +{ + builder->base = buffer; + builder->buf = buffer; + builder->len = buffer_size; + builder->overflowed = false; + builder->pending = 0; + + jwt_add_header(builder); + + return 0; +} diff --git a/tests/subsys/jwt/CMakeLists.txt b/tests/subsys/jwt/CMakeLists.txt new file mode 100644 index 00000000000..ff28d5e8a9a --- /dev/null +++ b/tests/subsys/jwt/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.8) + +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(NONE) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) +zephyr_include_directories(${APPLICATION_SOURCE_DIR}/src/tls_config) diff --git a/tests/subsys/jwt/jwt-test-cert.pem b/tests/subsys/jwt/jwt-test-cert.pem new file mode 100644 index 00000000000..39859e23eb5 --- /dev/null +++ b/tests/subsys/jwt/jwt-test-cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAIo6NLZ3yCHqMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV +BAMMCGp3dC10ZXN0MCAXDTE4MDcwMjE3MjExMVoYDzQ3NTYwNTI5MTcyMTExWjAT +MREwDwYDVQQDDAhqd3QtdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKZSmN8OM9BpYjLEQHzsv5+jNYpRze1Jmb74KT/R16j7xjaCRWTbbXPvU8oC +frCRBr3VZkvqh3ptlaVrZLnWw92yXAOLAFxGFouGyCgNtLE+tg2CIpdbdQIAl8dX +S6CB+y3Iu4E3xI7mfJr5LQzUuCLlO6D+qD5emTZXdiV+/IkXPDnwPC6zPxT1I5aS +Qnsd0AuxRHGJO0Tl6uosZ7vZ45yKGoMe6RPjPAawo5idK6WEZdsjd1nKZHadVAVX +pxDLYux/OfzXUz1GLewR0UIQanM5GBCgz5uIbx6IaIs8MFk/ZrUJZfw3959O1xrG +FmRnZEHAkFHZ2vUlXMBinw3sLNMCAwEAAaNQME4wHQYDVR0OBBYEFINKpDYiVZyi +JlzHhq5Xo0Ax4eTKMB8GA1UdIwQYMBaAFINKpDYiVZyiJlzHhq5Xo0Ax4eTKMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADzE5U+/LiDbI/PS+5o3J5z+ +0eYdIOYU4Qe/ltdnt0voRKY1a0WHkYYXoFiONSRuAK/6O3bJByfQCd/NSAObTkPY +R0WPCg2vriztxPxs2fdU2VBh4qB7WM/yNOPpMamCDPZzg5oubVIwecYwZ0V888xV +GfWHwRcKtN7QogNIpGhMJj65MYPuB1cXAdUq7/zpRAewCp472dcUKLzLTHq7z8VU +ko0u1uTemh+xHtJLpVxqq7a6cpgka3DS7qwjz5XUL6UWKyL3uJcUL36ghL0ZwyQv +HngefbPQFMDyyoPh6QPGUMLwgN5pMI5mvdtA0I7z0G67TLpB+hpf+Kgyzx5JjQ0= +-----END CERTIFICATE----- diff --git a/tests/subsys/jwt/jwt-test-private.der b/tests/subsys/jwt/jwt-test-private.der new file mode 100644 index 0000000000000000000000000000000000000000..85614d02363234ff839a6ae9d155d97a589b92c6 GIT binary patch literal 1216 zcmV;x1V8&Qf&{z*0RS)!1_>&LNQUrrZ9p8q5=T`0)hbn0H#uy-wrd- zX<{>fy8)+F4kpmEwYUr8Wd!#6 z+qe-Kg8rJZPfoN4@XQs@L$R|VjXtnWP>JKXn^wKk-gF(}0tw*Lc} z)w|}Ye%4Dtgf_(a#5nt}{2?~U5S~#V#>Z0bcb8;hY{dkCP_|kjPEoRvLeb};jD|^j%VG0{f1B8}m%$WGgIGER%NxFs52ydO&EE_h~LVC$&6;jH&)#rdk4w z0XBp}U8n_1-QagqY>l-e1I6Z!&j#7$U4|s`WOvpm)@#t$g$wJP%ly z!_JCWjjmJa-T7HNACh+}zxgJ%>y9l6PCx~=pnx(QGa%UL&!V3;ey$Rb^X;{*@2FB4TpG?ic$S2_u4JP!x>4-dlFBPPEZ>iIi`mf*hELH9@pm+;0 z#K-R1D?q+f_h$ex&`!61rv9FwX)juqO*v~{^rQ(Wn3fr4mC%i~S=OGz;3BmL+5nRN zTIm~Ex*X}fa$t(Pm+lxi*sS!F>s}f5(E@>hT+Qclw4#}T*|A{#8d(OE*AE$ahTkP? zkEquol9-3K>mvhWd9c3mkx(IcgQ_!BMlpi2Eya$?2xuKGLr9?78Lx5F$g)LAg#0d!?s(>U8c>Lx8=Z8!%~f@$ z0)c>N*~qAnkp4xCU%5>kh5{k!BM0`Xt8BJ;^7d1u?ubuFZuDeOVBw5AeV{8>avHZ( z*P9I#q!9u77QK5ru&ri-f$vT2C^+jGSn|P85}aZ-3P(#tNG~Gvv@)v?WH~alT63Az eQz&zhDHX9v1Z1 +#include +#include +#include +#include + +#include +#include +#include + +extern unsigned char jwt_test_private_der[]; +extern unsigned int jwt_test_private_der_len; + +void test_jwt(void) +{ + /* + * TODO: This length should be computable, based on the length + * of the audience string. + */ + char buf[460]; + struct jwt_builder build; + int res; + + res = jwt_init_builder(&build, buf, sizeof(buf)); + + zassert_equal(res, 0, "Setting up jwt"); + + res = jwt_add_payload(&build, 1530312026, 1530308426, + "iot-work-199419"); + zassert_equal(res, 0, "Adding payload"); + + res = jwt_sign(&build, jwt_test_private_der, jwt_test_private_der_len); + zassert_equal(res, 0, "Signing payload"); + + zassert_equal(build.overflowed, false, "Not overflow"); + + printk("JWT:\n%s\n", buf); + printk("len: %zd\n", jwt_payload_len(&build)); +} + +void test_main(void) +{ + ztest_test_suite(lib_jwt_test, + ztest_unit_test(test_jwt)); + + ztest_run_test_suite(lib_jwt_test); +} + diff --git a/tests/subsys/jwt/src/tls_config/user-tls.conf b/tests/subsys/jwt/src/tls_config/user-tls.conf new file mode 100644 index 00000000000..035f2b062ec --- /dev/null +++ b/tests/subsys/jwt/src/tls_config/user-tls.conf @@ -0,0 +1,5 @@ +#define MBEDTLS_AES_ROM_TABLES + +#define MBEDTLS_HAVE_TIME +#define MBEDTLS_HAVE_TIME_DATE +#define MBEDTLS_PLATFORM_TIME_ALT diff --git a/tests/subsys/jwt/testcase.yaml b/tests/subsys/jwt/testcase.yaml new file mode 100644 index 00000000000..9abe3df1e72 --- /dev/null +++ b/tests/subsys/jwt/testcase.yaml @@ -0,0 +1,5 @@ +tests: + libraries.encoding: + min_ram: 96 + tags: jwt + platform_exclude: esp32 qemu_x86_64 #no newlib