From 7de1357dde68933f9b1fa60eb69c7caf1f336f7a Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Tue, 6 Jan 2026 23:39:48 +0100 Subject: [PATCH 1/3] properly initialize AEAD cipher flags in OpenSSL backend --- ext/openssl/openssl_backend_common.c | 7 +++++ ext/openssl/tests/gh20851.phpt | 43 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 ext/openssl/tests/gh20851.phpt diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c index 611359cccaba6..9b86c1d6dee20 100644 --- a/ext/openssl/openssl_backend_common.c +++ b/ext/openssl/openssl_backend_common.c @@ -1637,6 +1637,13 @@ void php_openssl_load_cipher_mode(struct php_openssl_cipher_mode *mode, const EV { int cipher_mode = EVP_CIPHER_mode(cipher_type); memset(mode, 0, sizeof(struct php_openssl_cipher_mode)); + + #if defined(EVP_CIPH_FLAG_AEAD_CIPHER) + if (EVP_CIPHER_flags(cipher_type) & EVP_CIPH_FLAG_AEAD_CIPHER) { + php_openssl_set_aead_flags(mode); + } + #endif + switch (cipher_mode) { case EVP_CIPH_GCM_MODE: case EVP_CIPH_CCM_MODE: diff --git a/ext/openssl/tests/gh20851.phpt b/ext/openssl/tests/gh20851.phpt new file mode 100644 index 0000000000000..d3dff888d2761 --- /dev/null +++ b/ext/openssl/tests/gh20851.phpt @@ -0,0 +1,43 @@ +--TEST-- +openssl: AES-256-SIV AEAD tag and AAD roundtrip +--EXTENSIONS-- +openssl +--FILE-- + +--EXPECTF-- +input: Hello world! +tag: f6c98e3e785947502a09994d2757f9c1 +ciphertext: a430a41a9bc089fa45ad27be +combined: f6c98e3e785947502a09994d2757f9c1a430a41a9bc089fa45ad27be +decrypted: 'Hello world!' + From 59cba53d9ad97d8be956c73ef0fbad7decce7df8 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Wed, 7 Jan 2026 20:09:13 +0100 Subject: [PATCH 2/3] allow null AAD parameter in openssl_encrypt/decrypt --- ext/openssl/openssl.c | 4 +- ext/openssl/openssl.stub.php | 4 +- ext/openssl/openssl_arginfo.h | 6 +-- ext/openssl/openssl_backend_common.c | 4 +- .../{gh20851.phpt => gh20851_aad_empty.phpt} | 8 +++- ext/openssl/tests/gh20851_aad_null.phpt | 47 +++++++++++++++++++ 6 files changed, 63 insertions(+), 10 deletions(-) rename ext/openssl/tests/{gh20851.phpt => gh20851_aad_empty.phpt} (90%) create mode 100644 ext/openssl/tests/gh20851_aad_null.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 2c09b89e31200..557903495fe3a 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -4481,7 +4481,7 @@ PHP_FUNCTION(openssl_encrypt) zend_string *ret; zval *tag = NULL; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|lszsl", &data, &data_len, &method, &method_len, + if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|lszs!l", &data, &data_len, &method, &method_len, &password, &password_len, &options, &iv, &iv_len, &tag, &aad, &aad_len, &tag_len) == FAILURE) { RETURN_THROWS(); } @@ -4503,7 +4503,7 @@ PHP_FUNCTION(openssl_decrypt) size_t data_len, method_len, password_len, iv_len = 0, tag_len = 0, aad_len = 0; zend_string *ret; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|lss!s", &data, &data_len, &method, &method_len, + if (zend_parse_parameters(ZEND_NUM_ARGS(), "sss|lss!s!", &data, &data_len, &method, &method_len, &password, &password_len, &options, &iv, &iv_len, &tag, &tag_len, &aad, &aad_len) == FAILURE) { RETURN_THROWS(); } diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 94902a4acf0da..0111cc0cc7bc0 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -662,9 +662,9 @@ function openssl_digest(string $data, string $digest_algo, bool $binary = false) /** * @param string $tag */ -function openssl_encrypt(#[\SensitiveParameter] string $data, string $cipher_algo, #[\SensitiveParameter] string $passphrase, int $options = 0, string $iv = "", &$tag = null, string $aad = "", int $tag_length = 16): string|false {} +function openssl_encrypt(#[\SensitiveParameter] string $data, string $cipher_algo, #[\SensitiveParameter] string $passphrase, int $options = 0, string $iv = "", &$tag = null, ?string $aad = "", int $tag_length = 16): string|false {} -function openssl_decrypt(string $data, string $cipher_algo, #[\SensitiveParameter] string $passphrase, int $options = 0, string $iv = "", ?string $tag = null, string $aad = ""): string|false {} +function openssl_decrypt(string $data, string $cipher_algo, #[\SensitiveParameter] string $passphrase, int $options = 0, string $iv = "", ?string $tag = null, ?string $aad = ""): string|false {} function openssl_cipher_iv_length(string $cipher_algo): int|false {} diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index 796582c185bb6..fad3050b4ea34 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 8233a8abc8ab7145d905d0fa51478edfe1e55a06 */ + * Stub hash: a571945d38a3460de017405454b61609811fe1b1 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL) @@ -337,7 +337,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_encrypt, 0, 3, MAY_BE_ST ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "0") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, iv, IS_STRING, 0, "\"\"") ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, tag, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, aad, IS_STRING, 0, "\"\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, aad, IS_STRING, 1, "\"\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, tag_length, IS_LONG, 0, "16") ZEND_END_ARG_INFO() @@ -348,7 +348,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_decrypt, 0, 3, MAY_BE_ST ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_LONG, 0, "0") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, iv, IS_STRING, 0, "\"\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, tag, IS_STRING, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, aad, IS_STRING, 0, "\"\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, aad, IS_STRING, 1, "\"\"") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_cipher_iv_length, 0, 1, MAY_BE_LONG|MAY_BE_FALSE) diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c index 9b86c1d6dee20..4885c63c7b893 100644 --- a/ext/openssl/openssl_backend_common.c +++ b/ext/openssl/openssl_backend_common.c @@ -1804,7 +1804,9 @@ zend_result php_openssl_cipher_update(const EVP_CIPHER *cipher_type, return FAILURE; } - if (mode->is_aead && !EVP_CipherUpdate(cipher_ctx, NULL, &i, (const unsigned char *) aad, (int) aad_len)) { + /* Only pass AAD to OpenSSL if caller provided it. + This makes NULL mean zero AAD items, while "" with len 0 means one empty AAD item. */ + if (mode->is_aead && aad != NULL && !EVP_CipherUpdate(cipher_ctx, NULL, &i, (const unsigned char *)aad, (int)aad_len)) { php_openssl_store_errors(); php_error_docref(NULL, E_WARNING, "Setting of additional application data failed"); return FAILURE; diff --git a/ext/openssl/tests/gh20851.phpt b/ext/openssl/tests/gh20851_aad_empty.phpt similarity index 90% rename from ext/openssl/tests/gh20851.phpt rename to ext/openssl/tests/gh20851_aad_empty.phpt index d3dff888d2761..685e7dbb8f964 100644 --- a/ext/openssl/tests/gh20851.phpt +++ b/ext/openssl/tests/gh20851_aad_empty.phpt @@ -7,6 +7,7 @@ openssl $algo = 'aes-256-siv'; $key = str_repeat('1', 64); $tag = ''; +$aad = ''; $input = 'Hello world!'; $ciphertext = openssl_encrypt( @@ -15,7 +16,9 @@ $ciphertext = openssl_encrypt( $key, OPENSSL_RAW_DATA, '', // IV is empty for this cipher in PHP - $tag // gets filled with the SIV + $tag, // gets filled with the SIV + $aad, + 16 ); echo 'input: ' . $input . PHP_EOL; @@ -29,7 +32,8 @@ $dec = openssl_decrypt( $key, OPENSSL_RAW_DATA, '', - $tag + $tag, + $aad ); echo 'decrypted: ' . var_export($dec, true) . PHP_EOL; diff --git a/ext/openssl/tests/gh20851_aad_null.phpt b/ext/openssl/tests/gh20851_aad_null.phpt new file mode 100644 index 0000000000000..3528cd219c6cc --- /dev/null +++ b/ext/openssl/tests/gh20851_aad_null.phpt @@ -0,0 +1,47 @@ +--TEST-- +openssl: AES-256-SIV AEAD tag and AAD roundtrip +--EXTENSIONS-- +openssl +--FILE-- + +--EXPECTF-- +input: Hello world! +tag: c06f0df087e2784c5560ce5d0b378311 +ciphertext: 72fffba74d7bc3ddcceeb6d1 +combined: c06f0df087e2784c5560ce5d0b37831172fffba74d7bc3ddcceeb6d1 +decrypted: 'Hello world!' + From ca12f45a76f3242953e7e27a9b64e509e9f518e2 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Mon, 12 Jan 2026 20:07:15 +0100 Subject: [PATCH 3/3] Check for EVP_CIPH_SIV_MODE in switch --- ext/openssl/openssl_backend_common.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c index 4885c63c7b893..60c303e0f5f36 100644 --- a/ext/openssl/openssl_backend_common.c +++ b/ext/openssl/openssl_backend_common.c @@ -1638,16 +1638,13 @@ void php_openssl_load_cipher_mode(struct php_openssl_cipher_mode *mode, const EV int cipher_mode = EVP_CIPHER_mode(cipher_type); memset(mode, 0, sizeof(struct php_openssl_cipher_mode)); - #if defined(EVP_CIPH_FLAG_AEAD_CIPHER) - if (EVP_CIPHER_flags(cipher_type) & EVP_CIPH_FLAG_AEAD_CIPHER) { - php_openssl_set_aead_flags(mode); - } - #endif - switch (cipher_mode) { case EVP_CIPH_GCM_MODE: case EVP_CIPH_CCM_MODE: - /* We check for EVP_CIPH_OCB_MODE, because LibreSSL does not support it. */ + /* We check for EVP_CIPH_SIV_MODE and EVP_CIPH_SIV_MODE, because LibreSSL does not support it. */ +#ifdef EVP_CIPH_SIV_MODE + case EVP_CIPH_SIV_MODE: +#endif #ifdef EVP_CIPH_OCB_MODE case EVP_CIPH_OCB_MODE: /* For OCB mode, explicitly set the tag length even when decrypting,