From 3925bb8efcb8be87755c8a60c0b5d52386e4092b Mon Sep 17 00:00:00 2001 From: Remi Tricot-Le Breton Date: Tue, 10 Mar 2026 14:43:45 +0100 Subject: [PATCH] MINOR: jwt: Add ecdh-es+axxxkw support in jwt_decrypt_jwk converter This builds on the ECDH-ES processing and simply requires an extra AES Key Wrap operation between the built key and the token's CEK. --- reg-tests/jwt/jwt_decrypt.vtc | 27 +++++++++- src/jwe.c | 95 ++++++++++++++++++++++++++--------- 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/reg-tests/jwt/jwt_decrypt.vtc b/reg-tests/jwt/jwt_decrypt.vtc index 3d15a690b..05d1948e0 100644 --- a/reg-tests/jwt/jwt_decrypt.vtc +++ b/reg-tests/jwt/jwt_decrypt.vtc @@ -95,7 +95,7 @@ haproxy h1 -conf { http-request set-var(txn.decrypted) var(txn.jwe),jwt_decrypt_jwk(txn.jwk) .if ssllib_name_startswith(AWS-LC) - acl aws_unmanaged var(txn.jwe),jwt_header_query('$.alg') -m str "A128KW" + acl aws_unmanaged var(txn.jwe),jwt_header_query('$.alg') -m end "A128KW" -m end "A192KW" http-request set-var(txn.decrypted) str("AWS-LC UNMANAGED") if aws_unmanaged .endif @@ -277,3 +277,28 @@ client c9 -connect ${h1_mainfe_sock} { expect resp.http.x-decrypted == "Random test message for ECDH-ES encrypted tokens" } -run + +# ECDH-ES+A___KW +client c10 -connect ${h1_mainfe_sock} { + + # ECDH-ES+A128KW + txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhDQkMtSFMyNTYiLCJlcGsiOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiJtc2poQktWNW5oNnBjdjhoRnR0UDlFVXRzaURzWG83T3RCekVZYkVJM1EwIiwieSI6IloxQ3FPQlEya1RNR1lENWdMUWJCaHB0MzRKRkR3dW5TX2ZzSmhsMlc1OWcifX0.5l7YaATvAWFJnWK_HsBPmawJ0RMqrkiwyZ9xAuiYCFSiqWWSr8D82A.0sa1s5V2RcDf0FW6hA1lig.z2DVLxtHeY1fPp6dJHiHEuHLVIQHQ10GfYXeFxwNE7JGyto-D3K1elHQn0Yq4Pitaheja21gnXkJajXhOA0rwQ.YmpToFWmj8XQrXMeXTa9eQ" \ + -hdr "X-JWK: {\"crv\":\"P-256\",\"d\":\"6qbbYYII1zqqmlDHhTwJt-JYBe-ELI02yAecAx-nD4w\",\"kty\":\"EC\",\"x\":\"bASil7YpthReLltIsaJCaRrE7XtLCRVtOpGtdPO0jH0\",\"y\":\"9xj9qfSrVKFqN3lnaNDXAclGGnfmU_j7xsEocZdYmPs\"}" + rxresp + expect resp.http.x-decrypted ~ "(Random test message for ECDH-ES encrypted tokens|AWS-LC UNMANAGED)" + + + # ECDH-ES+A192KW + txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0ExOTJLVyIsImVuYyI6IkExOTJDQkMtSFMzODQiLCJlcGsiOnsiY3J2IjoiUC0zODQiLCJrdHkiOiJFQyIsIngiOiJDcTd3Y0MzUm92VFRZSTMzLU9DcXBocjFlN1NzeEZWY0dOQXhEOEpWZHBRQmROaGg3Z2dLNTJKVkJ1RF9uZXVHIiwieSI6IjlaLU1MV09TQ3VZd0JZVTEtcTd2YUREWUZ1WFhqc1EwSmxpWllLVmdOU0dqVHVLY3VXQnJHemV2RzZEeGgyRHQifX0.75lt6Ixq6UhlN8uiaEphy8SiqEVsuD4Rc3QbFcmP7MJUTyt15LcZ3y-M7TJeNBh3Ajy_6K2WooU.cO9tUaQ2eVo0tIuOqb5_Bw.HQ6DqnLhW2Ad0c78WFGgwCStefYdL37xmh2Fa2mCsVNW5q0K3-xeDHYuIP9Q5xBYEY70U6wV5a0iVN87ii_iMA.feLteQh1ickYVJ2ZZ2whoVzNGRHgUpjp" \ + -hdr "X-JWK: {\"alg\":\"ECDH-ES+A192KW\",\"crv\":\"P-384\",\"d\":\"pj6xIezfwtUakkkLtbRQ9FmN6uN1YJ-TSBkWn4awuDfWiHgqpQHA7_L95Hjks1cK\",\"key_ops\":[\"wrapKey\",\"unwrapKey\"],\"kty\":\"EC\",\"x\":\"JO3ojbUYOzoSb-7lAy-c7VhDIjhEtg4zrPn_NJKuGhat-cuI1c4LvOj3n8p3j4bn\",\"y\":\"CA3i4pN7t6liWxQXyxdDp9t79B8uWuubGADJuGn_2_yl6pufhnQ30OBA590fOtEm\"}" + rxresp + expect resp.http.x-decrypted ~ "(Random test message for ECDH-ES\\+A192KW encrypted tokens|AWS-LC UNMANAGED)" + + + # ECDH-ES+A256KW + txreq -url "/jwk" -hdr "Authorization: Bearer eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJlcGsiOnsiY3J2IjoiUC01MjEiLCJrdHkiOiJFQyIsIngiOiJBTFZuZXN6Tl93WVJSWVYtblp3dy1sSkVDTXB2eE1iSENXX3BjY3EyWlF2eFdsNzVKdm5TM3lKbjgzcTE1MlpnWU4zTTB4SUhzQmw1empWZS02OGR4TThwIiwieSI6IkFUX2pGel94RGt0VFY4WWYzZlo1MnRvbE5QWkwwNXlwa0dVTThPWFRNZTBaaVNfYnIzaS0xNHFlWG1OcjA3TFFjNUZMX1VTQkE5WmlyWGRaZkVLUnFqNmEifX0.MqGFvMzpIlwQHeXgPucBkXmS2BaXr2ByUugzD31XrPtxwlWw96vOmfcjSHvda2FGJ1u6InaMMVZMMp75P6AF0kvk8vuM7QF2.kHYblcqwHgXv0xRQrLHwoA.gwFUyTx3RRHWvmqyUL5N6W8HcwbNc1hPTImQPoCNPv6rkhzV1obikVj7sNuTh3Po0nBu2QCKrt-GjJTlD4Q5kw.Q_YZWSkVVxv1rcpySgENN3ZPp-chIYoCGC070kkqiXc" \ + -hdr "X-JWK: {\"alg\":\"ECDH-ES+A256KW\",\"crv\":\"P-521\",\"d\":\"AGGLpIzSL1jE34wGa-owWCVt2rgk8j3jqh33QQFKwYCJ9abp3vROyQ-dNv6j6PjrnF1EFyY9dDzChNpWmzoOZAp3\",\"key_ops\":[\"wrapKey\",\"unwrapKey\"],\"kty\":\"EC\",\"x\":\"AD0EIUE6Bt_TDcyOPM6VchRocp7AFSeVd6XkVALWf8AFebeMgKIvJsCsGeRdPTO3vWWrR5AOvvpiBfurb9M9Tus-\",\"y\":\"AOeI5d0iF463g3DolhmVFn6MWk764ONuXRexLApjN-Q6_RkcnCieRSZzqqSPMYuEn-N3i4aYfiEPZV0jk8oZKQMQ\"}" + rxresp + expect resp.http.x-decrypted == "Random test message for ECDH-ES+A256KW encrypted tokens" + +} -run diff --git a/src/jwe.c b/src/jwe.c index 04a98301f..78c42b91c 100644 --- a/src/jwe.c +++ b/src/jwe.c @@ -39,9 +39,9 @@ typedef enum { JWE_ALG_A256KW, JWE_ALG_DIR, JWE_ALG_ECDH_ES, - // JWE_ALG_ECDH_ES_A128KW, - // JWE_ALG_ECDH_ES_A192KW, - // JWE_ALG_ECDH_ES_A256KW, + JWE_ALG_ECDH_ES_A128KW, + JWE_ALG_ECDH_ES_A192KW, + JWE_ALG_ECDH_ES_A256KW, JWE_ALG_A128GCMKW, JWE_ALG_A192GCMKW, JWE_ALG_A256GCMKW, @@ -59,9 +59,9 @@ struct alg_enc jwe_algs[] = { { "A256KW", JWE_ALG_A256KW }, { "dir", JWE_ALG_DIR }, { "ECDH-ES", JWE_ALG_ECDH_ES }, - { "ECDH-ES+A128KW", JWE_ALG_UNMANAGED }, - { "ECDH-ES+A192KW", JWE_ALG_UNMANAGED }, - { "ECDH-ES+A256KW", JWE_ALG_UNMANAGED }, + { "ECDH-ES+A128KW", JWE_ALG_ECDH_ES_A128KW }, + { "ECDH-ES+A192KW", JWE_ALG_ECDH_ES_A192KW }, + { "ECDH-ES+A256KW", JWE_ALG_ECDH_ES_A256KW }, { "A128GCMKW", JWE_ALG_A128GCMKW }, { "A192GCMKW", JWE_ALG_A192GCMKW }, { "A256GCMKW", JWE_ALG_A256GCMKW }, @@ -237,6 +237,9 @@ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jo switch (*alg) { case JWE_ALG_ECDH_ES: + case JWE_ALG_ECDH_ES_A128KW: + case JWE_ALG_ECDH_ES_A192KW: + case JWE_ALG_ECDH_ES_A256KW: ec = 1; break; case JWE_ALG_A128GCMKW: @@ -944,15 +947,25 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E int key_size = 0; struct buffer *derived_secret = NULL; struct buffer *otherinfo = NULL; + struct buffer *tmpbuf = NULL; const char *alg_id = NULL; + jwe_alg kw_alg = JWE_ALG_UNMANAGED; + int ecdhes = 0; + unsigned char *concatkdf_ptr = NULL; + size_t *concatkdf_len = 0; /* rfc7518#section-4.6.2 * Key derivation is performed using the Concat KDF, as defined in * Section 5.8.1 of [NIST.800-56A], where the Digest Method is SHA-256. */ const EVP_MD *md = EVP_sha256(); + int hashlen = EVP_MD_size(md); EVP_MD_CTX *ctx = NULL; + int keydatalen = 0; + int counter = 0; + int offset = 0; + int reps = 0; switch(crypt_alg) { case JWE_ALG_ECDH_ES: @@ -981,6 +994,18 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E if (!alg_id) goto end; break; + case JWE_ALG_ECDH_ES_A128KW: + key_size = 128; + kw_alg = JWE_ALG_A128KW; + break; + case JWE_ALG_ECDH_ES_A192KW: + key_size = 192; + kw_alg = JWE_ALG_A192KW; + break; + case JWE_ALG_ECDH_ES_A256KW: + key_size = 256; + kw_alg = JWE_ALG_A256KW; + break; default: goto end; } @@ -1011,34 +1036,50 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E /* Data derivation as in Section 5.8.1 of [NIST.800-56A] * https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf + * + * For ECDH-ES the buffer built after the concatKDF operation will be + * used directly to decrypt the contents. When ECDH-ES+AES Key Wrap is + * used we must wrap the cek with the built buffer using the right AES + * KW algorithm. */ - if (ecdhes) { - /* The decrypted cek to be used for actual data decrypt - * operation will be built in the following block. */ - int hashlen = EVP_MD_size(md); + if (!ecdhes) { + tmpbuf = alloc_trash_chunk(); + if (!tmpbuf) + goto end; + concatkdf_ptr = (unsigned char*)tmpbuf->area; + concatkdf_len = &tmpbuf->data; + } else { + concatkdf_ptr = (unsigned char*)decrypted_cek->area; + concatkdf_len = &decrypted_cek->data; + } - int keydatalen = (key_size >> 3); + /* The decrypted cek to be used for actual data decrypt + * operation will be built in the following block. */ + keydatalen = (key_size >> 3); + reps = keydatalen / hashlen; - int reps = keydatalen / hashlen; - int counter = 0; - int offset = 0; + for (counter = 0; counter <= reps; ++counter) { - for (counter = 0; counter <= reps; ++counter) { + uint32_t be_counter = htonl(counter+1); - uint32_t be_counter = htonl(counter+1); + if (EVP_DigestInit_ex(ctx, md, NULL) != 1 || + EVP_DigestUpdate(ctx, (char*)&be_counter, sizeof(be_counter)) != 1 || + EVP_DigestUpdate(ctx, b_orig(derived_secret), b_data(derived_secret)) != 1 || + EVP_DigestUpdate(ctx, b_orig(otherinfo), b_data(otherinfo)) != 1 || + EVP_DigestFinal_ex(ctx, concatkdf_ptr + offset, NULL) != 1) + goto end; - if (EVP_DigestInit_ex(ctx, md, NULL) != 1 || - EVP_DigestUpdate(ctx, (char*)&be_counter, sizeof(be_counter)) != 1 || - EVP_DigestUpdate(ctx, b_orig(derived_secret), b_data(derived_secret)) != 1 || - EVP_DigestUpdate(ctx, b_orig(otherinfo), b_data(otherinfo)) != 1 || - EVP_DigestFinal_ex(ctx, (unsigned char*)(decrypted_cek->area + offset), NULL) != 1) - goto end; + offset += hashlen; - offset += hashlen; + } - } + *concatkdf_len = keydatalen; - decrypted_cek->data = keydatalen; + if (!ecdhes) { + /* Need to used the previously generated key to wrap the CEK + * with the "A128KW", "A192KW", or "A256KW" algorithms. */ + if (!decrypt_cek_aeskw(cek, decrypted_cek, tmpbuf, kw_alg)) + goto end; } retval = 0; @@ -1046,6 +1087,7 @@ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, E end: free_trash_chunk(derived_secret); free_trash_chunk(otherinfo); + free_trash_chunk(tmpbuf); EVP_MD_CTX_free(ctx); return retval; } @@ -1860,6 +1902,9 @@ static int sample_conv_jwt_decrypt_jwk(const struct arg *args, struct sample *sm oct = 1; break; case JWE_ALG_ECDH_ES: + case JWE_ALG_ECDH_ES_A128KW: + case JWE_ALG_ECDH_ES_A192KW: + case JWE_ALG_ECDH_ES_A256KW: ec = 1; break; default: