summaryrefslogtreecommitdiffstats
path: root/src/lib-oauth2/test-oauth2-jwt.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/lib-oauth2/test-oauth2-jwt.c
parentInitial commit. (diff)
downloaddovecot-upstream.tar.xz
dovecot-upstream.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/lib-oauth2/test-oauth2-jwt.c919
1 files changed, 919 insertions, 0 deletions
diff --git a/src/lib-oauth2/test-oauth2-jwt.c b/src/lib-oauth2/test-oauth2-jwt.c
new file mode 100644
index 0000000..890712e
--- /dev/null
+++ b/src/lib-oauth2/test-oauth2-jwt.c
@@ -0,0 +1,919 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "ostream.h"
+#include "hmac.h"
+#include "sha2.h"
+#include "base64.h"
+#include "randgen.h"
+#include "array.h"
+#include "json-parser.h"
+#include "iso8601-date.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+#include "dcrypt.h"
+#include "dict.h"
+#include "dict-private.h"
+#include "test-common.h"
+#include "unlink-directory.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define base64url_encode_str(str, dest) \
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX, (str), \
+ strlen((str)), (dest))
+
+/**
+ * Test keypair used only for this test.
+ */
+static const char *rsa_public_key =
+"-----BEGIN PUBLIC KEY-----\n"
+"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv\n"
+"vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc\n"
+"aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy\n"
+"tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0\n"
+"e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb\n"
+"V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9\n"
+"MwIDAQAB\n"
+"-----END PUBLIC KEY-----";
+static const char *rsa_private_key =
+"-----BEGIN PRIVATE KEY-----\n"
+"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfPKKzVmN80HRs\n"
+"GAoUxK++RO3CW8GxomrtLnAD6TN5U5WlVbCRZ1WFrizfxcz+lr/Kvjtq/v7PdVOa\n"
+"8NHIAdxpP3bCFEQWku/1yPmVN4lKJvKv8yub9i2MJlVaBo5giHCtfAouo+v/XWKd\n"
+"awCR8jK28dZPFlgRxcuABcW5S5pLe4X2ASI1DDMZNTW/QWqSpMGvgHydbccI3jtd\n"
+"S7S3xjR76V/izg7FBrBYPv0n3/l3dHLS9tXcCbUW0YmIm87BGwh9UKEOlhK1NwdM\n"
+"Iyq29ZtXovXUFaSnMZdJbge/jepr4ZJg4PZBTrwxvn2hKTY4H4G04ukmh+ZsYQaC\n"
+"+bDIIj0zAgMBAAECggEAKIBGrbCSW2O1yOyQW9nvDUkA5EdsS58Q7US7bvM4iWpu\n"
+"DIBwCXur7/VuKnhn/HUhURLzj/JNozynSChqYyG+CvL+ZLy82LUE3ZIBkSdv/vFL\n"
+"Ft+VvvRtf1EcsmoqenkZl7aN7HD7DJeXBoz5tyVQKuH17WW0fsi9StGtCcUl+H6K\n"
+"zV9Gif0Kj0uLQbCg3THRvKuueBTwCTdjoP0PwaNADgSWb3hJPeLMm/yII4tIMGbO\n"
+"w+xd9wJRl+ZN9nkNtQMxszFGdKjedB6goYLQuP0WRZx+YtykaVJdM75bDUvsQar4\n"
+"9Pc21Fp7UVk/CN11DX/hX3TmTJAUtqYADliVKkTbCQKBgQDLU48tBxm3g1CdDM/P\n"
+"ZIEmpA3Y/m7e9eX7M1Uo/zDh4G/S9a4kkX6GQY2dLFdCtOS8M4hR11Io7MceBKDi\n"
+"djorTZ5zJPQ8+b9Rm+1GlaucGNwRW0cQk2ltT2ksPmJnQn2xvM9T8vE+a4A/YGzw\n"
+"mZOfpoVGykWs/tbSzU2aTaOybQKBgQDIfRf6OmirGPh59l+RSuDkZtISF/51mCV/\n"
+"S1M4DltWDwhjC2Y2T+meIsb/Mjtz4aVNz0EHB8yvn0TMGr94Uwjv4uBdpVSwz+xL\n"
+"hHL7J4rpInH+i0gxa0N+rGwsPwI8wJG95wLY+Kni5KCuXQw55uX1cqnnsahpRZFZ\n"
+"EerBXhjqHwKBgBmEjiaHipm2eEqNjhMoOPFBi59dJ0sCL2/cXGa9yEPA6Cfgv49F\n"
+"V0zAM2azZuwvSbm4+fXTgTMzrDW/PPXPArPmlOk8jQ6OBY3XdOrz48q+b/gZrYyO\n"
+"A6A9ZCSyW6U7+gxxds/BYLeFxF2v21xC2f0iZ/2faykv/oQMUh34en/tAoGACqVZ\n"
+"2JexZyR0TUWf3X80YexzyzIq+OOTWicNzDQ29WLm9xtr2gZ0SUlfd72bGpQoyvDu\n"
+"awkm/UxfwtbIxALkvpg1gcN9s8XWrkviLyPyZF7H3tRWiQlBFEDjnZXa8I7pLkRO\n"
+"Cmdp3fp17cxTEeAI5feovfzZDH39MdWZuZrdh9ECgYBTEv8S7nK8wrxIC390kroV\n"
+"52eBwzckQU2mWa0thUtaGQiU1EYPCSDcjkrLXwB72ft0dW57KyWtvrB6rt1ORgOL\n"
+"eI5hFbwdGQhCHTrAR1vG3SyFPMAm+8JB+sGOD/fvjtZKx//MFNweKFNEF0C/o6Z2\n"
+"FXj90PlgF8sCQut36ZfuIQ==\n"
+"-----END PRIVATE KEY-----";
+
+static buffer_t *hs_sign_key = NULL;
+
+static struct dict *keys_dict = NULL;
+
+static bool skip_dcrypt = FALSE;
+
+static struct oauth2_validation_key_cache *key_cache = NULL;
+
+static int parse_jwt_token(struct oauth2_request *req, const char *token,
+ bool *is_jwt_r, const char **error_r)
+{
+ struct oauth2_settings set;
+
+ i_zero(&set);
+ set.scope = "mail";
+ set.key_dict = keys_dict;
+ set.key_cache = key_cache;
+ i_zero(req);
+ req->pool = pool_datastack_create();
+ req->set = &set;
+ t_array_init(&req->fields, 8);
+ return oauth2_try_parse_jwt(&set, token, &req->fields, is_jwt_r,
+ error_r);
+}
+
+static void test_jwt_token(const char *token)
+{
+ /* then see what the parser likes it */
+ struct oauth2_request req;
+ const char *error = NULL;
+
+ bool is_jwt;
+ test_assert(parse_jwt_token(&req, token, &is_jwt, &error) == 0);
+ test_assert(is_jwt == TRUE);
+ test_assert(error == NULL);
+
+ /* check fields */
+ test_assert(array_is_created(&req.fields));
+ if (array_is_created(&req.fields)) {
+ const struct oauth2_field *field;
+ bool got_sub = FALSE;
+ array_foreach(&req.fields, field) {
+ if (strcmp(field->name, "sub") == 0) {
+ test_assert_strcmp(field->value, "testuser");
+ got_sub = TRUE;
+ }
+ }
+ test_assert(got_sub == TRUE);
+ }
+
+ if (error != NULL)
+ i_error("%s", error);
+}
+
+static buffer_t *create_jwt_token_kid(const char *algo, const char *kid)
+{
+ /* make a token */
+ buffer_t *tokenbuf = t_buffer_create(64);
+
+ /* header */
+ base64url_encode_str(
+ t_strdup_printf(
+ "{\"alg\":\"%s\",\"typ\":\"JWT\",\"kid\":\"%s\"}",
+ algo, kid),
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ /* body */
+ base64url_encode_str(
+ t_strdup_printf("{\"sub\":\"testuser\","\
+ "\"iat\":%"PRIdTIME_T","
+ "\"exp\":%"PRIdTIME_T"}",
+ time(NULL), time(NULL)+600),
+ tokenbuf);
+ return tokenbuf;
+}
+
+static buffer_t *create_jwt_token(const char *algo)
+{
+ /* make a token */
+ buffer_t *tokenbuf = t_buffer_create(64);
+
+ /* header */
+ base64url_encode_str(
+ t_strdup_printf("{\"alg\":\"%s\",\"typ\":\"JWT\"}", algo),
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ /* body */
+ base64url_encode_str(
+ t_strdup_printf("{\"sub\":\"testuser\","\
+ "\"iat\":%"PRIdTIME_T","
+ "\"exp\":%"PRIdTIME_T"}",
+ time(NULL), time(NULL)+600),
+ tokenbuf);
+ return tokenbuf;
+}
+
+static void
+append_key_value(string_t *dest, const char *key, const char *value, bool str)
+{
+ str_append_c(dest, '"');
+ json_append_escaped(dest, key);
+ str_append(dest, "\":");
+ if (str)
+ str_append_c(dest, '"');
+ json_append_escaped(dest, value);
+ if (str)
+ str_append_c(dest, '"');
+
+}
+
+#define create_jwt_token_fields(algo, exp, iat, nbf, fields) \
+ create_jwt_token_fields_kid(algo, "default", exp, iat, nbf, fields)
+static buffer_t *
+create_jwt_token_fields_kid(const char *algo, const char *kid, time_t exp, time_t iat,
+ time_t nbf, ARRAY_TYPE(oauth2_field) *fields)
+{
+ const struct oauth2_field *field;
+ buffer_t *tokenbuf = t_buffer_create(64);
+ string_t *hdr = t_str_new(32);
+ str_printfa(hdr, "{\"alg\":\"%s\",\"typ\":\"JWT\"", algo);
+ if (kid != NULL && *kid != '\0') {
+ str_append(hdr, ",\"kid\":\"");
+ json_append_escaped(hdr, kid);
+ str_append_c(hdr, '"');
+ }
+ str_append(hdr, "}");
+ base64url_encode_str(str_c(hdr), tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ string_t *bodybuf = t_str_new(64);
+ str_append_c(bodybuf, '{');
+ if (exp > 0) {
+ append_key_value(bodybuf, "exp", dec2str(exp), FALSE);
+ }
+ if (iat > 0) {
+ if (exp > 0)
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, "iat", dec2str(iat), FALSE);
+ }
+ if (nbf > 0) {
+ if (exp > 0 || iat > 0)
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, "nbf", dec2str(nbf), FALSE);
+ }
+ array_foreach(fields, field) {
+ if (str_data(bodybuf)[bodybuf->used-1] != '{')
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, field->name, field->value, TRUE);
+ }
+ str_append_c(bodybuf, '}');
+ base64url_encode_str(str_c(bodybuf), tokenbuf);
+
+ return tokenbuf;
+}
+
+#define save_key(algo, key) save_key_to(algo, "default", (key))
+#define save_key_to(algo, name, key) save_key_azp_to(algo, "default", name, (key))
+static void save_key_azp_to(const char *algo, const char *azp,
+ const char *name, const char *keydata)
+{
+ const char *error;
+ struct dict_op_settings set = {
+ .username = "testuser",
+ };
+ struct dict_transaction_context *ctx =
+ dict_transaction_begin(keys_dict, &set);
+ algo = t_str_ucase(algo);
+ dict_set(ctx, t_strconcat(DICT_PATH_SHARED, azp, "/", algo, "/",
+ name, NULL),
+ keydata);
+ if (dict_transaction_commit(&ctx, &error) < 0)
+ i_error("dict_set(%s) failed: %s", name, error);
+}
+
+static void sign_jwt_token_hs256(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha256, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void sign_jwt_token_hs384(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha384, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void sign_jwt_token_hs512(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha512, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void test_jwt_hs_token(void)
+{
+ test_begin("JWT HMAC token");
+
+ buffer_t *sign_key_384 = t_buffer_create(384/8);
+ void *ptr = buffer_append_space_unsafe(sign_key_384, 384/8);
+ random_fill(ptr, 384/8);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ sign_key_384->data,
+ sign_key_384->used);
+ save_key_to("HS384", "default", str_c(b64_key));
+ buffer_t *sign_key_512 = t_buffer_create(512/8);
+ ptr = buffer_append_space_unsafe(sign_key_512, 512/8);
+ random_fill(ptr, 512/8);
+ b64_key = t_base64_encode(0, SIZE_MAX,
+ sign_key_512->data,
+ sign_key_512->used);
+ save_key_to("HS512", "default", str_c(b64_key));
+ /* make a token */
+ buffer_t *tokenbuf = create_jwt_token("HS256");
+ /* sign it */
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ tokenbuf = create_jwt_token("HS384");
+ sign_jwt_token_hs384(tokenbuf, sign_key_384);
+ test_jwt_token(str_c(tokenbuf));
+
+ tokenbuf = create_jwt_token("HS512");
+ sign_jwt_token_hs512(tokenbuf, sign_key_512);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_token_escape(void)
+{
+ struct test_case {
+ const char *azp;
+ const char *alg;
+ const char *kid;
+ const char *esc_azp;
+ const char *esc_kid;
+ } test_cases[] = {
+ { "", "hs256", "", "default", "default" },
+ { "", "hs256", "test", "default", "test" },
+ { "test", "hs256", "test", "test", "test" },
+ {
+ "http://test.unit/local%key",
+ "hs256",
+ "http://test.unit/local%key",
+ "http:%2f%2ftest.unit%2flocal%25key",
+ "http:%2f%2ftest.unit%2flocal%25key"
+ },
+ { "../", "hs256", "../", "..%2f", "..%2f" },
+ };
+
+ test_begin("JWT token escaping");
+
+ buffer_t *b64_key =
+ t_base64_encode(0, SIZE_MAX, hs_sign_key->data, hs_sign_key->used);
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *test_case = &test_cases[i];
+ array_clear(&fields);
+ struct oauth2_field *field = array_append_space(&fields);
+ field->name = "sub";
+ field->value = "testuser";
+ if (*test_case->azp != '\0') {
+ field = array_append_space(&fields);
+ field->name = "azp";
+ field->value = test_case->azp;
+ }
+ if (*test_case->kid != '\0') {
+ field = array_append_space(&fields);
+ field->name = "kid";
+ field->value = test_case->kid;
+ }
+ save_key_azp_to(test_case->alg, test_case->esc_azp, test_case->esc_kid,
+ str_c(b64_key));
+ buffer_t *token = create_jwt_token_fields_kid(test_case->alg,
+ test_case->kid,
+ time(NULL)+500,
+ time(NULL)-500,
+ 0, &fields);
+ sign_jwt_token_hs256(token, hs_sign_key);
+ test_jwt_token(str_c(token));
+ }
+
+ test_end();
+}
+
+static void test_jwt_broken_token(void)
+{
+ struct test_cases {
+ const char *token;
+ bool is_jwt;
+ } test_cases[] = {
+ { /* empty token */
+ .token = "",
+ .is_jwt = FALSE
+ },
+ { /* not base64 */
+ .token = "{\"alg\":\"HS256\":\"typ\":\"JWT\"}",
+ .is_jwt = FALSE
+ },
+ { /* not jwt */
+ .token = "aGVsbG8sIHdvcmxkCg",
+ .is_jwt = FALSE
+ },
+ { /* no alg field */
+ .token = "eyJ0eXAiOiAiSldUIn0",
+ .is_jwt = FALSE
+ },
+ { /* no typ field */
+ .token = "eyJhbGciOiAiSFMyNTYifQ",
+ .is_jwt = FALSE
+ },
+ { /* typ field is wrong */
+ .token = "eyJ0eXAiOiAiand0IiwgImFsZyI6ICJIUzI1NiJ9."
+ "eyJhbGdvIjogIldURiIsICJ0eXAiOiAiSldUIn0."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = FALSE
+ },
+ { /* unknown algorithm */
+ .token = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJXVEYifQ."
+ "eyJhbGdvIjogIldURiIsICJ0eXAiOiAiSldUIn0."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = TRUE
+ },
+ { /* truncated base64 */
+ .token = "yJhbGciOiJIUzI1NiIsInR5",
+ .is_jwt = FALSE
+ },
+ { /* missing body and signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
+ .is_jwt = FALSE
+ },
+ { /* empty body and signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..",
+ .is_jwt = TRUE
+ },
+ { /* empty signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJleHAiOjE1ODEzMzA3OTN9.",
+ .is_jwt = TRUE
+ },
+ { /* bad signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJleHAiOjE1ODEzMzA3OTN9."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = TRUE
+ },
+ };
+
+ test_begin("JWT broken tokens");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ struct test_cases *test_case = &test_cases[i];
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+ test_assert_idx(parse_jwt_token(&req, test_case->token,
+ &is_jwt, &error) != 0, i);
+ test_assert_idx(test_case->is_jwt == is_jwt, i);
+ test_assert_idx(error != NULL, i);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_bad_valid_token(void)
+{
+ test_begin("JWT bad token tests");
+ time_t now = time(NULL);
+
+ struct test_cases {
+ time_t exp;
+ time_t iat;
+ time_t nbf;
+ const char *key_values[20];
+ const char *error;
+ } test_cases[] =
+ {
+ { /* "empty" token */
+ .exp = 0,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { NULL },
+ .error = "Missing 'sub' field",
+ },
+ { /* missing sub field */
+ .exp = now+500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { NULL },
+ .error = "Missing 'sub' field",
+ },
+ { /* no expiration */
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ .error = "Missing 'exp' field",
+ },
+ { /* non-ISO date as iat */
+ .exp = now+500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", "iat",
+ "1.1.2019 16:00", NULL },
+ .error = "Malformed 'iat' field"
+ },
+ { /* expired token */
+ .exp = now-500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token has expired",
+ },
+ { /* future token */
+ .exp = now+1000,
+ .iat = now+500,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token is issued in future",
+ },
+ { /* token not valid yet */
+ .exp = now+500,
+ .iat = now,
+ .nbf = now+250,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token is not valid yet",
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ const struct test_cases *test_case = &test_cases[i];
+ const char *key = NULL;
+ ARRAY_TYPE(oauth2_field) fields;
+
+ t_array_init(&fields, 8);
+ for (const char *const *value = test_case->key_values;
+ *value != NULL; value++) {
+ if (key == NULL) {
+ key = *value;
+ } else {
+ struct oauth2_field *field =
+ array_append_space(&fields);
+ field->name = key;
+ field->value = *value;
+ key = NULL;
+ }
+ }
+
+ buffer_t *tokenbuf =
+ create_jwt_token_fields("HS256", test_case->exp,
+ test_case->iat, test_case->nbf,
+ &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+
+ test_assert_idx(parse_jwt_token(&req, str_c(tokenbuf),
+ &is_jwt, &error) != 0, i);
+ test_assert_idx(is_jwt == TRUE, i);
+ if (test_case->error != NULL) {
+ test_assert_strcmp(test_case->error, error);
+ }
+ test_assert(error != NULL);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_valid_token(void)
+{
+ test_begin("JWT valid token tests");
+ time_t now = time(NULL);
+
+ struct test_cases {
+ time_t exp;
+ time_t iat;
+ time_t nbf;
+ const char *key_values[20];
+ } test_cases[] = {
+ { /* valid token */
+ .exp = now + 500,
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ },
+ {
+ .exp = now + 500,
+ .nbf = now - 500,
+ .iat = now - 250,
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ },
+ { /* token issued in advance */
+ .exp = now + 500,
+ .nbf = now - 500,
+ .iat = now - 3600,
+ .key_values = {
+ "sub", "testuser",
+ NULL,
+ },
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ const struct test_cases *test_case = &test_cases[i];
+ ARRAY_TYPE(oauth2_field) fields;
+
+ t_array_init(&fields, 8);
+ for (unsigned int i = 0; test_case->key_values[i] != NULL; i += 2) {
+ struct oauth2_field *field = array_append_space(&fields);
+ field->name = test_case->key_values[i];
+ field->value = test_case->key_values[i+1];
+ }
+
+ buffer_t *tokenbuf =
+ create_jwt_token_fields("HS256", test_case->exp,
+ test_case->iat, test_case->nbf,
+ &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+
+ test_assert_idx(parse_jwt_token(&req, str_c(tokenbuf),
+ &is_jwt, &error) == 0, i);
+ test_assert_idx(is_jwt == TRUE, i);
+ test_assert_idx(error == NULL, i);
+ if (error != NULL)
+ i_error("JWT validation error: %s", error);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_dates(void)
+{
+ test_begin("JWT Token dates");
+
+ /* simple check to make sure ISO8601 dates work too */
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+ struct oauth2_field *field;
+ struct tm tm_b;
+ struct tm *tm;
+ time_t now = time(NULL);
+ time_t exp = now+500;
+ time_t nbf = now-250;
+ time_t iat = now-500;
+
+ field = array_append_space(&fields);
+ field->name = "sub";
+ field->value = "testuser";
+ field = array_append_space(&fields);
+ field->name = "exp";
+ tm = gmtime_r(&exp, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ field = array_append_space(&fields);
+ field->name = "nbf";
+ tm = gmtime_r(&nbf, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ field = array_append_space(&fields);
+ field->name = "iat";
+ tm = gmtime_r(&iat, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ buffer_t *tokenbuf = create_jwt_token_fields("HS256", 0, 0, 0, &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ str_truncate(tokenbuf, 0);
+ base64url_encode_str("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", tokenbuf);
+ str_append_c(tokenbuf, '.');
+ base64url_encode_str(t_strdup_printf("{\"sub\":\"testuser\","
+ "\"exp\":%"PRIdTIME_T","
+ "\"nbf\":0,\"iat\":%"PRIdTIME_T"}",
+ exp, iat),
+ tokenbuf);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_key_files(void)
+{
+ test_begin("JWT key id");
+ /* write HMAC secrets */
+ struct oauth2_request req;
+ bool is_jwt;
+ const char *error = NULL;
+
+ buffer_t *secret = t_buffer_create(32);
+ void *ptr = buffer_append_space_unsafe(secret, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ secret->data, secret->used);
+ save_key_to("HS256", "first", str_c(b64_key));
+ buffer_t *secret2 = t_buffer_create(32);
+ ptr = buffer_append_space_unsafe(secret2, 32);
+ random_fill(ptr, 32);
+ b64_key = t_base64_encode(0, SIZE_MAX, secret2->data, secret2->used);
+ save_key_to("HS256", "second", str_c(b64_key));
+
+ /* create and sign token */
+ buffer_t *token_1 = create_jwt_token_kid("HS256", "first");
+ buffer_t *token_2 = create_jwt_token_kid("HS256", "second");
+ buffer_t *token_3 = create_jwt_token_kid("HS256", "missing");
+ buffer_t *token_4 = create_jwt_token_kid("HS256", "");
+
+ sign_jwt_token_hs256(token_1, secret);
+ sign_jwt_token_hs256(token_2, secret2);
+ sign_jwt_token_hs256(token_3, secret);
+ sign_jwt_token_hs256(token_4, secret);
+
+ test_jwt_token(str_c(token_1));
+ test_jwt_token(str_c(token_2));
+
+ test_assert(parse_jwt_token(&req, str_c(token_3), &is_jwt, &error) != 0);
+ test_assert(is_jwt == TRUE);
+ test_assert_strcmp(error, "HS256 key 'missing' not found");
+ test_assert(parse_jwt_token(&req, str_c(token_4), &is_jwt, &error) != 0);
+ test_assert(is_jwt == TRUE);
+ test_assert_strcmp(error, "'kid' field is empty");
+
+ test_end();
+}
+
+static void test_jwt_kid_escape(void)
+{
+ test_begin("JWT kid escape");
+ /* save a token */
+ buffer_t *secret = t_buffer_create(32);
+ void *ptr = buffer_append_space_unsafe(secret, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ secret->data, secret->used);
+ save_key_to("HS256", "hello.world%2f%25", str_c(b64_key));
+ /* make a token */
+ buffer_t *tokenbuf = create_jwt_token_kid("HS256", "hello.world/%");
+ /* sign it */
+ sign_jwt_token_hs256(tokenbuf, secret);
+ test_jwt_token(str_c(tokenbuf));
+ test_end();
+}
+
+static void test_jwt_rs_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT RSA token");
+ /* write public key to file */
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("RS256", rsa_public_key);
+
+ buffer_t *tokenbuf = create_jwt_token("RS256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ struct dcrypt_private_key *key;
+ if (!dcrypt_key_load_private(&key, rsa_private_key, NULL, NULL,
+ &error) ||
+ !dcrypt_sign(key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_RSA_PKCS1, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_key_unref_private(&key);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_ps_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT RSAPSS token");
+ /* write public key to file */
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("PS256", rsa_public_key);
+
+ buffer_t *tokenbuf = create_jwt_token("PS256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ struct dcrypt_private_key *key;
+ if (!dcrypt_key_load_private(&key, rsa_private_key, NULL, NULL,
+ &error) ||
+ !dcrypt_sign(key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_RSA_PKCS1_PSS, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_key_unref_private(&key);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_ec_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT ECDSA token");
+ struct dcrypt_keypair pair;
+ i_zero(&pair);
+ if (!dcrypt_keypair_generate(&pair, DCRYPT_KEY_EC, 0,
+ "prime256v1", &error)) {
+ i_error("dcrypt keypair generate failed: %s", error);
+ lib_exit(1);
+ }
+ /* export public key */
+ buffer_t *keybuf = t_buffer_create(256);
+ if (!dcrypt_key_store_public(pair.pub, DCRYPT_FORMAT_PEM, keybuf,
+ &error)) {
+ i_error("dcrypt key store failed: %s", error);
+ lib_exit(1);
+ }
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("ES256", str_c(keybuf));
+
+ buffer_t *tokenbuf = create_jwt_token("ES256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ if (!dcrypt_sign(pair.priv, "sha256", DCRYPT_SIGNATURE_FORMAT_X962,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_DEFAULT, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_keypair_unref(&pair);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_do_init(void)
+{
+ const char *error;
+ struct dcrypt_settings dcrypt_set = {
+ .module_dir = "../lib-dcrypt/.libs",
+ };
+ struct dict_settings dict_set = {
+ .base_dir = ".",
+ };
+
+ i_unlink_if_exists(".keys");
+ dict_driver_register(&dict_driver_file);
+ if (dict_init("file:.keys", &dict_set, &keys_dict, &error) < 0)
+ i_fatal("dict_init(file:.keys): %s", error);
+ if (!dcrypt_initialize(NULL, &dcrypt_set, &error)) {
+ i_error("No functional dcrypt backend found - "
+ "skipping some tests: %s", error);
+ skip_dcrypt = TRUE;
+ }
+ key_cache = oauth2_validation_key_cache_init();
+
+ /* write HMAC secret */
+ hs_sign_key =buffer_create_dynamic(default_pool, 32);
+ void *ptr = buffer_append_space_unsafe(hs_sign_key, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ hs_sign_key->data,
+ hs_sign_key->used);
+ save_key("HS256", str_c(b64_key));
+}
+
+static void test_do_deinit(void)
+{
+ dict_deinit(&keys_dict);
+ dict_driver_unregister(&dict_driver_file);
+ oauth2_validation_key_cache_deinit(&key_cache);
+ i_unlink(".keys");
+ buffer_free(&hs_sign_key);
+ dcrypt_deinitialize();
+}
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_do_init,
+ test_jwt_hs_token,
+ test_jwt_token_escape,
+ test_jwt_valid_token,
+ test_jwt_bad_valid_token,
+ test_jwt_broken_token,
+ test_jwt_dates,
+ test_jwt_key_files,
+ test_jwt_kid_escape,
+ test_jwt_rs_token,
+ test_jwt_ps_token,
+ test_jwt_ec_token,
+ test_do_deinit,
+ NULL
+ };
+ int ret;
+ ret = test_run(test_functions);
+ return ret;
+}