Commit b5fe9a162c7 for php.net
commit b5fe9a162c79395d86d6d92e6ea48aa9bbd53e05
Author: Jordi Kroon <jkroon@onyourmarks.agency>
Date: Tue Jan 6 23:39:48 2026 +0100
Add AES-SIV support with optional AAD setting
Closes GH-20853
diff --git a/NEWS b/NEWS
index d2614887767..a5393d50bfc 100644
--- a/NEWS
+++ b/NEWS
@@ -88,6 +88,7 @@ PHP NEWS
preloading). (Arnaud, welcomycozyhom)
- OpenSSL:
+ . Added AES-SIV support. (jordikroon)
. Implemented GH-20310 (No critical extension indication in
openssl_x509_parse() output). (StephenWall)
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 381a369d571..de52191c441 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -4571,7 +4571,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();
}
@@ -4593,7 +4593,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 94902a4acf0..0111cc0cc7b 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 bae435e9024..32002cd81d5 100644
Binary files a/ext/openssl/openssl_arginfo.h and b/ext/openssl/openssl_arginfo.h differ
diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c
index 6eae715d432..99193615bec 100644
--- a/ext/openssl/openssl_backend_common.c
+++ b/ext/openssl/openssl_backend_common.c
@@ -1650,10 +1650,14 @@ 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));
+
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,
@@ -1663,6 +1667,7 @@ void php_openssl_load_cipher_mode(struct php_openssl_cipher_mode *mode, const EV
php_openssl_set_aead_flags(mode);
mode->set_tag_length_when_encrypting = cipher_mode == EVP_CIPH_CCM_MODE;
mode->is_single_run_aead = cipher_mode == EVP_CIPH_CCM_MODE;
+ mode->aad_supports_vector = cipher_mode == EVP_CIPH_SIV_MODE;
break;
#ifdef NID_chacha20_poly1305
default:
@@ -1804,13 +1809,21 @@ zend_result php_openssl_cipher_update(const EVP_CIPHER *cipher_type,
{
int i = 0;
+ /* For AEAD modes that do not support vector AAD, treat NULL AAD as zero-length AAD */
+ if (!mode->aad_supports_vector && aad == NULL) {
+ aad_len = 0;
+ aad = "";
+ }
+
if (mode->is_single_run_aead && !EVP_CipherUpdate(cipher_ctx, NULL, &i, NULL, (int)data_len)) {
php_openssl_store_errors();
php_error_docref(NULL, E_WARNING, "Setting of data length failed");
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/php_openssl_backend.h b/ext/openssl/php_openssl_backend.h
index 79082f4eca2..2d37e594ea5 100644
--- a/ext/openssl/php_openssl_backend.h
+++ b/ext/openssl/php_openssl_backend.h
@@ -346,6 +346,7 @@ struct php_openssl_cipher_mode {
bool is_single_run_aead;
bool set_tag_length_always;
bool set_tag_length_when_encrypting;
+ bool aad_supports_vector;
int aead_get_tag_flag;
int aead_set_tag_flag;
int aead_ivlen_flag;
diff --git a/ext/openssl/tests/cipher_tests.inc b/ext/openssl/tests/cipher_tests.inc
index f543a42c9f4..9d17e847bfa 100644
--- a/ext/openssl/tests/cipher_tests.inc
+++ b/ext/openssl/tests/cipher_tests.inc
@@ -160,6 +160,59 @@ $php_openssl_cipher_tests = array(
'ct' => '1792A4E31E0755FB03E31B22116E6C2DDF9EFD6E33D536F1A0124B0A55BAE884ED93481529C76B6A',
),
),
+ 'aes-128-siv' => array(
+ array(
+ 'key' => 'fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0' .
+ '0f0e0d0c0b0a09080706050403020100',
+ 'iv' => '',
+ 'aad' => '',
+ 'tag' => 'baba5b99dfc42fa9810fb2eb71ac2e9c',
+ 'pt' => 'b1677d933fa706f7ef349f9dd569c028' .
+ '279a5e2219728e77cfe916d5db979942' .
+ '5d8fb93b0e26dbc85ed14c050dc9f054' .
+ 'd9153c2be1e9b99ae7a109aba1e5a7f1' .
+ 'f2131786da90fe998d3571c144d066c3',
+ 'ct' => '91416054151e844965ad20a2057e2baa' .
+ '0e785269b152ba9d4dc834777e0d5376' .
+ 'db611856ae0d5d826f446c8eef47acb4' .
+ '83dccb37da9481648a4907fd3d65335b' .
+ 'd9585361c0c1834ac2b975f3238ea7c6',
+ ),
+ array(
+ 'key' => 'fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0' .
+ '0f0e0d0c0b0a09080706050403020100',
+ 'iv' => '',
+ 'aad' => null,
+ 'tag' => '606ac96568128a278b02e3e04de97b7e',
+ 'pt' => 'ea597a2f9fb0b5c4d5a6f215047b58a3' .
+ '3d2c885bf67cbb09239239f5aecafd6f' .
+ 'd2401391154b024b05cd938b40fdc749' .
+ 'ebccb3f48a3156c0bad69cfc5035360d' .
+ '21ad626dc866cc539f2d0e34b6824fc3',
+ 'ct' => '9c75fa0345b35e2d6cbcc91ed3fc7feb' .
+ '84fea50c35766db0c847fb627385107b' .
+ '4f257548d8b80ccd04261fa651fb89cc' .
+ 'e6815ecf0c8c4586ce68544ddce4c3af' .
+ '01e9587282256569194b1dca788fd987',
+ ),
+ array(
+ 'key' => 'fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0' .
+ '0f0e0d0c0b0a09080706050403020100',
+ 'iv' => '',
+ 'aad' => 'c0ef488e684e6fc95e0bd1da59861259',
+ 'tag' => 'a24cd6dcc0791bd7719a7f4fcb16de81',
+ 'pt' => 'b1677d933fa706f7ef349f9dd569c028' .
+ '279a5e2219728e77cfe916d5db979942' .
+ '5d8fb93b0e26dbc85ed14c050dc9f054' .
+ 'd9153c2be1e9b99ae7a109aba1e5a7f1' .
+ 'f2131786da90fe998d3571c144d066c3',
+ 'ct' => 'ea597a2f9fb0b5c4d5a6f215047b58a3' .
+ '3d2c885bf67cbb09239239f5aecafd6f' .
+ 'd2401391154b024b05cd938b40fdc749' .
+ 'ebccb3f48a3156c0bad69cfc5035360d' .
+ '21ad626dc866cc539f2d0e34b6824fc3',
+ ),
+ ),
'chacha20-poly1305' => array(
array(
'key' => '808182838485868788898a8b8c8d8e8f' .
diff --git a/ext/openssl/tests/openssl_decrypt_siv.phpt b/ext/openssl/tests/openssl_decrypt_siv.phpt
new file mode 100644
index 00000000000..6fc299546e2
--- /dev/null
+++ b/ext/openssl/tests/openssl_decrypt_siv.phpt
@@ -0,0 +1,43 @@
+--TEST--
+openssl_decrypt() with SIV cipher algorithm tests
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!in_array('aes-128-siv', openssl_get_cipher_methods()))
+ die("skip: aes-128-siv not available");
+?>
+--FILE--
+<?php
+require_once __DIR__ . "/cipher_tests.inc";
+$method = 'aes-128-siv';
+$tests = openssl_get_cipher_tests($method);
+
+foreach ($tests as $idx => $test) {
+ echo "TEST $idx\n";
+ $pt = openssl_decrypt($test['ct'], $method, $test['key'], OPENSSL_RAW_DATA,
+ $test['iv'], $test['tag'], $test['aad']);
+ var_dump($test['pt'] === $pt);
+}
+
+// failed because no AAD
+echo "TEST AAD\n";
+var_dump(openssl_decrypt($test['ct'], $method, $test['key'], OPENSSL_RAW_DATA,
+ $test['iv'], $test['tag']));
+// failed because wrong tag
+echo "TEST WRONGTAG\n";
+var_dump(openssl_decrypt($test['ct'], $method, $test['key'], OPENSSL_RAW_DATA,
+ $test['iv'], str_repeat('x', 16), $test['aad']));
+
+?>
+--EXPECTF--
+TEST 0
+bool(true)
+TEST 1
+bool(true)
+TEST 2
+bool(true)
+TEST AAD
+bool(false)
+TEST WRONGTAG
+bool(false)
diff --git a/ext/openssl/tests/openssl_encrypt_siv.phpt b/ext/openssl/tests/openssl_encrypt_siv.phpt
new file mode 100644
index 00000000000..43a5c5b34b5
--- /dev/null
+++ b/ext/openssl/tests/openssl_encrypt_siv.phpt
@@ -0,0 +1,51 @@
+--TEST--
+openssl_encrypt() with SIV cipher algorithm tests
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!in_array('aes-128-siv', openssl_get_cipher_methods()))
+ die("skip: aes-128-siv not available");
+?>
+--FILE--
+<?php
+require_once __DIR__ . "/cipher_tests.inc";
+$method = 'aes-128-siv';
+$tests = openssl_get_cipher_tests($method);
+
+foreach ($tests as $idx => $test) {
+ echo "TEST $idx\n";
+ $ct = openssl_encrypt($test['pt'], $method, $test['key'], OPENSSL_RAW_DATA,
+ $test['iv'], $tag, $test['aad'], strlen($test['tag']));
+ var_dump($test['ct'] === $ct);
+ var_dump($test['tag'] === $tag);
+}
+
+// Empty tag should not be equivalent to null tag
+echo "TEST AAD\n";
+var_dump(openssl_encrypt('data', $method, 'password', 0, '', $tag, '') !== openssl_encrypt('data', $method, 'password', 0, '', $tag, null));
+
+// Failing to retrieve tag (max is 16 bytes)
+var_dump(openssl_encrypt('data', $method, 'password', 0, str_repeat('x', 32), $tag, '', 20));
+
+// Failing when no tag supplied
+var_dump(openssl_encrypt('data', $method, 'password', 0, str_repeat('x', 32)));
+?>
+--EXPECTF--
+TEST 0
+bool(true)
+bool(true)
+TEST 1
+bool(true)
+bool(true)
+TEST 2
+bool(true)
+bool(true)
+TEST AAD
+bool(true)
+
+Warning: openssl_encrypt(): Retrieving verification tag failed in %s on line %d
+bool(false)
+
+Warning: openssl_encrypt(): A tag should be provided when using AEAD mode in %s on line %d
+bool(false)