From 5a8f02ae6686c72e30bff7e5dbece67a5d438551 Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Wed, 18 Jan 2023 15:32:28 +0100 Subject: [PATCH] BUG/MEDIUM: jwt: Properly process ecdsa signatures (concatenated R and S params) When the JWT token signature is using ECDSA algorithm (ES256 for instance), the signature is a direct concatenation of the R and S parameters instead of OpenSSL's DER format (see section 3.4 of RFC7518). The code that verified the signatures wrongly assumed that they came in OpenSSL's format and it did not actually work. We now have the extra step of converting the signature into a complete ECDSA_SIG that can be fed into OpenSSL's digest verification functions. The ECDSA signatures in the regtest had to be recalculated and it was made via the PyJWT python library so that we don't end up checking signatures that we built ourselves anymore. This patch should fix GitHub issue #2001. It should be backported up to branch 2.5. --- reg-tests/jwt/build_token.py | 22 +++++++++ reg-tests/jwt/jws_verify.vtc | 20 ++++----- src/jwt.c | 86 +++++++++++++++++++++++++++++++++--- 3 files changed, 113 insertions(+), 15 deletions(-) create mode 100755 reg-tests/jwt/build_token.py diff --git a/reg-tests/jwt/build_token.py b/reg-tests/jwt/build_token.py new file mode 100755 index 000000000..2f368abf9 --- /dev/null +++ b/reg-tests/jwt/build_token.py @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# JWT package can be installed via 'pip install pyjwt' command + +import sys +import jwt +import json + +if len(sys.argv) != 4: + print(sys.argv[0]," ") + quit() + + +alg=sys.argv[1] +json_to_sign=sys.argv[2] +priv_key_file=sys.argv[3] + +with open(priv_key_file) as file: + priv_key = file.read() + +print(jwt.encode(json.loads(json_to_sign),priv_key,algorithm=alg)) + diff --git a/reg-tests/jwt/jws_verify.vtc b/reg-tests/jwt/jws_verify.vtc index d8afcae5b..3aaf8d8b7 100644 --- a/reg-tests/jwt/jws_verify.vtc +++ b/reg-tests/jwt/jws_verify.vtc @@ -220,9 +220,9 @@ client c9 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES256","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} # Key gen process : openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out es256-private.pem; openssl ec -in es256-private.pem -pubout -out es256-public.pem - # OpenSSL cmd : openssl dgst -sha256 -sign es256-private.pem data.txt | base64 | tr -d '=\n' | tr '/+' '_-' + # Token creation : ./build_token.py ES256 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' es256-private.pem - txreq -url "/es256" -hdr "Authorization: Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MEYCIQCkHcfMhzhP3FvZqjaqEDW89_5QEhBwUvpXv535lAnRuQIhALc62LiFZz0oDuKeqI3ogto336D7kEg4Uat8qm_iW6ur" + txreq -url "/es256" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.pNI_c5mHE3mLV0YDpstlP4l3t5XARLl6OmcKLuvF5r60m-C63mbgfKWdPjmJPMTCmX_y50YW_v2SKw0ju0tJHw" rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES256" @@ -233,9 +233,9 @@ client c10 -connect ${h1_mainfe_sock} { # Token content : {"alg":"ES384","typ":"JWT"} # {"sub":"1234567890","name":"John Doe","iat":1516239022} # Key gen process : openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out es384-private.pem; openssl ec -in es384-private.pem -pubout -out es384-public.pem - # OpenSSL cmd : openssl dgst -sha384 -sign es384-private.pem data.txt | base64 | tr -d '=\n' | tr '/+' '_-' + # Token creation : ./build_token.py ES384 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' es384-private.pem - txreq -url "/es384" -hdr "Authorization: Bearer eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MGUCMQDQFs6fqnmoxbw3eIQCT6km0TnMakpGy2F-8ZgGu5G8nFQKzCAO-V-UTOD0OqxHUa8CMBqHfZ6pjqRaLK-PebsvbGSzneAG7Id3oN78n2wWGKcYCI_s0KSO88thboaR9AS4tA" + txreq -url "/es384" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cs59CQiCI_Pl8J-PKQ2y73L5IJascZXkf7MfRXycO1HkT9pqDW2bFr1bh7pFyPA85GaML4BPYVH_zDhcmjSMn_EIvUV8cPDuuUu69Au7n9LYGVkVJ-k7qN4DAR5eLCiU" rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES384" @@ -246,9 +246,9 @@ client c11 -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 - # OpenSSL cmd : openssl dgst -sha512 -sign es512-private.pem data.txt | base64 | tr -d '=\n' | tr '/+' '_-' + # Token creation : ./build_token.py ES512 '{"sub":"1234567890","name":"John Doe","iat":1516239022}' es512-private.pem - txreq -url "/es512" -hdr "Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.MIGHAkEEPEgIrFKIDofBpFKX_mtya55QboGr09P6--v8uO85DwQWR0iKgMNSzYkL3K1lwyExG0Vtwfnife0lNe7Fn5TigAJCAY95NShiTn3tvleXVGCkkD0-HcribnMhd34QPGRc4rlwTkUg9umIUhxnEhPR--OohlmhJyIYGHuH8Ksm5fSIWfRa" + txreq -url "/es512" -hdr "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.AJcyt0OYf2wg7SggJJVKYysLUkBQA0f0Zc0EbKgud2fQLeT65n42A9l9hhGje79VLWhEyisQmDpFXTpfFXeD_NiaAXyNnX5b8TbZALqxbjx8iIpbcObgUh_g5Gi81bKmRmfXUHW7L5iAwoNjYbUpXGipCpCD0N6-8zCrjcFD2UX01f0Y" rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES512" @@ -301,7 +301,7 @@ client c15 -connect ${h1_mainfe_sock} { rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES512" - # Unmanaged algorithm + # Invalid token expect resp.http.x-jwt-verify == "-3" } -run @@ -313,7 +313,7 @@ client c16 -connect ${h1_mainfe_sock} { rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES512" - # Unmanaged algorithm + # Invalid token expect resp.http.x-jwt-verify == "-3" } -run @@ -325,7 +325,7 @@ client c17 -connect ${h1_mainfe_sock} { rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES512" - # Unmanaged algorithm + # Invalid token expect resp.http.x-jwt-verify == "-3" } -run @@ -340,7 +340,7 @@ client c18 -connect ${h1_mainfe_sock} { rxresp expect resp.status == 200 expect resp.http.x-jwt-alg == "ES512" - # Unmanaged algorithm + # Unknown certificate expect resp.http.x-jwt-verify == "-5" } -run diff --git a/src/jwt.c b/src/jwt.c index 7f20e374b..a17af1847 100644 --- a/src/jwt.c +++ b/src/jwt.c @@ -18,6 +18,7 @@ #include #include #include +#include #ifdef USE_OPENSSL @@ -213,32 +214,94 @@ jwt_jwsverify_hmac(const struct jwt_ctx *ctx, const struct buffer *decoded_signa return retval; } +/* + * Convert a JWT ECDSA signature (R and S parameters concatenatedi, see section + * 3.4 of RFC7518) into an ECDSA_SIG that can be fed back into OpenSSL's digest + * verification functions. + * Returns 0 in case of success. + */ +static int convert_ecdsa_sig(const struct jwt_ctx *ctx, EVP_PKEY *pkey, struct buffer *signature) +{ + int retval = 0; + ECDSA_SIG *ecdsa_sig = NULL; + BIGNUM *ec_R = NULL, *ec_S = NULL; + unsigned int bignum_len; + unsigned char *p; + + ecdsa_sig = ECDSA_SIG_new(); + if (!ecdsa_sig) { + retval = JWT_VRFY_OUT_OF_MEMORY; + goto end; + } + + if (b_data(signature) % 2) { + retval = JWT_VRFY_INVALID_TOKEN; + goto end; + } + + bignum_len = b_data(signature) / 2; + + ec_R = BN_bin2bn((unsigned char*)b_orig(signature), bignum_len, NULL); + ec_S = BN_bin2bn((unsigned char *)(b_orig(signature) + bignum_len), bignum_len, NULL); + + if (!ec_R || !ec_S) { + retval = JWT_VRFY_INVALID_TOKEN; + goto end; + } + + /* Build ecdsa out of R and S values. */ + ECDSA_SIG_set0(ecdsa_sig, ec_R, ec_S); + + p = (unsigned char*)signature->area; + + signature->data = i2d_ECDSA_SIG(ecdsa_sig, &p); + if (signature->data == 0) { + retval = JWT_VRFY_INVALID_TOKEN; + goto end; + } + +end: + ECDSA_SIG_free(ecdsa_sig); + 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) +jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, struct buffer *decoded_signature) { const EVP_MD *evp = NULL; EVP_MD_CTX *evp_md_ctx; enum jwt_vrfy_status retval = JWT_VRFY_KO; struct ebmb_node *eb; struct jwt_cert_tree_entry *entry = NULL; + int is_ecdsa = 0; 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: + evp = EVP_sha512(); + break; + + case JWS_ALG_ES256: + evp = EVP_sha256(); + is_ecdsa = 1; + break; + case JWS_ALG_ES384: + evp = EVP_sha384(); + is_ecdsa = 1; + break; case JWS_ALG_ES512: evp = EVP_sha512(); + is_ecdsa = 1; break; default: break; } @@ -261,9 +324,22 @@ jwt_jwsverify_rsa_ecdsa(const struct jwt_ctx *ctx, const struct buffer *decoded_ goto end; } - if (EVP_DigestVerifyInit(evp_md_ctx, NULL, evp, NULL,entry-> pkey) == 1 && + /* + * ECXXX signatures are a direct concatenation of the (R, S) pair and + * need to be converted back to asn.1 in order for verify operations to + * work with OpenSSL. + */ + if (is_ecdsa) { + int conv_retval = convert_ecdsa_sig(ctx, entry->pkey, decoded_signature); + if (retval != 0) { + retval = conv_retval; + 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 && + 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; }