Commit 58f032a042 for openssl.org
commit 58f032a04254baab114449e018eb7eaff6e4f648
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Tue Jun 23 23:45:51 2026 +0200
cms: fix AuthEnvelopedData authAttrs tags and verify them as AEAD AAD
The CMS_AuthEnvelopedData ASN.1 template used the implicit tags and the
X509_ALGOR type copied from CMS_AuthenticatedData. Per RFC 5083 the authAttrs
and unauthAttrs fields are [1] and [2] (not [2] and [3]) and are SET OF
Attribute, so use X509_ATTRIBUTE with the correct tags, matching the
STACK_OF(X509_ATTRIBUTE) members already declared in the structure.
With the tags fixed, authEnvelopedData carrying authAttrs now parses, so the
authenticated attributes must also be fed to the content cipher as the AEAD
associated data required by RFC 5083 section 2.1. Encode their DER (with the
universal SET OF tag) for both encryption and decryption; without this the GCM
tag fails to verify against compliant senders such as BouncyCastle.
RFC 5083 also requires that plaintext is not released until its integrity has
been verified. The AEAD tag is only checked once all the ciphertext has been
processed, so buffer the decrypted content and forward it to the output BIO
only after that check succeeds; a tampered message then leaks nothing to -out.
Add an interop test using a BouncyCastle-generated AES-128-GCM message with
authenticated and unauthenticated attributes, plus a tampered copy that must
fail the tag check and leave -out empty.
Closes #31635
Closes #26101
Closes #31629
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Mon Jul 13 14:41:34 2026
(Merged from https://github.com/openssl/openssl/pull/31695)
diff --git a/crypto/cms/cms_asn1.c b/crypto/cms/cms_asn1.c
index 63a26de742..96b125e930 100644
--- a/crypto/cms/cms_asn1.c
+++ b/crypto/cms/cms_asn1.c
@@ -306,9 +306,9 @@ ASN1_NDEF_SEQUENCE(CMS_AuthEnvelopedData) = {
ASN1_IMP_OPT(CMS_AuthEnvelopedData, originatorInfo, CMS_OriginatorInfo, 0),
ASN1_SET_OF(CMS_AuthEnvelopedData, recipientInfos, CMS_RecipientInfo),
ASN1_SIMPLE(CMS_AuthEnvelopedData, authEncryptedContentInfo, CMS_EncryptedContentInfo),
- ASN1_IMP_SET_OF_OPT(CMS_AuthEnvelopedData, authAttrs, X509_ALGOR, 2),
+ ASN1_IMP_SET_OF_OPT(CMS_AuthEnvelopedData, authAttrs, X509_ATTRIBUTE, 1),
ASN1_SIMPLE(CMS_AuthEnvelopedData, mac, ASN1_OCTET_STRING),
- ASN1_IMP_SET_OF_OPT(CMS_AuthEnvelopedData, unauthAttrs, X509_ALGOR, 3)
+ ASN1_IMP_SET_OF_OPT(CMS_AuthEnvelopedData, unauthAttrs, X509_ATTRIBUTE, 2)
} ASN1_NDEF_SEQUENCE_END(CMS_AuthEnvelopedData)
ASN1_NDEF_SEQUENCE(CMS_AuthenticatedData) = {
diff --git a/crypto/cms/cms_env.c b/crypto/cms/cms_env.c
index e702703758..8413f497db 100644
--- a/crypto/cms/cms_env.c
+++ b/crypto/cms/cms_env.c
@@ -1236,6 +1236,35 @@ BIO *ossl_cms_EnvelopedData_init_bio(CMS_ContentInfo *cms)
return cms_EnvelopedData_Decryption_init_bio(cms);
}
+/* The DER encoding of authAttrs, with the universal SET OF tag, is the AAD */
+static int cms_AuthEnvelopedData_set_aad(BIO *b,
+ STACK_OF(X509_ATTRIBUTE) *authAttrs)
+{
+ EVP_CIPHER_CTX *ctx;
+ unsigned char *aad = NULL;
+ int aadlen, outl, ok = 0;
+ const ASN1_ITEM *item;
+
+ if (!BIO_get_cipher_ctx(b, &ctx))
+ return 0;
+ item = EVP_CIPHER_CTX_is_encrypting(ctx)
+ ? ASN1_ITEM_rptr(CMS_Attributes_AadEncrypt)
+ : ASN1_ITEM_rptr(CMS_Attributes_AadDecrypt);
+ aadlen = ASN1_item_i2d((ASN1_VALUE *)authAttrs, &aad, item);
+ if (aadlen <= 0 || aad == NULL) {
+ ERR_raise(ERR_LIB_CMS, ERR_R_ASN1_LIB);
+ goto err;
+ }
+ if (EVP_CipherUpdate(ctx, NULL, &outl, aad, aadlen) <= 0) {
+ ERR_raise(ERR_LIB_CMS, CMS_R_CTRL_FAILURE);
+ goto err;
+ }
+ ok = 1;
+err:
+ OPENSSL_free(aad);
+ return ok;
+}
+
BIO *ossl_cms_AuthEnvelopedData_init_bio(CMS_ContentInfo *cms)
{
CMS_EncryptedContentInfo *ec;
@@ -1252,9 +1281,16 @@ BIO *ossl_cms_AuthEnvelopedData_init_bio(CMS_ContentInfo *cms)
ec->taglen = aenv->mac->length;
}
ret = ossl_cms_EncryptedContent_init_bio(ec, ossl_cms_get0_cmsctx(cms), 1);
+ if (ret == NULL)
+ return NULL;
+
+ /* authAttrs, if present, are the AEAD associated data */
+ if (aenv->authAttrs != NULL
+ && !cms_AuthEnvelopedData_set_aad(ret, aenv->authAttrs))
+ goto err;
- /* If error or no cipher end of processing */
- if (ret == NULL || ec->cipher == NULL)
+ /* If no cipher end of processing */
+ if (ec->cipher == NULL)
return ret;
/* Now encrypt content key according to each RecipientInfo type */
diff --git a/crypto/cms/cms_local.h b/crypto/cms/cms_local.h
index 5e0ac3907f..bfb0ca729b 100644
--- a/crypto/cms/cms_local.h
+++ b/crypto/cms/cms_local.h
@@ -401,6 +401,9 @@ DECLARE_ASN1_ITEM(CMS_EncryptedContentInfo)
DECLARE_ASN1_ITEM(CMS_IssuerAndSerialNumber)
DECLARE_ASN1_ITEM(CMS_Attributes_Sign)
DECLARE_ASN1_ITEM(CMS_Attributes_Verify)
+/* The authAttrs AAD encoding matches the signed-attributes one */
+#define CMS_Attributes_AadEncrypt_it CMS_Attributes_Sign_it
+#define CMS_Attributes_AadDecrypt_it CMS_Attributes_Verify_it
DECLARE_ASN1_ITEM(CMS_RecipientInfo)
DECLARE_ASN1_ITEM(CMS_PasswordRecipientInfo)
DECLARE_ASN1_ALLOC_FUNCTIONS(CMS_IssuerAndSerialNumber)
diff --git a/crypto/cms/cms_smime.c b/crypto/cms/cms_smime.c
index 659c033482..044cb2326f 100644
--- a/crypto/cms/cms_smime.c
+++ b/crypto/cms/cms_smime.c
@@ -36,6 +36,7 @@ static int cms_copy_content(BIO *out, BIO *in, unsigned int flags)
unsigned char buf[4096];
int r = 0, i;
BIO *tmpout;
+ BIO *aeadbuf = NULL;
tmpout = cms_get_text_bio(out, flags);
@@ -44,6 +45,33 @@ static int cms_copy_content(BIO *out, BIO *in, unsigned int flags)
goto err;
}
+ /*
+ * For AEAD content (AuthEnvelopedData) the integrity tag is only verified
+ * once all the ciphertext has been processed, by the
+ * BIO_get_cipher_status() call below. RFC 5083 requires that the plaintext
+ * is not released to the caller until that verification succeeds, so
+ * buffer it in memory and only forward it to the output BIO once the tag
+ * has been checked. When CMS_TEXT is set tmpout is already a memory BIO
+ * that is flushed only on success, so the extra buffering is not needed.
+ */
+ if (tmpout == out && BIO_method_type(in) == BIO_TYPE_CIPHER) {
+ EVP_CIPHER_CTX *ctx = NULL;
+
+ if (BIO_get_cipher_ctx(in, &ctx) > 0 && ctx != NULL
+ && (EVP_CIPHER_get_flags(EVP_CIPHER_CTX_get0_cipher(ctx))
+ & EVP_CIPH_FLAG_AEAD_CIPHER)
+ != 0) {
+ aeadbuf = BIO_new(BIO_s_mem());
+ if (aeadbuf == NULL) {
+ ERR_raise(ERR_LIB_CMS, ERR_R_BIO_LIB);
+ goto err;
+ }
+ /* Return 0 (EOF) rather than a retryable -1 once drained. */
+ BIO_set_mem_eof_return(aeadbuf, 0);
+ tmpout = aeadbuf;
+ }
+ }
+
/* Read all content through chain to process digest, decrypt etc */
for (;;) {
i = BIO_read(in, buf, sizeof(buf));
@@ -66,6 +94,17 @@ static int cms_copy_content(BIO *out, BIO *in, unsigned int flags)
ERR_raise(ERR_LIB_CMS, CMS_R_SMIME_TEXT_ERROR);
goto err;
}
+ } else if (aeadbuf != NULL) {
+ /* Forward the AEAD BIO to out BIO as the tag has been verified. */
+ for (;;) {
+ i = BIO_read(aeadbuf, buf, sizeof(buf));
+ if (i < 0)
+ goto err;
+ if (i == 0)
+ break;
+ if (BIO_write(out, buf, i) != i)
+ goto err;
+ }
}
r = 1;
diff --git a/test/recipes/80-test_cms.t b/test/recipes/80-test_cms.t
index 078e50123d..7516cf024e 100644
--- a/test/recipes/80-test_cms.t
+++ b/test/recipes/80-test_cms.t
@@ -56,7 +56,7 @@ my ($no_des, $no_dh, $no_dsa, $no_ec, $no_ec2m, $no_rc2, $no_zlib)
$no_rc2 = 1 if disabled("legacy");
-plan tests => 39;
+plan tests => 40;
ok(run(test(["pkcs7_test"])), "test pkcs7");
@@ -822,6 +822,18 @@ sub zero_compare {
return (-e "$opts{output}.txt" && -z "$opts{output}.txt");
}
+sub read_file_text {
+ my ($file) = @_;
+ open(my $fh, "<", $file) or return undef;
+ binmode $fh;
+ local $/;
+ my $data = <$fh>;
+ close($fh);
+ # Normalise line endings as -out is written in text mode on Windows.
+ $data =~ s/\r\n/\n/g if defined $data;
+ return $data;
+}
+
subtest "CMS => PKCS#7 compatibility tests\n" => sub {
plan tests => scalar @smime_pkcs7_tests;
@@ -1030,6 +1042,33 @@ subtest "CMS Decrypt message encrypted with OpenSSL 1.1.1\n" => sub {
}
};
+subtest "CMS decrypt authEnvelopedData with authenticated attributes\n" => sub {
+ plan tests => 4;
+
+ # BouncyCastle AES-128-GCM authEnvelopedData (KEK) carrying authAttrs;
+ # a clean decrypt confirms the authAttrs are verified as the AEAD AAD.
+ 1 while unlink "authattrs.txt";
+ ok(run(app(["openssl", "cms", @defaultprov, "-decrypt", "-inform", "PEM",
+ "-secretkey", "000102030405060708090A0B0C0D0E0F",
+ "-secretkeyid", "C0FEE0",
+ "-in", catfile($datadir, "authenveloped_attrs.pem"),
+ "-out", "authattrs.txt" ])),
+ "decrypt authEnvelopedData with authAttrs");
+ is(read_file_text("authattrs.txt"), "Hello AuthEnvelopedData world\n",
+ "decrypted authEnvelopedData plaintext matches expected");
+
+ # A flipped authAttrs byte must fail the tag check and leave -out empty.
+ 1 while unlink "bad_authattrs.txt";
+ ok(!run(app(["openssl", "cms", @defaultprov, "-decrypt", "-inform", "PEM",
+ "-secretkey", "000102030405060708090A0B0C0D0E0F",
+ "-secretkeyid", "C0FEE0",
+ "-in", catfile($datadir, "bad_authenveloped_attrs.pem"),
+ "-out", "bad_authattrs.txt" ])),
+ "reject authEnvelopedData with tampered authAttrs");
+ ok(!-s "bad_authattrs.txt",
+ "tampered authEnvelopedData leaks no plaintext to -out");
+};
+
subtest "CAdES <=> CAdES consistency tests\n" => sub {
plan tests => (scalar @smime_cms_cades_tests);
diff --git a/test/recipes/80-test_cms_data/authenveloped_attrs.pem b/test/recipes/80-test_cms_data/authenveloped_attrs.pem
new file mode 100644
index 0000000000..75c8eab00b
--- /dev/null
+++ b/test/recipes/80-test_cms_data/authenveloped_attrs.pem
@@ -0,0 +1,7 @@
+-----BEGIN CMS-----
+MIAGCyqGSIb3DQEJEAEXoIAwgAIBADEzojECAQQwBQQDwP7gMAsGCWCGSAFlAwQB
+BQQYknpV85muZoLZSPkwi5Ll1Z1HwzAeZThVMIAGCSqGSIb3DQEHATAeBglghkgB
+ZQMEAQYwEQQMkCQb305essfGO2nqAgEQoIAEHvYjM7EK9qZAHgoohdcbSHXe0lGJ
+/Hjk3nkK5VsHxgAAAAChFjAUBgkrBgEEAYaNHwExBwwFaGVsbG8EENeGq4IXAd1O
+iv8hMl+lHZOiFjAUBgkrBgEEAYaNHwIxBwwFd29ybGQAAAAAAAA=
+-----END CMS-----
diff --git a/test/recipes/80-test_cms_data/bad_authenveloped_attrs.pem b/test/recipes/80-test_cms_data/bad_authenveloped_attrs.pem
new file mode 100644
index 0000000000..e14946c96d
--- /dev/null
+++ b/test/recipes/80-test_cms_data/bad_authenveloped_attrs.pem
@@ -0,0 +1,7 @@
+-----BEGIN CMS-----
+MIAGCyqGSIb3DQEJEAEXoIAwgAIBADEzojECAQQwBQQDwP7gMAsGCWCGSAFlAwQB
+BQQYknpV85muZoLZSPkwi5Ll1Z1HwzAeZThVMIAGCSqGSIb3DQEHATAeBglghkgB
+ZQMEAQYwEQQMkCQb305essfGO2nqAgEQoIAEHvYjM7EK9qZAHgoohdcbSHXe0lGJ
+/Hjk3nkK5VsHxgAAAAChFjAUBgkrBgEEAYaNHwExBwwFaWVsbG8EENeGq4IXAd1O
+iv8hMl+lHZOiFjAUBgkrBgEEAYaNHwIxBwwFd29ybGQAAAAAAAA=
+-----END CMS-----