mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-08-14 02:57:01 +02:00
Replace the test based on the enum value of the algorithm by an explicit switch statement in case someone reorders it for some reason (while still managing not to break the regtest).
392 lines
9.5 KiB
C
392 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 (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 */
|