haproxy/src/jwt.c
Remi Tricot-Le Breton d544d33e10 BUG/MINOR: jwt: Memory leak if same key is used in multiple jwt_verify calls
If the same filename was specified in multiple calls of the jwt_verify
converter, we would have parsed the contents of the file every time it
was used instead of checking if the entry already existed in the tree.
This lead to memory leaks because we would not insert the duplicated
entry and we would not free it (as well as the EVP_PKEY it referenced).
We now check the return value of ebst_insert and free the current entry
if it is a duplicate of an existing entry.
The order in which the tree insert and the pkey parsing happen was also
switched in order to avoid parsing key files in case of duplicates.

Should be backported to 2.5.
2022-02-15 20:08:20 +01:00

386 lines
9.5 KiB
C

/*
* 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 (alg_len == sizeof("none")-1 && strcmp("none", alg_str) == 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;
entry = calloc(1, sizeof(*entry) + pathlen + 1);
if (!entry) {
memprintf(err, "%sunable to allocate memory (jwt_cert_tree_entry).\n", err && *err ? *err : "");
return -1;
}
memcpy(entry->path, path, pathlen + 1);
if (ebst_insert(&jwt_cert_tree, &entry->node) != &entry->node) {
free(entry);
return 0; /* Entry already in the tree */
}
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->pkey = pkey;
retval = 0;
}
end:
if (retval) {
/* Some error happened during pkey parsing, remove the already
* inserted node from the tree and free it.
*/
ebmb_delete(&entry->node);
free(entry);
}
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[EVP_MAX_MD_SIZE];
unsigned int signature_length = 0;
unsigned char *hmac_res = NULL;
enum jwt_vrfy_status retval = JWT_VRFY_KO;
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;
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 ebmb_node *eb;
struct jwt_cert_tree_entry *entry = NULL;
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)
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);
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);
ebmb_delete(node);
EVP_PKEY_free(entry->pkey);
ha_free(&entry);
node = ebmb_first(&jwt_cert_tree);
}
}
REGISTER_POST_DEINIT(jwt_deinit);
#endif /* USE_OPENSSL */