/* SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(HAVE_JWS) #ifdef USE_OPENSSL struct alg_enc { const char *name; int value; }; /* https://datatracker.ietf.org/doc/html/rfc7518#section-4.1 */ typedef enum { JWE_ALG_UNMANAGED = -1, JWE_ALG_RSA1_5, JWE_ALG_RSA_OAEP, JWE_ALG_RSA_OAEP_256, JWE_ALG_A128KW, JWE_ALG_A192KW, 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_A128GCMKW, JWE_ALG_A192GCMKW, JWE_ALG_A256GCMKW, // JWE_ALG_PBES2_HS256_A128KW, // JWE_ALG_PBES2_HS384_A192KW, // JWE_ALG_PBES2_HS512_A256KW, } jwe_alg; struct alg_enc jwe_algs[] = { { "RSA1_5", JWE_ALG_RSA1_5 }, { "RSA-OAEP", JWE_ALG_RSA_OAEP }, { "RSA-OAEP-256", JWE_ALG_RSA_OAEP_256 }, { "A128KW", JWE_ALG_A128KW }, { "A192KW", JWE_ALG_A192KW }, { "A256KW", JWE_ALG_A256KW }, { "dir", JWE_ALG_DIR }, { "ECDH-ES", JWE_ALG_ECDH_ES }, { "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 }, { "PBES2-HS256+A128KW", JWE_ALG_UNMANAGED }, { "PBES2-HS384+A192KW", JWE_ALG_UNMANAGED }, { "PBES2-HS512+A256KW", JWE_ALG_UNMANAGED }, { NULL, JWE_ALG_UNMANAGED }, }; /* https://datatracker.ietf.org/doc/html/rfc7518#section-5.1 */ typedef enum { JWE_ENC_UNMANAGED = -1, JWE_ENC_A128CBC_HS256, JWE_ENC_A192CBC_HS384, JWE_ENC_A256CBC_HS512, JWE_ENC_A128GCM, JWE_ENC_A192GCM, JWE_ENC_A256GCM, } jwe_enc; struct alg_enc jwe_encodings[] = { { "A128CBC-HS256", JWE_ENC_A128CBC_HS256 }, { "A192CBC-HS384", JWE_ENC_A192CBC_HS384 }, { "A256CBC-HS512", JWE_ENC_A256CBC_HS512 }, { "A128GCM", JWE_ENC_A128GCM }, { "A192GCM", JWE_ENC_A192GCM }, { "A256GCM", JWE_ENC_A256GCM }, { NULL, JWE_ENC_UNMANAGED }, }; /* * In the JWE Compact Serialization, a JWE is represented as the concatenation: * BASE64URL(UTF8(JWE Protected Header)) || '.' || * BASE64URL(JWE Encrypted Key) || '.' || * BASE64URL(JWE Initialization Vector) || '.' || * BASE64URL(JWE Ciphertext) || '.' || * BASE64URL(JWE Authentication Tag) */ enum jwe_elt { JWE_ELT_JOSE = 0, JWE_ELT_CEK, JWE_ELT_IV, JWE_ELT_CIPHERTEXT, JWE_ELT_TAG, JWE_ELT_MAX }; struct jose_fields { struct buffer *tag; struct buffer *iv; struct buffer *epk; EVP_PKEY *pubkey; struct buffer *apu; struct buffer *apv; }; /* * Parse contents of "alg" or "enc" field of the JOSE header. */ static inline int parse_alg_enc(struct buffer *buf, struct alg_enc *array) { struct alg_enc *item = array; int val = -1; while (item->name) { if (strncmp(item->name, b_orig(buf), (int)b_data(buf)) == 0) { val = item->value; break; } ++item; } return val; } /* * Get the string corresponding to that can be either an encoding or an * algorithm. */ static inline const char* algenc2str(int id, struct alg_enc *array) { struct alg_enc *item = array; while (item->name) { if (item->value == id) return item->name; ++item; } return NULL; } /* * Look for field in JSON and if it is found, * allocate an buffer and base64url decode the field's content in it. * If is set to 1, an error will be raised if the field is not * found. * Returns 0 in case of success, 1 in case of error (decoding or alloc error). */ static inline int decode_jose_field(struct buffer *decoded_jose, const char *field_name, struct buffer **out, int mandatory) { struct buffer *trash = get_trash_chunk(); int size = 0; if (!out) return 0; size = mjson_get_string(b_orig(decoded_jose), b_data(decoded_jose), field_name, b_orig(trash), b_size(trash)); if (size != -1) { trash->data = size; *out = alloc_trash_chunk(); if (!*out) return 1; size = base64urldec(b_orig(trash), b_data(trash), b_orig(*out), b_size(*out)); if (size < 0) { free_trash_chunk(*out); *out = NULL; return 1; } (*out)->data = size; } else if (mandatory) return 1; return 0; } static int build_EC_PKEY_from_buf(struct buffer *jwk, EVP_PKEY **pkey); /* * Extract multiple base64url encoded fields from the JOSE header that are * required for the subsequent decrypt operations. Depending on the used * algorithms the required fields will change. */ static int parse_jose(struct buffer *decoded_jose, int *alg, int *enc, struct jose_fields *jose_fields) { struct buffer *trash = NULL; int retval = 0; int size = 0; int gcm = 0; int ec = 0; /* Look for "alg" field */ trash = get_trash_chunk(); size = mjson_get_string(b_orig(decoded_jose), b_data(decoded_jose), "$.alg", b_orig(trash), b_size(trash)); if (size == -1) goto end; trash->data = size; *alg = parse_alg_enc(trash, jwe_algs); if (*alg == JWE_ALG_UNMANAGED) goto end; /* Look for "enc" field */ chunk_reset(trash); size = mjson_get_string(b_orig(decoded_jose), b_data(decoded_jose), "$.enc", b_orig(trash), b_size(trash)); if (size == -1) goto end; trash->data = size; *enc = parse_alg_enc(trash, jwe_encodings); if (*enc == JWE_ENC_UNMANAGED) goto end; 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: case JWE_ALG_A192GCMKW: case JWE_ALG_A256GCMKW: gcm = 1; break; default: break; } if (gcm) { /* Look for "tag" field (used by aes gcm encryption) */ if (decode_jose_field(decoded_jose, "$.tag", &jose_fields->tag, 1)) goto end; /* Look for "iv" field (used by aes gcm encryption) */ if (decode_jose_field(decoded_jose, "$.iv", &jose_fields->iv, 1)) goto end; } if (ec) { /* Look for mandatory "epk" field (used by ecdh encryption) */ const char *tokptr = NULL; int toklen = 0; int token_type = mjson_find(b_orig(decoded_jose), b_data(decoded_jose), "$.epk", &tokptr, &toklen); if (token_type != MJSON_TOK_OBJECT) goto end; jose_fields->epk = alloc_trash_chunk(); if (!jose_fields->epk) goto end; if (!chunk_memcpy(jose_fields->epk, tokptr, toklen)) goto end; if (build_EC_PKEY_from_buf(jose_fields->epk, &jose_fields->pubkey)) goto end; /* Look for optional "apu" field (used by ecdh encryption) */ if (decode_jose_field(decoded_jose, "$.apu", &jose_fields->apu, 0)) goto end; /* Look for optional "apv" field (used by ecdh encryption) */ if (decode_jose_field(decoded_jose, "$.apv", &jose_fields->apv, 0)) goto end; } retval = 1; end: return retval; } static void clear_jose_fields(struct jose_fields *jose_fields) { if (!jose_fields) return; free_trash_chunk(jose_fields->tag); free_trash_chunk(jose_fields->iv); free_trash_chunk(jose_fields->epk); EVP_PKEY_free(jose_fields->pubkey); free_trash_chunk(jose_fields->apu); free_trash_chunk(jose_fields->apv); } /* * Decrypt Encrypted Key encrypted with AES GCM Key Wrap algorithm and * dump the decrypted key into buffer. The decryption is done * thanks to Initialization Vector, key and authentication check * is performed with . All those buffers must be in raw format, * already base64url decoded. * Return 0 in case of error, 1 otherwise. */ static int decrypt_cek_aesgcmkw(struct buffer *cek, struct buffer *aead_tag, struct buffer *iv, struct buffer *decrypted_cek, struct buffer *secret, jwe_alg crypt_alg) { int retval = 0; int key_size = 0; int size = 0; switch(crypt_alg) { case JWE_ALG_A128GCMKW: key_size = 128; break; case JWE_ALG_A192GCMKW: key_size = 192; break; case JWE_ALG_A256GCMKW: key_size = 256; break; default: goto end; } size = aes_process(cek, iv, secret, key_size, aead_tag, NULL, decrypted_cek, 1, 1); if (size < 0) goto end; decrypted_cek->data = size; retval = 1; end: return retval; } /* * Decrypt Encrypted Key encrypted with AES CBC Key Wrap algorithm and * dump the decrypted key into buffer. The decryption is done * thanks to Initialization Vector and key. All those buffers must * be in raw format, already base64url decoded. * Return 0 in case of error, 1 otherwise. */ static int decrypt_cek_aeskw(struct buffer *cek, struct buffer *decrypted_cek, struct buffer *secret, jwe_alg crypt_alg) { EVP_CIPHER_CTX *ctx = NULL; const EVP_CIPHER *cipher = NULL; struct buffer *iv = NULL; int iv_size = 0; int retval = 0; int length = 0; ctx = EVP_CIPHER_CTX_new(); if (!ctx) goto end; switch(crypt_alg) { #ifndef OPENSSL_IS_AWSLC /* AWS-LC does not support EVP_aes_128_wrap or EVP_aes_192_wrap */ case JWE_ALG_A128KW: cipher = EVP_aes_128_wrap(); break; case JWE_ALG_A192KW: cipher = EVP_aes_192_wrap(); break; #endif case JWE_ALG_A256KW: cipher = EVP_aes_256_wrap(); break; default: goto end; } #ifndef OPENSSL_IS_AWSLC /* Comment from AWS-LC (in include/openssl/cipher.h): * EVP_aes_256_wrap implements AES-256 in Key Wrap mode. OpenSSL 1.1.1 * required |EVP_CIPHER_CTX_FLAG_WRAP_ALLOW| to be set with * |EVP_CIPHER_CTX_set_flags|, in order for |EVP_aes_256_wrap| to work. * This is not required in AWS-LC and they are no-op flags maintained * for compatibility. */ EVP_CIPHER_CTX_set_flags(ctx, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); #endif iv_size = EVP_CIPHER_iv_length(cipher); iv = alloc_trash_chunk(); if (!iv) goto end; /* Default IV for AES KW (see RFC3394 section-2.2.3.1) */ memset(iv->area, 0xA6, iv_size); iv->data = iv_size; /* Initialise IV and key */ if (EVP_DecryptInit_ex(ctx, cipher, NULL, (unsigned char*)b_orig(secret), (unsigned char*)b_orig(iv)) <= 0) goto end; if (EVP_DecryptUpdate(ctx, (unsigned char*)b_orig(decrypted_cek), &length, (unsigned char*)b_orig(cek), b_data(cek)) <= 0) goto end; if (EVP_DecryptFinal_ex(ctx, (unsigned char*)decrypted_cek->area + length, (int*)&decrypted_cek->data) <= 0) goto end; decrypted_cek->data += length; retval = 1; end: EVP_CIPHER_CTX_free(ctx); free_trash_chunk(iv); return retval; } /* * Build a signature tag when AES-CBC encoding is used and check that it matches * the one found in the JWE token. * The tag is built out of a HMAC of some concatenated data taken from the JWE * token (see https://datatracker.ietf.org/doc/html/rfc7518#section-5.2). The * firest half of the previously decrypted cek is used as HMAC key. * Returns 0 in case of success, 1 otherwise. */ static int build_and_check_tag(jwe_enc enc, struct jwt_item items[JWE_ELT_MAX], struct buffer *decoded_items[JWE_ELT_MAX], struct buffer *decrypted_cek) { int retval = 1; const EVP_MD *hash = NULL; int mac_key_len = 0; uint64_t aad_len = my_htonll(items[JWE_ELT_JOSE].length << 3); struct buffer *tag_data = alloc_trash_chunk(); struct buffer *hmac = alloc_trash_chunk(); if (!tag_data || !hmac) goto end; /* * Concatenate the AAD (base64url encoded JOSE header), * the Initialization Vector, the ciphertext, * and the AL value (number of bits in the AAD in 64bits big endian) */ if (!chunk_memcpy(tag_data, items[JWE_ELT_JOSE].start, items[JWE_ELT_JOSE].length) || !chunk_memcat(tag_data, b_orig(decoded_items[JWE_ELT_IV]), b_data(decoded_items[JWE_ELT_IV])) || !chunk_memcat(tag_data, b_orig(decoded_items[JWE_ELT_CIPHERTEXT]), b_data(decoded_items[JWE_ELT_CIPHERTEXT])) || !chunk_memcat(tag_data, (char*)&aad_len, sizeof(aad_len))) goto end; switch(enc) { case JWE_ENC_A128CBC_HS256: mac_key_len = 16; hash = EVP_sha256(); break; case JWE_ENC_A192CBC_HS384: mac_key_len = 24; hash = EVP_sha384(); break; case JWE_ENC_A256CBC_HS512: mac_key_len = 32; hash = EVP_sha512(); break; default: goto end; } if (b_data(decrypted_cek) < mac_key_len) goto end; /* Compute the HMAC SHA-XXX of the concatenated value above */ if (!HMAC(hash, b_orig(decrypted_cek), mac_key_len, (unsigned char*)b_orig(tag_data), b_data(tag_data), (unsigned char*)b_orig(hmac), (unsigned int*)&hmac->data)) goto end; /* Double check that buffer lengths line up before the comparison */ if (unlikely(b_data(decoded_items[JWE_ELT_TAG]) != b_data(hmac) >> 1)) goto end; /* Use the first half of the HMAC output M as the Authentication Tag output T */ retval = CRYPTO_memcmp(b_orig(decoded_items[JWE_ELT_TAG]), b_orig(hmac), b_data(hmac) >> 1); end: free_trash_chunk(tag_data); free_trash_chunk(hmac); return retval; } /* * Decrypt the ciphertext. * Returns 0 in case of success, 1 otherwise. */ static int decrypt_ciphertext(jwe_enc enc, struct jwt_item items[JWE_ELT_MAX], struct buffer *decoded_items[JWE_ELT_MAX], struct buffer *decrypted_cek, struct buffer **out) { struct buffer **ciphertext = NULL, **iv = NULL, **aead_tag = NULL, *aad = NULL; int size = 0; int gcm = 0; int key_size = 0; struct buffer *aes_key = NULL; int retval = 1; switch (enc) { case JWE_ENC_A128CBC_HS256: gcm = 0; key_size = 16; break; case JWE_ENC_A192CBC_HS384: gcm = 0; key_size = 24; break; case JWE_ENC_A256CBC_HS512: gcm = 0; key_size = 32; break; case JWE_ENC_A128GCM: gcm = 1; key_size = 16; break; case JWE_ENC_A192GCM: gcm = 1; key_size = 24; break; case JWE_ENC_A256GCM: gcm = 1; key_size = 32; break; default: goto end; } /* Base64 decode cipher text */ ciphertext = &decoded_items[JWE_ELT_CIPHERTEXT]; *ciphertext = alloc_trash_chunk(); if (!*ciphertext) goto end; size = base64urldec(items[JWE_ELT_CIPHERTEXT].start, items[JWE_ELT_CIPHERTEXT].length, (*ciphertext)->area, (*ciphertext)->size); if (size < 0) goto end; (*ciphertext)->data = size; /* Base64 decode Initialization Vector */ iv = &decoded_items[JWE_ELT_IV]; *iv = alloc_trash_chunk(); if (!*iv) goto end; size = base64urldec(items[JWE_ELT_IV].start, items[JWE_ELT_IV].length, (*iv)->area, (*iv)->size); if (size < 0) goto end; (*iv)->data = size; /* Base64 decode Additional Data */ aead_tag = &decoded_items[JWE_ELT_TAG]; *aead_tag = alloc_trash_chunk(); if (!*aead_tag) goto end; size = base64urldec(items[JWE_ELT_TAG].start, items[JWE_ELT_TAG].length, (*aead_tag)->area, (*aead_tag)->size); if (size < 0) goto end; (*aead_tag)->data = size; if (gcm) { aad = alloc_trash_chunk(); if (!aad) goto end; chunk_memcpy(aad, items[JWE_ELT_JOSE].start, items[JWE_ELT_JOSE].length); aes_key = decrypted_cek; } else { /* https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.2.1 * Build the authentication tag out of the first part of the * cipher key and a combination of information extracted from * the JWE token. */ if (build_and_check_tag(enc, items, decoded_items, decrypted_cek)) goto end; aes_key = alloc_trash_chunk(); if (!aes_key) goto end; /* Only use the second part of the decrypted key for actual * content decryption. */ if (b_data(decrypted_cek) != key_size * 2) goto end; chunk_memcpy(aes_key, decrypted_cek->area + key_size, key_size); } *out = alloc_trash_chunk(); if (!*out) goto end; size = aes_process(*ciphertext, *iv, aes_key, key_size*8, *aead_tag, aad, *out, 1, gcm); if (size < 0) goto end; retval = 0; end: free_trash_chunk(aad); if (!gcm) free_trash_chunk(aes_key); return retval; } static inline void clear_decoded_items(struct buffer *decoded_items[JWE_ELT_MAX]) { struct buffer *buf = NULL; int idx = JWE_ELT_JOSE; while(idx != JWE_ELT_MAX) { buf = decoded_items[idx]; free_trash_chunk(buf); ++idx; } } /* * Decrypt the contents of a JWE token thanks to the user-provided base64 * encoded secret. This converter can only be used for tokens that have a * symetric algorithm (AESKW, AESGCMKW or "dir" special case). * Returns the decrypted contents, or nothing if any error happened. */ static int sample_conv_jwt_decrypt_secret(const struct arg *args, struct sample *smp, void *private) { struct buffer *input = NULL; unsigned int item_num = JWE_ELT_MAX; int retval = 0; struct jwt_item items[JWE_ELT_MAX] = {}; struct buffer *decoded_items[JWE_ELT_MAX] = {}; struct sample secret_smp; struct buffer *secret = NULL; struct buffer **cek = NULL; struct buffer *decrypted_cek = NULL; struct buffer *out = NULL; int size = 0; jwe_alg alg = JWE_ALG_UNMANAGED; jwe_enc enc = JWE_ENC_UNMANAGED; int gcm = 0; struct jose_fields fields = {}; input = alloc_trash_chunk(); if (!input) return 0; if (!chunk_cpy(input, &smp->data.u.str)) goto end; if (jwt_tokenize(input, items, item_num)) goto end; /* 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 secret-based JWEs */ switch (alg) { case JWE_ALG_A128KW: case JWE_ALG_A192KW: case JWE_ALG_A256KW: gcm = 0; break; case JWE_ALG_A128GCMKW: case JWE_ALG_A192GCMKW: case JWE_ALG_A256GCMKW: gcm = 1; break; case JWE_ALG_DIR: break; default: /* Cannot use a secret for this type of "alg" */ goto end; } /* Parse secret argument and base64dec it if it comes from a variable. */ smp_set_owner(&secret_smp, smp->px, smp->sess, smp->strm, smp->opt); if (!sample_conv_var2smp_str(&args[0], &secret_smp)) goto end; if (args[0].type == ARGT_VAR) { secret = alloc_trash_chunk(); if (!secret) goto end; size = base64dec(secret_smp.data.u.str.area, secret_smp.data.u.str.data, secret->area, secret->size); if (size < 0) goto end; secret->data = size; secret_smp.data.u.str = *secret; } if (items[JWE_ELT_CEK].length) { int cek_size = 0; cek = &decoded_items[JWE_ELT_CEK]; *cek = alloc_trash_chunk(); if (!*cek) goto end; decrypted_cek = alloc_trash_chunk(); if (!decrypted_cek) { goto end; } cek_size = base64urldec(items[JWE_ELT_CEK].start, items[JWE_ELT_CEK].length, (*cek)->area, (*cek)->size); if (cek_size < 0) { goto end; } (*cek)->data = cek_size; if (gcm) { if (!decrypt_cek_aesgcmkw(*cek, fields.tag, fields.iv, decrypted_cek, &secret_smp.data.u.str, alg)) goto end; } else { if (!decrypt_cek_aeskw(*cek, decrypted_cek, &secret_smp.data.u.str, alg)) goto end; } } else if (alg == JWE_ALG_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, secret_smp.data.u.str.area, secret_smp.data.u.str.data); } /* Decode the encrypted content thanks to decrypted_cek secret */ 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_jose_fields(&fields); free_trash_chunk(input); free_trash_chunk(decrypted_cek); free_trash_chunk(out); clear_decoded_items(decoded_items); return retval; } /* * Decrypt the content of buffer into buffer thanks to the * private key using algorithm (RSA). * Returns 0 in case of success, 1 otherwise. */ static int do_decrypt_cek_rsa(struct buffer *cek, struct buffer *decrypted_cek, EVP_PKEY *pkey, jwe_alg crypt_alg) { EVP_PKEY_CTX *ctx = NULL; const EVP_MD *md = NULL; int retval = 1; int pad = 0; size_t outl = b_size(decrypted_cek); switch(crypt_alg) { case JWE_ALG_RSA1_5: pad = RSA_PKCS1_PADDING; md = EVP_sha1(); break; case JWE_ALG_RSA_OAEP: pad = RSA_PKCS1_OAEP_PADDING; md = EVP_sha1(); break; case JWE_ALG_RSA_OAEP_256: pad = RSA_PKCS1_OAEP_PADDING; md = EVP_sha256(); break; default: goto end; } ctx = EVP_PKEY_CTX_new(pkey, NULL); if (!ctx) goto end; if (EVP_PKEY_decrypt_init(ctx) <= 0) goto end; if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad) <= 0) goto end; if (pad == RSA_PKCS1_OAEP_PADDING) { if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) goto end; if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0) goto end; } if (EVP_PKEY_decrypt(ctx, (unsigned char*)b_orig(decrypted_cek), &outl, (unsigned char*)b_orig(cek), b_data(cek)) <= 0) goto end; decrypted_cek->data = outl; retval = 0; end: EVP_PKEY_CTX_free(ctx); return retval; } /* * Look for in the ckch_store tree and use its private key to decrypt * into using algorithm (of the RSA alg * family). * Returns 0 in case of success, 1 otherwise. */ static int decrypt_cek_rsa(struct buffer *cek, struct buffer *decrypted_cek, struct buffer *cert, jwe_alg crypt_alg) { EVP_PKEY *pkey = NULL; int retval = 1; struct ckch_store *store = NULL; if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) goto end; store = ckchs_lookup(b_orig(cert)); if (!store || !store->data->key || !store->conf.jwt) { HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); goto end; } pkey = store->data->key; EVP_PKEY_up_ref(pkey); HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); if (do_decrypt_cek_rsa(cek, decrypted_cek, pkey, crypt_alg)) goto end; retval = 0; end: EVP_PKEY_free(pkey); return retval; } /* * Derive an ECDH secret out of the user-provided EC private key and * the public key built out of the JWK found in the JOSE header ("epk" field). * Return 0 in case of success, 1 otherwise. */ static int derive_ecdh_secret(EVP_PKEY *privkey, EVP_PKEY *epk_key, struct buffer *derived) { EVP_PKEY_CTX *ctx = NULL; int retval = 1; size_t derived_len = 0; if (!derived) goto end; ctx = EVP_PKEY_CTX_new(privkey, NULL); if (!ctx) goto end; if (EVP_PKEY_derive_init(ctx) != 1) goto end; if (EVP_PKEY_derive_set_peer(ctx, epk_key) != 1) goto end; if (EVP_PKEY_derive(ctx, NULL, &derived_len) != 1) goto end; if (derived_len > b_size(derived)) goto end; if (EVP_PKEY_derive(ctx, (unsigned char*)derived->area, &derived_len) != 1) goto end; derived->data = derived_len; retval = 0; end: EVP_PKEY_CTX_free(ctx); return retval; } /* * Append in big endian form and 4 bytes to the buffer as well * as the content. */ static inline void append_data_len(struct buffer *out, const char *val, int val_len) { uint32_t be_len = htonl(val_len); chunk_memcat(out, (char*)&be_len, (int)sizeof(be_len)); chunk_memcat(out, val, val_len); } /* * Build the concatKDF buffer (see section 4.6.2 of RFC7518). * Return 0 in case of success, 1 otherwise. */ static int build_concatkdf_otherinfo(const char *alg_id, struct buffer *apu, struct buffer *apv, uint32_t keydatalen, struct buffer *out) { int retval = 1; if (!out) goto end; /* * Key derivation is performed using the Concat KDF, as defined in * Section 5.8.1 of [NIST.800-56A] * "For this format, OtherInfo is a bit string equal to the * following concatenation: * AlgorithmID || PartyUInfo || PartyVInfo {|| SuppPubInfo }{||SuppPrivInfo }" * * From https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 * SuppPubInfo : This is set to the keydatalen represented as a * 32-bit big-endian integer. */ append_data_len(out, alg_id, strlen(alg_id)); append_data_len(out, (apu ? b_orig(apu) : NULL), (apu ? b_data(apu) : 0)); append_data_len(out, (apv ? b_orig(apv) : NULL), (apv ? b_data(apv) : 0)); keydatalen = htonl(keydatalen); chunk_memcat(out, (char*)&keydatalen, (int)sizeof(keydatalen)); retval = 0; end: return retval; } /* * Decrypt the content of buffer into buffer thanks to the * private key using algorithm (RSA). * Returns 0 in case of success, 1 otherwise. */ static int do_decrypt_cek_ec(struct buffer *cek, struct buffer *decrypted_cek, EVP_PKEY *privkey, EVP_PKEY *pubkey, jwe_alg crypt_alg, jwe_enc crypt_enc, struct buffer *apu, struct buffer *apv) { int retval = 1; 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: ecdhes = 1; switch(crypt_enc) { case JWE_ENC_A128GCM: key_size = 128; break; case JWE_ENC_A192GCM: key_size = 192; break; case JWE_ENC_A256GCM: case JWE_ENC_A128CBC_HS256: key_size = 256; break; case JWE_ENC_A192CBC_HS384: key_size = 384; break; case JWE_ENC_A256CBC_HS512: key_size = 512; break; default: goto end; } alg_id = algenc2str(crypt_enc, jwe_encodings); 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; } if (!alg_id) { alg_id = algenc2str(crypt_alg, jwe_algs); if (!alg_id) goto end; } derived_secret = alloc_trash_chunk(); if (!derived_secret) goto end; otherinfo = alloc_trash_chunk(); if (!otherinfo) goto end; if (derive_ecdh_secret(privkey, pubkey, derived_secret)) goto end; if (build_concatkdf_otherinfo(alg_id, apu, apv, key_size, otherinfo)) goto end; ctx = EVP_MD_CTX_create(); if (!ctx) goto end; /* 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) { 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; } /* 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; for (counter = 0; counter <= reps; ++counter) { 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; offset += hashlen; } *concatkdf_len = 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; end: free_trash_chunk(derived_secret); free_trash_chunk(otherinfo); free_trash_chunk(tmpbuf); EVP_MD_CTX_free(ctx); return retval; } /* * Decrypt the contents of a JWE token thanks to the user-provided certificate * and private key. This converter can only be used for tokens that have an * asymetric algorithm (RSA only for now). * Returns the decrypted contents, or nothing if any error happened. */ static int sample_conv_jwt_decrypt_cert(const struct arg *args, struct sample *smp, void *private) { struct sample cert_smp; struct buffer *input = NULL; unsigned int item_num = JWE_ELT_MAX; int retval = 0; 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 rsa = 0; int ec = 0; int size = 0; struct buffer *cert = NULL; struct buffer **cek = NULL; struct buffer *decrypted_cek = NULL; struct buffer *out = NULL; struct jose_fields fields = {}; EVP_PKEY *privkey = NULL; input = alloc_trash_chunk(); if (!input) return 0; if (!chunk_cpy(input, &smp->data.u.str)) goto end; if (jwt_tokenize(input, items, item_num)) goto end; /* 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_ECDH_ES: case JWE_ALG_ECDH_ES_A128KW: case JWE_ALG_ECDH_ES_A192KW: case JWE_ALG_ECDH_ES_A256KW: ec = 1; break; default: /* Not managed yet */ goto end; } cert = alloc_trash_chunk(); if (!cert) goto end; smp_set_owner(&cert_smp, smp->px, smp->sess, smp->strm, smp->opt); if (!sample_conv_var2smp_str(&args[0], &cert_smp)) goto end; if (chunk_printf(cert, "%.*s", (int)b_data(&cert_smp.data.u.str), b_orig(&cert_smp.data.u.str)) <= 0) goto end; /* With ECDH-ES no CEK will be provided. */ if (!ec || alg != JWE_ALG_ECDH_ES) { /* With asymetric crypto algorithms 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; size = base64urldec(items[JWE_ELT_CEK].start, items[JWE_ELT_CEK].length, (*cek)->area, (*cek)->size); if (size < 0) { goto end; } (*cek)->data = size; } decrypted_cek = alloc_trash_chunk(); if (!decrypted_cek) { goto end; } if (rsa && decrypt_cek_rsa(*cek, decrypted_cek, cert, alg)) goto end; else if (ec) { struct ckch_store *store = NULL; if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) goto end; store = ckchs_lookup(b_orig(cert)); if (!store || !store->data->key || !store->conf.jwt) { HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); goto end; } privkey = store->data->key; EVP_PKEY_up_ref(privkey); HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); if (do_decrypt_cek_ec((cek != NULL) ? *cek : NULL, decrypted_cek, privkey, fields.pubkey, alg, enc, fields.apu, fields.apv)) 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: free_trash_chunk(input); free_trash_chunk(cert); free_trash_chunk(decrypted_cek); free_trash_chunk(out); clear_decoded_items(decoded_items); clear_jose_fields(&fields); EVP_PKEY_free(privkey); return retval; } /* "jwt_decrypt_cert" converter check function. * The first and only parameter should be a path to a pem certificate or a * variable holding a path to a pem certificate. The certificate must already * exist in the certificate store. * This converter will be used for JWEs with an RSA type "alg" field in their * JOSE header. */ static int sample_conv_jwt_decrypt_cert_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) { struct ckch_store *store = NULL; if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) return 0; store = ckchs_lookup(args[0].data.str.area); if (!store) { memprintf(err, "unknown certificate %s", args[0].data.str.area); HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); return 0; } else if (!store->conf.jwt) { memprintf(err, "unusable certificate %s (\"jwt\" option not set to \"on\")", args[0].data.str.area); HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); return 0; } HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); } return 1; } /* "jwt_decrypt_secret" converter check function. * The first and only parameter should be a base64 encoded secret or a variable * holding a base64 encoded secret. This converter will be used mainly for JWEs * with an AES type "alg" field in their JOSE header. */ static int sample_conv_jwt_decrypt_secret_check(struct arg *args, struct sample_conv *conv, const char *file, int line, char **err) { /* Try to decode variables. */ if (!sample_check_arg_base64(&args[0], err)) { memprintf(err, "failed to parse secret: %s", *err); return 0; } return 1; } /* * Convert a base64url encoded buffer into a BIGNUM. */ static BIGNUM *base64url_to_BIGNUM(struct buffer *b64url_buf) { BIGNUM *bn = NULL; struct buffer *decoded = get_trash_chunk(); int size = 0; if (!b64url_buf) return NULL; size = base64urldec(b_orig(b64url_buf), b_data(b64url_buf), b_orig(decoded), b_size(decoded)); if (size < 0) return NULL; decoded->data = size; bn = BN_bin2bn((const unsigned char *)b_orig(decoded), b_data(decoded), NULL); return bn; } /* * Extract a field named of type string out of the JSON buffer and * dump its value in . * Return 0 in case of success, 1 in case of error (JSON parsing error or value * not found). */ static int get_jwk_field(struct buffer *jwk, const char *field, struct buffer *out) { int size = 0; chunk_reset(out); size = mjson_get_string(b_orig(jwk), b_data(jwk), field, b_orig(out), b_size(out)); if (size == -1) return 1; out->data = size; return 0; } enum { RSA_BIGNUM_N, RSA_BIGNUM_E, RSA_BIGNUM_D, RSA_BIGNUM_P, RSA_BIGNUM_Q, RSA_BIGNUM_DP, RSA_BIGNUM_DQ, RSA_BIGNUM_QI, RSA_BIGNUM_COUNT }; /* * Build the EVP_PKEY out of the BIGNUMs parsed by the caller. * The RSA_set0_ functions were deprecated in OpenSSL3, hence the two different * code blocks. * Returns 0 in case of success, 1 otherwise. */ static int do_build_RSA_PKEY(BIGNUM *nums[RSA_BIGNUM_COUNT], EVP_PKEY **pkey) #if HA_OPENSSL_VERSION_NUMBER >= 0x30000000L { int retval = 1; OSSL_PARAM *params = NULL; OSSL_PARAM_BLD *param_bld = NULL; EVP_PKEY_CTX *pctx = NULL; param_bld = OSSL_PARAM_BLD_new(); if (!OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_N, nums[RSA_BIGNUM_N]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_E, nums[RSA_BIGNUM_E]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_D, nums[RSA_BIGNUM_D]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_FACTOR1, nums[RSA_BIGNUM_P]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_FACTOR2, nums[RSA_BIGNUM_Q]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_EXPONENT1, nums[RSA_BIGNUM_DP]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_EXPONENT2, nums[RSA_BIGNUM_DQ]) || !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, nums[RSA_BIGNUM_QI])) goto end; params = OSSL_PARAM_BLD_to_param(param_bld); if (!params) goto end; pctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL); if (!pctx) goto end; if (EVP_PKEY_fromdata_init(pctx) != 1) goto end; if (EVP_PKEY_fromdata(pctx, pkey, EVP_PKEY_KEYPAIR, params) != 1) goto end; retval = 0; end: OSSL_PARAM_BLD_free(param_bld); OSSL_PARAM_free(params); EVP_PKEY_CTX_free(pctx); return retval; } #else /* HA_OPENSSL_VERSION_NUMBER < 0x30000000L */ { int retval = 1; RSA *rsa = NULL; rsa = RSA_new(); if (!rsa) goto end; if (RSA_set0_key(rsa, nums[RSA_BIGNUM_N], nums[RSA_BIGNUM_E], nums[RSA_BIGNUM_D]) != 1 || RSA_set0_factors(rsa, nums[RSA_BIGNUM_P], nums[RSA_BIGNUM_Q]) != 1 || RSA_set0_crt_params(rsa, nums[RSA_BIGNUM_DP], nums[RSA_BIGNUM_DQ], nums[RSA_BIGNUM_QI]) != 1) goto end; *pkey = EVP_PKEY_new(); if (!*pkey) goto end; if (EVP_PKEY_set1_RSA(*pkey, rsa) != 1) goto end; retval = 0; end: RSA_free(rsa); return retval; } #endif static inline void clear_bignums(BIGNUM **nums, int count) { int idx = 0; while (idx < count) BN_free(nums[idx++]); } /* * Build an EVP_PKEY that contains an RSA private key out of a JWK buffer that * must have an "RSA" key type ("kty" field). * Return 0 in case of success, 1 otherwise. */ static int build_RSA_PKEY_from_buf(struct buffer *jwk, EVP_PKEY **pkey) { BIGNUM *nums[RSA_BIGNUM_COUNT] = {}; int retval = 1; struct buffer *tmpbuf = alloc_trash_chunk(); if (!tmpbuf) goto end; /* * Extract all the mandatory fields that all represent BIGNUMs out of * the JWK buffer. */ if (get_jwk_field(jwk, "$.n", tmpbuf) || (nums[RSA_BIGNUM_N] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.e", tmpbuf) || (nums[RSA_BIGNUM_E] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.d", tmpbuf) || (nums[RSA_BIGNUM_D] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.p", tmpbuf) || (nums[RSA_BIGNUM_P] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.q", tmpbuf) || (nums[RSA_BIGNUM_Q] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.dp", tmpbuf) || (nums[RSA_BIGNUM_DP] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.dq", tmpbuf) || (nums[RSA_BIGNUM_DQ] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.qi", tmpbuf) || (nums[RSA_BIGNUM_QI] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; retval = do_build_RSA_PKEY(nums, pkey); end: /* The bignums are duplicated with OpenSSL3+ but not with the older API */ #if HA_OPENSSL_VERSION_NUMBER >= 0x30000000L clear_bignums(nums, RSA_BIGNUM_COUNT); #else if (retval) clear_bignums(nums, RSA_BIGNUM_COUNT); #endif free_trash_chunk(tmpbuf); if (retval) { EVP_PKEY_free(*pkey); *pkey = NULL; } return retval; } enum { EC_BIGNUM_X, EC_BIGNUM_Y, EC_BIGNUM_D, EC_BIGNUM_COUNT }; #if HA_OPENSSL_VERSION_NUMBER >= 0x30000000L /* * Build an EC pubkey out of its 'x' and 'y' parameters for EC curve with . * Dump the corresponding pubkey in buffer. * Returns 0 in case of success, 1 otherwise. */ static inline int curve_param_to_pubkey(int nid, BIGNUM *x, BIGNUM *y, struct buffer *out) { EC_POINT *ec_point = NULL; EC_GROUP *group = NULL; BN_CTX *bnctx = NULL; int retval = 1; if (!out) goto end; group = EC_GROUP_new_by_curve_name(nid); if (!group) goto end; bnctx = BN_CTX_new(); if (!bnctx) goto end; ec_point = EC_POINT_new(group); if (!ec_point) goto end; if (EC_POINT_set_affine_coordinates(group, ec_point, x, y, bnctx) < 0) goto end; out->data = EC_POINT_point2oct(group, ec_point, POINT_CONVERSION_UNCOMPRESSED, (unsigned char*)b_orig(out), b_size(out), bnctx); if (out->data == 0) goto end; retval = 0; end: EC_POINT_free(ec_point); EC_GROUP_free(group); BN_CTX_free(bnctx); return retval; } #endif /* * Build an EVP_PKEY that contains an EC key (either public or private) out of a * JWK buffer that must have an "EC" key type ("kty" field). A public key must * have valid "x" and "y" fields and a private key must have a "d" field as * well. * Return 0 in case of success, 1 otherwise. */ static int do_build_EC_PKEY(struct buffer *curve, BIGNUM *nums[EC_BIGNUM_COUNT], EVP_PKEY **pkey) #if HA_OPENSSL_VERSION_NUMBER >= 0x30000000L { int retval = 1; struct buffer *pubkey = NULL; int nid = 0; OSSL_PARAM *params = NULL; OSSL_PARAM_BLD *param_bld = NULL; EVP_PKEY_CTX *pctx = NULL; nid = curves2nid(b_orig(curve)); if (nid == -1) goto end; pubkey = alloc_trash_chunk(); if (!pubkey) goto end; if (curve_param_to_pubkey(nid, nums[EC_BIGNUM_X], nums[EC_BIGNUM_Y], pubkey)) goto end; param_bld = OSSL_PARAM_BLD_new(); if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, OSSL_PKEY_PARAM_GROUP_NAME, b_orig(curve), b_data(curve)) || !OSSL_PARAM_BLD_push_octet_string(param_bld, OSSL_PKEY_PARAM_PUB_KEY, pubkey->area, pubkey->data) || (nums[EC_BIGNUM_D] && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_PRIV_KEY, nums[EC_BIGNUM_D]))) goto end; params = OSSL_PARAM_BLD_to_param(param_bld); if (!params) goto end; pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); if (!pctx) goto end; if (EVP_PKEY_fromdata_init(pctx) != 1 || EVP_PKEY_fromdata(pctx, pkey, EVP_PKEY_KEYPAIR, params) != 1) goto end; retval = 0; end: free_trash_chunk(pubkey); OSSL_PARAM_BLD_free(param_bld); EVP_PKEY_CTX_free(pctx); OSSL_PARAM_free(params); return retval; } #else { EC_KEY *ec_key = NULL; int retval = 1; int nid = 0; nid = curves2nid(b_orig(curve)); if (nid == -1) goto end; ec_key = EC_KEY_new_by_curve_name(nid); if (!ec_key) goto end; if (nums[EC_BIGNUM_D] && EC_KEY_set_private_key(ec_key, nums[EC_BIGNUM_D]) < 0) goto end; if (EC_KEY_set_public_key_affine_coordinates(ec_key, nums[EC_BIGNUM_X], nums[EC_BIGNUM_Y]) < 0) goto end; if (EC_KEY_check_key(ec_key) == 0) goto end; *pkey = EVP_PKEY_new(); if (!*pkey) goto end; if (EVP_PKEY_set1_EC_KEY(*pkey, ec_key) == 0) goto end; retval = 0; end: EC_KEY_free(ec_key); if (retval) { EVP_PKEY_free(*pkey); *pkey = NULL; } return retval; } #endif /* * Build an EVP_PKEY that contains an EC key out of a JWK buffer that must have * an "EC" key type ("kty" field). * Return 0 in case of success, 1 otherwise. */ static int build_EC_PKEY_from_buf(struct buffer *jwk, EVP_PKEY **pkey) { BIGNUM *nums[EC_BIGNUM_COUNT] = {}; int retval = 1; struct buffer *crv = NULL, *tmpbuf = NULL; crv = alloc_trash_chunk(); if (!crv) goto end; tmpbuf = alloc_trash_chunk(); if (!tmpbuf) goto end; if (get_jwk_field(jwk, "$.x", tmpbuf) || (nums[EC_BIGNUM_X] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.y", tmpbuf) || (nums[EC_BIGNUM_Y] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; /* "d" is optional, we might have a pure public key with only "x" and * "y" provided. */ if (get_jwk_field(jwk, "$.d", tmpbuf) == 0 && (nums[EC_BIGNUM_D] = base64url_to_BIGNUM(tmpbuf)) == NULL) goto end; if (get_jwk_field(jwk, "$.crv", crv)) goto end; retval = do_build_EC_PKEY(crv, nums, pkey); #if 0 if (!retval) { BIO *bp = BIO_new_fp(stdout, BIO_NOCLOSE); EVP_PKEY_print_private(bp, *pkey, 1, NULL); BIO_free(bp); } #endif end: clear_bignums(nums, EC_BIGNUM_COUNT); free_trash_chunk(crv); free_trash_chunk(tmpbuf); if (retval) { EVP_PKEY_free(*pkey); *pkey = NULL; } return retval; } 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: case JWK_KTY_EC: EVP_PKEY_free(jwk->pkey); jwk->pkey = NULL; break; default: break; } } /* * Convert a JWK in buffer 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 if (chunk_strcmp(kty, "EC") == 0) { jwk->type = JWK_KTY_EC; if (build_EC_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_jwk_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 RSA JWK"); return 0; } EVP_PKEY_free(pkey); } else if (chunk_strcmp(trash, "EC") == 0) { if (build_EC_PKEY_from_buf(&args[0].data.str, &pkey)) { memprintf(err, "Failed to parse EC JWK"); return 0; } EVP_PKEY_free(pkey); } else { memprintf(err, "Unmanaged key type (expected 'oct', 'RSA' or 'EC'"); return 0; } } else { memprintf(err, "Missing key type (expected 'oct', 'RSA' or 'EC')"); 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_jwk(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 ec = 0; int retval = 0; struct buffer **cek = NULL; struct buffer *decrypted_cek = NULL; struct buffer *out = NULL; struct jose_fields fields = {}; 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)) goto end; /* 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; 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: /* 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) || (ec && jwk.type != JWK_KTY_EC)) 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 if (ec) { /* With algorithms other than "ECDH-ES" we should always have a CEK */ if (alg != JWE_ALG_ECDH_ES) { if (!items[JWE_ELT_CEK].length) goto end; cek = &decoded_items[JWE_ELT_CEK]; *cek = alloc_trash_chunk(); if (!*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; } decrypted_cek = alloc_trash_chunk(); if (!decrypted_cek) { goto end; } if (do_decrypt_cek_ec((cek != NULL) ? *cek : NULL, decrypted_cek, jwk.pkey, fields.pubkey, alg, enc, fields.apu, fields.apv)) goto end; } 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; size = base64urldec(items[JWE_ELT_CEK].start, items[JWE_ELT_CEK].length, (*cek)->area, (*cek)->size); if (size < 0) { goto end; } (*cek)->data = size; decrypted_cek = alloc_trash_chunk(); if (!decrypted_cek) { goto end; } if (rsa) { if (do_decrypt_cek_rsa(*cek, decrypted_cek, jwk.pkey, alg)) goto end; } else { if (gcm) { if (!decrypt_cek_aesgcmkw(*cek, fields.tag, fields.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); clear_jose_fields(&fields); free_trash_chunk(jwk_buf); free_trash_chunk(input); free_trash_chunk(decrypted_cek); free_trash_chunk(out); 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_jwk", sample_conv_jwt_decrypt_jwk, ARG1(1,STR), sample_conv_jwt_decrypt_jwk_check, SMP_T_BIN, SMP_T_BIN }, { NULL, NULL, 0, 0, 0 }, }}; INITCALL1(STG_REGISTER, sample_register_convs, &sample_conv_kws); #endif /* USE_OPENSSL */ #endif /* HAVE_JWS */