From 447a38f3872e1ffb9251813bd0848543d84ad5f1 Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Tue, 7 Mar 2023 17:43:57 +0100 Subject: [PATCH] MINOR: jwt: Add support for RSA-PSS signatures (PS256 algorithm) This patch adds the support for the PS algorithms when verifying JWT signatures (rsa-pss). It was not managed during the first implementation and previously raised an "Unmanaged algorithm" error. The tests use the same rsa signature as the plain rsa tests (RS256 ...) and the implementation simply adds a call to EVP_PKEY_CTX_set_rsa_padding in the function that manages rsa and ecdsa signatures. The signatures in the reg-test were built thanks to the PyJWT python library once again. --- doc/configuration.txt | 7 ++- reg-tests/jwt/jws_verify.vtc | 87 ++++++++++++++++++++++++++---------- src/jwt.c | 39 +++++++++++----- src/sample.c | 12 +---- 4 files changed, 95 insertions(+), 50 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index d437946151..2d5768980b 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -17867,9 +17867,8 @@ jwt_verify(,) 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. + processed (three dot-separated base64-url encoded strings). All the + algorithms mentioned in section 3.1 of RFC7518 are managed. 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 @@ -17894,7 +17893,7 @@ jwt_verify(,) | 0 | "Verification failure" | | 1 | "Verification success" | | -1 | "Unknown algorithm (not mentioned in RFC7518)" | - | -2 | "Unmanaged algorithm (PSXXX algorithm family)" | + | -2 | "Unmanaged algorithm" | | -3 | "Invalid token" | | -4 | "Out of memory" | | -5 | "Unknown certificate" | diff --git a/reg-tests/jwt/jws_verify.vtc b/reg-tests/jwt/jws_verify.vtc index d9a6328f3d..43d37c7552 100644 --- a/reg-tests/jwt/jws_verify.vtc +++ b/reg-tests/jwt/jws_verify.vtc @@ -16,7 +16,7 @@ feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL)'" feature cmd "command -v socat" feature ignore_unknown_macro -server s1 -repeat 22 { +server s1 -repeat 24 { rxreq txresp } -start @@ -39,6 +39,7 @@ haproxy h1 -conf { use_backend hsXXX_be if { path_beg /hs } use_backend rsXXX_be if { path_beg /rs } use_backend esXXX_be if { path_beg /es } + use_backend psXXX_be if { path_beg /ps } use_backend auth_bearer_be if { path /auth_bearer } default_backend dflt_be @@ -85,6 +86,20 @@ haproxy h1 -conf { http-response set-header x-jwt-verify-ES512 %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/es512-public.pem")] if { var(txn.jwt_alg) -m str "ES512" } server s1 ${s1_addr}:${s1_port} + backend psXXX_be + 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) -m beg "PS" } + + http-response set-header x-jwt-token %[var(txn.bearer)] + http-response set-header x-jwt-alg %[var(txn.jwt_alg)] + + http-response set-header x-jwt-verify-PS256 %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/rsa-public.pem")] if { var(txn.jwt_alg) -m str "PS256" } + http-response set-header x-jwt-verify-PS384 %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/rsa-public.pem")] if { var(txn.jwt_alg) -m str "PS384" } + http-response set-header x-jwt-verify-PS512 %[var(txn.bearer),jwt_verify(txn.jwt_alg,"${testdir}/rsa-public.pem")] if { var(txn.jwt_alg) -m str "PS512" } + server s1 ${s1_addr}:${s1_port} + # This backend will only be used to test the http_auth_bearer sample fetch. # No jwt_verify will then be performed. @@ -255,8 +270,45 @@ client c11 -connect ${h1_mainfe_sock} { expect resp.http.x-jwt-verify-ES512 == "1" } -run -# The following token is invalid (too short) + + client c12 -connect ${h1_mainfe_sock} { + # Token content : {"alg":"PS256","typ":"JWT"} + # {"sub":"1234567890","name":"John Doe","iat":1516239022} + # Token creation : ./build_token.py PS256 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' rsa-private.pem + txreq -url "/ps256" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.eXzN8m41ejgmbtJPhgifU_jMuYwVXL3HsLMOJ_ERipNcuqVQUmfHib1MWydSOYkgNBIm2lK9LjUmRCs1LvMUsbcqnokebFUNmO6IFdbMj3kD4cvqXHyK0yogQ7fdtJZf3_ukcJQ_-IdCG6mwowq6-OLjv-v2EflwPsT33EGmEDuE-7Z8AVTOVPiKUrqq1KqBi7NnnzdghqKfXn4b0yT7CnxQ_GK4F-ghBxBiMXK2J8M6pvS1vof7PyzVQmpeNzn2Rpbk-Ez88WeoTQXqZL1_BeW0z8FeyWXoIiqAzluRHSfZf2iUwrHuiH-tZ5BkAsJXHMDhMoL8_TKdD2hAnCWdVA9W9bQpzfaCbF5xv8lkGcy01ekrh-rN6ZOjItYeDj3BuaQgrKa5YAs_Grei_iSLqAu_YmDiVJxBfv5ahe1I8rwBQ7lIsZqv6p8BKqBFNylLzIFioAtmHJBF0HtItLoj0Mp_bUuU6RLIwf7C8ZWPQVTVsTgHMAlnZLNnQ3vhcxCjLm-r45M3AUFQfMEy1ajiqpFb3z2ElEwiOS9uLYJs3AOAoJDc-e62VJ7tRlw7KB-Vw0mvztvXgYdit48KOxdbn15HQ0lbBM_jJHvbYjDFC0iGUaizBPqmOJcTvObvKv5itEhPT6ffsv9XBnRSv9f3kW_rI7chrCyRZc0nFUvEJ9o" + rxresp + expect resp.status == 200 + expect resp.http.x-jwt-alg == "PS256" + expect resp.http.x-jwt-verify-PS256 == "1" +} -run + +client c13 -connect ${h1_mainfe_sock} { + # Token content : {"alg":"PS384","typ":"JWT"} + # {"sub":"1234567890","name":"John Doe","iat":1516239022} + # Token creation : ./build_token.py PS384 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' rsa-private.pem + txreq -url "/ps384" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzM4NCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.f-il5pRvC_vYuJ5jI-q9zxgqStCzvICKJyJEmVSjK47uLtt24SNLsQ1V24gqGuDOkXAhxlVu9rUwvtbzOQbF6N1YFFKbCuJ7zbGG81j5r3IuFl_5y6v077PW3hSNn62WX1GDv8w_kGedAZqGwKhJR7D1CbPBE5v-b4PskVF1V7IrFx8PufS_LUeJq1Etei0iU7H9OWD0yVApE_nmeELy4Kz1cc1fQZTBzd-b6kB562JbUbENM14HoiuKpnZvDtQks93A7y_B14SZPrxDaiVI-fR1n8Ja10wyBqbw8mWzt4s7vkxQI8U0eTBcj6bpWcm6S947G_jjoum_Lu3ZSKXE4UxcZ2IIuM74PEUgWJUjr4f9klB8kplJS5AIXMUNG6QbgZhOdSfZmlfzZUmSt1CLI22rTadXjvn-5CG_VxWJUjcPF9hViFFKQ7qQw3Tcn73ZKf5toK3imQBay4vR11DYWP5flLscFtqPvFcV4qhNarG-kVTI2xO8wXDXEoKeIXvsr3GTmCmBbs-kxvtyI80GUTzKN2I9vp0W9Qo5GNa3DDU1-io3olVwtMFh_0qfhmdO1Rt-j11gGnYTz3S5zMMMG2Ihy8ho3ayNZlZf7MJvVBSPqbCpHdiRa8VgTyYdYvK81lgkSc3wE8CygFEBMEi9b181OKPODlpux6k-3AL_2Hs" + rxresp + expect resp.status == 200 + expect resp.http.x-jwt-alg == "PS384" + expect resp.http.x-jwt-verify-PS384 == "1" +} -run + +client c14 -connect ${h1_mainfe_sock} { + # Token content : {"alg":"PS512","typ":"JWT"} + # {"sub":"1234567890","name":"John Doe","iat":1516239022} + # Token creation : ./build_token.py PS512 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' rsa-private.pem + txreq -url "/ps512" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.tJZQn0ksGx7vFpBzhNkP8vupyRiAAy5Rf6UdR2MEnO6-iwisbXOUrwwh8XQWngEe2O5FJabCxJRI_shSVEUuWY2Vz6kvRAQ6sWv_4uoPTUk9zjSXkS6C_nb_UY_6tUz39qA-OI80JKcLadvjB66CGWHI00C5Xz2gyWQuFgSItBIV6l0wI6Spf4NJa2Lefo7XbobQ7-u-yzgbIJ1BgXFOTWHYsgJ67n39gj7MDDsUjSaNbFlKfbvGJrdli5_PNNSdoNiF0pdsd6vldnucs5Rfysp4V-nbBzrORuJhl0_BlPG7_Wbap0sm6NCnzp1ks3D5_OWLZxJZNw_TJ2OuVHOX2PNj2MuHjMPDMKKxgxIXQJ8ry39-sk56ZrCJ8UqZofk8NX7Z4ypeWrK62BNSTLY8Le4WzF6dYcuawxiyt7xsC0MkaplXpRFLdmHrMhvyZz6S8BFhtlGD-PnRnEr8qZkThiZSs5kcEW8ryneKlN5TQ7E0H1HekUUii3_T9MtC5rNsT1vzyGr0XAn5TLxeal4Gvp3WyOHs4l7Q1EyQXPkAX8bWwODtLZ3DrREwdLb7Ex2k9wRDF52aww9EMpeLM3at6MQKggWQhNEClahN9AWBj7Vz-RqliWEIdUdNTL3d1JgLX41GZqXjOGZIwiVJwYpVRh1jKVhUn8pN8jCtoeiUxh8" + rxresp + expect resp.status == 200 + expect resp.http.x-jwt-alg == "PS512" + expect resp.http.x-jwt-verify-PS512 == "1" +} -run + + + +# The following token is invalid (too short) +client c15 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES512","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} # OpenSSL cmd : openssl dgst -sha512 -sign es512-private.pem data.txt | base64 | tr -d '=\n' | tr '/+' '_-' @@ -269,21 +321,8 @@ client c12 -connect ${h1_mainfe_sock} { expect resp.http.x-jwt-verify-ES512 == "-3" } -run - -# Unmanaged algorithm -client c13 -connect ${h1_mainfe_sock} { - # Token content : {"alg":"PS512","typ":"JWT"} - # {"sub":"1234567890","name":"John Doe","iat":1516239022} - txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MIGHAkEEPEgIrFKIDofBpFKX_mtya55QboGr09P6--v8uO85DwQWR0iKgMNSzYkL3K1lwyExG0Vtwfnife0lNe7Fn5TigAJCAY95NShiTn3tvleXVGCkkD0-HcribnMhd34QPGRc4rlwTkUg9umIUhxnEhPR--OohlmhJyIYGHuH8Ksm5f" - rxresp - expect resp.status == 200 - expect resp.http.x-jwt-alg == "PS512" - # Unmanaged algorithm - expect resp.http.x-jwt-verify == "-2" -} -run - # Unknown algorithm -client c14 -connect ${h1_mainfe_sock} { +client c16 -connect ${h1_mainfe_sock} { # Token content : {"alg":"UNKNOWN_ALG","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJVTktOT1dOX0FMRyIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MIGHAkEEPEgIrFKIDofBpFKX_mtya55QboGr09P6--v8uO85DwQWR0iKgMNSzYkL3K1lwyExG0Vtwfnife0lNe7Fn5TigAJCAY95NShiTn3tvleXVGCkkD0-HcribnMhd34QPGRc4rlwTkUg9umIUhxnEhPR--OohlmhJyIYGHuH8Ksm5f" @@ -295,7 +334,7 @@ client c14 -connect ${h1_mainfe_sock} { } -run # Invalid token (not enough fields) -client c15 -connect ${h1_mainfe_sock} { +client c17 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES512","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" @@ -307,7 +346,7 @@ client c15 -connect ${h1_mainfe_sock} { } -run # Invalid token (too many fields) -client c16 -connect ${h1_mainfe_sock} { +client c18 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES512","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MIGHAkEEPEgIrFKIDofBpFKX_mtya55QboGr09P6--v8uO85DwQWR0iKgMNSzYkL3K1lwyExG0Vtwfnife0lNe7Fn5TigAJCAY95NShiTn3tvleXVGCkkD0-HcribnMhd34QPGRc4rlwTkUg9umIUhxnEhPR--OohlmhJyIYGHuH8Ksm5f.unexpectedextrafield" @@ -319,7 +358,7 @@ client c16 -connect ${h1_mainfe_sock} { } -run # Invalid token (empty signature) -client c17 -connect ${h1_mainfe_sock} { +client c19 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES512","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} txreq -url "/errors" -hdr "Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." @@ -331,7 +370,7 @@ client c17 -connect ${h1_mainfe_sock} { } -run # Unknown certificate -client c18 -connect ${h1_mainfe_sock} { +client c20 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES512","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} # Key gen process : openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-521 -out es512-private.pem; openssl ec -in es512-private.pem -pubout -out es512-public.pem @@ -347,7 +386,7 @@ client c18 -connect ${h1_mainfe_sock} { # Test the http_auth_bearer special cases (other header than the default "Authorization" one) -client c19 -connect ${h1_mainfe_sock} { +client c21 -connect ${h1_mainfe_sock} { txreq -url "/auth_bearer" -hdr "Custom-Authorization: Bearer random_value" rxresp expect resp.status == 200 @@ -355,7 +394,7 @@ client c19 -connect ${h1_mainfe_sock} { } -run # Test the http_auth_bearer special cases (multiple spaces after the scheme) -client c20 -connect ${h1_mainfe_sock} { +client c22 -connect ${h1_mainfe_sock} { txreq -url "/auth_bearer" -hdr "Custom-Authorization: Bearer random_value" rxresp expect resp.status == 200 @@ -363,7 +402,7 @@ client c20 -connect ${h1_mainfe_sock} { } -run # Test the http_auth_bearer special cases (no value after the scheme) -client c21 -connect ${h1_mainfe_sock} { +client c23 -connect ${h1_mainfe_sock} { txreq -url "/auth_bearer" -hdr "Custom-Authorization: Bearer " rxresp expect resp.status == 200 @@ -371,7 +410,7 @@ client c21 -connect ${h1_mainfe_sock} { } -run # Test the http_auth_bearer special cases (no value after the scheme) -client c22 -connect ${h1_mainfe_sock} { +client c24 -connect ${h1_mainfe_sock} { txreq -url "/errors" -hdr "Authorization: Bearer " rxresp expect resp.status == 200 diff --git a/src/jwt.c b/src/jwt.c index b901588dbf..6c4cbd3102 100644 --- a/src/jwt.c +++ b/src/jwt.c @@ -275,10 +275,12 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat { const EVP_MD *evp = NULL; EVP_MD_CTX *evp_md_ctx; + EVP_PKEY_CTX *pkey_ctx = NULL; enum jwt_vrfy_status retval = JWT_VRFY_KO; struct ebmb_node *eb; struct jwt_cert_tree_entry *entry = NULL; int is_ecdsa = 0; + int padding = RSA_PKCS1_PADDING; switch(ctx->alg) { case JWS_ALG_RS256: @@ -303,6 +305,19 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat evp = EVP_sha512(); is_ecdsa = 1; break; + + case JWS_ALG_PS256: + evp = EVP_sha256(); + padding = RSA_PKCS1_PSS_PADDING; + break; + case JWS_ALG_PS384: + evp = EVP_sha384(); + padding = RSA_PKCS1_PSS_PADDING; + break; + case JWS_ALG_PS512: + evp = EVP_sha512(); + padding = RSA_PKCS1_PSS_PADDING; + break; default: break; } @@ -337,11 +352,14 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signat } } - 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; + if (EVP_DigestVerifyInit(evp_md_ctx, &pkey_ctx, evp, NULL, entry->pkey) == 1) { + if (is_ecdsa || EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding) > 0) { + if (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: @@ -420,16 +438,15 @@ enum jwt_vrfy_status jwt_verify(const struct buffer *token, const struct buffer 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-PKCS1-v1_5 + SHA-XXX */ + /* ECDSA using P-XXX and SHA-XXX */ /* RSASSA-PSS using SHA-XXX and MGF1 with SHA-XXX */ - + retval = jwt_jwsverify_rsa_ecdsa(&ctx, decoded_sig); + break; + default: /* Not managed yet */ retval = JWT_VRFY_UNMANAGED_ALG; break; diff --git a/src/sample.c b/src/sample.c index 2e04c74c9f..ce01b0f8bb 100644 --- a/src/sample.c +++ b/src/sample.c @@ -3867,19 +3867,9 @@ static int sample_conv_jwt_verify_check(struct arg *args, struct sample_conv *co 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: + if (alg == JWT_ALG_DEFAULT) { memprintf(err, "unknown JWT algorithm: %s", args[0].data.str.area); return 0; - - case JWS_ALG_PS256: - case JWS_ALG_PS384: - case JWS_ALG_PS512: - memprintf(err, "RSASSA-PSS JWS signing not managed yet"); - return 0; - - default: - break; } }