mail archive of the barebox mailing list
 help / color / mirror / Atom feed
From: Ahmad Fatoum <a.fatoum@pengutronix.de>
To: barebox@lists.infradead.org
Cc: Ahmad Fatoum <a.fatoum@pengutronix.de>
Subject: [PATCH 2/3] crypto: add JSON Web Token (JWT) support
Date: Mon, 23 Oct 2023 16:31:22 +0200	[thread overview]
Message-ID: <20231023143122.1760217-3-a.fatoum@pengutronix.de> (raw)
In-Reply-To: <20231023143122.1760217-1-a.fatoum@pengutronix.de>

JSON Web Token is a proposed Internet standard for creating tokens with
optional signature and/or optional encryption whose payload holds JSON that
asserts some number of claims. The tokens are signed either using a private
secret or a public/private key.

In the context of barebox, a JSON Web Token can be used as unlock token
for a system: By default, the system would be locked and only boot
signed payloads, but when a valid unlock token is provided, board code
can selectively allow access to disallowed features, such as booting
unsigned payloads or provide access to the console and shell.

This commit adds first support for JSON Web Tokens on top of the already
existing JSON support. RS256 is the only currently supported format, but
more may be added in future.

Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 crypto/Kconfig       |   6 ++
 crypto/Makefile      |   2 +
 crypto/jwt.c         | 241 +++++++++++++++++++++++++++++++++++++++++++
 include/crypto/jwt.h |  55 ++++++++++
 4 files changed, 304 insertions(+)
 create mode 100644 crypto/jwt.c
 create mode 100644 include/crypto/jwt.h

diff --git a/crypto/Kconfig b/crypto/Kconfig
index 4ad7bd844fa1..d1360a2101b3 100644
--- a/crypto/Kconfig
+++ b/crypto/Kconfig
@@ -147,4 +147,10 @@ config CRYPTO_KEYSTORE
 	  This is a simple keystore, which can be used to pass keys
 	  between several components via simple interface.
 
+config JWT
+	bool "JSON Web Token support" if COMPILE_TEST
+	select JSMN
+	select BASE64
+	select CRYPTO_RSA
+
 endmenu
diff --git a/crypto/Makefile b/crypto/Makefile
index 4a1c7e9615b8..cf041dd6b3ed 100644
--- a/crypto/Makefile
+++ b/crypto/Makefile
@@ -19,6 +19,8 @@ obj-$(CONFIG_CRYPTO_PBKDF2)	+= pbkdf2.o
 obj-$(CONFIG_CRYPTO_RSA)	+= rsa.o
 obj-$(CONFIG_CRYPTO_KEYSTORE)	+= keystore.o
 
+obj-$(CONFIG_JWT)		+= jwt.o
+
 extra-$(CONFIG_CRYPTO_RSA_BUILTIN_KEYS) += rsa-keys.h
 
 ifdef CONFIG_CRYPTO_RSA_BUILTIN_KEYS
