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
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ PHP NEWS
(Sam James, Weilin Du, Shivam Mathur)
. Fixed bug GH-24784 (openssl_pkcs7_read() and openssl_cms_read() overwrite
certificates with CRLs). (Alexander Danilov)
. Fixed bug GH-25088 (SNI server contexts do not inherit ciphers, ALPN,
DH params and security level). (Roman Ondráček)

- PDO:
. Fixed invalid ATTR_ORACLE_NULLS values being accepted and truncated.
Expand Down
97 changes: 97 additions & 0 deletions ext/openssl/tests/gh25088.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
--TEST--
GH-25088: SNI server contexts do not inherit ciphers, ALPN and DH params
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php
$baseCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_base_cert.pem.tmp';
$baseKeyFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_base_key.pem.tmp';
$sniCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_sni_cert.pem.tmp';
$sniKeyFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_sni_key.pem.tmp';

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAndKey('base.php.net', $baseCertFile, $baseKeyFile);
$certificateGenerator->saveNewCertAndKey('sni.php.net', $sniCertFile, $sniKeyFile);

$serverCode = <<<'CODE'
$flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
$ctx = stream_context_create(['ssl' => [
'local_cert' => '%s',
'local_pk' => '%s',
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER,
'ciphers' => 'ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384',
'alpn_protocols' => 'h2,http/1.1',
'dh_param' => '%s',
'SNI_server_certs' => [
'sni.php.net' => [
'local_cert' => '%s',
'local_pk' => '%s',
],
],
]]);

$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
phpt_notify_server_start($server);

for ($i = 0; $i < 8; $i++) {
$conn = @stream_socket_accept($server, 3);
if ($conn) {
fclose($conn);
}
}
CODE;
$serverCode = sprintf($serverCode, $baseCertFile, $baseKeyFile, __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_dh.pem', $sniCertFile, $sniKeyFile);

$clientCode = <<<'CODE'
function connect(string $peerName, array $opts) {
$ctx = stream_context_create(['ssl' => $opts + [
'verify_peer' => false,
'verify_peer_name' => false,
'peer_name' => $peerName,
'capture_peer_cert' => true,
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
]]);
$client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
if (!$client) {
return 'handshake failed';
}
$cn = openssl_x509_parse(stream_context_get_options($ctx)['ssl']['peer_certificate'])['subject']['CN'];
$crypto = stream_get_meta_data($client)['crypto'];
return sprintf('%s %s ALPN=%s', $cn, $crypto['cipher_name'], $crypto['alpn_protocol'] ?? 'none');
}

foreach (['base.php.net', 'sni.php.net'] as $peerName) {
echo "$peerName:\n";
echo " disallowed cipher: ", connect($peerName, ['ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256']), "\n";
echo " allowed cipher: ", connect($peerName, ['ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384']), "\n";
echo " DHE cipher: ", connect($peerName, ['ciphers' => 'DHE-RSA-AES256-GCM-SHA384']), "\n";
echo " ALPN: ", connect($peerName, ['alpn_protocols' => 'h2']), "\n";
}
CODE;

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_base_cert.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_base_key.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_sni_cert.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_sni_key.pem.tmp');
?>
--EXPECT--
base.php.net:
disallowed cipher: handshake failed
allowed cipher: base.php.net ECDHE-RSA-AES256-GCM-SHA384 ALPN=none
DHE cipher: base.php.net DHE-RSA-AES256-GCM-SHA384 ALPN=none
ALPN: base.php.net ECDHE-RSA-AES256-GCM-SHA384 ALPN=h2
sni.php.net:
disallowed cipher: handshake failed
allowed cipher: sni.php.net ECDHE-RSA-AES256-GCM-SHA384 ALPN=none
DHE cipher: sni.php.net DHE-RSA-AES256-GCM-SHA384 ALPN=none
ALPN: sni.php.net ECDHE-RSA-AES256-GCM-SHA384 ALPN=h2
8 changes: 8 additions & 0 deletions ext/openssl/tests/gh25088_dh.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----
79 changes: 79 additions & 0 deletions ext/openssl/tests/gh25088_security_level.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
--TEST--
GH-25088: SNI server contexts inherit the security level
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php
$baseCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_base_cert.pem.tmp';
$baseKeyFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_base_key.pem.tmp';
$sniCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_sni_cert.pem.tmp';
$sniKeyFile = __DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_sni_key.pem.tmp';

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAndKey('base.php.net', $baseCertFile, $baseKeyFile);
// A 768-bit key is refused at security level 1 and above.
$certificateGenerator->saveNewCertAndKey('sni.php.net', $sniCertFile, $sniKeyFile, 768);

$serverCode = <<<'CODE'
$flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
$ctx = stream_context_create(['ssl' => [
'local_cert' => '%s',
'local_pk' => '%s',
'security_level' => 0,
'SNI_server_certs' => [
'sni.php.net' => [
'local_cert' => '%s',
'local_pk' => '%s',
],
],
]]);

$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
phpt_notify_server_start($server);

for ($i = 0; $i < 2; $i++) {
$conn = @stream_socket_accept($server, 3);
if ($conn) {
fclose($conn);
}
}
CODE;
$serverCode = sprintf($serverCode, $baseCertFile, $baseKeyFile, $sniCertFile, $sniKeyFile);

$clientCode = <<<'CODE'
foreach (['base.php.net', 'sni.php.net'] as $peerName) {
$ctx = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'peer_name' => $peerName,
'capture_peer_cert' => true,
'security_level' => 0,
]]);
$client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
if (!$client) {
echo "$peerName: handshake failed\n";
continue;
}
$cert = openssl_x509_parse(stream_context_get_options($ctx)['ssl']['peer_certificate']);
echo "$peerName: ", $cert['subject']['CN'], "\n";
}
CODE;

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_base_cert.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_base_key.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_sni_cert.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh25088_security_level_sni_key.pem.tmp');
?>
--EXPECT--
base.php.net: base.php.net
sni.php.net: sni.php.net
81 changes: 65 additions & 16 deletions ext/openssl/xp_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,31 @@ static zend_result php_openssl_set_server_specific_opts(php_stream *stream, SSL_
}
/* }}} */

