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.
This commit is contained in:
Remi Tricot-Le Breton 2021-10-01 15:36:58 +02:00 committed by William Lallemand
parent 0a72f5ee7c
commit 130e142ee2
5 changed files with 311 additions and 0 deletions

View File

@ -16637,6 +16637,56 @@ jwt_payload_query([<json_path>],[<output_type>])
Please note that this converter is only available when HAProxy has been
compiled with USE_OPENSSL.
jwt_verify(<alg>,<key>)
Performs a signature verification for the JSON Web Token (JWT) given in input
by using the <alg> algorithm and the <key> parameter, which should either
hold a secret or a path to a public certificate. Returns 1 in cae of
verification success. See below for a full list of the possible return
values.
For now, only JWS tokens using the Compact Serialization format can be
processed (three dot-separated base64-url encoded strings). Among the
accepted algorithms for a JWS (see section 3.1 of RFC7518), the PSXXX ones
are not managed yet.
If the used algorithm is of the HMAC family, <key> should be the secret used
in the HMAC signature calculation. Otherwise, <key> should be the path to the
public certificate that can be used to validate the token's signature. All
the certificates that might be used to verify JWTs must be known during init
in order to be added into a dedicated certificate cache so that no disk
access is required during runtime. For this reason, any used certificate must
be mentioned explicitely at least once in a jwt_verify call. Passing an
intermediate variable as second parameter is then not advised.
This converter only verifies the signature of the token and does not perform
a full JWT validation as specified in section 7.2 of RFC7519. We do not
ensure that the header and payload contents are fully valid JSON's once
decoded for instance, and no checks are performed regarding their respective
contents.
The possible return values are the following :
+----+---------------------------------------------------------------------------+
| ID | message |
+----+---------------------------------------------------------------------------+
| 0 | "Verification failure" |
| 1 | "Verification sucess" |
| 2 | "Unknown algorithm (not mentioned in RFC7518)" |
| 3 | "Unmanaged algorithm (PSXXX algorithm family)" |
| 4 | "Invalid token" |
| 5 | "Out of memory" |
| 6 | "Unknown certificate" |
+----+---------------------------------------------------------------------------+
Please note that this converter is only available when HAProxy has been
compiled with USE_OPENSSL.
Example:
# Get a JWT from the authorization header, extract the "alg" field of its
# JOSE header and use a public certificate to verify a signature
http-request set-var(txn.bearer) http_auth_bearer
http-request set-var(txn.jwt_alg) var(txn.bearer),jwt_header_query('$.alg')
http-request deny unless { var(txn.jwt_alg) "RS256" }
http-request deny unless { var(txn.bearer),jwt_verify(txn.jwt_alg,"/path/to/crt.pem") 1 }
language(<value>[,<default>])
Returns the value with the highest q-factor from a list as extracted from the
"accept-language" header using "req.fhdr". Values with no q-factor have a

View File

@ -67,6 +67,17 @@ struct jwt_cert_tree_entry {
struct ebmb_node node;
char path[VAR_ARRAY];
};
enum jwt_vrfy_status {
JWT_VRFY_KO = 0,
JWT_VRFY_OK = 1,
JWT_VRFY_UNKNOWN_ALG,
JWT_VRFY_UNMANAGED_ALG,
JWT_VRFY_INVALID_TOKEN,
JWT_VRFY_OUT_OF_MEMORY,
JWT_VRFY_UNKNOWN_CERT
};
#endif /* USE_OPENSSL */

View File

@ -29,6 +29,9 @@
enum jwt_alg jwt_parse_alg(const char *alg_str, unsigned int alg_len);
int jwt_tokenize(const struct buffer *jwt, struct jwt_item *items, unsigned int *item_num);
int jwt_tree_load_cert(char *path, int pathlen, char **err);
enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer *alg,
const struct buffer *key);
#endif /* USE_OPENSSL */
#endif /* _HAPROXY_JWT_H */

192
src/jwt.c
View File

@ -165,4 +165,196 @@ int jwt_tree_load_cert(char *path, int pathlen, char **err)
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 */

View File

@ -3495,6 +3495,60 @@ static int sample_conv_json_query(const struct arg *args, struct sample *smp, vo
}
#ifdef USE_OPENSSL
static int sample_conv_jwt_verify_check(struct arg *args, struct sample_conv *conv,
const char *file, int line, char **err)
{
vars_check_arg(&args[0], NULL);
vars_check_arg(&args[1], NULL);
if (args[0].type == ARGT_STR) {
enum jwt_alg alg = jwt_parse_alg(args[0].data.str.area, args[0].data.str.data);
switch(alg) {
case JWT_ALG_DEFAULT:
memprintf(err, "unknown JWT algorithm : %s", *err);
break;
case JWS_ALG_PS256:
case JWS_ALG_PS384:
case JWS_ALG_PS512:
memprintf(err, "RSASSA-PSS JWS signing not managed yet");
break;
default:
break;
}
}
if (args[1].type == ARGT_STR) {
jwt_tree_load_cert(args[1].data.str.area, args[1].data.str.data, err);
}
return 1;
}
/* Check that a JWT's signature is correct */
static int sample_conv_jwt_verify(const struct arg *args, struct sample *smp, void *private)
{
struct sample alg_smp, key_smp;
smp->data.type = SMP_T_SINT;
smp->data.u.sint = 0;
smp_set_owner(&alg_smp, smp->px, smp->sess, smp->strm, smp->opt);
smp_set_owner(&key_smp, smp->px, smp->sess, smp->strm, smp->opt);
if (!sample_conv_var2smp_str(&args[0], &alg_smp))
return 0;
if (!sample_conv_var2smp_str(&args[1], &key_smp))
return 0;
smp->data.u.sint = jwt_verify(&smp->data.u.str, &alg_smp.data.u.str,
&key_smp.data.u.str);
return 1;
}
/*
* Returns the decoded header or payload of a JWT if no parameter is given, or
* the value of the specified field of the corresponding JWT subpart if a
@ -4091,6 +4145,7 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, {
/* JSON Web Token converters */
{ "jwt_header_query", sample_conv_jwt_header_query, ARG2(0,STR,STR), sample_conv_jwt_query_check, SMP_T_BIN, SMP_T_ANY },
{ "jwt_payload_query", sample_conv_jwt_payload_query, ARG2(0,STR,STR), sample_conv_jwt_query_check, SMP_T_BIN, SMP_T_ANY },
{ "jwt_verify", sample_conv_jwt_verify, ARG2(2,STR,STR), sample_conv_jwt_verify_check, SMP_T_BIN, SMP_T_SINT },
#endif
{ NULL, NULL, 0, 0, 0 },
}};