Skip to content

CCM Final accepts an invalid tag for an empty ciphertext when payload Update is skipped #32253

Description

@idrassi

I found an edge case in the provider CCM implementations where an empty ciphertext can reach Final without its tag being checked. It happens when the caller declares a zero message length, supplies AAD, and doesn't make a separate payload Update call:

EVP_DecryptUpdate(ctx, NULL, &outl, NULL, 0);       /* declare length 0 */
EVP_DecryptUpdate(ctx, NULL, &outl, aad, aad_len);  /* optional AAD */
EVP_DecryptFinal_ex(ctx, out, &outl);               /* accepts a bad tag */

I see the same result with EVP_CipherFinal_ex, EVP_Cipher(ctx, out, NULL, 0), and EVP_Cipher(ctx, NULL, NULL, 0). A NULL output buffer doesn't change the result. AES-CCM, ARIA-CCM, and SM4-CCM are all affected.

There is a return value detail worth pointing out for the EVP_Cipher case.
With a provider cipher, an empty successful operation returns its output length, which is zero, while failure is reported as -1. So, a return value of zero here means that the invalid tag was accepted.

Empty encryption shows the other side of the problem: Final reports success, but GET_TAG can't retrieve a tag when the payload Update was skipped.

I reproduced this on OpenSSL 3.5.7 and on 4.1.0-dev at commit 7fba23f. Looking through the released source, the affected provider CCM finalization behavior is present from OpenSSL 3.0 onward.

The attached ccm_empty_aad_repro.c uses an AES-CCM NIST CAVS vector marked invalid. It has empty plaintext and ciphertext, non-empty AAD, and an invalid tag.

On an affected build I get:

stream final with invalid tag: ret=1 (ACCEPTED - BUG)
EVP_Cipher final with invalid tag: ret=0 (ACCEPTED - BUG)
zero length payload update control: ret=0 (rejected, tag was checked)
RESULT: invalid CCM tag accepted for empty ciphertext

For an empty ciphertext, I would expect Final to reject an invalid tag and to reject a missing tag with PROV_R_TAG_NOT_SET. A correct tag should succeed with outl == 0. In the encryption direction, Final should make the tag available through GET_TAG. Final should also fail if a nonzero payload length was declared but no payload was supplied.

I spent some time checking whether an OpenSSL protocol path reaches this case and didn't find one.

Generic AEAD wrappers can still run into it if they skip the payload call for an empty buffer, or normalize an empty input buffer to NULL and rely on Final for authentication. Code that makes the non-NULL zero-length payload Update and checks its return value is not affected.

I consider this an API correctness and fail-closed issue. It is closely related to the empty AEAD finalization work in #31555 / #32173 and the missing-tag Final behavior in #28730 / #28872.

I have a fix and I will open a PR shortly.

ccm_empty_aad_repro.zip

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

branch: 3.4Applies to openssl-3.4branch: 3.5Applies to openssl-3.5branch: 3.6Applies to openssl-3.6branch: 4.0Applies to openssl-4.0branch: masterApplies to master branchseverity: importantImportant bugs affecting a released versiontriaged: bugThe issue/pr is/fixes a bug

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions