mirror of
https://github.com/haproxy/haproxy.git
synced 2026-02-18 18:19:39 -05:00
MINOR: jwt: Add new jwt_decrypt converter
This converter takes a private key in the JWK format (RFC7517) that can be provided as a string of via a variable. The only keys managed for now are of type 'RSA' or 'oct'.
This commit is contained in:
parent
0023596164
commit
c98002337c
2 changed files with 354 additions and 2 deletions
|
|
@ -20595,6 +20595,7 @@ ip.ver binary integer
|
|||
ipmask(mask4[,mask6]) address address
|
||||
json([input-code]) string string
|
||||
json_query(json_path[,output_type]) string _outtype_
|
||||
jwk_decrypt(<jwk>) string binary
|
||||
jwt_decrypt_cert(<cert>) string binary
|
||||
jwt_decrypt_secret(<secret>) string binary
|
||||
jwt_header_query([json_path[,output_type]]) string string
|
||||
|
|
@ -21424,6 +21425,44 @@ json_query(<json_path>[,<output_type>])
|
|||
# get the value of the key 'iss' from a JWT Bearer token
|
||||
http-request set-var(txn.token_payload) req.hdr(Authorization),word(2,.),ub64dec,json_query('$.iss')
|
||||
|
||||
jwk_decrypt(<jwk>)
|
||||
Performs a signature validation of a JSON Web Token following the JSON Web
|
||||
Encryption format (see RFC 7516) given in input and return its content
|
||||
decrypted thanks to the provided JSON Web Key (RFC7517).
|
||||
The <jwk> parameter must be a valid JWK of type 'oct' or 'RSA' ('kty' field
|
||||
of the JSON key) that can be provided either as a string or via a variable.
|
||||
|
||||
The only tokens managed yet are the ones using the Compact Serialization
|
||||
format (five dot-separated base64-url encoded strings).
|
||||
|
||||
This converter can be used to decode token that have a symmetric-type
|
||||
algorithm ("alg" field of the JOSE header) among the following: A128KW,
|
||||
A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW, dir. In this case, we expect
|
||||
the provided JWK to be of the 'oct' type. Please note that the A128KW and
|
||||
A192KW algorithms are not available on AWS-LC and decryption will not work.
|
||||
This converter also manages tokens that have an algorithm ("alg" field of
|
||||
the JOSE header) among the following: RSA1_5, RSA-OAEP or RSA-OAEP-256. In
|
||||
such a case an 'RSA' type JWK representing a private key must be provided.
|
||||
|
||||
The JWE token must be provided base64url-encoded and the output will be
|
||||
provided "raw". If an error happens during token parsing, signature
|
||||
verification or content decryption, an empty string will be returned.
|
||||
|
||||
Because of the way quotes, commas and double quotes are treated in the
|
||||
configuration, the contents of the JWK must be properly escaped for this
|
||||
converter to work properly (see section 2.2 for more information).
|
||||
|
||||
Example:
|
||||
# Get a JWT from the authorization header, put its decrypted content in an
|
||||
# HTTP header
|
||||
http-request set-var(txn.bearer) http_auth_bearer
|
||||
http-request set-header X-Decrypted %[var(txn.bearer),jwt_decrypt_secret(\'{\"kty\":\"oct\",\"k\":\"wAsgsg\"}\')
|
||||
|
||||
# or via a variable
|
||||
http-request set-var(txn.bearer) http_auth_bearer
|
||||
http-request set-var(txn.jwk) str(\'{\"kty\":\"oct\",\"k\":\"Q-NFLlghQ\"}\')
|
||||
http-request set-header X-Decrypted %[var(txn.bearer),jwt_decrypt_secret(txn.jwk)
|
||||
|
||||
jwt_decrypt_cert(<cert>)
|
||||
Performs a signature validation of a JSON Web Token following the JSON Web
|
||||
Encryption format (see RFC 7516) given in input and return its content
|
||||
|
|
@ -31730,8 +31769,8 @@ ocsp-update [ off | on ]
|
|||
|
||||
jwt [ off | on ]
|
||||
Allow for this certificate to be used for JWT validation or decryption via
|
||||
the "jwt_verify_cert" or "jwt_decrypt_cert" converters when set to 'on'. Its
|
||||
value defaults to 'off'.
|
||||
the "jwt_verify_cert", "jwt_decrypt_cert" or "jwt_decrypt" converters when
|
||||
set to 'on'. Its value defaults to 'off'.
|
||||
|
||||
When set to 'on' for a given certificate, the CLI command "del ssl cert" will
|
||||
not work. In order to be deleted, a certificate must not be used, either for
|
||||
|
|
|
|||
313
src/jwe.c
313
src/jwe.c
|
|
@ -1118,10 +1118,323 @@ end:
|
|||
}
|
||||
|
||||
|
||||
|
||||
typedef enum {
|
||||
JWK_KTY_OCT,
|
||||
JWK_KTY_RSA,
|
||||
// JWK_KTY_EC
|
||||
} jwk_type;
|
||||
|
||||
struct jwk {
|
||||
jwk_type type;
|
||||
struct buffer *kid;
|
||||
union {
|
||||
EVP_PKEY *pkey;
|
||||
struct buffer *secret;
|
||||
};
|
||||
};
|
||||
|
||||
static void clear_jwk(struct jwk *jwk)
|
||||
{
|
||||
if (!jwk)
|
||||
return;
|
||||
|
||||
free_trash_chunk(jwk->kid);
|
||||
jwk->kid = NULL;
|
||||
|
||||
switch (jwk->type) {
|
||||
case JWK_KTY_OCT:
|
||||
free_trash_chunk(jwk->secret);
|
||||
jwk->secret = NULL;
|
||||
break;
|
||||
case JWK_KTY_RSA:
|
||||
EVP_PKEY_free(jwk->pkey);
|
||||
jwk->pkey = NULL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Convert a JWK in buffer <jwk_buf> into either an RSA private key stored in an
|
||||
* EVP_PKEY or a secret (for symmetric algorithms).
|
||||
* Returns 0 in case of success, 1 otherwise.
|
||||
*/
|
||||
static int process_jwk(struct buffer *jwk_buf, struct jwk *jwk)
|
||||
{
|
||||
struct buffer *kty = NULL;
|
||||
int retval = 1;
|
||||
|
||||
kty = get_trash_chunk();
|
||||
if (get_jwk_field(jwk_buf, "$.kty", kty))
|
||||
goto end;
|
||||
|
||||
/* Look for optional "kid" field */
|
||||
jwk->kid = alloc_trash_chunk();
|
||||
if (!jwk->kid)
|
||||
goto end;
|
||||
get_jwk_field(jwk_buf, "$.kid", jwk->kid);
|
||||
|
||||
if (chunk_strcmp(kty, "oct") == 0) {
|
||||
struct buffer *tmpbuf = get_trash_chunk();
|
||||
int size = 0;
|
||||
|
||||
jwk->type = JWK_KTY_OCT;
|
||||
|
||||
jwk->secret = alloc_trash_chunk();
|
||||
if (!jwk->secret)
|
||||
goto end;
|
||||
|
||||
if (get_jwk_field(jwk_buf, "$.k", tmpbuf))
|
||||
goto end;
|
||||
|
||||
size = base64urldec(b_orig(tmpbuf), b_data(tmpbuf),
|
||||
b_orig(jwk->secret), b_size(jwk->secret));
|
||||
if (size < 0) {
|
||||
goto end;
|
||||
}
|
||||
jwk->secret->data = size;
|
||||
|
||||
} else if (chunk_strcmp(kty, "RSA") == 0) {
|
||||
jwk->type = JWK_KTY_RSA;
|
||||
|
||||
if (build_RSA_PKEY_from_buf(jwk_buf, &jwk->pkey))
|
||||
goto end;
|
||||
} else
|
||||
goto end;
|
||||
|
||||
retval = 0;
|
||||
|
||||
end:
|
||||
if (retval)
|
||||
clear_jwk(jwk);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
static int sample_conv_jwt_decrypt_check(struct arg *args, struct sample_conv *conv,
|
||||
const char *file, int line, char **err)
|
||||
{
|
||||
vars_check_arg(&args[0], NULL);
|
||||
|
||||
if (args[0].type == ARGT_STR) {
|
||||
EVP_PKEY *pkey = NULL;
|
||||
struct buffer *trash = get_trash_chunk();
|
||||
|
||||
if (get_jwk_field(&args[0].data.str, "$.kty", trash) == 0) {
|
||||
if (chunk_strcmp(trash, "oct") == 0) {
|
||||
struct buffer *key = get_trash_chunk();
|
||||
if (get_jwk_field(&args[0].data.str, "$.k", key)) {
|
||||
memprintf(err, "Missing 'k' field in JWK");
|
||||
return 0;
|
||||
}
|
||||
} else if (chunk_strcmp(trash, "RSA") == 0) {
|
||||
if (build_RSA_PKEY_from_buf(&args[0].data.str, &pkey)) {
|
||||
memprintf(err, "Failed to parse JWK");
|
||||
return 0;
|
||||
}
|
||||
EVP_PKEY_free(pkey);
|
||||
} else {
|
||||
memprintf(err, "Unmanaged key type (expected 'oct' or 'RSA'");
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
memprintf(err, "Missing key type (expected 'oct' or 'RSA')");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Decrypt the contents of a JWE token thanks to the user-provided JWK that can
|
||||
* either contain an RSA private key or a secret.
|
||||
* Returns the decrypted contents, or nothing if any error happened.
|
||||
*/
|
||||
static int sample_conv_jwt_decrypt(const struct arg *args, struct sample *smp, void *private)
|
||||
{
|
||||
struct buffer *input = NULL;
|
||||
unsigned int item_num = JWE_ELT_MAX;
|
||||
struct sample jwk_smp;
|
||||
struct jwt_item items[JWE_ELT_MAX] = {};
|
||||
struct buffer *decoded_items[JWE_ELT_MAX] = {};
|
||||
jwe_alg alg = JWE_ALG_UNMANAGED;
|
||||
jwe_enc enc = JWE_ENC_UNMANAGED;
|
||||
int size = 0;
|
||||
int rsa = 0;
|
||||
int dir = 0;
|
||||
int gcm = 0;
|
||||
int oct = 0;
|
||||
int retval = 0;
|
||||
struct buffer **cek = NULL;
|
||||
struct buffer *decrypted_cek = NULL;
|
||||
struct buffer *out = NULL;
|
||||
struct jose_fields fields = {};
|
||||
|
||||
struct buffer *alg_tag = NULL;
|
||||
struct buffer *alg_iv = NULL;
|
||||
|
||||
struct buffer *jwk_buf = NULL;
|
||||
struct jwk jwk = {};
|
||||
|
||||
smp_set_owner(&jwk_smp, smp->px, smp->sess, smp->strm, smp->opt);
|
||||
if (!sample_conv_var2smp_str(&args[0], &jwk_smp))
|
||||
goto end;
|
||||
|
||||
/* Copy JWK parameter */
|
||||
jwk_buf = alloc_trash_chunk();
|
||||
if (!jwk_buf)
|
||||
goto end;
|
||||
if (!chunk_cpy(jwk_buf, &jwk_smp.data.u.str))
|
||||
goto end;
|
||||
|
||||
/* Copy JWE input token */
|
||||
input = alloc_trash_chunk();
|
||||
if (!input)
|
||||
goto end;
|
||||
if (!chunk_cpy(input, &smp->data.u.str))
|
||||
goto end;
|
||||
|
||||
if (jwt_tokenize(input, items, &item_num) || item_num != JWE_ELT_MAX)
|
||||
goto end;
|
||||
|
||||
alg_tag = alloc_trash_chunk();
|
||||
if (!alg_tag)
|
||||
goto end;
|
||||
alg_iv = alloc_trash_chunk();
|
||||
if (!alg_iv)
|
||||
goto end;
|
||||
|
||||
fields.tag = alg_tag;
|
||||
fields.iv = alg_iv;
|
||||
|
||||
/* Base64Url decode the JOSE header */
|
||||
decoded_items[JWE_ELT_JOSE] = alloc_trash_chunk();
|
||||
if (!decoded_items[JWE_ELT_JOSE])
|
||||
goto end;
|
||||
size = base64urldec(items[JWE_ELT_JOSE].start, items[JWE_ELT_JOSE].length,
|
||||
b_orig(decoded_items[JWE_ELT_JOSE]), b_size(decoded_items[JWE_ELT_JOSE]));
|
||||
if (size < 0)
|
||||
goto end;
|
||||
decoded_items[JWE_ELT_JOSE]->data = size;
|
||||
|
||||
if (!parse_jose(decoded_items[JWE_ELT_JOSE], &alg, &enc, &fields))
|
||||
goto end;
|
||||
|
||||
/* Check if "alg" fits certificate-based JWEs */
|
||||
switch (alg) {
|
||||
case JWE_ALG_RSA1_5:
|
||||
case JWE_ALG_RSA_OAEP:
|
||||
case JWE_ALG_RSA_OAEP_256:
|
||||
rsa = 1;
|
||||
break;
|
||||
case JWE_ALG_A128KW:
|
||||
case JWE_ALG_A192KW:
|
||||
case JWE_ALG_A256KW:
|
||||
gcm = 0;
|
||||
oct = 1;
|
||||
break;
|
||||
case JWE_ALG_A128GCMKW:
|
||||
case JWE_ALG_A192GCMKW:
|
||||
case JWE_ALG_A256GCMKW:
|
||||
gcm = 1;
|
||||
oct = 1;
|
||||
break;
|
||||
case JWE_ALG_DIR:
|
||||
dir = 1;
|
||||
oct = 1;
|
||||
break;
|
||||
default:
|
||||
/* Not managed yet */
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Parse JWK argument. */
|
||||
if (process_jwk(jwk_buf, &jwk))
|
||||
goto end;
|
||||
|
||||
/* Check that the provided JWK is of the proper type */
|
||||
if ((oct && jwk.type != JWK_KTY_OCT) ||
|
||||
(rsa && jwk.type != JWK_KTY_RSA))
|
||||
goto end;
|
||||
|
||||
if (dir) {
|
||||
/* The secret given as parameter should be used directly to
|
||||
* decode the encrypted content. */
|
||||
decrypted_cek = alloc_trash_chunk();
|
||||
if (!decrypted_cek)
|
||||
goto end;
|
||||
|
||||
chunk_memcpy(decrypted_cek, b_orig(jwk.secret), b_data(jwk.secret));
|
||||
} else {
|
||||
/* With algorithms other than "dir" we should always have a CEK */
|
||||
if (!items[JWE_ELT_CEK].length)
|
||||
goto end;
|
||||
|
||||
cek = &decoded_items[JWE_ELT_CEK];
|
||||
|
||||
*cek = alloc_trash_chunk();
|
||||
if (!*cek)
|
||||
goto end;
|
||||
|
||||
decrypted_cek = alloc_trash_chunk();
|
||||
if (!decrypted_cek) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
size = base64urldec(items[JWE_ELT_CEK].start, items[JWE_ELT_CEK].length,
|
||||
(*cek)->area, (*cek)->size);
|
||||
if (size < 0) {
|
||||
goto end;
|
||||
}
|
||||
(*cek)->data = size;
|
||||
|
||||
if (rsa) {
|
||||
if (do_decrypt_cek_rsa(*cek, decrypted_cek, jwk.pkey, alg))
|
||||
goto end;
|
||||
} else {
|
||||
if (gcm) {
|
||||
if (!decrypt_cek_aesgcmkw(*cek, alg_tag, alg_iv, decrypted_cek, jwk.secret, alg))
|
||||
goto end;
|
||||
} else {
|
||||
if (!decrypt_cek_aeskw(*cek, decrypted_cek, jwk.secret, alg))
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (decrypt_ciphertext(enc, items, decoded_items, decrypted_cek, &out))
|
||||
goto end;
|
||||
|
||||
smp->data.u.str.data = b_data(out);
|
||||
smp->data.u.str.area = b_orig(out);
|
||||
smp->data.type = SMP_T_BIN;
|
||||
smp_dup(smp);
|
||||
|
||||
retval = 1;
|
||||
|
||||
end:
|
||||
clear_jwk(&jwk);
|
||||
free_trash_chunk(jwk_buf);
|
||||
free_trash_chunk(input);
|
||||
free_trash_chunk(decrypted_cek);
|
||||
free_trash_chunk(out);
|
||||
free_trash_chunk(alg_tag);
|
||||
free_trash_chunk(alg_iv);
|
||||
clear_decoded_items(decoded_items);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
static struct sample_conv_kw_list sample_conv_kws = {ILH, {
|
||||
/* JSON Web Token converters */
|
||||
{ "jwt_decrypt_secret", sample_conv_jwt_decrypt_secret, ARG1(1,STR), sample_conv_jwt_decrypt_secret_check, SMP_T_BIN, SMP_T_BIN },
|
||||
{ "jwt_decrypt_cert", sample_conv_jwt_decrypt_cert, ARG1(1,STR), sample_conv_jwt_decrypt_cert_check, SMP_T_BIN, SMP_T_BIN },
|
||||
{ "jwt_decrypt", sample_conv_jwt_decrypt, ARG1(1,STR), sample_conv_jwt_decrypt_check, SMP_T_BIN, SMP_T_BIN },
|
||||
{ NULL, NULL, 0, 0, 0 },
|
||||
|
||||
}};
|
||||
|
|
|
|||
Loading…
Reference in a new issue