diff --git a/doc/configuration.txt b/doc/configuration.txt index 465e326db..982d9b374 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -16637,6 +16637,56 @@ jwt_payload_query([],[]) Please note that this converter is only available when HAProxy has been compiled with USE_OPENSSL. +jwt_verify(,) + Performs a signature verification for the JSON Web Token (JWT) given in input + by using the algorithm and the 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, should be the secret used + in the HMAC signature calculation. Otherwise, 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([,]) 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 diff --git a/include/haproxy/jwt-t.h b/include/haproxy/jwt-t.h index 4189e6506..3e7d57757 100644 --- a/include/haproxy/jwt-t.h +++ b/include/haproxy/jwt-t.h @@ -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 */ diff --git a/include/haproxy/jwt.h b/include/haproxy/jwt.h index 6ae6e6380..a343ffaf7 100644 --- a/include/haproxy/jwt.h +++ b/include/haproxy/jwt.h @@ -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 */ diff --git a/src/jwt.c b/src/jwt.c index 0f2e00c49..fd4626215 100644 --- a/src/jwt.c +++ b/src/jwt.c @@ -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 that was signed via algorithm using the + * (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 */ diff --git a/src/sample.c b/src/sample.c index 7b7843304..de45245e9 100644 --- a/src/sample.c +++ b/src/sample.c @@ -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 }, }};