haproxy/src/jwt.c
Remi Tricot-Le Breton 130e142ee2 MEDIUM: jwt: Add jwt_verify converter to verify JWT integrity
This new converter takes a JSON Web Token, an algorithm (among the ones
specified for JWS tokens in RFC 7518) and a public key or a secret, and
it returns a verdict about the signature contained in the token. It does
not simply return a boolean because some specific error cases cas be
specified by returning an integer instead, such as unmanaged algorithms
or invalid tokens. This enables to distinguich malformed tokens from
tampered ones, that would be valid format-wise but would have a bad
signature.
This converter does not perform a full JWT validation as decribed in
section 7.2 of RFC 7519. For instance it does not ensure that the header
and payload parts of the token are completely valid JSON objects because
it would need a complete JSON parser. It only focuses on the signature
and checks that it matches the token's contents.
2021-10-14 16:38:14 +02:00

360 lines
9 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 &&
(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;
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;
decoded_sig->data = base64urldec(ctx.signature.start, ctx.signature.length,
decoded_sig->area, decoded_sig->size);
if (decoded_sig->data == (unsigned int)-1) {
retval = JWT_VRFY_INVALID_TOKEN;
goto end;
}
ctx.key = key->area;
ctx.key_length = key->data;
/* We have all three sections, signature calculation can begin. */
if (ctx.alg <= JWS_ALG_HS512) {
/* HMAC + SHA-XXX */
retval = jwt_jwsverify_hmac(&ctx, decoded_sig);
} else if (ctx.alg <= JWS_ALG_ES512) {
/* RSASSA-PKCS1-v1_5 + SHA-XXX */
/* ECDSA using P-XXX and SHA-XXX */
retval = jwt_jwsverify_rsa_ecdsa(&ctx, decoded_sig);
} else if (ctx.alg <= JWS_ALG_PS512) {
/* RSASSA-PSS using SHA-XXX and MGF1 with SHA-XXX */
/* Not managed yet */
retval = JWT_VRFY_UNMANAGED_ALG;
}
end:
free_trash_chunk(decoded_sig);
return retval;
}
#endif /* USE_OPENSSL */