static zend_result php_openssl_set_cipher_list(php_stream *stream, SSL_CTX *ctx) /* {{{ */
{
zval *val;
char *cipherlist = NULL;

GET_VER_OPT_STRING("ciphers", cipherlist);
#ifndef USE_OPENSSL_SYSTEM_CIPHERS
if (!cipherlist) {
cipherlist = OPENSSL_DEFAULT_STREAM_CIPHERS;
}
#endif
if (cipherlist && SSL_CTX_set_cipher_list(ctx, cipherlist) != 1) {
return FAILURE;
}

return SUCCESS;
}
/* }}} */

#ifdef HAVE_TLS_ALPN
static int php_openssl_server_alpn_callback(SSL *ssl_handle,
const unsigned char **out, unsigned char *outlen,
const unsigned char *in, unsigned int inlen, void *arg);
#endif

#ifdef HAVE_TLS_SNI
static int php_openssl_server_sni_callback(SSL *ssl_handle, int *al, void *arg) /* {{{ */
{
Expand Down Expand Up @@ -1461,7 +1486,33 @@ static int php_openssl_server_sni_callback(SSL *ssl_handle, int *al, void *arg)
}
/* }}} */

static SSL_CTX *php_openssl_create_sni_server_ctx(char *cert_path, char *key_path) /* {{{ */
/* Per-host SNI contexts replace the base one, so apply the base policy to them. */
static zend_result php_openssl_apply_server_policy_to_sni_ctx(php_stream *stream,
php_openssl_netstream_data_t *sslsock, SSL_CTX *ctx)
{
if (FAILURE == php_openssl_set_cipher_list(stream, ctx)) {
return FAILURE;
}

#ifdef HAVE_SEC_LEVEL
SSL_CTX_set_security_level(ctx, SSL_CTX_get_security_level(sslsock->ctx));
#endif

if (FAILURE == php_openssl_set_server_dh_param(stream, ctx)) {
return FAILURE;
}

#ifdef HAVE_TLS_ALPN
if (sslsock->alpn_ctx.data) {
SSL_CTX_set_alpn_select_cb(ctx, php_openssl_server_alpn_callback, sslsock);
}
#endif

return SUCCESS;
}

static SSL_CTX *php_openssl_create_sni_server_ctx(php_stream *stream,
php_openssl_netstream_data_t *sslsock, char *cert_path, char *key_path)
{
/* The hello method is not inherited by SSL structs when assigning a new context
* inside the SNI callback, so the just use SSLv23 */
Expand All @@ -1471,6 +1522,13 @@ static SSL_CTX *php_openssl_create_sni_server_ctx(char *cert_path, char *key_pat
return NULL;
}

/* Must be done before loading the cert and key, OpenSSL checks them against the
* security level. */
if (FAILURE == php_openssl_apply_server_policy_to_sni_ctx(stream, sslsock, ctx)) {
SSL_CTX_free(ctx);
return NULL;
}

if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) != 1) {
php_error_docref(NULL, E_WARNING,
"Failed setting local cert chain file `%s'; " \
Expand All @@ -1480,7 +1538,8 @@ static SSL_CTX *php_openssl_create_sni_server_ctx(char *cert_path, char *key_pat
);
SSL_CTX_free(ctx);
return NULL;
} else if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) != 1) {
}
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) != 1) {
php_error_docref(NULL, E_WARNING,
"Failed setting private key from file `%s'",
key_path
Expand All @@ -1491,7 +1550,6 @@ static SSL_CTX *php_openssl_create_sni_server_ctx(char *cert_path, char *key_pat

return ctx;
}
/* }}} */

static zend_result php_openssl_enable_server_sni(
php_stream *stream, php_openssl_netstream_data_t *sslsock, bool verify_peer)
Expand Down Expand Up @@ -1599,10 +1657,10 @@ static zend_result php_openssl_enable_server_sni(
}
zend_string_release(local_pk_str);

ctx = php_openssl_create_sni_server_ctx(resolved_cert_path_buff, resolved_pk_path_buff);
ctx = php_openssl_create_sni_server_ctx(stream, sslsock, resolved_cert_path_buff, resolved_pk_path_buff);
} else if (Z_TYPE_P(current) == IS_STRING) {
if (php_openssl_check_path_str_ex(Z_STR_P(current), resolved_path_buff, 0, false, false, "SNI_server_certs in ssl stream context")) {
ctx = php_openssl_create_sni_server_ctx(resolved_path_buff, resolved_path_buff);
ctx = php_openssl_create_sni_server_ctx(stream, sslsock, resolved_path_buff, resolved_path_buff);
} else {
php_error_docref(NULL, E_WARNING,
"Failed setting local cert chain file `%s'; file not found",
Expand Down Expand Up @@ -1721,7 +1779,6 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
int method_flags;
zend_long min_version = 0;
zend_long max_version = 0;
char *cipherlist = NULL;
char *alpn_protocols = NULL;
zval *val;
bool verify_peer = false;
Expand Down Expand Up @@ -1789,16 +1846,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
SSL_CTX_set_default_passwd_cb(sslsock->ctx, php_openssl_passwd_callback);
}

GET_VER_OPT_STRING("ciphers", cipherlist);
#ifndef USE_OPENSSL_SYSTEM_CIPHERS
if (!cipherlist) {
cipherlist = OPENSSL_DEFAULT_STREAM_CIPHERS;
}
#endif
if (cipherlist) {
if (SSL_CTX_set_cipher_list(sslsock->ctx, cipherlist) != 1) {
return FAILURE;
}
if (FAILURE == php_openssl_set_cipher_list(stream, sslsock->ctx)) {
return FAILURE;
}

if (GET_VER_OPT("security_level")) {
Expand Down
Loading