@@ -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
63336597static 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