Skip to content

Commit 843c9b9

Browse files
Viktor Dukhovnit8m
authored andcommitted
Apply the buffered IV on the AES-OCB EVP_Cipher() path
aes_ocb_cipher(), the OCB provider's OSSL_FUNC_CIPHER_CIPHER slot, processed input without flushing the buffered IV into the OCB context. Effective nonce was 0 regardless of the caller's IV; EVP_*Final_ex() then emitted a tag depending only on (key, iv). This gave (key, nonce) reuse and single-query universal forgery on the EVP_Cipher() path. Apply update_iv() at the head of aes_ocb_cipher() to mirror the streaming handler. The matching GCM one-shot does this already. Add a cross-driver round-trip test for AES-{GCM,CCM,OCB} and ChaCha20-Poly1305 in test/evp_extra_test.c. Each cipher is exercised with and without AAD; the no-AAD case is needed because any prior EVP_CipherUpdate(NULL, aad, ...) routes through the streaming handler and applies the IV itself, masking the bug. Fixes CVE-2026-45445 Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org> Reviewed-by: Tomas Mraz <tomas@openssl.foundation> MergeDate: Mon Jun 8 20:02:00 2026 (cherry picked from commit 50c95c5d1e83f4f46a555dfa7fd9c632d3eba9dc)
1 parent ffce42f commit 843c9b9

2 files changed

Lines changed: 279 additions & 0 deletions

File tree

providers/implementations/ciphers/cipher_aes_ocb.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,19 @@ static int aes_ocb_cipher(void *vctx, unsigned char *out, size_t *outl,
498498
return 0;
499499
}
500500

