diff --git a/src/wp_aes_aead.c b/src/wp_aes_aead.c index fb0893cb..c5e58bc5 100644 --- a/src/wp_aes_aead.c +++ b/src/wp_aes_aead.c @@ -353,6 +353,47 @@ static int wp_aead_cache_in(wp_AeadCtx *ctx, const unsigned char *in, } #endif +/** + * Check whether a tag length is one of the algorithm's allowed sizes. + * + * GCM tags must be 4, 8, 12, 13, 14, 15 or 16 bytes (NIST SP 800-38D). + * CCM tags must be 4, 6, 8, 10, 12, 14 or 16 bytes (NIST SP 800-38C / + * RFC 3610). + * + * Note: for GCM this is intentionally stricter than OpenSSL's default + * provider, which accepts any non-zero tag length up to 16 on + * EVP_CTRL_AEAD_SET_TAG and EVP_CTRL_AEAD_GET_TAG. wolfProvider enforces the + * NIST SP 800-38D set on both the set path (decrypt/verify) and the get path + * (encrypt/tag retrieval), so tag lengths such as 5, 6, 7, 9, 10 or 11 that + * stock OpenSSL would accept are rejected here. + * + * @param [in] mode Cipher mode: EVP_CIPH_GCM_MODE or EVP_CIPH_CCM_MODE. + * @param [in] sz Tag length in bytes to check. + * @return 1 if sz is an allowed length for mode. + * @return 0 otherwise. + */ +static int wp_aead_tag_len_valid(int mode, size_t sz) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_AES, "wp_aead_tag_len_valid"); + + if (mode == EVP_CIPH_GCM_MODE) { + ok = (sz == 4) || (sz == 8) || (sz == 12) || (sz == 13) || + (sz == 14) || (sz == 15) || (sz == 16); + } + else if (mode == EVP_CIPH_CCM_MODE) { + ok = (sz == 4) || (sz == 6) || (sz == 8) || (sz == 10) || + (sz == 12) || (sz == 14) || (sz == 16); + } + else { + ok = 0; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + /** * Get the AEAD context parameters. * @@ -432,7 +473,8 @@ static int wp_aead_get_ctx_params(wp_AeadCtx* ctx, OSSL_PARAM params[]) if (p != NULL) { size_t sz = p->data_size; if ((!ctx->enc) || (ctx->tagLen == UNINITIALISED_SIZET) || - (sz == 0) || (sz > ctx->tagLen)) { + (sz == 0) || (sz > ctx->tagLen) || + (!wp_aead_tag_len_valid(ctx->mode, sz))) { ok = 0; } if (ok && (!OSSL_PARAM_set_octet_string(p, ctx->buf, sz))) { @@ -488,6 +530,9 @@ static int wp_aead_set_param_tag(wp_AeadCtx* ctx, if (ok && ((sz == 0) || ((p->data != NULL) && ctx->enc))) { ok = 0; } + if (ok && !wp_aead_tag_len_valid(ctx->mode, sz)) { + ok = 0; + } if (ok) { ctx->tagLen = sz; } @@ -959,9 +1004,10 @@ static int wp_aesgcm_tls_iv_set_fixed(wp_AeadCtx* ctx, unsigned char* iv, } else { /* Fixed field must be at least 4 bytes and invocation field at least 8 - */ - if ((len < EVP_GCM_TLS_FIXED_IV_LEN) || - (ctx->ivLen - (int)len) < EVP_GCM_TLS_EXPLICIT_IV_LEN) { + * bytes */ + if ((len < EVP_GCM_TLS_FIXED_IV_LEN) || (len > ctx->ivLen) || + (len > sizeof(ctx->iv)) || + (ctx->ivLen - len) < EVP_GCM_TLS_EXPLICIT_IV_LEN) { return 0; } if (ctx->enc) { diff --git a/test/test_aestag.c b/test/test_aestag.c index ee9e2062..44850338 100644 --- a/test/test_aestag.c +++ b/test/test_aestag.c @@ -1416,6 +1416,225 @@ int test_aes_gcm_bad_tag(void *data) return err; } +static int test_aes_gcm_tls_iv_fixed_oversized_helper(OSSL_LIB_CTX *libCtx, + const char *cipherName, int keyLen) +{ + int err; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + unsigned char key[32]; + /* ivLen (12, the GCM default) + EVP_GCM_TLS_EXPLICIT_IV_LEN (8) = 20: */ + unsigned char iv[20]; + + memset(key, 0xCC, keyLen); + memset(iv, 0xDD, sizeof(iv)); + + cipher = EVP_CIPHER_fetch(libCtx, cipherName, ""); + err = cipher == NULL; + + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + } + if (err == 0) { + err = EVP_DecryptInit_ex(ctx, cipher, NULL, key, NULL) != 1; + } + if (err == 0) { + err = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IV_FIXED, + EVP_GCM_TLS_FIXED_IV_LEN, iv) != 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + } + if (err == 0) { + err = EVP_DecryptInit_ex(ctx, cipher, NULL, key, NULL) != 1; + } + if (err == 0) { + int ivLen = EVP_GCM_TLS_FIXED_IV_LEN + EVP_GCM_TLS_EXPLICIT_IV_LEN; + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IV_FIXED, + ivLen, iv) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_GCM_SET_IV_FIXED incorrectly " + "accepted a fixed IV length (%d) equal to the " + "IV length, leaving no room for the explicit " + "field", cipherName, ivLen); + err = 1; + } + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + + /* Oversized fixed len (> ctx->ivLen, default 12) must be rejected. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + } + if (err == 0) { + err = EVP_DecryptInit_ex(ctx, cipher, NULL, key, NULL) != 1; + } + if (err == 0) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IV_FIXED, + (int)sizeof(iv), iv) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_GCM_SET_IV_FIXED incorrectly " + "accepted a fixed IV length (%d) larger than " + "the IV length", cipherName, (int)sizeof(iv)); + err = 1; + } + } + + EVP_CIPHER_CTX_free(ctx); + EVP_CIPHER_free(cipher); + return err; +} + +int test_aes_gcm_tls_iv_fixed_oversized(void *data) +{ + int err; + + (void)data; + + PRINT_MSG("AES-128-GCM TLS1 fixed-IV length boundary rejection"); + err = test_aes_gcm_tls_iv_fixed_oversized_helper(wpLibCtx, + "AES-128-GCM", 16); + if (err == 0) { + PRINT_MSG("AES-256-GCM TLS1 fixed-IV length boundary rejection"); + err = test_aes_gcm_tls_iv_fixed_oversized_helper(wpLibCtx, + "AES-256-GCM", 32); + } + + return err; +} + +/* + * Test that GCM tag lengths are validated against the NIST SP 800-38D table. + */ +static int test_aes_gcm_tag_len_helper(OSSL_LIB_CTX *libCtx, + const char *cipherName) +{ + int err; + int i; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + unsigned char tag[16]; + static const int invalidLen[] = { 1, 2, 3, 5, 6, 7 }; + static const int validLen[] = { 4, 8, 12, 13, 14, 15, 16 }; + int numInvalid = (int)(sizeof(invalidLen) / sizeof(invalidLen[0])); + int numValid = (int)(sizeof(validLen) / sizeof(validLen[0])); + + memset(tag, 0, sizeof(tag)); + + cipher = EVP_CIPHER_fetch(libCtx, cipherName, ""); + err = cipher == NULL; + + for (i = 0; (err == 0) && (i < numInvalid); i++) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_DecryptInit(ctx, cipher, NULL, NULL) != 1; + } + if ((err == 0) && + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, invalidLen[i], + tag) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_SET_TAG incorrectly accepted " + "tag length %d", cipherName, invalidLen[i]); + err = 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + for (i = 0; (err == 0) && (i < numValid); i++) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_DecryptInit(ctx, cipher, NULL, NULL) != 1; + } + if ((err == 0) && + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, validLen[i], + tag) != 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_SET_TAG incorrectly rejected " + "tag length %d", cipherName, validLen[i]); + err = 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + /* Get-tag (encrypt) path, after a normal encrypt, EVP_CTRL_AEAD_GET_TAG + * must reject tag lengths outside the allowed set and accept allowed ones, + * mirroring the set-tag validation above. */ + if (err == 0) { + unsigned char key[32]; + unsigned char iv[12]; + unsigned char aad[16]; + unsigned char msg[32]; + unsigned char enc[32]; + int encLen; + + memset(key, 0, sizeof(key)); + memset(iv, 0, sizeof(iv)); + memset(aad, 0, sizeof(aad)); + memset(msg, 0, sizeof(msg)); + + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_EncryptInit(ctx, cipher, key, iv) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, NULL, &encLen, aad, + (int)sizeof(aad)) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, enc, &encLen, msg, + (int)sizeof(msg)) != 1; + } + if (err == 0) { + err = EVP_EncryptFinal_ex(ctx, enc + encLen, &encLen) != 1; + } + for (i = 0; (err == 0) && (i < numInvalid); i++) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, invalidLen[i], + tag) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_GET_TAG incorrectly accepted " + "tag length %d", cipherName, invalidLen[i]); + err = 1; + } + } + for (i = 0; (err == 0) && (i < numValid); i++) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, validLen[i], + tag) != 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_GET_TAG incorrectly rejected " + "tag length %d", cipherName, validLen[i]); + err = 1; + } + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + EVP_CIPHER_free(cipher); + return err; +} + +int test_aes_gcm_tag_len_undersized(void *data) +{ + int err; + + (void)data; + + PRINT_MSG("AES-128-GCM tag length validation"); + err = test_aes_gcm_tag_len_helper(wpLibCtx, "AES-128-GCM"); + if (err == 0) { + PRINT_MSG("AES-256-GCM tag length validation"); + err = test_aes_gcm_tag_len_helper(wpLibCtx, "AES-256-GCM"); + } + + return err; +} + #endif /* WP_HAVE_AESGCM */ /******************************************************************************/ @@ -1601,5 +1820,157 @@ int test_aes_ccm_bad_tag(void *data) return err; } +/* + * Test that CCM tag lengths are validated against the NIST SP 800-38C. + */ +static int test_aes_ccm_tag_len_helper(OSSL_LIB_CTX *libCtx, + const char *cipherName) +{ + int err; + int i; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + unsigned char iv[13]; + unsigned char tag[16]; + static const int invalidLen[] = { 1, 2, 3, 5, 7, 9 }; + static const int validLen[] = { 4, 6, 8, 10, 12, 14, 16 }; + int numInvalid = (int)(sizeof(invalidLen) / sizeof(invalidLen[0])); + int numValid = (int)(sizeof(validLen) / sizeof(validLen[0])); + + memset(tag, 0, sizeof(tag)); + + cipher = EVP_CIPHER_fetch(libCtx, cipherName, ""); + err = cipher == NULL; + + for (i = 0; (err == 0) && (i < numInvalid); i++) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_DecryptInit(ctx, cipher, NULL, NULL) != 1; + } + if (err == 0) { + err = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, + (int)sizeof(iv), NULL) != 1; + } + if ((err == 0) && + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, invalidLen[i], + tag) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_SET_TAG incorrectly accepted " + "tag length %d", cipherName, invalidLen[i]); + err = 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + for (i = 0; (err == 0) && (i < numValid); i++) { + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_DecryptInit(ctx, cipher, NULL, NULL) != 1; + } + if (err == 0) { + err = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, + (int)sizeof(iv), NULL) != 1; + } + if ((err == 0) && + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, validLen[i], + tag) != 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_SET_TAG incorrectly rejected " + "tag length %d", cipherName, validLen[i]); + err = 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + /* Get-tag (encrypt) path: after a normal encrypt, EVP_CTRL_AEAD_GET_TAG + * must reject tag lengths outside the allowed set and accept allowed ones, + * mirroring the set-tag validation above. */ + if (err == 0) { + unsigned char key[32]; + unsigned char aad[16]; + unsigned char msg[32]; + unsigned char enc[32]; + int encLen; + + memset(key, 0, sizeof(key)); + memset(iv, 0, sizeof(iv)); + memset(aad, 0, sizeof(aad)); + memset(msg, 0, sizeof(msg)); + + ctx = EVP_CIPHER_CTX_new(); + err = ctx == NULL; + if (err == 0) { + err = EVP_EncryptInit(ctx, cipher, NULL, NULL) != 1; + } + if (err == 0) { + err = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, + (int)sizeof(iv), NULL) != 1; + } + if (err == 0) { + /* CCM needs the tag length fixed before encryption. */ + err = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, + NULL) != 1; + } + if (err == 0) { + err = EVP_EncryptInit(ctx, NULL, key, iv) != 1; + } + if (err == 0) { + /* CCM requires the total plaintext length up front. */ + err = EVP_EncryptUpdate(ctx, NULL, &encLen, NULL, + (int)sizeof(msg)) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, NULL, &encLen, aad, + (int)sizeof(aad)) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, enc, &encLen, msg, + (int)sizeof(msg)) != 1; + } + if (err == 0) { + err = EVP_EncryptFinal_ex(ctx, enc + encLen, &encLen) != 1; + } + for (i = 0; (err == 0) && (i < numInvalid); i++) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, invalidLen[i], + tag) == 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_GET_TAG incorrectly accepted " + "tag length %d", cipherName, invalidLen[i]); + err = 1; + } + } + for (i = 0; (err == 0) && (i < numValid); i++) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, validLen[i], + tag) != 1) { + PRINT_ERR_MSG("%s: EVP_CTRL_AEAD_GET_TAG incorrectly rejected " + "tag length %d", cipherName, validLen[i]); + err = 1; + } + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + } + + EVP_CIPHER_free(cipher); + return err; +} + +int test_aes_ccm_tag_len_undersized(void *data) +{ + int err; + + (void)data; + + PRINT_MSG("AES-128-CCM tag length validation"); + err = test_aes_ccm_tag_len_helper(wpLibCtx, "AES-128-CCM"); + if (err == 0) { + PRINT_MSG("AES-256-CCM tag length validation"); + err = test_aes_ccm_tag_len_helper(wpLibCtx, "AES-256-CCM"); + } + + return err; +} + #endif /* WP_HAVE_AESCCM */ diff --git a/test/unit.c b/test/unit.c index e116f9b5..cb02a4b9 100644 --- a/test/unit.c +++ b/test/unit.c @@ -288,6 +288,8 @@ TEST_CASE test_case[] = { TEST_DECL(test_aes128_gcm_set_iv_inv, NULL), TEST_DECL(test_aes128_gcm_key_then_iv, NULL), TEST_DECL(test_aes_gcm_bad_tag, NULL), + TEST_DECL(test_aes_gcm_tls_iv_fixed_oversized, NULL), + TEST_DECL(test_aes_gcm_tag_len_undersized, NULL), #endif #ifdef WP_HAVE_AESCCM TEST_DECL(test_aes128_ccm, NULL), @@ -296,6 +298,7 @@ TEST_CASE test_case[] = { #if OPENSSL_VERSION_NUMBER >= 0x10100000L TEST_DECL(test_aes128_ccm_tls, NULL), TEST_DECL(test_aes_ccm_bad_tag, NULL), + TEST_DECL(test_aes_ccm_tag_len_undersized, NULL), #endif #endif #ifdef WP_HAVE_RANDOM diff --git a/test/unit.h b/test/unit.h index 9aab6828..26b6349d 100644 --- a/test/unit.h +++ b/test/unit.h @@ -219,6 +219,8 @@ int test_aes128_gcm_tls(void *data); int test_aes128_gcm_set_iv_inv(void *data); int test_aes128_gcm_key_then_iv(void *data); int test_aes_gcm_bad_tag(void *data); +int test_aes_gcm_tls_iv_fixed_oversized(void *data); +int test_aes_gcm_tag_len_undersized(void *data); #endif /* WP_HAVE_AESGCM */ @@ -229,6 +231,7 @@ int test_aes192_ccm(void *data); int test_aes256_ccm(void *data); int test_aes128_ccm_tls(void *data); int test_aes_ccm_bad_tag(void *data); +int test_aes_ccm_tag_len_undersized(void *data); #endif /* WP_HAVE_AESCCM */