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.
This commit is contained in:
Remi Tricot-Le Breton 2023-01-18 15:32:28 +01:00 committed by William Lallemand
parent 86aac23e6b
commit 5a8f02ae66
3 changed files with 113 additions and 15 deletions

22
reg-tests/jwt/build_token.py Executable file
View File

@ -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],"<alg> <json_to_sign> <priv_key>")
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))

View File

@ -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

View File

@ -18,6 +18,7 @@
#include <haproxy/openssl-compat.h>
#include <haproxy/base64.h>
#include <haproxy/jwt.h>
#include <haproxy/buf.h>
#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;
}