501+
/*
502+
* Mirror the streaming handler: refuse if the key has not been set,
503+
* and push the buffered IV into the OCB context before any data is
504+
* processed. Without this, CRYPTO_ocb128_encrypt/decrypt runs with
505+
* Offset_0 = 0 regardless of the caller's IV -- catastrophic
506+
* (key, nonce) reuse, and a subsequent EVP_*Final_ex() emits a tag
507+
* that is a function of (key, iv) only.
508+
*/
509+
if (!ctx->key_set || !update_iv(ctx)) {
510+
ERR_raise(ERR_LIB_PROV, PROV_R_CIPHER_OPERATION_FAILED);
511+
return 0;
512+
}
513+
501514
if (!aes_generic_ocb_cipher(ctx, in, out, inl)) {
502515
ERR_raise(ERR_LIB_PROV, PROV_R_CIPHER_OPERATION_FAILED);
503516
return 0;

test/evp_extra_test.c

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6329,6 +6329,270 @@ static int test_evp_cipher_negative_length(void)
63296329
EVP_CIPHER_CTX_free(ctx);
63306330
return ret;
63316331
}
6332+
6333+
/*
6334+
* Cross-driver round-trip test for AEAD one-shot vs streaming paths.
6335+
*
6336+
* The streaming path (EVP_CipherUpdate/Final, dispatched to
6337+
* OSSL_FUNC_CIPHER_UPDATE/_FINAL) is treated as the oracle. For each
6338+
* AEAD configuration we encrypt and decrypt the same (key, iv, aad, pt),
6339+
* driving the body in two combinations:
6340+
*
6341+
* 1. body encrypt via EVP_Cipher() (one-shot, OSSL_FUNC_CIPHER_CIPHER),
6342+
* body decrypt via EVP_CipherUpdate (streaming).
6343+
* 2. body encrypt via EVP_CipherUpdate, body decrypt via EVP_Cipher().
6344+
*
6345+
* Both combinations must recover the plaintext and verify the tag. AAD
6346+
* is always fed via EVP_CipherUpdate(NULL, ...): OCB's one-shot is body
6347+
* only and the asymmetric "AAD streaming, body one-shot" call shape is
6348+
* the natural pattern a caller reaching for EVP_Cipher() for throughput
6349+
* would write anyway.
6350+
*
6351+
* CVE-2026-45445 (AES-OCB EVP_Cipher() ignored IV) was a silent failure
6352+
* in this matrix: the one-shot encrypt path produced ciphertext under
6353+
* Offset_0 = 0 regardless of IV, which the streaming decrypt path then
6354+
* could not verify. Adding this cross-check catches the same class of
6355+
* bug for any future AEAD whose one-shot dispatch diverges from its
6356+
* streaming dispatch.
6357+
*/
6358+
typedef struct {
6359+
const char *name; /* EVP_CIPHER fetch name */
6360+
size_t keylen;
6361+
size_t ivlen;
6362+
size_t taglen;
6363+
int is_ccm; /* needs length-up-front + tag-before-body dance */
6364+
} AEAD_ONESHOT_CFG;
6365+
6366+
static const AEAD_ONESHOT_CFG aead_oneshot_cfgs[] = {
6367+
{ "AES-128-GCM", 16, 12, 16, 0 },
6368+
{ "AES-256-GCM", 32, 12, 16, 0 },
6369+
{ "AES-128-CCM", 16, 12, 16, 1 },
6370+
{ "AES-256-CCM", 32, 12, 16, 1 },
6371+
{ "AES-128-OCB", 16, 12, 16, 0 },
6372+
{ "AES-256-OCB", 32, 12, 16, 0 },
6373+
{ "ChaCha20-Poly1305", 32, 12, 16, 0 }
6374+
};
6375+
6376+
/*
6377+
* Drive an encrypt or decrypt operation. AAD always via EVP_CipherUpdate.
6378+
* Body via EVP_Cipher() when oneshot_body is non-zero, EVP_CipherUpdate
6379+
* otherwise. On encrypt, fills *out and the caller-provided tag buffer.
6380+
* On decrypt, reads from in and verifies tag; returns 0 if verification
6381+
* fails (the test asserts the expected outcome).
6382+
*/
6383+
static int aead_oneshot_op(const AEAD_ONESHOT_CFG *cfg, int enc,
6384+
int oneshot_body, const unsigned char *key,
6385+
const unsigned char *iv, const unsigned char *aad,
6386+
size_t aad_len, const unsigned char *in, size_t in_len,
6387+
unsigned char *out, unsigned char *tag, const char **why)
6388+
{
6389+
EVP_CIPHER_CTX *ctx = NULL;
6390+
EVP_CIPHER *cipher = NULL;
6391+
int outl = 0, tmpl = 0;
6392+
int ok = 0;
6393+
int body_rv;
6394+
6395+
*why = NULL;
6396+
6397+
if (!TEST_ptr(cipher = EVP_CIPHER_fetch(testctx, cfg->name, testpropq))) {
6398+
*why = "CIPHER_FETCH";
6399+
goto end;
6400+
}
6401+
if (!TEST_ptr(ctx = EVP_CIPHER_CTX_new())) {
6402+
*why = "CTX_NEW";
6403+
goto end;
6404+
}
6405+
if (!TEST_true(EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, enc))) {
6406+
*why = "INIT_CIPHER";
6407+
goto end;
6408+
}
6409+
if (!TEST_int_gt(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN,
6410+
(int)cfg->ivlen, NULL),
6411+
0)) {
6412+
*why = "SET_IVLEN";
6413+
goto end;
6414+
}
6415+
if (cfg->is_ccm) {
6416+
/* Placeholder taglen on encrypt, real tag on decrypt; both before key+iv. */
6417+
if (!TEST_int_gt(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
6418+
(int)cfg->taglen, enc ? NULL : tag),
6419+
0)) {
6420+
*why = "CCM_SET_TAG";
6421+
goto end;
6422+
}
6423+
}
6424+
if (!TEST_true(EVP_CipherInit_ex(ctx, NULL, NULL, key, iv, enc))) {
6425+
*why = "INIT_KEY_IV";
6426+
goto end;
6427+
}
6428+
if (cfg->is_ccm) {
6429+
if (!TEST_true(EVP_CipherUpdate(ctx, NULL, &outl, NULL, (int)in_len))) {
6430+
*why = "CCM_LEN_DECL";
6431+
goto end;
6432+
}
6433+
}
6434+
if (aad_len > 0
6435+
&& !TEST_true(EVP_CipherUpdate(ctx, NULL, &outl, aad, (int)aad_len))) {
6436+
*why = "AAD";
6437+
goto end;
6438+
}
6439+
if (!enc && !cfg->is_ccm
6440+
&& !TEST_int_gt(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
6441+
(int)cfg->taglen, tag),
6442+
0)) {
6443+
*why = "SET_TAG";
6444+
goto end;
6445+
}
6446+
6447+
if (oneshot_body) {
6448+
body_rv = EVP_Cipher(ctx, out, in, (unsigned int)in_len);
6449+
if (cfg->is_ccm && !enc) {
6450+
/* CCM decrypt: 0 means tag verify failed, < 0 means error. */
6451+
if (!TEST_int_gt(body_rv, 0)) {
6452+
*why = "ONESHOT_DECRYPT";
6453+
goto end;
6454+
}
6455+
} else {
6456+
if (!TEST_int_ge(body_rv, 0)) {
6457+
*why = "ONESHOT_BODY";
6458+
goto end;
6459+
}
6460+
}
6461+
outl = (int)in_len;
6462+
} else {
6463+
if (!TEST_true(EVP_CipherUpdate(ctx, out, &outl, in, (int)in_len))) {
6464+
*why = enc ? "STREAM_BODY_ENC" : "STREAM_BODY_DEC";
6465+
goto end;
6466+
}
6467+
}
6468+
6469+
if (!cfg->is_ccm) {
6470+
if (!TEST_true(EVP_CipherFinal_ex(ctx, out + outl, &tmpl))) {
6471+
*why = enc ? "FINAL_ENC" : "FINAL_DEC";
6472+
goto end;
6473+
}
6474+
}
6475+
6476+
if (enc) {
6477+
if (!TEST_int_gt(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG,
6478+
(int)cfg->taglen, tag),
6479+
0)) {
6480+
*why = "GET_TAG";
6481+
goto end;
6482+
}
6483+
}
6484+
ok = 1;
6485+
end:
6486+
EVP_CIPHER_CTX_free(ctx);
6487+
EVP_CIPHER_free(cipher);
6488+
return ok;
6489+
}
6490+
6491+
/*
6492+
* For each AEAD row we run two AAD modes, and within each AAD mode two
6493+
* cross-driver round trips:
6494+
*
6495+
* aad_mode 0: no AAD. Critical for catching the OCB-style bug: any
6496+
* EVP_CipherUpdate(NULL, aad, ...) call before the body
6497+
* would itself pass through the (correct) streaming
6498+
* handler and apply the buffered IV, masking the one-shot
6499+
* handler's failure to do so. With aad_len == 0 we make
6500+
* EVP_Cipher() the very first cipher operation on the
6501+
* context, which is the shape the bug requires.
6502+
*
6503+
* aad_mode 1: with AAD via streaming. Catches divergence between the
6504+
* drivers when AAD is in play.
6505+
*
6506+
* leg 0: encrypt-oneshot + decrypt-streaming
6507+
* leg 1: encrypt-streaming + decrypt-oneshot
6508+
*
6509+
* The test index encodes (cipher, aad_mode) so a failure points at both.
6510+
*/
6511+
static int test_aead_oneshot_roundtrip(int idx)
6512+
{
6513+
static const unsigned char fixed_key[32] = {
6514+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
6515+
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
6516+
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
6517+
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
6518+
};
6519+
static const unsigned char fixed_iv[12] = {
6520+
0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab
6521+
};
6522+
static const unsigned char fixed_aad[] = "extra:context";
6523+
static const unsigned char fixed_pt[] = "THE QUICK BROWN FOX JUMPS OVER LAZY!!";
6524+
const AEAD_ONESHOT_CFG *cfg = &aead_oneshot_cfgs[idx / 2];
6525+
int with_aad = idx % 2;
6526+
size_t aad_len = with_aad ? sizeof(fixed_aad) - 1 : 0;
6527+
size_t pt_len = sizeof(fixed_pt) - 1;
6528+
EVP_CIPHER *probe = NULL;
6529+
unsigned char ct[64], pt[64];
6530+
unsigned char tag_oneshot[16], tag_stream[16];
6531+
const char *why = NULL;
6532+
int leg, ok = 0;
6533+
6534+
/*
6535+
* Probe for the cipher: a build with no-ocb / no-chacha / etc. will
6536+
* not have it, and we treat that as a pass (nothing to test here).
6537+
*/
6538+
ERR_set_mark();
6539+
probe = EVP_CIPHER_fetch(testctx, cfg->name, testpropq);
6540+
ERR_pop_to_mark();
6541+
if (probe == NULL) {
6542+
TEST_info("skipping, '%s' is not available", cfg->name);
6543+
return 1;
6544+
}
6545+
EVP_CIPHER_free(probe);
6546+
6547+
for (leg = 0; leg <= 1; leg++) {
6548+
int enc_oneshot = (leg == 0);
6549+
unsigned char *tag = enc_oneshot ? tag_oneshot : tag_stream;
6550+
6551+
memset(ct, 0, sizeof(ct));
6552+
memset(pt, 0, sizeof(pt));
6553+
memset(tag, 0, cfg->taglen);
6554+
6555+
if (!aead_oneshot_op(cfg, /*enc=*/1, /*oneshot_body=*/enc_oneshot,
6556+
fixed_key, fixed_iv, fixed_aad, aad_len,
6557+
fixed_pt, pt_len, ct, tag, &why)) {
6558+
TEST_error("%s (%s): encrypt leg %d (%s body) failed at %s",
6559+
cfg->name, with_aad ? "with AAD" : "no AAD",
6560+
leg, enc_oneshot ? "oneshot" : "stream",
6561+
why ? why : "?");
6562+
goto end;
6563+
}
6564+
if (!aead_oneshot_op(cfg, /*enc=*/0, /*oneshot_body=*/!enc_oneshot,
6565+
fixed_key, fixed_iv, fixed_aad, aad_len,
6566+
ct, pt_len, pt, tag, &why)) {
6567+
TEST_error("%s (%s): decrypt leg %d (%s body) failed at %s",
6568+
cfg->name, with_aad ? "with AAD" : "no AAD",
6569+
leg, enc_oneshot ? "stream" : "oneshot",
6570+
why ? why : "?");
6571+
goto end;
6572+
}
6573+
if (!TEST_mem_eq(pt, pt_len, fixed_pt, pt_len)) {
6574+
TEST_error("%s (%s): leg %d: recovered plaintext differs",
6575+
cfg->name, with_aad ? "with AAD" : "no AAD", leg);
6576+
goto end;
6577+
}
6578+
}
6579+
6580+
/*
6581+
* Both legs share the same (key, iv, aad, pt) and must therefore
6582+
* agree on the tag bit-for-bit, regardless of which driver computed
6583+
* it. This catches the OCB-style failure where the one-shot path
6584+
* silently emits a different ciphertext/tag from the streaming path.
6585+
*/
6586+
if (!TEST_mem_eq(tag_oneshot, cfg->taglen, tag_stream, cfg->taglen)) {
6587+
TEST_error("%s (%s): oneshot-encrypt tag != streaming-encrypt tag",
6588+
cfg->name, with_aad ? "with AAD" : "no AAD");
6589+
goto end;
6590+
}
6591+
ok = 1;
6592+
end:
6593+
return ok;
6594+
}
6595+
63326596
#ifndef OPENSSL_NO_DES
63336597
static int test_EVP_CIPHER_get_type_des_ede3(void)
63346598
{
@@ -7239,6 +7503,8 @@ int setup_tests(void)
72397503
ADD_TEST(test_aes_rc4_keylen_change_cve_2023_5363);
72407504
#endif
72417505

7506+
ADD_ALL_TESTS(test_aead_oneshot_roundtrip, 2 * OSSL_NELEM(aead_oneshot_cfgs));
7507+
72427508
ADD_TEST(test_invalid_ctx_for_digest);
72437509

72447510
ADD_TEST(test_evp_cipher_negative_length);

0 commit comments

Comments
 (0)