]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: jwt: Add ecdh-es+axxxkw support in jwt_decrypt_jwk converter
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Tue, 10 Mar 2026 13:43:45 +0000 (14:43 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Tue, 10 Mar 2026 13:58:47 +0000 (14:58 +0100)
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
src/jwe.c

index 3d15a690b22d4b5284bd0350c69605050553580c..05d1948e0b881362770e86a2445980a4db991ab8 100644 (file)
@@ -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
index 04a98301f80ee8cf2ddda1fe1b4bb4069e76fb16..78c42b91c2ec6e088813e0acad6e2b97455c03af 100644 (file)
--- 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: