Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ PHP 8.6 UPGRADE NOTES
max_early_data and receive it through the early_data_cb callback. The
outcome is reported as 'accepted', 'rejected' or 'not_sent' in the
early_data key of the crypto stream_get_meta_data() array.
. Added the $data_is_digest parameter to openssl_sign() and
openssl_verify(). When set to true, $data is treated as an
already-computed digest and is signed/verified directly instead of being
hashed again first.

- PDO_PGSQL:
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE, the number of rows a statement fetches
Expand Down
122 changes: 81 additions & 41 deletions ext/openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -4539,23 +4539,24 @@ PHP_FUNCTION(openssl_sign)
zend_string *sigbuf = NULL;
char * data;
size_t data_len;
EVP_MD_CTX *md_ctx;
zend_string *method_str = NULL;
zend_long method_long = OPENSSL_ALGO_SHA1;
const EVP_MD *mdtype;
zend_long padding = 0;
zend_long salt_length = RSA_PSS_SALTLEN_AUTO;
bool data_is_digest = 0;
EVP_PKEY_CTX *pctx;
bool can_default_digest = ZEND_THREEWAY_COMPARE(PHP_OPENSSL_API_VERSION, 0x30000) >= 0;

ZEND_PARSE_PARAMETERS_START(3, 6)
ZEND_PARSE_PARAMETERS_START(3, 7)
Z_PARAM_STRING(data, data_len)
Z_PARAM_ZVAL(signature)
Z_PARAM_ZVAL(key)
Z_PARAM_OPTIONAL
Z_PARAM_STR_OR_LONG(method_str, method_long)
Z_PARAM_LONG(padding)
Z_PARAM_LONG(salt_length)
Z_PARAM_BOOL(data_is_digest)
ZEND_PARSE_PARAMETERS_END();

pkey = php_openssl_pkey_from_zval(key, 0, "", 0, 3);
Expand All @@ -4578,25 +4579,49 @@ PHP_FUNCTION(openssl_sign)
}
PHP_OPENSSL_CHECK_LONG_TO_INT(salt_length, salt_length, 6);

md_ctx = EVP_MD_CTX_create();
size_t siglen;
if (md_ctx != NULL &&
EVP_DigestSignInit(md_ctx, &pctx, mdtype, NULL, pkey) &&
php_openssl_setup_rsa_padding(pctx, pkey, padding) == SUCCESS &&
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == SUCCESS &&
EVP_DigestSign(md_ctx, NULL, &siglen, (unsigned char*)data, data_len) &&
(sigbuf = zend_string_alloc(siglen, 0)) != NULL &&
EVP_DigestSign(md_ctx, (unsigned char*)ZSTR_VAL(sigbuf), &siglen, (unsigned char*)data, data_len)) {
ZSTR_VAL(sigbuf)[siglen] = '\0';
ZSTR_LEN(sigbuf) = siglen;
ZEND_TRY_ASSIGN_REF_NEW_STR(signature, sigbuf);
RETVAL_TRUE;
if (data_is_digest) {
/* $data is already a digest: sign it directly with the low-level
* EVP_PKEY_sign() API instead of hashing it again via EVP_DigestSign(). */
pctx = EVP_PKEY_CTX_new(pkey, NULL);
if (pctx != NULL &&
EVP_PKEY_sign_init(pctx) > 0 &&
(mdtype == NULL || EVP_PKEY_CTX_set_signature_md(pctx, mdtype) > 0) &&
php_openssl_setup_rsa_padding(pctx, pkey, padding) == SUCCESS &&
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == SUCCESS &&
EVP_PKEY_sign(pctx, NULL, &siglen, (unsigned char *) data, data_len) > 0 &&
(sigbuf = zend_string_alloc(siglen, 0)) != NULL &&
EVP_PKEY_sign(pctx, (unsigned char *) ZSTR_VAL(sigbuf), &siglen, (unsigned char *) data, data_len) > 0) {
ZSTR_VAL(sigbuf)[siglen] = '\0';
ZSTR_LEN(sigbuf) = siglen;
ZEND_TRY_ASSIGN_REF_NEW_STR(signature, sigbuf);
RETVAL_TRUE;
} else {
php_openssl_store_errors();
efree(sigbuf);
RETVAL_FALSE;
}
EVP_PKEY_CTX_free(pctx);
} else {
php_openssl_store_errors();
efree(sigbuf);
RETVAL_FALSE;
EVP_MD_CTX *md_ctx = EVP_MD_CTX_create();
if (md_ctx != NULL &&
EVP_DigestSignInit(md_ctx, &pctx, mdtype, NULL, pkey) &&
php_openssl_setup_rsa_padding(pctx, pkey, padding) == SUCCESS &&
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == SUCCESS &&
EVP_DigestSign(md_ctx, NULL, &siglen, (unsigned char*)data, data_len) &&
(sigbuf = zend_string_alloc(siglen, 0)) != NULL &&
EVP_DigestSign(md_ctx, (unsigned char*)ZSTR_VAL(sigbuf), &siglen, (unsigned char*)data, data_len)) {
ZSTR_VAL(sigbuf)[siglen] = '\0';
ZSTR_LEN(sigbuf) = siglen;
ZEND_TRY_ASSIGN_REF_NEW_STR(signature, sigbuf);
RETVAL_TRUE;
} else {
php_openssl_store_errors();
efree(sigbuf);
RETVAL_FALSE;
}
EVP_MD_CTX_destroy(md_ctx);
}
EVP_MD_CTX_destroy(md_ctx);
php_openssl_release_evp_md(mdtype);
EVP_PKEY_free(pkey);
}
Expand All @@ -4608,7 +4633,6 @@ PHP_FUNCTION(openssl_verify)
zval *key;
EVP_PKEY *pkey;
int err = 0;
EVP_MD_CTX *md_ctx;
const EVP_MD *mdtype;
char * data;
size_t data_len;
Expand All @@ -4618,17 +4642,19 @@ PHP_FUNCTION(openssl_verify)
zend_long method_long = OPENSSL_ALGO_SHA1;
zend_long padding = 0;
zend_long salt_length = RSA_PSS_SALTLEN_AUTO;
bool data_is_digest = 0;
EVP_PKEY_CTX *pctx;
bool can_default_digest = ZEND_THREEWAY_COMPARE(PHP_OPENSSL_API_VERSION, 0x30000) >= 0;

ZEND_PARSE_PARAMETERS_START(3, 6)
ZEND_PARSE_PARAMETERS_START(3, 7)
Z_PARAM_STRING(data, data_len)
Z_PARAM_STRING(signature, signature_len)
Z_PARAM_ZVAL(key)
Z_PARAM_OPTIONAL
Z_PARAM_STR_OR_LONG(method_str, method_long)
Z_PARAM_LONG(padding)
Z_PARAM_LONG(salt_length)
Z_PARAM_BOOL(data_is_digest)
ZEND_PARSE_PARAMETERS_END();

PHP_OPENSSL_CHECK_SIZE_T_TO_UINT(signature_len, signature, 2);
Expand All @@ -4653,28 +4679,42 @@ PHP_FUNCTION(openssl_verify)
RETURN_FALSE;
}

md_ctx = EVP_MD_CTX_create();
if (md_ctx == NULL) {
php_openssl_store_errors();
err = -1;
goto cleanup;
}

if (!EVP_DigestVerifyInit(md_ctx, &pctx, mdtype, NULL, pkey) ||
php_openssl_setup_rsa_padding(pctx, pkey, padding) == FAILURE ||
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == FAILURE) {
php_openssl_store_errors();
err = -1;
goto cleanup;
}

err = EVP_DigestVerify(md_ctx, (unsigned char *)signature, signature_len, (unsigned char*)data, data_len);
if (err < 0) {
php_openssl_store_errors();
if (data_is_digest) {
/* $data is already a digest: verify it directly with the low-level
* EVP_PKEY_verify() API instead of hashing it again via EVP_DigestVerify(). */
pctx = EVP_PKEY_CTX_new(pkey, NULL);
if (pctx == NULL ||
EVP_PKEY_verify_init(pctx) <= 0 ||
(mdtype != NULL && EVP_PKEY_CTX_set_signature_md(pctx, mdtype) <= 0) ||
php_openssl_setup_rsa_padding(pctx, pkey, padding) == FAILURE ||
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == FAILURE) {
php_openssl_store_errors();
err = -1;
} else {
err = EVP_PKEY_verify(pctx, (unsigned char *) signature, signature_len, (unsigned char *) data, data_len);
if (err < 0) {
php_openssl_store_errors();
}
}
EVP_PKEY_CTX_free(pctx);
} else {
EVP_MD_CTX *md_ctx = EVP_MD_CTX_create();
if (md_ctx == NULL) {
php_openssl_store_errors();
err = -1;
} else if (!EVP_DigestVerifyInit(md_ctx, &pctx, mdtype, NULL, pkey) ||
php_openssl_setup_rsa_padding(pctx, pkey, padding) == FAILURE ||
php_openssl_setup_rsa_pss_salt_length(pctx, pkey, padding, salt_length) == FAILURE) {
php_openssl_store_errors();
err = -1;
} else {
err = EVP_DigestVerify(md_ctx, (unsigned char *)signature, signature_len, (unsigned char*)data, data_len);
if (err < 0) {
php_openssl_store_errors();
}
}
EVP_MD_CTX_destroy(md_ctx);
}

cleanup:
EVP_MD_CTX_destroy(md_ctx);
php_openssl_release_evp_md(mdtype);
EVP_PKEY_free(pkey);
RETURN_LONG(err);
Expand Down
4 changes: 2 additions & 2 deletions ext/openssl/openssl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -695,10 +695,10 @@ function openssl_error_string(): string|false {}
* @param string $signature
* @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key
*/
function openssl_sign(string $data, &$signature, #[\SensitiveParameter] $private_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0, int $salt_length = OPENSSL_RSA_PSS_SALTLEN_AUTO): bool {}
function openssl_sign(string $data, &$signature, #[\SensitiveParameter] $private_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0, int $salt_length = OPENSSL_RSA_PSS_SALTLEN_AUTO, bool $data_is_digest = false): bool {}

/** @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key */
function openssl_verify(string $data, string $signature, $public_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0, int $salt_length = OPENSSL_RSA_PSS_SALTLEN_AUTO): int|false {}
function openssl_verify(string $data, string $signature, $public_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0, int $salt_length = OPENSSL_RSA_PSS_SALTLEN_AUTO, bool $data_is_digest = false): int|false {}

/**
* @param string $sealed_data
Expand Down
4 changes: 3 additions & 1 deletion ext/openssl/openssl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions ext/openssl/tests/gh23422.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
GH-23422 (openssl_sign()/openssl_verify() cannot sign/verify a precomputed digest)
--EXTENSIONS--
openssl
--FILE--
<?php
$conf = ['config' => __DIR__ . DIRECTORY_SEPARATOR . 'openssl.cnf'];
$privkey = openssl_pkey_new($conf);
$pubkey = openssl_pkey_get_details($privkey)['key'];

$data = "Testing openssl_sign() with a precomputed digest";
$digest = hash('sha256', $data, true);

// Signing the data the normal way hashes it internally with SHA-256 before
// signing. Signing the precomputed digest directly (data_is_digest = true)
// must produce the exact same PKCS#1 v1.5 signature, since both ultimately
// sign the same DigestInfo(SHA-256, digest) structure.
var_dump(openssl_sign($data, $sign_from_data, $privkey, OPENSSL_ALGO_SHA256));
var_dump(openssl_sign($digest, $sign_from_digest, $privkey, OPENSSL_ALGO_SHA256, 0, OPENSSL_RSA_PSS_SALTLEN_AUTO, true));
var_dump($sign_from_data === $sign_from_digest);

// A signature produced from the precomputed digest must verify both against
// the precomputed digest and against the original data.
var_dump(openssl_verify($digest, $sign_from_digest, $pubkey, OPENSSL_ALGO_SHA256, 0, OPENSSL_RSA_PSS_SALTLEN_AUTO, true));
var_dump(openssl_verify($data, $sign_from_digest, $pubkey, OPENSSL_ALGO_SHA256));

// A tampered digest must fail verification instead of silently succeeding.
$tampered_digest = $digest;
$tampered_digest[0] = chr(ord($digest[0]) ^ 1);
var_dump(openssl_verify($tampered_digest, $sign_from_digest, $pubkey, OPENSSL_ALGO_SHA256, 0, OPENSSL_RSA_PSS_SALTLEN_AUTO, true));

// RSA-PSS padding is also honored on the precomputed-digest code path.
var_dump(openssl_sign($digest, $sign_pss, $privkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING, OPENSSL_RSA_PSS_SALTLEN_DIGEST, true));
var_dump(openssl_verify($digest, $sign_pss, $pubkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING, OPENSSL_RSA_PSS_SALTLEN_DIGEST, true));
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
int(1)
int(1)
int(0)
bool(true)
int(1)
Loading