diff --git a/crypto/jwt.c b/crypto/jwt.c
new file mode 100644
index 000000000000..146ddeff1e8b
--- /dev/null
+++ b/crypto/jwt.c
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define pr_fmt(fmt) "jwt: " fmt
+
+#include <crypto/jwt.h>
+#include <rsa.h>
+#include <errno.h>
+#include <linux/printk.h>
+#include <base64.h>
+#include <jsmn.h>
+#include <linux/ctype.h>
+
+#define JP(...)	(const char *[]) { __VA_ARGS__, NULL }
+
+static enum hash_algo digest_algo_by_jwt_alg(enum jwt_alg alg)
+{
+	switch (alg) {
+		case JWT_ALG_RS256:
+			return HASH_ALGO_SHA256;
+		case JWT_ALG_RS384:
+			return HASH_ALGO_SHA384;
+		case JWT_ALG_RS512:
+			return HASH_ALGO_SHA512;
+		default:
+			BUG();
+	}
+}
+
+static u8 *do_hash(const u8 *buf, size_t len, enum hash_algo algo)
+{
+	struct digest *digest;
+	int ret = 0;
+	u8 *hash;
+
+	digest = digest_alloc_by_algo(algo);
+	if (!digest) {
+		pr_err("signature algorithm not supported\n");
+		return ERR_PTR(-ENOSYS);
+	}
+
+	hash = xzalloc(digest_length(digest));
+	ret = digest_digest(digest, buf, len, hash);
+	digest_free(digest);
+
+	if (ret) {
+		free(hash);
+		return ERR_PTR(ret);
+	}
+
+	return hash;
+}
+
+static int jwt_part_parse(struct jwt_part *part, const char *content, size_t len)
+{
+	size_t decoded_len;
+
+	part->content = xmalloc(len);
+	decoded_len = decode_base64url(part->content, len, content);
+	part->content[decoded_len] = '\0';
+	part->tokens = jsmn_parse_alloc(part->content, decoded_len, &part->token_count);
+	if (!part->tokens) {
+		free(part->content);
+		return -EILSEQ;
+	}
+
+	return 0;
+}
+
+static void jwt_part_free(struct jwt_part *part)
+{
+	free(part->tokens);
+	free(part->content);
+}
+
+static const char *jwt_alg_names[] = {
+	[JWT_ALG_NONE]   = "none",
+	[JWT_ALG_HS256]  = "HS256",
+	[JWT_ALG_HS384]  = "HS384",
+	[JWT_ALG_HS512]  = "HS512",
+	[JWT_ALG_PS256]  = "PS256",
+	[JWT_ALG_PS384]  = "PS384",
+	[JWT_ALG_PS512]  = "PS512",
+	[JWT_ALG_RS256]  = "RS256",
+	[JWT_ALG_RS384]  = "RS384",
+	[JWT_ALG_RS512]  = "RS512",
+	[JWT_ALG_ES256]  = "ES256",
+	[JWT_ALG_ES256K] = "ES256K",
+	[JWT_ALG_ES384]  = "ES384",
+	[JWT_ALG_ES512]  = "ES512",
+	[JWT_ALG_EDDSA]  = "EDDSA",
+};
+
+static bool jwt_header_ok(struct jwt *jwt, enum jwt_alg alg)
+{
+	struct jwt_part *header = &jwt->header;
+	const jsmntok_t *token;
+
+	token = jsmn_locate(JP("typ"), header->content, header->tokens);
+	if (!token)
+		return false;
+
+	if (!jsmn_strcase_eq("JWT", header->content, token))
+		return false;
+
+	if (alg >= ARRAY_SIZE(jwt_alg_names))
+		return false;
+
+	token = jsmn_locate(JP("alg"), header->content, header->tokens);
+	if (!token)
+		return false;
+
+	return jsmn_strcase_eq(jwt_alg_names[alg], header->content, token);
+}
+
+void jwt_free(struct jwt *jwt)
+{
+	jwt_part_free(&jwt->payload);
+	jwt_part_free(&jwt->header);
+	free(jwt);
+}
+
+const char *jwt_split(const char *token,
+		      const char **payload, const char **signature, const char **end)
+{
+	const char *p, *p_end;
+
+	token = skip_spaces(token);
+
+	p = strchr(token, '.');
+	if (!p)
+		return ERR_PTR(-EINVAL);
+	if (payload)
+		*payload = ++p;
+
+	p = strchr(p, '.');
+	if (!p)
+		return ERR_PTR(-EINVAL);
+	if (signature)
+		*signature = ++p;
+
+	/* seek to first space or '\0' */
+	for (p_end = p; *p_end && !isspace(*p_end); p_end++)
+		;
+
+	/* ensure the trailing spaces aren't followed by anything */
+	if (*skip_spaces(p_end) != '\0')
+		return ERR_PTR(-EINVAL);
+
+	*end = p_end;
+
+	return token;
+}
+
+struct jwt *jwt_decode(const char *token, const struct jwt_key *key)
+{
+	const char *alg_name = jwt_alg_names[key->alg];
+	enum hash_algo hash_algo;
+	const char *payload, *signature, *end;
+	u8 *sigbin;
+	size_t sig_len, sigbin_len;
+	struct jwt *jwt;
+	u8 *hash;
+	int ret;
+
+	token = jwt_split(token, &payload, &signature, &end);
+	if (IS_ERR(token))
+		return ERR_CAST(token);
+
+	sig_len = end - signature;
+
+	switch (key->alg) {
+	case JWT_ALG_RS256:
+	case JWT_ALG_RS384:
+	case JWT_ALG_RS512:
+		if (sig_len == 0)
+			return ERR_PTR(-EILSEQ);
+
+		sigbin = xzalloc(sig_len);
+		sigbin_len = decode_base64url(sigbin, sig_len, signature);
+
+		hash_algo = digest_algo_by_jwt_alg(key->alg);
+		hash = do_hash(token, signature - token - 1, hash_algo);
+		if (IS_ERR(hash)) {
+			free(sigbin);
+			return ERR_CAST(hash);
+		}
+
+		ret = rsa_verify(key->material.rsa_pub, sigbin, sigbin_len, hash,
+				 hash_algo);
+		free(hash);
+		free(sigbin);
+		if (ret < 0) {
+			pr_debug("%s signature does not match: %pe\n",
+				 alg_name, ERR_PTR(ret));
+			return ERR_PTR(ret);
+		}
+
+		break;
+	default:
+		return ERR_PTR(-ENOSYS);
+	}
+
+	pr_debug("verification for algo %s ok\n", alg_name);
+
+	jwt = xzalloc(sizeof(*jwt));
+
+	ret = jwt_part_parse(&jwt->header, token, payload - token - 1);
+	if (ret || !jwt_header_ok(jwt, key->alg)) {
+		ret = ret ?: -EINVAL;
+		pr_debug("failed to parse header: %pe\n", ERR_PTR(ret));
+		goto err;
+	}
+
+	ret = jwt_part_parse(&jwt->payload, payload, signature - payload - 1);
+	if (ret) {
+		ret = ret ?: -EINVAL;
+		pr_debug("failed to parse payload: %pe\n", ERR_PTR(ret));
+		goto err;
+	}
+
+	return jwt;
+
+err:
+	jwt_free(jwt);
+	return ERR_PTR(ret);
+}
+
+const char *jwt_get_payload(const struct jwt *t)
+{
+	return t->payload.content;
+}
+
+const jsmntok_t *jwt_get_claim(const struct jwt *t, const char *claim)
+{
+	return jsmn_locate(JP(claim), t->payload.content, t->payload.tokens);
+}
+
+char *jwt_get_claim_str(const struct jwt *t, const char *claim)
+{
+	return jsmn_strdup(JP(claim), t->payload.content, t->payload.tokens);
+}
diff --git a/include/crypto/jwt.h b/include/crypto/jwt.h
new file mode 100644
index 000000000000..4e20b5950e69
--- /dev/null
+++ b/include/crypto/jwt.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __JWT_H_
+#define __JWT_H_
+
+#include <linux/types.h>
+#include <jsmn.h>
+
+enum jwt_alg {
+	JWT_ALG_NONE,
+	JWT_ALG_HS256,
+	JWT_ALG_HS384,
+	JWT_ALG_HS512,
+	JWT_ALG_PS256,
+	JWT_ALG_PS384,
+	JWT_ALG_PS512,
+	JWT_ALG_RS256, /* supported */
+	JWT_ALG_RS384, /* supported */
+	JWT_ALG_RS512, /* supported */
+	JWT_ALG_ES256,
+	JWT_ALG_ES256K,
+	JWT_ALG_ES384,
+	JWT_ALG_ES512,
+	JWT_ALG_EDDSA,
+};
+
+struct jwt_key {
+	enum jwt_alg alg;
+	union {
+		const struct rsa_public_key *rsa_pub;
+	} material;
+};
+
+struct jwt_part {
+	char *content;
+	int token_count;
+	jsmntok_t *tokens;
+};
+
+struct jwt {
+	struct jwt_part header;
+	struct jwt_part payload;
+};
+
+const char *jwt_split(const char *token,
+		      const char **payload, const char **signature, const char **end);
+
+struct jwt *jwt_decode(const char *token, const struct jwt_key *key);
+void jwt_free(struct jwt *jwt);
+
+const char *jwt_get_payload(const struct jwt *t);
+
+const jsmntok_t *jwt_get_claim(const struct jwt *t, const char *claim);
+char *jwt_get_claim_str(const struct jwt *t, const char *claim);
+
+#endif
-- 
2.39.2




  parent reply	other threads:[~2023-10-23 14:33 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-10-23 14:31 [PATCH 0/3] " Ahmad Fatoum
2023-10-23 14:31 ` [PATCH 1/3] lib: base64: add support for base64url Ahmad Fatoum
2023-10-23 14:31 ` Ahmad Fatoum [this message]
2023-11-01  9:13   ` [PATCH 2/3] crypto: add JSON Web Token (JWT) support Sascha Hauer
2023-10-23 14:31 ` [PATCH 3/3] test: self: add JSON Web Token tests Ahmad Fatoum
2023-11-02  7:20   ` Sascha Hauer
2023-11-02  8:07     ` Ahmad Fatoum
2023-11-01  9:10 ` [PATCH 0/3] crypto: add JSON Web Token (JWT) support Sascha Hauer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20231023143122.1760217-3-a.fatoum@pengutronix.de \
    --to=a.fatoum@pengutronix.de \
    --cc=barebox@lists.infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox