diff --git a/.gitignore b/.gitignore index f24529d1..39d458fe 100644 --- a/.gitignore +++ b/.gitignore @@ -111,7 +111,10 @@ IDE/WINVS/.vs/ IDE/WINVS/**/*.user IDE/WINVS/**/*.vcxproj.filters -examples/openssl_example +examples/openssl_example* +examples/.deps +examples/.dirstamp +examples/.libs cscope.out diff --git a/src/wp_aes_stream.c b/src/wp_aes_stream.c index 36ac7504..0d159535 100644 --- a/src/wp_aes_stream.c +++ b/src/wp_aes_stream.c @@ -59,10 +59,20 @@ typedef struct wp_AesStreamCtx { #if defined(WP_HAVE_AESCTS) /* Only single shot allowed */ unsigned int updated:1; + /** Ciphertext stealing variant in use. */ + int ctsMode; #endif } wp_AesStreamCtx; +#if defined(WP_HAVE_AESCTS) +/** Ciphertext stealing variants. The last two blocks are swapped in CS3, and + * in CS2 only when the final block is short. */ +#define WP_CTS_MODE_CS1 1 +#define WP_CTS_MODE_CS2 2 +#define WP_CTS_MODE_CS3 3 +#endif + /* Prototype for initialization to call. */ static int wp_aes_stream_set_ctx_params(wp_AesStreamCtx *ctx, const OSSL_PARAM params[]); @@ -402,6 +412,15 @@ static int wp_aes_stream_dinit(wp_AesStreamCtx *ctx, const unsigned char *key, #ifdef WP_HAVE_AESCTS +/* CS3 always swaps the final two blocks, CS2 only when the last block is + * short, and CS1 never does. */ +static int wp_aes_cts_swapped(const wp_AesStreamCtx *ctx, size_t partialSz) +{ + return (ctx->ctsMode == WP_CTS_MODE_CS3) || + ((ctx->ctsMode == WP_CTS_MODE_CS2) && + (partialSz != AES_BLOCK_SIZE)); +} + static int wp_aes_cts_encrypt(wp_AesStreamCtx *ctx, unsigned char *out, const unsigned char *in, size_t inLen) { @@ -457,8 +476,17 @@ static int wp_aes_cts_encrypt(wp_AesStreamCtx *ctx, unsigned char *out, } } if (ok) { - XMEMCPY(out, ctsBlock + AES_BLOCK_SIZE, AES_BLOCK_SIZE); - XMEMCPY(out + AES_BLOCK_SIZE, ctsBlock, inLen - AES_BLOCK_SIZE); + size_t partialSz = inLen - AES_BLOCK_SIZE; + + if (wp_aes_cts_swapped(ctx, partialSz)) { + XMEMCPY(out, ctsBlock + AES_BLOCK_SIZE, AES_BLOCK_SIZE); + XMEMCPY(out + AES_BLOCK_SIZE, ctsBlock, partialSz); + } + else { + XMEMCPY(out, ctsBlock, partialSz); + XMEMCPY(out + partialSz, ctsBlock + AES_BLOCK_SIZE, + AES_BLOCK_SIZE); + } } OPENSSL_cleanse(ctsBlock, sizeof(ctsBlock)); @@ -518,7 +546,13 @@ static int wp_aes_cts_decrypt(wp_AesStreamCtx *ctx, unsigned char *out, } } if (ok) { - XMEMCPY(ctsBlock, in, inLen); + if (wp_aes_cts_swapped(ctx, partialSz)) { + XMEMCPY(ctsBlock, in, inLen); + } + else { + XMEMCPY(ctsBlock, in + partialSz, AES_BLOCK_SIZE); + XMEMCPY(ctsBlock + AES_BLOCK_SIZE, in, partialSz); + } XMEMCPY(&ctx->aes.reg, ctsBlock + AES_BLOCK_SIZE, AES_BLOCK_SIZE); rc = wc_AesCbcDecrypt(&ctx->aes, tmp, ctsBlock, AES_BLOCK_SIZE); if (rc != 0) { @@ -804,7 +838,9 @@ static int wp_aes_stream_get_ctx_params(wp_AesStreamCtx* ctx, #ifdef WP_HAVE_AESCTS if (ok && ctx->mode == EVP_CIPH_CBC_MODE) { p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_CTS_MODE); - if ((p != NULL) && (!OSSL_PARAM_set_utf8_string(p, "CS3"))) { + if ((p != NULL) && (!OSSL_PARAM_set_utf8_string(p, + (ctx->ctsMode == WP_CTS_MODE_CS1) ? "CS1" : + (ctx->ctsMode == WP_CTS_MODE_CS2) ? "CS2" : "CS3"))) { ok = 0; } } @@ -858,8 +894,19 @@ static int wp_aes_stream_set_ctx_params(wp_AesStreamCtx *ctx, sizeof(cts_mode))) { ok = 0; } - if (ok && (XSTRCMP(cts_mode, "CS3") != 0)) { - ok = 0; /* Only CS3 supported */ + if (ok) { + if (XSTRCMP(cts_mode, "CS1") == 0) { + ctx->ctsMode = WP_CTS_MODE_CS1; + } + else if (XSTRCMP(cts_mode, "CS2") == 0) { + ctx->ctsMode = WP_CTS_MODE_CS2; + } + else if (XSTRCMP(cts_mode, "CS3") == 0) { + ctx->ctsMode = WP_CTS_MODE_CS3; + } + else { + ok = 0; + } } } } @@ -886,6 +933,9 @@ static void wp_aes_stream_init_ctx(wp_AesStreamCtx* ctx, size_t kBits, ctx->keyLen = ((kBits) / 8); ctx->ivLen = ((ivBits) / 8); ctx->mode = mode; +#if defined(WP_HAVE_AESCTS) + ctx->ctsMode = WP_CTS_MODE_CS1; +#endif } diff --git a/test/test_cipher.c b/test/test_cipher.c index 667044f5..f0172569 100644 --- a/test/test_cipher.c +++ b/test/test_cipher.c @@ -1611,13 +1611,8 @@ int test_aes128_cts_split_init(void *data) err = (ctx = EVP_CIPHER_CTX_new()) == NULL; } if (err == 0) { - OSSL_PARAM params[2]; - - params[0] = OSSL_PARAM_construct_utf8_string(OSSL_CIPHER_PARAM_CTS_MODE, - (char *)"CS3", 0); - params[1] = OSSL_PARAM_construct_end(); - - err = EVP_CipherInit_ex2(ctx, ocipher, key, iv, 1, params) != 1 + /* No variant requested, so both providers use their default. */ + err = EVP_CipherInit_ex2(ctx, ocipher, key, iv, 1, NULL) != 1 || EVP_CipherUpdate(ctx, ref, &outlen, msg, sizeof(msg)) != 1 || EVP_CipherFinal_ex(ctx, ref + outlen, &finallen) != 1; } @@ -1784,6 +1779,197 @@ int test_aes128_cts_one_block_split_init(void *data) return err; } +/** + * Encrypt with the given provider and ciphertext stealing variant. + * + * @param [in] cipher Cipher fetched from the provider under test. + * @param [in] ctsMode Variant name, CS1, CS2 or CS3. + * @param [in] key Key data. + * @param [in] iv IV data. + * @param [in] msg Plaintext. + * @param [in] len Length of plaintext in bytes. + * @param [out] enc Buffer for the ciphertext. + * @param [out] encLen Length of ciphertext in bytes. + * @return 0 on success, non-zero on failure. + */ +static int test_cipher_cts_mode_enc(const EVP_CIPHER *cipher, + const char *ctsMode, unsigned char *key, unsigned char *iv, + unsigned char *msg, int len, unsigned char *enc, int *encLen) +{ + int err; + EVP_CIPHER_CTX *ctx = NULL; + OSSL_PARAM params[2]; + int outLen = 0; + int total = 0; + + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_CIPHER_PARAM_CTS_MODE, + (char *)ctsMode, 0); + params[1] = OSSL_PARAM_construct_end(); + + err = (ctx = EVP_CIPHER_CTX_new()) == NULL; + if (err == 0) { + err = EVP_CipherInit_ex2(ctx, cipher, key, iv, 1, params) != 1; + } + if (err == 0) { + err = EVP_CipherUpdate(ctx, enc, &outLen, msg, len) != 1; + total = outLen; + } + if (err == 0) { + err = EVP_CipherFinal_ex(ctx, enc + total, &outLen) != 1; + total += outLen; + } + if (err == 0) { + *encLen = total; + } + + EVP_CIPHER_CTX_free(ctx); + return err; +} + +/** + * Decrypt with the given provider and ciphertext stealing variant. + */ +static int test_cipher_cts_mode_dec(const EVP_CIPHER *cipher, + const char *ctsMode, unsigned char *key, unsigned char *iv, + unsigned char *enc, int encLen, unsigned char *dec, int *decLen) +{ + int err; + EVP_CIPHER_CTX *ctx = NULL; + OSSL_PARAM params[2]; + int outLen = 0; + int total = 0; + + params[0] = OSSL_PARAM_construct_utf8_string(OSSL_CIPHER_PARAM_CTS_MODE, + (char *)ctsMode, 0); + params[1] = OSSL_PARAM_construct_end(); + + err = (ctx = EVP_CIPHER_CTX_new()) == NULL; + if (err == 0) { + err = EVP_CipherInit_ex2(ctx, cipher, key, iv, 0, params) != 1; + } + if (err == 0) { + err = EVP_CipherUpdate(ctx, dec, &outLen, enc, encLen) != 1; + total = outLen; + } + if (err == 0) { + err = EVP_CipherFinal_ex(ctx, dec + total, &outLen) != 1; + total += outLen; + } + if (err == 0) { + *decLen = total; + } + + EVP_CIPHER_CTX_free(ctx); + return err; +} + +/** + * Compare AES-CTS against the OpenSSL default provider for one variant. + * + * The variants differ only in the order of the final two ciphertext blocks, + * so a mismatch means data written by one provider cannot be read by the + * other. Lengths cover a short final block and an exact block multiple. + */ +static int test_cipher_cts_mode_helper(const char *cipherName, + const char *ctsMode, int keyLen) +{ + static const int lengths[] = { 17, 31, 33, 64 }; + int err = 0; + size_t i; + EVP_CIPHER *wcipher = NULL; + EVP_CIPHER *ocipher = NULL; + unsigned char key[32]; + unsigned char iv[16]; + unsigned char msg[64]; + unsigned char wEnc[128]; + unsigned char oEnc[128]; + unsigned char dec[128]; + int wLen = 0; + int oLen = 0; + int decLen = 0; + + memset(key, 0xA5, (size_t)keyLen); + memset(iv, 0x5A, sizeof(iv)); + for (i = 0; i < sizeof(msg); i++) { + msg[i] = (unsigned char)i; + } + + wcipher = EVP_CIPHER_fetch(wpLibCtx, cipherName, ""); + ocipher = EVP_CIPHER_fetch(osslLibCtx, cipherName, ""); + err = (wcipher == NULL) || (ocipher == NULL); + + for (i = 0; (err == 0) && (i < sizeof(lengths) / sizeof(lengths[0])); + i++) { + int len = lengths[i]; + + PRINT_MSG("%s %s length %d", cipherName, ctsMode, len); + + err = test_cipher_cts_mode_enc(wcipher, ctsMode, key, iv, msg, len, + wEnc, &wLen); + if (err == 0) { + err = test_cipher_cts_mode_enc(ocipher, ctsMode, key, iv, msg, + len, oEnc, &oLen); + } + if (err == 0) { + err = (wLen != oLen) || (memcmp(wEnc, oEnc, (size_t)wLen) != 0); + if (err) { + PRINT_ERR_MSG("%s %s length %d: ciphertext differs", + cipherName, ctsMode, len); + } + } + /* Each provider must read what the other wrote. */ + if (err == 0) { + err = test_cipher_cts_mode_dec(ocipher, ctsMode, key, iv, wEnc, + wLen, dec, &decLen); + } + if (err == 0) { + err = (decLen != len) || (memcmp(dec, msg, (size_t)len) != 0); + if (err) { + PRINT_ERR_MSG("%s %s length %d: OpenSSL decrypt mismatch", + cipherName, ctsMode, len); + } + } + if (err == 0) { + err = test_cipher_cts_mode_dec(wcipher, ctsMode, key, iv, oEnc, + oLen, dec, &decLen); + } + if (err == 0) { + err = (decLen != len) || (memcmp(dec, msg, (size_t)len) != 0); + if (err) { + PRINT_ERR_MSG("%s %s length %d: wolfProvider decrypt mismatch", + cipherName, ctsMode, len); + } + } + } + + EVP_CIPHER_free(wcipher); + EVP_CIPHER_free(ocipher); + return err; +} + +/** + * Test every ciphertext stealing variant against the OpenSSL default + * provider, including the CS1 default used when no variant is requested. + */ +int test_aes_cts_modes(void *data) +{ + static const char *modes[] = { "CS1", "CS2", "CS3" }; + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < sizeof(modes) / sizeof(modes[0])); i++) { + err = test_cipher_cts_mode_helper("AES-128-CBC-CTS", modes[i], 16); + if (err == 0) { + err = test_cipher_cts_mode_helper("AES-256-CBC-CTS", modes[i], + 32); + } + } + + return err; +} + #endif /* WP_HAVE_AESCTS */ #ifdef WP_HAVE_AESCBC diff --git a/test/unit.c b/test/unit.c index da5d820d..944b2f48 100644 --- a/test/unit.c +++ b/test/unit.c @@ -308,6 +308,7 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_AESCTS TEST_DECL(test_aes128_cts, NULL), TEST_DECL(test_aes256_cts, NULL), + TEST_DECL(test_aes_cts_modes, NULL), TEST_DECL(test_aes128_cts_one_block, NULL), TEST_DECL(test_aes128_cts_split_init, NULL), TEST_DECL(test_aes128_cts_one_block_split_init, NULL), diff --git a/test/unit.h b/test/unit.h index 86c255ca..a7832144 100644 --- a/test/unit.h +++ b/test/unit.h @@ -317,6 +317,7 @@ int test_aes_ccm_key_no_iv(void *data); int test_aes128_cts(void *data); int test_aes256_cts(void *data); +int test_aes_cts_modes(void *data); int test_aes128_cts_one_block(void *data); int test_aes128_cts_split_init(void *data); int test_aes128_cts_one_block_split_init(void *data);