blob: e29a1c797b343615a54b08223b7526c3028a3177 [file] [log] [blame]
/*
* JSON Web Token (JWT) processing
*
* Copyright 2021 HAProxy Technologies
* Remi Tricot-Le Breton <rlebreton@haproxy.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*/
#include <import/ebmbtree.h>
#include <import/ebsttree.h>
#include <haproxy/api.h>
#include <haproxy/tools.h>
#include <haproxy/openssl-compat.h>
#include <haproxy/base64.h>
#include <haproxy/jwt.h>
#ifdef USE_OPENSSL
/* Tree into which the public certificates used to validate JWTs will be stored. */
static struct eb_root jwt_cert_tree = EB_ROOT_UNIQUE;
/*
* The possible algorithm strings that can be found in a JWS's JOSE header are
* defined in section 3.1 of RFC7518.
*/
enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len)
{
enum jwt_alg alg = JWT_ALG_DEFAULT;
/* Algorithms are all 5 characters long apart from "none". */
if (alg_len < sizeof("HS256")-1) {
if (strncmp("none", alg_str, alg_len) == 0)
alg = JWS_ALG_NONE;
return alg;
}
if (alg == JWT_ALG_DEFAULT) {
switch(*alg_str++) {
case 'H':
if (strncmp(alg_str, "S256", alg_len-1) == 0)
alg = JWS_ALG_HS256;
else if (strncmp(alg_str, "S384", alg_len-1) == 0)
alg = JWS_ALG_HS384;
else if (strncmp(alg_str, "S512", alg_len-1) == 0)
alg = JWS_ALG_HS512;
break;
case 'R':
if (strncmp(alg_str, "S256", alg_len-1) == 0)
alg = JWS_ALG_RS256;
else if (strncmp(alg_str, "S384", alg_len-1) == 0)
alg = JWS_ALG_RS384;
else if (strncmp(alg_str, "S512", alg_len-1) == 0)
alg = JWS_ALG_RS512;
break;
case 'E':
if (strncmp(alg_str, "S256", alg_len-1) == 0)
alg = JWS_ALG_ES256;
else if (strncmp(alg_str, "S384", alg_len-1) == 0)
alg = JWS_ALG_ES384;
else if (strncmp(alg_str, "S512", alg_len-1) == 0)
alg = JWS_ALG_ES512;
break;
case 'P':
if (strncmp(alg_str, "S256", alg_len-1) == 0)
alg = JWS_ALG_PS256;
else if (strncmp(alg_str, "S384", alg_len-1) == 0)
alg = JWS_ALG_PS384;
else if (strncmp(alg_str, "S512", alg_len-1) == 0)
alg = JWS_ALG_PS512;
break;
default:
break;
}
}
return alg;
}
/*
* Split a JWT into its separate dot-separated parts.
* Since only JWS following the Compact Serialization format are managed for
* now, we don't need to manage more than three subparts in the tokens.
* See section 3.1 of RFC7515 for more information about JWS Compact
* Serialization.
* Returns 0 in case of success.
*/
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num)
{
char *ptr = jwt->area;
char *jwt_end = jwt->area + jwt->data;
unsigned int index = 0;
unsigned int length = 0;
if (index < *item_num) {
items[index].start = ptr;
items[index].length = 0;
}
while (index < *item_num && ptr < jwt_end) {
if (*ptr++ == '.') {
items[index++].length = length;
if (index == *item_num)
return -1;
items[index].start = ptr;
items[index].length = 0;
length = 0;
} else
++length;
}
if (index < *item_num)
items[index].length = length;
*item_num = (index+1);
return (ptr != jwt_end);
}
/*
* Parse a public certificate and insert it into the jwt_cert_tree.
* Returns 0 in case of success.
*/
int jwt_tree_load_cert(char *path, int pathlen, char **err)
{
int retval = -1;
struct jwt_cert_tree_entry *entry = NULL;
EVP_PKEY *pkey = NULL;
BIO *bio = NULL;
bio = BIO_new(BIO_s_file());
if (!bio) {
memprintf(err, "%sunable to allocate memory (BIO).\n", err && *err ? *err : "");
goto end;
}
if (BIO_read_filename(bio, path) == 1) {
pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
if (!pkey) {
memprintf(err, "%sfile not found (%s)\n", err && *err ? *err : "", path);
goto end;
}
entry = calloc(1, sizeof(*entry) + pathlen + 1);
if (!entry) {
memprintf(err, "%sunable to allocate memory (jwt_cert_tree_entry).\n", err && *err ? *err : "");
goto end;
}
memcpy(entry->path, path, pathlen + 1);
entry->pkey = pkey;
ebst_insert(&jwt_cert_tree, &entry->node);
retval = 0;
}
end:
BIO_free(bio);
return retval;
}
/*
* Calculate the HMAC signature of a specific JWT and check that it matches the
* one included in the token.
* Returns 1 in case of success.
*/
static enum jwt_vrfy_status
jwt_jwsverify_hmac(const struct jwt_ctx *ctx, const struct buffer *decoded_signature)
{
const EVP_MD *evp = NULL;
unsigned char *signature = NULL;
unsigned int signature_length = 0;
struct buffer *trash = NULL;
unsigned char *hmac_res = NULL;
enum jwt_vrfy_status retval = JWT_VRFY_KO;
trash = alloc_trash_chunk();
if (!trash)
return JWT_VRFY_OUT_OF_MEMORY;
signature = (unsigned char*)trash->area;
signature_length = trash->size;
switch(ctx->alg) {
case JWS_ALG_HS256:
evp = EVP_sha256();
break;
case JWS_ALG_HS384:
evp = EVP_sha384();
break;
case JWS_ALG_HS512:
evp = EVP_sha512();
break;
default: break;
}
hmac_res = HMAC(evp, ctx->key, ctx->key_length, (const unsigned char*)ctx->jose.start,
ctx->jose.length + ctx->claims.length + 1, signature, &signature_length);
if (hmac_res && signature_length == decoded_signature->data &&
(CRYPTO_memcmp(decoded_signature->area, signature, signature_length) == 0))
retval = JWT_VRFY_OK;
free_trash_chunk(trash);
return retval;
}
/*
* Check that the signature included in a JWT signed via RSA or ECDSA is valid
* and can be verified thanks to a given public certificate.
* Returns 1 in case of success.
*/
static enum jwt_vrfy_status
jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, const struct buffer *decoded_signature)
{
const EVP_MD *evp = NULL;
EVP_MD_CTX *evp_md_ctx;
enum jwt_vrfy_status retval = JWT_VRFY_KO;
struct buffer *trash = NULL;
struct ebmb_node *eb;
struct jwt_cert_tree_entry *entry = NULL;
trash = alloc_trash_chunk();
if (!trash)
return JWT_VRFY_OUT_OF_MEMORY;
switch(ctx->alg) {
case JWS_ALG_RS256:
case JWS_ALG_ES256:
evp = EVP_sha256();
break;
case JWS_ALG_RS384:
case JWS_ALG_ES384:
evp = EVP_sha384();
break;
case JWS_ALG_RS512:
case JWS_ALG_ES512:
evp = EVP_sha512();
break;
default: break;
}
evp_md_ctx = EVP_MD_CTX_new();
if (!evp_md_ctx) {
free_trash_chunk(trash);
return JWT_VRFY_OUT_OF_MEMORY;
}
eb = ebst_lookup(&jwt_cert_tree, ctx->key);
if (!eb) {
retval = JWT_VRFY_UNKNOWN_CERT;
goto end;
}
entry = ebmb_entry(eb, struct jwt_cert_tree_entry, node);
if (!entry->pkey) {
retval = JWT_VRFY_UNKNOWN_CERT;
goto end;
}
if (EVP_DigestVerifyInit(evp_md_ctx, NULL, evp, NULL,entry-> pkey) == 1 &&
EVP_DigestVerifyUpdate(evp_md_ctx, (const unsigned char*)ctx->jose.start,
ctx->jose.length + ctx->claims.length + 1) == 1 &&
EVP_DigestVerifyFinal(evp_md_ctx, (const unsigned char*)decoded_signature->area, decoded_signature->data) == 1) {
retval = JWT_VRFY_OK;
}
end:
EVP_MD_CTX_free(evp_md_ctx);
free_trash_chunk(trash);
return retval;
}
/*
* Check that the <token> that was signed via algorithm <alg> using the <key>
* (either an HMAC secret or the path to a public certificate) has a valid
* signature.
* Returns 1 in case of success.
*/
enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg,
const struct buffer *key)
{
struct jwt_item items[JWT_ELT_MAX] = { { 0 } };
unsigned int item_num = JWT_ELT_MAX;
struct buffer *decoded_sig = NULL;
struct jwt_ctx ctx = {};
enum jwt_vrfy_status retval = JWT_VRFY_KO;
int ret;
ctx.alg = jwt_parse_alg(alg->area, alg->data);
if (ctx.alg == JWT_ALG_DEFAULT)
return JWT_VRFY_UNKNOWN_ALG;
if (jwt_tokenize(token, items, &item_num))
return JWT_VRFY_INVALID_TOKEN;
if (item_num != JWT_ELT_MAX)
if (ctx.alg != JWS_ALG_NONE || item_num != JWT_ELT_SIG)
return JWT_VRFY_INVALID_TOKEN;
ctx.jose = items[JWT_ELT_JOSE];
ctx.claims = items[JWT_ELT_CLAIMS];
ctx.signature = items[JWT_ELT_SIG];
/* "alg" is "none", the signature must be empty for the JWS to be valid. */
if (ctx.alg == JWS_ALG_NONE) {
return (ctx.signature.length == 0) ? JWT_VRFY_OK : JWT_VRFY_KO;
}
if (ctx.signature.length == 0)
return JWT_VRFY_INVALID_TOKEN;
decoded_sig = alloc_trash_chunk();
if (!decoded_sig)
return JWT_VRFY_OUT_OF_MEMORY;
ret = base64urldec(ctx.signature.start, ctx.signature.length,
decoded_sig->area, decoded_sig->size);
if (ret == -1) {
retval = JWT_VRFY_INVALID_TOKEN;
goto end;
}
decoded_sig->data = ret;
ctx.key = key->area;
ctx.key_length = key->data;
/* We have all three sections, signature calculation can begin. */
switch(ctx.alg) {
case JWS_ALG_HS256:
case JWS_ALG_HS384:
case JWS_ALG_HS512:
/* HMAC + SHA-XXX */
retval = jwt_jwsverify_hmac(&ctx, decoded_sig);
break;
case JWS_ALG_RS256:
case JWS_ALG_RS384:
case JWS_ALG_RS512:
case JWS_ALG_ES256:
case JWS_ALG_ES384:
case JWS_ALG_ES512:
/* RSASSA-PKCS1-v1_5 + SHA-XXX */
/* ECDSA using P-XXX and SHA-XXX */
retval = jwt_jwsverify_rsa_ecdsa(&ctx, decoded_sig);
break;
case JWS_ALG_PS256:
case JWS_ALG_PS384:
case JWS_ALG_PS512:
default:
/* RSASSA-PSS using SHA-XXX and MGF1 with SHA-XXX */
/* Not managed yet */
retval = JWT_VRFY_UNMANAGED_ALG;
break;
}
end:
free_trash_chunk(decoded_sig);
return retval;
}
static void jwt_deinit(void)
{
struct ebmb_node *node = NULL;
struct jwt_cert_tree_entry *entry = NULL;
node = ebmb_first(&jwt_cert_tree);
while (node) {
entry = ebmb_entry(node, struct jwt_cert_tree_entry, node);
ha_free(&entry);
node = ebmb_first(&jwt_cert_tree);
}
}
REGISTER_POST_DEINIT(jwt_deinit);
#endif /* USE_OPENSSL */