diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32
index 4b7f4b8b8565..ba3018ce6bb2 100644
--- a/ext/openssl/config.w32
+++ b/ext/openssl/config.w32
@@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") {
var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL);
if (ret >= 2) {
- EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c");
+ EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_bio.c xp_ssl.c xp_dtls.c");
AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available.");
if (PHP_OPENSSL_LEGACY_PROVIDER != "no") {
AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider.");
diff --git a/ext/openssl/config0.m4 b/ext/openssl/config0.m4
index 15d1feb96eea..66ac555d4353 100644
--- a/ext/openssl/config0.m4
+++ b/ext/openssl/config0.m4
@@ -26,7 +26,7 @@ PHP_ARG_WITH([openssl-argon2],
if test "$PHP_OPENSSL" != "no"; then
PHP_NEW_EXTENSION([openssl],
- [openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c],
+ [openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_bio.c xp_ssl.c xp_dtls.c],
[$ext_shared])
PHP_SUBST([OPENSSL_SHARED_LIBADD])
PHP_SETUP_OPENSSL([OPENSSL_SHARED_LIBADD],
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index c2a05d43e44e..0d02a289b2ce 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -815,6 +815,8 @@ PHP_MINIT_FUNCTION(openssl)
* OpenSSL callbacks */
ssl_stream_data_index = SSL_get_ex_new_index(0, "PHP stream index", NULL, NULL, NULL);
+ php_openssl_bio_minit();
+
php_stream_xport_register("ssl", php_openssl_ssl_socket_factory);
#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
php_stream_xport_register("sslv3", php_openssl_ssl_socket_factory);
@@ -825,6 +827,16 @@ PHP_MINIT_FUNCTION(openssl)
php_stream_xport_register("tlsv1.2", php_openssl_ssl_socket_factory);
php_stream_xport_register("tlsv1.3", php_openssl_ssl_socket_factory);
+#if !defined(OPENSSL_NO_DTLS) && !defined(OPENSSL_NO_DTLS1_2)
+ php_stream_xport_register("dtls", php_openssl_ssl_socket_factory);
+ php_stream_xport_register("dtlsv1.2", php_openssl_ssl_socket_factory);
+#if defined(DTLS1_3_VERSION) && !defined(OPENSSL_NO_DTLS1_3)
+ php_stream_xport_register("dtlsv1.3", php_openssl_ssl_socket_factory);
+#endif
+ /* override the default udp socket provider */
+ php_stream_xport_register("udp", php_openssl_ssl_socket_factory);
+#endif
+
/* override the default tcp socket provider */
php_stream_xport_register("tcp", php_openssl_ssl_socket_factory);
@@ -899,6 +911,18 @@ PHP_MSHUTDOWN_FUNCTION(openssl)
php_stream_xport_unregister("tlsv1.2");
php_stream_xport_unregister("tlsv1.3");
+#if !defined(OPENSSL_NO_DTLS) && !defined(OPENSSL_NO_DTLS1_2)
+ php_stream_xport_unregister("dtls");
+ php_stream_xport_unregister("dtlsv1.2");
+#if defined(DTLS1_3_VERSION) && !defined(OPENSSL_NO_DTLS1_3)
+ php_stream_xport_unregister("dtlsv1.3");
+#endif
+ /* reinstate the default udp handler */
+ php_stream_xport_register("udp", php_stream_generic_socket_factory);
+#endif
+
+ php_openssl_bio_mshutdown();
+
/* reinstate the default tcp handler */
php_stream_xport_register("tcp", php_stream_generic_socket_factory);
@@ -2742,7 +2766,7 @@ PHP_FUNCTION(openssl_pkey_get_details)
array_init(return_value);
add_assoc_long(return_value, "bits", EVP_PKEY_bits(pkey));
add_assoc_stringl(return_value, "key", pbio, pbio_len);
-
+
zend_long ktype = php_openssl_pkey_get_details(return_value, pkey);
add_assoc_long(return_value, "type", ktype);
diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h
index 588cca0e93d8..31b4bb32f97b 100644
--- a/ext/openssl/php_openssl.h
+++ b/ext/openssl/php_openssl.h
@@ -98,6 +98,8 @@ ZEND_TSRMLS_CACHE_EXTERN();
#endif
php_stream_transport_factory_func php_openssl_ssl_socket_factory;
+void php_openssl_bio_minit(void);
+void php_openssl_bio_mshutdown(void);
void php_openssl_store_errors(void);
void php_openssl_errors_set_mark(void);
diff --git a/ext/openssl/tests/dtls13_basic.phpt b/ext/openssl/tests/dtls13_basic.phpt
new file mode 100644
index 000000000000..3db30669962c
--- /dev/null
+++ b/ext/openssl/tests/dtls13_basic.phpt
@@ -0,0 +1,51 @@
+--TEST--
+dtls:// DTLS 1.3 is negotiated by default between a PHP server and a PHP client
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)) . ' ' . stream_get_meta_data($peer)['crypto']['protocol']);
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ echo "client: ", stream_get_meta_data($client)['crypto']['protocol'], "\n";
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+client: DTLSv1.3
+string(13) "PING DTLSv1.3"
diff --git a/ext/openssl/tests/dtls13_session_resume.phpt b/ext/openssl/tests/dtls13_session_resume.phpt
new file mode 100644
index 000000000000..3a4ae593f25a
--- /dev/null
+++ b/ext/openssl/tests/dtls13_session_resume.phpt
@@ -0,0 +1,87 @@
+--TEST--
+dtls:// DTLS 1.3 session resumption with the ticket the client received
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'dtls13-resume',
+ ]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer1 = stream_socket_accept($server, 10);
+ fwrite($peer1, strtoupper(fread($peer1, 64)));
+ fread($peer1, 1);
+ fclose($peer1);
+
+ phpt_notify();
+ $peer2 = stream_socket_accept($server, 10);
+ $crypto = stream_get_meta_data($peer2)['crypto'];
+ fwrite($peer2, strtoupper(fread($peer2, 64)));
+ fread($peer2, 1);
+ fclose($peer2);
+ fclose($server);
+
+ phpt_notify(message: $crypto['protocol'] . " reused=" . ($crypto['session_reused'] ? "yes" : "no"));
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $session = null;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function ($stream, $s) use (&$session) { $session = $s; },
+ ]]);
+ $c1 = stream_socket_client("dtls://{{ ADDR }}", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+ fwrite($c1, "ping");
+ // The ticket follows the handshake: it is there once the reply was read
+ var_dump(fread($c1, 64));
+ $meta = stream_get_meta_data($c1)['crypto'];
+ echo "captured=" . ($session instanceof Openssl\Session ? "yes" : "no") . "\n";
+ echo "first_reused=" . ($meta['session_reused'] ? "yes" : "no") . "\n";
+ fclose($c1);
+
+ phpt_wait();
+ $ctx2 = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_data' => $session,
+ ]]);
+ $c2 = stream_socket_client("dtls://{{ ADDR }}", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx2);
+ fwrite($c2, "ping");
+ $meta = stream_get_meta_data($c2)['crypto'];
+ echo "second: " . $meta['protocol'] . " reused=" . ($meta['session_reused'] ? "yes" : "no") . "\n";
+ fread($c2, 64);
+ fclose($c2);
+
+ echo "server: " . trim(phpt_wait()) . "\n";
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+string(4) "PING"
+captured=yes
+first_reused=no
+second: DTLSv1.3 reused=yes
+server: DTLSv1.3 reused=yes
diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt
new file mode 100644
index 000000000000..70f8a5dc29ec
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_basic.phpt
@@ -0,0 +1,110 @@
+--TEST--
+dtls:// client: DTLS 1.2 handshake and data round trip against openssl s_server
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+// Grab a free UDP port, then hand it to s_server.
+$probe = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
+$port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+fclose($probe);
+
+// Plain relay mode: data from the client is printed to s_server's stdout, and
+// s_server's stdin is sent to the client.
+$cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+$descriptors = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']];
+$server = proc_open($cmd, $descriptors, $pipes);
+if (!is_resource($server)) {
+ die("skip-like: could not start s_server\n");
+}
+
+// s_server prints "ACCEPT" once it is bound and listening; wait for it so the
+// client does not fire its ClientHello at an unbound port.
+stream_set_blocking($pipes[1], false);
+stream_set_blocking($pipes[2], false);
+$ready = false;
+$deadline = microtime(true) + 10;
+$buf = '';
+while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $e = [];
+ if (stream_select($r, $w, $e, 1)) {
+ foreach ($r as $pipe) {
+ $buf .= (string) fread($pipe, 8192);
+ }
+ if (strpos($buf, 'ACCEPT') !== false) { $ready = true; break; }
+ }
+}
+
+// Self-signed s_server cert: skip verification for this round-trip scenario.
+$ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+$client = $ready
+ ? stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx)
+ : false;
+var_dump($client !== false);
+
+if ($client !== false) {
+ var_dump(stream_get_meta_data($client)['crypto']['protocol'] === 'DTLSv1.2');
+ stream_set_timeout($client, 10);
+
+ // client -> server: our datagram should show up on s_server's stdout.
+ fwrite($client, "PING\n");
+ // server -> client: feed s_server's stdin, it relays to the client.
+ fwrite($pipes[0], "PONG\n");
+
+ // stream_select() on the dtls:// stream exercises the transport cast op.
+ $r = [$client]; $w = $e = [];
+ var_dump(stream_select($r, $w, $e, 10) === 1);
+ var_dump(rtrim((string) fread($client, 8192)) === 'PONG');
+
+ $srvOut = '';
+ $deadline = microtime(true) + 5;
+ while (microtime(true) < $deadline && strpos($srvOut, 'PING') === false) {
+ $srvOut .= (string) fread($pipes[1], 8192);
+ usleep(50000);
+ }
+ var_dump(strpos($srvOut, 'PING') !== false);
+
+ // A read with nothing pending times out; the metadata reflects it.
+ stream_set_timeout($client, 0, 200000);
+ fread($client, 8192);
+ var_dump(stream_get_meta_data($client)['timed_out']);
+
+ // A non-blocking read with nothing pending returns '' (would block), not false.
+ stream_set_blocking($client, false);
+ var_dump(fread($client, 8192));
+
+ fclose($client);
+}
+
+proc_terminate($server);
+foreach ($pipes as $pipe) {
+ if (is_resource($pipe)) fclose($pipe);
+}
+proc_close($server);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(0) ""
diff --git a/ext/openssl/tests/dtls_client_cert.phpt b/ext/openssl/tests/dtls_client_cert.phpt
new file mode 100644
index 000000000000..f2fb11473806
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_cert.phpt
@@ -0,0 +1,86 @@
+--TEST--
+dtls:// client: client certificate for mutual authentication (local_cert)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveCaCert($caFile);
+$gen->saveNewCertAsFileWithKey('dtls-server', $serverCert);
+$gen->saveNewCertAsFileWithKey('dtls-client', $clientCert);
+
+// s_server with -Verify requires a client certificate signed by -CAfile.
+function start_server($serverCert, $caFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $serverCert, '-key', $serverCert, '-Verify', '1', '-CAfile', $caFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+// 1) with a client certificate -> the handshake succeeds.
+[$proc, $pipes, $port] = start_server($serverCert, $caFile);
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false, 'verify_peer_name' => false,
+ 'local_cert' => $clientCert,
+]]);
+$client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+var_dump($client !== false);
+if ($client !== false) {
+ fclose($client);
+}
+stop_server($proc, $pipes);
+
+// 2) without a client certificate -> the server rejects the handshake.
+[$proc, $pipes, $port] = start_server($serverCert, $caFile);
+$ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+$client = @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+var_dump($client === false);
+stop_server($proc, $pipes);
+
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_client_fingerprint.phpt b/ext/openssl/tests/dtls_client_fingerprint.phpt
new file mode 100644
index 000000000000..0a9c833e8462
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_fingerprint.phpt
@@ -0,0 +1,88 @@
+--TEST--
+dtls:// client: peer authentication by certificate fingerprint (peer_fingerprint)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+$fingerprint = $gen->getCertDigest('sha256');
+
+function start_server($certFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+// 1) matching fingerprint authenticates the peer on its own (no CA needed).
+[$proc, $pipes, $port] = start_server($certFile);
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'peer_fingerprint' => ['sha256' => $fingerprint],
+]]);
+$client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+var_dump($client !== false);
+if ($client !== false) {
+ fclose($client);
+}
+stop_server($proc, $pipes);
+
+// 2) wrong fingerprint -> rejected after the handshake.
+[$proc, $pipes, $port] = start_server($certFile);
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'peer_fingerprint' => ['sha256' => str_repeat('00', 32)],
+]]);
+$warnings = [];
+set_error_handler(function ($no, $str) use (&$warnings) { $warnings[] = $str; return true; });
+$client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+restore_error_handler();
+var_dump($client === false);
+var_dump(count(preg_grep('/fingerprint/', $warnings)) > 0);
+stop_server($proc, $pipes);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_client_nonblocking.phpt b/ext/openssl/tests/dtls_client_nonblocking.phpt
new file mode 100644
index 000000000000..f6a35db6ac14
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_nonblocking.phpt
@@ -0,0 +1,64 @@
+--TEST--
+dtls:// client: a non-blocking handshake reports WANT_READ until it completes
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('udp://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ stream_set_blocking($client, false);
+
+ $rounds = 0;
+ $waited = 0;
+ while (($r = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT)) === 0) {
+ $rounds++;
+ $read = [$client]; $write = $except = [];
+ if (stream_select($read, $write, $except, 5) > 0) {
+ $waited++;
+ }
+ if ($rounds > 100) break;
+ }
+ var_dump($r);
+ var_dump($rounds > 0 && $waited > 0);
+
+ stream_set_blocking($client, true);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_client_timeout.phpt b/ext/openssl/tests/dtls_client_timeout.phpt
new file mode 100644
index 000000000000..4053bd3f2204
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_timeout.phpt
@@ -0,0 +1,27 @@
+--TEST--
+dtls:// client: the handshake is bounded by the connect timeout
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+ ['verify_peer' => false, 'verify_peer_name' => false]]));
+$elapsed = microtime(true) - $start;
+
+var_dump($client === false);
+var_dump($elapsed >= 1 && $elapsed < 5);
+
+fclose($sink);
+?>
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_client_verify.phpt b/ext/openssl/tests/dtls_client_verify.phpt
new file mode 100644
index 000000000000..a7641e12a0a4
--- /dev/null
+++ b/ext/openssl/tests/dtls_client_verify.phpt
@@ -0,0 +1,91 @@
+--TEST--
+dtls:// client: peer certificate verification (verify_peer)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveCaCert($caFile);
+$gen->saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+// Start a DTLS s_server on a fresh UDP port; returns [proc, pipes, port].
+function start_server($certFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+// 1) verify_peer with the matching CA -> the handshake succeeds.
+[$proc, $pipes, $port] = start_server($certFile);
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => true,
+ 'verify_peer_name' => false,
+ 'cafile' => $caFile,
+]]);
+$client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+var_dump($client !== false);
+if ($client !== false) {
+ fclose($client);
+}
+stop_server($proc, $pipes);
+
+// 2) verify_peer without the CA -> the handshake fails on verification.
+[$proc, $pipes, $port] = start_server($certFile);
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => true,
+ 'verify_peer_name' => false,
+]]);
+$warnings = [];
+set_error_handler(function ($no, $str) use (&$warnings) { $warnings[] = $str; return true; });
+$client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+restore_error_handler();
+var_dump($client === false);
+var_dump(count(preg_grep('/verif/', $warnings)) > 0);
+stop_server($proc, $pipes);
+
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_empty_datagram.phpt b/ext/openssl/tests/dtls_empty_datagram.phpt
new file mode 100644
index 000000000000..dd8b4ad6533b
--- /dev/null
+++ b/ext/openssl/tests/dtls_empty_datagram.phpt
@@ -0,0 +1,55 @@
+--TEST--
+dtls:// client: an empty datagram does not end the connection
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5, $clientAddr);
+ fwrite($peer, "one");
+ usleep(200000);
+ // A zero-length datagram from the server's address, as a spoofer could send.
+ stream_socket_sendto($server, "", 0, $clientAddr);
+ usleep(200000);
+ fwrite($peer, "two");
+ usleep(500000);
+ fclose($peer);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ stream_set_timeout($client, 5);
+ var_dump(fread($client, 100));
+ // The empty datagram is not a record and must not end the connection.
+ var_dump(fread($client, 100));
+ var_dump(feof($client));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+string(3) "one"
+string(3) "two"
+bool(false)
diff --git a/ext/openssl/tests/dtls_keying_material.phpt b/ext/openssl/tests/dtls_keying_material.phpt
new file mode 100644
index 000000000000..3f3f60c0d59c
--- /dev/null
+++ b/ext/openssl/tests/dtls_keying_material.phpt
@@ -0,0 +1,57 @@
+--TEST--
+dtls://: RFC 5705 exported keying material matches on both peers
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'keying_material_label' => 'EXTRACTOR-dtls_srtp',
+ 'keying_material_length' => 60,
+ ]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ $meta = stream_get_meta_data($peer);
+ fwrite($peer, bin2hex($meta['crypto']['keying_material']));
+ fclose($peer);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false, 'verify_peer_name' => false,
+ 'keying_material_label' => 'EXTRACTOR-dtls_srtp',
+ 'keying_material_length' => 60,
+ ]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ $meta = stream_get_meta_data($client);
+ $km = $meta['crypto']['keying_material'];
+ var_dump(strlen($km) === 60);
+ var_dump(fread($client, 8192) === bin2hex($km));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_local_pk_passphrase.phpt b/ext/openssl/tests/dtls_local_pk_passphrase.phpt
new file mode 100644
index 000000000000..969a98bdddfd
--- /dev/null
+++ b/ext/openssl/tests/dtls_local_pk_passphrase.phpt
@@ -0,0 +1,97 @@
+--TEST--
+dtls:// client: an encrypted local_pk is unlocked with the passphrase option
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $serverCert);
+
+// A client certificate with a passphrase-encrypted private key in a separate file.
+$key = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
+$csr = openssl_csr_new(['commonName' => 'dtls-client'], $key);
+$cert = openssl_csr_sign($csr, null, $key, 365);
+openssl_x509_export($cert, $certPem);
+openssl_pkey_export($key, $keyPem, 'secret');
+file_put_contents($clientCert, $certPem);
+file_put_contents($clientKey, $keyPem);
+
+function start_server($certFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+function connect_with_passphrase($port, $clientCert, $clientKey, $passphrase) {
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false, 'verify_peer_name' => false,
+ 'local_cert' => $clientCert,
+ 'local_pk' => $clientKey,
+ 'passphrase' => $passphrase,
+ ]]);
+ $errno = $errstr = null;
+ return @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10,
+ STREAM_CLIENT_CONNECT, $ctx);
+}
+
+// 1) correct passphrase -> the key is loaded and the handshake succeeds.
+[$proc, $pipes, $port] = start_server($serverCert);
+$client = connect_with_passphrase($port, $clientCert, $clientKey, 'secret');
+var_dump($client !== false);
+if ($client !== false) {
+ fclose($client);
+}
+stop_server($proc, $pipes);
+
+// 2) wrong passphrase -> the key cannot be loaded and the connection fails.
+[$proc, $pipes, $port] = start_server($serverCert);
+$client = connect_with_passphrase($port, $clientCert, $clientKey, 'wrong');
+var_dump($client === false);
+stop_server($proc, $pipes);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_mtu.phpt b/ext/openssl/tests/dtls_mtu.phpt
new file mode 100644
index 000000000000..b9e3b3c106ab
--- /dev/null
+++ b/ext/openssl/tests/dtls_mtu.phpt
@@ -0,0 +1,54 @@
+--TEST--
+dtls://: a small dtls_link_mtu fragments the handshake and still completes
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'dtls_link_mtu' => 600,
+ ]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ if ($peer === false) {
+ echo "accept failed\n";
+ } else {
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'dtls_link_mtu' => 600]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_peer_fingerprint_invalid.phpt b/ext/openssl/tests/dtls_peer_fingerprint_invalid.phpt
new file mode 100644
index 000000000000..fa3ae2b436a2
--- /dev/null
+++ b/ext/openssl/tests/dtls_peer_fingerprint_invalid.phpt
@@ -0,0 +1,88 @@
+--TEST--
+dtls:// client: a malformed peer_fingerprint is rejected with a warning
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+function start_server($certFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+// Connect with the given peer_fingerprint and return [connected, captured warnings].
+function connect_fingerprint($port, $fingerprint) {
+ $warnings = [];
+ set_error_handler(function ($no, $str) use (&$warnings) { $warnings[] = $str; return true; });
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false, 'verify_peer_name' => false,
+ 'peer_fingerprint' => $fingerprint,
+ ]]);
+ $client = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+ restore_error_handler();
+ if ($client !== false) {
+ fclose($client);
+ }
+ return [$client !== false, $warnings];
+}
+
+// A string fingerprint that is not a md5 (32) or sha1 (40) hex length.
+[$proc, $pipes, $port] = start_server($certFile);
+[$ok, $warnings] = connect_fingerprint($port, 'not-a-real-hash');
+var_dump($ok);
+var_dump((bool) array_filter($warnings, fn($w) => str_contains($w, 'md5 or sha1 hash')));
+stop_server($proc, $pipes);
+
+// An array without an [algo => hash] shape.
+[$proc, $pipes, $port] = start_server($certFile);
+[$ok, $warnings] = connect_fingerprint($port, ['sha256']);
+var_dump($ok);
+var_dump((bool) array_filter($warnings, fn($w) => str_contains($w, '[algo => fingerprint]')));
+stop_server($proc, $pipes);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(false)
+bool(true)
+bool(false)
+bool(true)
diff --git a/ext/openssl/tests/dtls_peer_name.phpt b/ext/openssl/tests/dtls_peer_name.phpt
new file mode 100644
index 000000000000..cfa31bfafde2
--- /dev/null
+++ b/ext/openssl/tests/dtls_peer_name.phpt
@@ -0,0 +1,86 @@
+--TEST--
+dtls:// client: peer name verification matches the certificate SAN
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveCaCert($caFile);
+$gen->saveNewCertAsFileWithKey('dtls-server', $certFile, null, 'DNS:dtls.example.org');
+
+function start_server($certFile) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile];
+ $proc = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+ $deadline = microtime(true) + 10;
+ $buf = '';
+ while (microtime(true) < $deadline) {
+ $r = [$pipes[1], $pipes[2]]; $w = $ex = [];
+ if (stream_select($r, $w, $ex, 1)) {
+ foreach ($r as $p) $buf .= (string) fread($p, 8192);
+ if (strpos($buf, 'ACCEPT') !== false) break;
+ }
+ }
+ return [$proc, $pipes, $port];
+}
+
+function stop_server($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) {
+ if (is_resource($p)) fclose($p);
+ }
+ proc_close($proc);
+}
+
+function connect_with_name($port, $caFile, $name) {
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => true,
+ 'cafile' => $caFile,
+ 'peer_name' => $name,
+ ]]);
+ $errno = $errstr = null;
+ return @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10,
+ STREAM_CLIENT_CONNECT, $ctx);
+}
+
+// 1) peer_name matches a SAN entry -> the handshake succeeds.
+[$proc, $pipes, $port] = start_server($certFile);
+$client = connect_with_name($port, $caFile, 'dtls.example.org');
+var_dump($client !== false);
+if ($client !== false) {
+ fclose($client);
+}
+stop_server($proc, $pipes);
+
+// 2) peer_name does not match -> the handshake is rejected.
+[$proc, $pipes, $port] = start_server($certFile);
+$client = connect_with_name($port, $caFile, 'wrong.example.org');
+var_dump($client === false);
+stop_server($proc, $pipes);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_server.phpt b/ext/openssl/tests/dtls_server.phpt
new file mode 100644
index 000000000000..fcf3aaf917bc
--- /dev/null
+++ b/ext/openssl/tests/dtls_server.phpt
@@ -0,0 +1,52 @@
+--TEST--
+dtls:// server: accept a peer and exchange data with a dtls:// client
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ if ($peer === false) {
+ echo "accept failed\n";
+ } else {
+ $data = fread($peer, 8192);
+ fwrite($peer, strtoupper($data));
+ fclose($peer);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_server_multi_peer.phpt b/ext/openssl/tests/dtls_server_multi_peer.phpt
new file mode 100644
index 000000000000..bec80676d852
--- /dev/null
+++ b/ext/openssl/tests/dtls_server_multi_peer.phpt
@@ -0,0 +1,74 @@
+--TEST--
+dtls:// server: several peers share one socket and are demultiplexed by address
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peers = [];
+ for ($i = 0; $i < 3; $i++) {
+ $peer = stream_socket_accept($server, 5, $addr);
+ if (stream_socket_get_name($peer, true) !== $addr) {
+ echo "peer name mismatch\n";
+ }
+ $peers[] = $peer;
+ }
+ // Read in reverse order: datagrams for the other peers arrive meanwhile and are queued
+ foreach (array_reverse($peers, true) as $i => $peer) {
+ $data = fread($peer, 8192);
+ fwrite($peer, strtoupper($data) . " from peer $i");
+ }
+ foreach ($peers as $peer) {
+ fclose($peer);
+ }
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $clients = [];
+ for ($i = 0; $i < 3; $i++) {
+ $clients[$i] = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($clients[$i] !== false);
+ }
+ foreach ($clients as $i => $client) {
+ fwrite($client, "ping $i");
+ }
+ foreach ($clients as $i => $client) {
+ var_dump(fread($client, 8192));
+ }
+ foreach ($clients as $client) {
+ fclose($client);
+ }
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+string(18) "PING 0 from peer 0"
+string(18) "PING 1 from peer 1"
+string(18) "PING 2 from peer 2"
diff --git a/ext/openssl/tests/dtls_server_pending_cookie.phpt b/ext/openssl/tests/dtls_server_pending_cookie.phpt
new file mode 100644
index 000000000000..630ce6401334
--- /dev/null
+++ b/ext/openssl/tests/dtls_server_pending_cookie.phpt
@@ -0,0 +1,69 @@
+--TEST--
+dtls:// server: peers that never return the cookie do not hold the pending slots
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'dtls_max_pending' => 4,
+ 'dtls_pending_timeout' => 30,
+ ]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 10);
+ if ($peer !== false) {
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
+
+ // As many peers as there are pending slots send a ClientHello, get the cookie back and
+ // never answer.
+ $silent = [];
+ for ($i = 0; $i < 4; $i++) {
+ $s = stream_socket_client('udp://{{ ADDR }}', $errno, $errstr, 2, STREAM_CLIENT_CONNECT,
+ stream_context_create($ctx));
+ stream_set_blocking($s, false);
+ @stream_socket_enable_crypto($s, true, STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT);
+ $silent[] = $s;
+ }
+ usleep(200000);
+
+ // A real peer still connects, well before dtls_pending_timeout reaps them.
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 3,
+ STREAM_CLIENT_CONNECT, stream_context_create($ctx));
+ var_dump($client !== false);
+ if ($client !== false) {
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+ }
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_server_robustness.phpt b/ext/openssl/tests/dtls_server_robustness.phpt
new file mode 100644
index 000000000000..ec49f2c0e951
--- /dev/null
+++ b/ext/openssl/tests/dtls_server_robustness.phpt
@@ -0,0 +1,60 @@
+--TEST--
+dtls:// server: bogus datagrams are ignored, a real peer still connects
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ if ($peer === false) {
+ echo "accept failed\n";
+ } else {
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ // Flood the server's port with garbage that is not a valid (cookied)
+ // ClientHello; the server must ignore it.
+ $junk = stream_socket_client('udp://{{ ADDR }}', $errno, $errstr, 2, STREAM_CLIENT_CONNECT);
+ for ($i = 0; $i < 8; $i++) {
+ fwrite($junk, random_bytes(80));
+ }
+ fclose($junk);
+
+ // A real DTLS client must still complete the handshake.
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_server_unaccepted_memory.phpt b/ext/openssl/tests/dtls_server_unaccepted_memory.phpt
new file mode 100644
index 000000000000..f82f61d65a15
--- /dev/null
+++ b/ext/openssl/tests/dtls_server_unaccepted_memory.phpt
@@ -0,0 +1,71 @@
+--TEST--
+dtls:// server: peers that are never accepted cannot exhaust memory
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ ini_set('memory_limit', '8M');
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s', 'dtls_max_pending' => 4]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ stream_set_timeout($peer, 20);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+
+ // Peers that complete the handshake, are never accepted and send as much as they can.
+ // Only dtls_max_pending of them get a handshake.
+ $big = str_repeat('A', 8192);
+ $flood = [];
+ $handshakes = 0;
+ for ($i = 0; $i < 8; $i++) {
+ $c = stream_socket_client('udp://{{ ADDR }}', $errno, $errstr, 1, STREAM_CLIENT_CONNECT, $ctx);
+ if (!@stream_socket_enable_crypto($c, true, STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT)) {
+ continue;
+ }
+ $handshakes++;
+ for ($k = 0; $k < 256; $k++) {
+ stream_socket_sendto($c, $big);
+ if ($k % 8 == 0) {
+ usleep(5000);
+ }
+ }
+ $flood[] = $c;
+ }
+
+ var_dump($handshakes);
+
+ // The server is still serving the accepted peer.
+ stream_set_timeout($client, 5);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+int(4)
+string(4) "PING"
diff --git a/ext/openssl/tests/dtls_server_verify.phpt b/ext/openssl/tests/dtls_server_verify.phpt
new file mode 100644
index 000000000000..4bb9a2e4cdf9
--- /dev/null
+++ b/ext/openssl/tests/dtls_server_verify.phpt
@@ -0,0 +1,73 @@
+--TEST--
+dtls:// server: verify the client certificate (mutual authentication)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveCaCert($caFile);
+$gen->saveNewCertAsFileWithKey('dtls-server', $serverCert);
+$gen->saveNewCertAsFileWithKey('dtls-client', $clientCert);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'verify_peer' => true,
+ 'verify_peer_name' => false,
+ 'cafile' => '%s',
+ ]]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = @stream_socket_accept($server, 5);
+ if ($peer !== false) {
+ fwrite($peer, "verified");
+ fclose($peer);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $serverCert, $caFile);
+
+include 'ServerClientTestCase.inc';
+
+// 1) The client presents a CA-signed certificate -> the server accepts it.
+$clientOk = sprintf(<<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s', 'verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ var_dump(fread($client, 8192));
+CODE, $clientCert);
+ServerClientTestCase::getInstance()->run($clientOk, $serverCode);
+
+// 2) The client presents no certificate -> the server rejects the connection.
+$clientNoCert = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($client !== false);
+ var_dump(fread($client, 8192));
+CODE;
+ServerClientTestCase::getInstance()->run($clientNoCert, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(8) "verified"
+bool(true)
+string(0) ""
diff --git a/ext/openssl/tests/dtls_session_resume.phpt b/ext/openssl/tests/dtls_session_resume.phpt
new file mode 100644
index 000000000000..48f39ab94f06
--- /dev/null
+++ b/ext/openssl/tests/dtls_session_resume.phpt
@@ -0,0 +1,82 @@
+--TEST--
+dtls:// client: captures a resumable session and accepts session_data
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+function start_s_server($certFile, &$port) {
+ $probe = stream_socket_server('udp://127.0.0.1:0', $e, $s, STREAM_SERVER_BIND);
+ $port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
+ fclose($probe);
+ $cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
+ '-cert', $certFile, '-key', $certFile, '-quiet'];
+ $proc = proc_open($cmd, [['pipe','r'], ['pipe','w'], ['pipe','w']], $pipes);
+ $deadline = microtime(true) + 5;
+ while (microtime(true) < $deadline) {
+ usleep(100000);
+ $t = @stream_socket_client("udp://127.0.0.1:$port", $e, $s, 0.2);
+ if ($t) { fclose($t); break; }
+ }
+ return [$proc, $pipes];
+}
+
+function stop($proc, $pipes) {
+ proc_terminate($proc);
+ foreach ($pipes as $p) if (is_resource($p)) fclose($p);
+ proc_close($proc);
+}
+
+// 1) Full handshake: capture the negotiated session.
+[$proc, $pipes] = start_s_server($certFile, $port);
+$session = null;
+$ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function ($stream, $s) use (&$session) { $session = $s; },
+]]);
+$c = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+$crypto = stream_get_meta_data($c)['crypto'];
+var_dump($crypto['session_reused']);
+var_dump($session instanceof Openssl\Session);
+var_dump($session->isResumable());
+var_dump(strlen($session->export()) > 0);
+fclose($c);
+stop($proc, $pipes);
+
+// 2) session_data is accepted; against a fresh server it falls back to a full
+// handshake without error.
+[$proc, $pipes] = start_s_server($certFile, $port);
+$ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'session_data' => $session]]);
+$c = stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+var_dump($c !== false);
+fclose($c);
+stop($proc, $pipes);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/dtls_session_resume_server.phpt b/ext/openssl/tests/dtls_session_resume_server.phpt
new file mode 100644
index 000000000000..ee68785b9725
--- /dev/null
+++ b/ext/openssl/tests/dtls_session_resume_server.phpt
@@ -0,0 +1,95 @@
+--TEST--
+dtls:// server: DTLS 1.2 session resumption via a userland cache (session_new_cb/get_cb)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+// The session_new_cb/session_get_cb options let userland hold the session cache (keyed by
+// session id). One server serves two connections and resumes the second from the session
+// stored during the first. DTLS 1.3 resumes with tickets, see dtls13_session_resume.phpt.
+$serverCode = <<<'CODE'
+ $store = [];
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'crypto_method' => STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER,
+ 'session_id_context' => 'dtls-resume',
+ 'session_new_cb' => function ($stream, $session) use (&$store) {
+ $store[bin2hex($session->id)] = $session;
+ },
+ 'session_get_cb' => function ($stream, $id) use (&$store) {
+ return $store[bin2hex($id)] ?? null;
+ },
+ ]]);
+
+ $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ // First connection: full handshake, session_new_cb stores the session.
+ $peer1 = stream_socket_accept($server, 10);
+ fwrite($peer1, strtoupper(fread($peer1, 64)));
+ fread($peer1, 1);
+ fclose($peer1);
+
+ // Second connection: session_get_cb resumes.
+ phpt_notify();
+ $peer2 = stream_socket_accept($server, 10);
+ $reused = stream_get_meta_data($peer2)['crypto']['session_reused'];
+ fwrite($peer2, strtoupper(fread($peer2, 64)));
+ fread($peer2, 1);
+ fclose($peer2);
+ fclose($server);
+
+ phpt_notify(message: "reused=" . ($reused ? "yes" : "no") . " stored=" . count($store));
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ // First connection: capture the negotiated session.
+ $session = null;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function ($stream, $s) use (&$session) { $session = $s; },
+ ]]);
+ $c1 = stream_socket_client("dtls://{{ ADDR }}", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx);
+ fwrite($c1, "ping");
+ $meta = stream_get_meta_data($c1)['crypto'];
+ echo "captured=" . ($session instanceof Openssl\Session ? "yes" : "no") . "\n";
+ echo "first_reused=" . ($meta['session_reused'] ? "yes" : "no") . "\n";
+ fread($c1, 64);
+ fclose($c1);
+
+ // Reconnect presenting the session.
+ phpt_wait();
+ $ctx2 = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'session_data' => $session]]);
+ $c2 = stream_socket_client("dtls://{{ ADDR }}", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx2);
+ fwrite($c2, "ping");
+ echo "second_reused=" . (stream_get_meta_data($c2)['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ fread($c2, 64);
+ fclose($c2);
+
+ echo "server: " . trim(phpt_wait()) . "\n";
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+captured=yes
+first_reused=no
+second_reused=yes
+server: reused=yes stored=1
diff --git a/ext/openssl/tests/dtls_skipif.inc b/ext/openssl/tests/dtls_skipif.inc
new file mode 100644
index 000000000000..2973e4557f8d
--- /dev/null
+++ b/ext/openssl/tests/dtls_skipif.inc
@@ -0,0 +1,23 @@
+
+ *
+ * Skips when the openssl extension does not expose a usable dtls:// transport,
+ * for example an OpenSSL build with OPENSSL_NO_DTLS or a php-src that does not
+ * register the transport yet. Tests that also spawn a peer process should add
+ * their own proc_open() check on top of this one.
+ */
+
+if (!extension_loaded('openssl')) {
+ die('skip openssl extension not available');
+}
+
+if (!in_array('dtls', stream_get_transports(), true)) {
+ die('skip dtls:// stream transport not available');
+}
diff --git a/ext/openssl/tests/dtls_udp_enable_crypto.phpt b/ext/openssl/tests/dtls_udp_enable_crypto.phpt
new file mode 100644
index 000000000000..2329f43cd3d4
--- /dev/null
+++ b/ext/openssl/tests/dtls_udp_enable_crypto.phpt
@@ -0,0 +1,52 @@
+--TEST--
+udp:// client enables DTLS with stream_socket_enable_crypto()
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('dtls-server', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('dtls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]);
+ $client = stream_socket_client('udp://{{ ADDR }}', $errno, $errstr, 5,
+ STREAM_CLIENT_CONNECT, $ctx);
+ var_dump(stream_get_meta_data($client)['stream_type']);
+ var_dump(stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT));
+ var_dump(stream_get_meta_data($client)['crypto']['protocol']);
+ fwrite($client, "ping");
+ var_dump(fread($client, 8192));
+ fclose($client);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECTF--
+string(15) "udp_socket/dtls"
+bool(true)
+string(8) "DTLSv1.%d"
+string(4) "PING"
diff --git a/ext/openssl/tests/tls_inner_stream.phpt b/ext/openssl/tests/tls_inner_stream.phpt
new file mode 100644
index 000000000000..46b1794b9bc7
--- /dev/null
+++ b/ext/openssl/tests/tls_inner_stream.phpt
@@ -0,0 +1,68 @@
+--TEST--
+TLS over an inner stream: the ciphertext goes through a tcp:// stream given by inner_stream
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('inner-stream', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fwrite($peer, str_repeat('x', 100000));
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $inner = stream_socket_client('tcp://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT);
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'inner_stream' => $inner,
+ ]]);
+ // Nothing is connected here: the inner stream carries the ciphertext
+ $tls = stream_socket_client('tls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($tls !== false);
+ var_dump(stream_get_meta_data($tls)['crypto']['protocol'] !== '');
+ fwrite($tls, "ping");
+ var_dump(fread($tls, 4));
+ $total = 0;
+ while (!feof($tls) && ($chunk = fread($tls, 8192)) !== false && $chunk !== '') {
+ $total += strlen($chunk);
+ }
+ var_dump($total);
+ // The inner stream cannot be closed from under the TLS stream
+ var_dump(@fclose($inner));
+ fclose($tls);
+ var_dump(is_resource($inner));
+ var_dump(fclose($inner));
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+bool(true)
+string(4) "PING"
+int(100000)
+bool(false)
+bool(true)
+bool(true)
diff --git a/ext/openssl/tests/tls_inner_stream_shared.phpt b/ext/openssl/tests/tls_inner_stream_shared.phpt
new file mode 100644
index 000000000000..5c33ba4cde0f
--- /dev/null
+++ b/ext/openssl/tests/tls_inner_stream_shared.phpt
@@ -0,0 +1,74 @@
+--TEST--
+TLS over an inner stream: the inner stream carries one stream at a time and tcp:// ignores it
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('inner-stream', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $inner = stream_socket_client('tcp://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT);
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'inner_stream' => $inner,
+ ]]);
+ $first = stream_socket_client('tls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($first !== false);
+
+ // The inner stream is taken
+ $second = stream_socket_client('tls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($second);
+ // The stream that failed to take it did not release it either
+ var_dump(@fclose($inner));
+
+ // A plain transport connects on its own
+ $plain = stream_socket_client('tcp://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ var_dump(stream_socket_get_name($plain, false) !== stream_socket_get_name($inner, false));
+ fclose($plain);
+ var_dump(@fclose($inner));
+
+ fwrite($first, "ping");
+ var_dump(fread($first, 4));
+ fclose($first);
+ var_dump(fclose($inner));
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECTF--
+bool(true)
+
+Warning: stream_socket_client(): inner_stream is already used by another stream in %s on line %d
+
+Warning: stream_socket_client(): Unable to connect to tls://%s in %s on line %d
+bool(false)
+bool(false)
+bool(true)
+bool(false)
+string(4) "PING"
+bool(true)
diff --git a/ext/openssl/tests/tls_inner_stream_user_wrapper.phpt b/ext/openssl/tests/tls_inner_stream_user_wrapper.phpt
new file mode 100644
index 000000000000..49e2708bb922
--- /dev/null
+++ b/ext/openssl/tests/tls_inner_stream_user_wrapper.phpt
@@ -0,0 +1,82 @@
+--TEST--
+TLS over an inner stream of a user wrapper
+--EXTENSIONS--
+openssl
+--SKIPIF--
+
+--FILE--
+saveNewCertAsFileWithKey('inner-stream', $certFile);
+
+$serverCode = <<<'CODE'
+ $ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
+ STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
+ phpt_notify_server_start($server);
+
+ $peer = stream_socket_accept($server, 5);
+ fwrite($peer, strtoupper(fread($peer, 8192)));
+ fclose($peer);
+ fclose($server);
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ class Tunnel {
+ public $context;
+ private $sock;
+ public static int $reads = 0;
+ public static int $writes = 0;
+
+ public function stream_open($path, $mode, $options, &$opened) {
+ $this->sock = stream_socket_client('tcp://' . substr($path, strlen('tunnel://')),
+ $errno, $errstr, 5);
+ return $this->sock !== false;
+ }
+ public function stream_read($count) {
+ self::$reads++;
+ return fread($this->sock, $count);
+ }
+ public function stream_write($data) {
+ self::$writes++;
+ return fwrite($this->sock, $data);
+ }
+ public function stream_eof() {
+ return feof($this->sock);
+ }
+ public function stream_close() {
+ fclose($this->sock);
+ }
+ }
+ stream_wrapper_register('tunnel', 'Tunnel');
+
+ $inner = fopen('tunnel://{{ ADDR }}', 'r+');
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'inner_stream' => $inner,
+ ]]);
+ $tls = stream_socket_client('tls://{{ ADDR }}', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
+ var_dump($tls !== false);
+ fwrite($tls, "ping");
+ var_dump(fread($tls, 4));
+ var_dump(Tunnel::$reads > 0 && Tunnel::$writes > 0);
+ fclose($tls);
+ fclose($inner);
+CODE;
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+
+--EXPECT--
+bool(true)
+string(4) "PING"
+bool(true)
diff --git a/ext/openssl/tests/tls_inner_stream_would_block.phpt b/ext/openssl/tests/tls_inner_stream_would_block.phpt
new file mode 100644
index 000000000000..30a3fec2c46f
--- /dev/null
+++ b/ext/openssl/tests/tls_inner_stream_would_block.phpt
@@ -0,0 +1,109 @@
+--TEST--
+TLS over an inner stream that would block: the connect fails instead of sending plaintext
+--EXTENSIONS--
+openssl
+--FILE--
+saveNewCertAsFileWithKey('inner-stream', $certFile);
+
+class Pending
+{
+ public $context;
+ public static $sent = '';
+ private $sock;
+
+ function stream_open($path, $mode, $options, &$opened)
+ {
+ $this->sock = stream_socket_client('tcp://' . substr($path, strlen('pending://')));
+ return $this->sock !== false;
+ }
+ // An asynchronous tunnel with nothing received yet.
+ function stream_read($count) { return ''; }
+ function stream_write($data)
+ {
+ self::$sent .= $data;
+ return fwrite($this->sock, $data);
+ }
+ function stream_eof() { return false; }
+ function stream_close() { fclose($this->sock); }
+}
+stream_wrapper_register('pending', 'Pending');
+
+$ssl = ['verify_peer' => false, 'verify_peer_name' => false];
+
+// A non-blocking inner stream: the server has not answered the ClientHello yet.
+$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+$addr = stream_socket_get_name($server, false);
+$inner = stream_socket_client("tcp://$addr");
+stream_set_blocking($inner, false);
+$tls = stream_socket_client("tls://$addr", $errno, $errstr, 2, STREAM_CLIENT_CONNECT,
+ stream_context_create(['ssl' => $ssl + ['inner_stream' => $inner]]));
+var_dump($tls);
+if ($tls) {
+ fwrite($tls, "secret");
+}
+$peer = stream_socket_accept($server);
+stream_set_blocking($peer, false);
+usleep(100000);
+var_dump(str_contains((string) fread($peer, 65536), 'secret'));
+
+// A user wrapper that has nothing to read yet.
+$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+$addr = stream_socket_get_name($server, false);
+$inner = fopen("pending://$addr", 'r+');
+$tls = stream_socket_client("tls://$addr", $errno, $errstr, 2, STREAM_CLIENT_CONNECT,
+ stream_context_create(['ssl' => $ssl + ['inner_stream' => $inner]]));
+var_dump($tls);
+if ($tls) {
+ fwrite($tls, "secret");
+}
+var_dump(str_contains(Pending::$sent, 'secret'));
+
+// An asynchronous connect still hands the handshake over to the caller.
+$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
+$addr = stream_socket_get_name($server, false);
+$inner = stream_socket_client("tcp://$addr");
+stream_set_blocking($inner, false);
+$tls = stream_socket_client("tls://$addr", $errno, $errstr, 2,
+ STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT,
+ stream_context_create(['ssl' => $ssl + ['inner_stream' => $inner]]));
+stream_set_blocking($tls, false);
+$peer = stream_socket_accept($server);
+stream_set_blocking($peer, false);
+stream_context_set_option($peer, 'ssl', 'local_cert', $certFile);
+$client = $srv = 0;
+for ($i = 0; $i < 200 && ($client !== true || $srv !== true); $i++) {
+ if ($client !== true) {
+ $client = stream_socket_enable_crypto($tls, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+ }
+ if ($srv !== true) {
+ $srv = stream_socket_enable_crypto($peer, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
+ }
+ usleep(1000);
+}
+var_dump($client, $srv);
+fwrite($tls, "ping");
+usleep(100000);
+var_dump(fread($peer, 4));
+?>
+--CLEAN--
+
+--EXPECTF--
+Warning: stream_socket_client(): Failed to enable crypto in %s on line %d
+
+Warning: stream_socket_client(): Unable to connect to tls://%s in %s on line %d
+bool(false)
+bool(false)
+
+Warning: stream_socket_client(): Failed to enable crypto in %s on line %d
+
+Warning: stream_socket_client(): Unable to connect to tls://%s in %s on line %d
+bool(false)
+bool(false)
+bool(true)
+bool(true)
+string(4) "ping"
diff --git a/ext/openssl/xp_bio.c b/ext/openssl/xp_bio.c
new file mode 100644
index 000000000000..86a570997cc8
--- /dev/null
+++ b/ext/openssl/xp_bio.c
@@ -0,0 +1,886 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka |
+ +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+#include
+#endif
+
+#include "php.h"
+#include "php_network.h"
+#include "php_streams.h"
+#include "xp_bio.h"
+
+#include
+#include
+#include
+
+#ifndef PHP_WIN32
+#include
+#include
+#endif
+
+/* Deadlines */
+
+void php_openssl_deadline_init(php_openssl_deadline *dl, const struct timeval *tv)
+{
+ if (tv == NULL || tv->tv_sec < 0 || (tv->tv_sec == 0 && tv->tv_usec <= 0)) {
+ php_openssl_deadline_init_infinite(dl);
+ return;
+ }
+ dl->infinite = false;
+ dl->at = zend_hrtime() + (zend_hrtime_t) tv->tv_sec * ZEND_NANO_IN_SEC
+ + (zend_hrtime_t) tv->tv_usec * 1000;
+}
+
+void php_openssl_deadline_init_infinite(php_openssl_deadline *dl)
+{
+ dl->infinite = true;
+ dl->at = 0;
+}
+
+void php_openssl_deadline_init_nonblock(php_openssl_deadline *dl)
+{
+ dl->infinite = false;
+ dl->at = 0;
+}
+
+bool php_openssl_deadline_expired(const php_openssl_deadline *dl)
+{
+ return !dl->infinite && (dl->at == 0 || zend_hrtime() >= dl->at);
+}
+
+int php_openssl_deadline_remaining_ms(const php_openssl_deadline *dl)
+{
+ if (dl->infinite) {
+ return -1;
+ }
+ if (dl->at == 0) {
+ return 0;
+ }
+ zend_hrtime_t now = zend_hrtime();
+ if (now >= dl->at) {
+ return 0;
+ }
+ zend_hrtime_t left = (dl->at - now + 999999) / 1000000;
+ return left > INT_MAX ? INT_MAX : (int) left;
+}
+
+struct timeval *php_openssl_deadline_remaining_tv(const php_openssl_deadline *dl, struct timeval *tv)
+{
+ if (dl->infinite) {
+ return NULL;
+ }
+ zend_hrtime_t now = zend_hrtime();
+ zend_hrtime_t left = (dl->at == 0 || now >= dl->at) ? 0 : dl->at - now;
+ tv->tv_sec = (time_t) (left / ZEND_NANO_IN_SEC);
+ tv->tv_usec = (long) ((left % ZEND_NANO_IN_SEC) / 1000);
+ return tv;
+}
+
+void php_openssl_deadline_cap(php_openssl_deadline *dl, const php_openssl_deadline *other)
+{
+ if (other->infinite) {
+ return;
+ }
+ if (dl->infinite || other->at < dl->at) {
+ *dl = *other;
+ }
+}
+
+/* Queues */
+
+static void php_openssl_bytebuf_free(php_openssl_bytebuf *buf, bool persistent)
+{
+ if (buf->data) {
+ pefree(buf->data, persistent);
+ }
+ memset(buf, 0, sizeof(*buf));
+}
+
+static void php_openssl_bytebuf_compact(php_openssl_bytebuf *buf)
+{
+ if (buf->pos == buf->len) {
+ buf->pos = buf->len = 0;
+ } else if (buf->pos > 0) {
+ memmove(buf->data, buf->data + buf->pos, buf->len - buf->pos);
+ buf->len -= buf->pos;
+ buf->pos = 0;
+ }
+}
+
+/* Room for n more bytes at the end, compacting first */
+static char *php_openssl_bytebuf_reserve(php_openssl_bytebuf *buf, size_t n, bool persistent)
+{
+ php_openssl_bytebuf_compact(buf);
+ if (buf->len + n > buf->cap) {
+ size_t cap = buf->cap ? buf->cap : 4096;
+ while (cap < buf->len + n) {
+ cap *= 2;
+ }
+ buf->data = perealloc(buf->data, cap, persistent);
+ buf->cap = cap;
+ }
+ return buf->data + buf->len;
+}
+
+static void php_openssl_bytebuf_append(php_openssl_bytebuf *buf, const char *data, size_t n, bool persistent)
+{
+ memcpy(php_openssl_bytebuf_reserve(buf, n, persistent), data, n);
+ buf->len += n;
+}
+
+static size_t php_openssl_bytebuf_avail(const php_openssl_bytebuf *buf)
+{
+ return buf->len - buf->pos;
+}
+
+static php_openssl_dgram *php_openssl_dgram_new(const char *data, size_t len, const struct sockaddr *peer,
+ socklen_t peerlen, bool persistent)
+{
+ php_openssl_dgram *dg = pemalloc(sizeof(*dg) + len, persistent);
+ dg->next = NULL;
+ dg->len = len;
+ dg->peerlen = 0;
+ if (peer != NULL && peerlen > 0 && peerlen <= sizeof(dg->peer)) {
+ memcpy(&dg->peer, peer, peerlen);
+ dg->peerlen = peerlen;
+ }
+ if (len) {
+ memcpy(dg->data, data, len);
+ }
+ return dg;
+}
+
+static void php_openssl_dgram_queue_push(php_openssl_dgram_queue *q, php_openssl_dgram *dg)
+{
+ if (q->tail) {
+ q->tail->next = dg;
+ } else {
+ q->head = dg;
+ }
+ q->tail = dg;
+ q->count++;
+ q->size += sizeof(*dg) + dg->len;
+}
+
+static php_openssl_dgram *php_openssl_dgram_queue_pop(php_openssl_dgram_queue *q)
+{
+ php_openssl_dgram *dg = q->head;
+ if (dg) {
+ q->head = dg->next;
+ if (q->head == NULL) {
+ q->tail = NULL;
+ }
+ q->count--;
+ q->size -= sizeof(*dg) + dg->len;
+ }
+ return dg;
+}
+
+static void php_openssl_dgram_queue_free(php_openssl_dgram_queue *q, bool persistent)
+{
+ php_openssl_dgram *dg;
+ while ((dg = php_openssl_dgram_queue_pop(q)) != NULL) {
+ pefree(dg, persistent);
+ }
+}
+
+/* Transports */
+
+php_openssl_xport *php_openssl_xport_new_fd(php_socket_t fd, bool dgram, bool persistent)
+{
+ php_openssl_xport *xport = pecalloc(1, sizeof(*xport), persistent);
+ xport->refcount = 1;
+ xport->persistent = persistent;
+ xport->dgram = dgram;
+ xport->fd = fd;
+ return xport;
+}
+
+php_openssl_xport *php_openssl_xport_new_stream(php_stream *inner, bool dgram)
+{
+ php_openssl_xport *xport = ecalloc(1, sizeof(*xport));
+ xport->refcount = 1;
+ xport->dgram = dgram;
+ xport->fd = SOCK_ERR;
+ xport->inner = inner;
+ if (inner->res) {
+ GC_ADDREF(inner->res);
+ }
+ if (dgram) {
+ /* One read is one datagram */
+ inner->flags |= PHP_STREAM_FLAG_NO_BUFFER;
+ }
+ return xport;
+}
+
+void php_openssl_xport_addref(php_openssl_xport *xport)
+{
+ xport->refcount++;
+}
+
+void php_openssl_xport_release(php_openssl_xport *xport)
+{
+ if (xport == NULL || --xport->refcount > 0) {
+ return;
+ }
+ if (xport->inner) {
+ if (xport->inner->res) {
+ zend_list_delete(xport->inner->res);
+ }
+ xport->inner = NULL;
+ }
+ pefree(xport, xport->persistent);
+}
+
+int php_openssl_xport_wait(php_openssl_xport *xport, short events, php_openssl_deadline *dl)
+{
+ struct timeval tv;
+
+ if (xport->fd == SOCK_ERR) {
+ errno = EAGAIN;
+ return -1;
+ }
+ for (;;) {
+ struct timeval *ptv = php_openssl_deadline_remaining_tv(dl, &tv);
+ int n = php_pollfd_for(xport->fd, events, ptv);
+ if (n >= 0) {
+ return n > 0 ? 1 : 0;
+ }
+ if (php_socket_errno() != EINTR) {
+ return -1;
+ }
+ }
+}
+
+/* The last socket error as errno, EAGAIN for every would-block code */
+static int php_openssl_xport_errno(void)
+{
+ int err = php_socket_errno();
+ if (PHP_IS_TRANSIENT_ERROR(err)) {
+ return EAGAIN;
+ }
+ /* An asynchronous connect still in progress: OpenSSL's socket BIO retries it too */
+#ifdef PHP_WIN32
+ if (err == WSAENOTCONN) {
+ return EAGAIN;
+ }
+#else
+ if (err == ENOTCONN) {
+ return EAGAIN;
+ }
+#endif
+#ifdef PHP_WIN32
+ if (err == WSAEMSGSIZE) {
+ return EMSGSIZE;
+ }
+#endif
+ return err;
+}
+
+/* Stream transport send: sends what it can, returns bytes sent, 0 when it would block, -1 on error */
+static ssize_t php_openssl_xport_send(php_openssl_xport *xport, const char *data, size_t len)
+{
+ if (xport->inner) {
+ ssize_t n = php_stream_write(xport->inner, data, len);
+ if (n < 0) {
+ errno = EIO;
+ return -1;
+ }
+ return n;
+ }
+
+ ssize_t n;
+#ifdef PHP_WIN32
+ n = send(xport->fd, data, len > INT_MAX ? INT_MAX : (int) len, 0);
+#else
+ n = send(xport->fd, data, len, 0);
+#endif
+ if (n < 0) {
+ errno = php_openssl_xport_errno();
+ return errno == EAGAIN ? 0 : -1;
+ }
+ return n;
+}
+
+/* Datagram transport send of one datagram; 1 sent, 0 would block, -1 error (EMSGSIZE included) */
+static int php_openssl_xport_send_dgram(php_openssl_xport *xport, const php_openssl_dgram *dg)
+{
+ if (xport->inner) {
+ ssize_t n = php_stream_write(xport->inner, dg->data, dg->len);
+ if (n < 0) {
+ errno = EIO;
+ return -1;
+ }
+ if (n == 0) {
+ return 0;
+ }
+ return 1;
+ }
+
+ ssize_t n;
+#ifdef PHP_WIN32
+ int len = dg->len > INT_MAX ? INT_MAX : (int) dg->len;
+#else
+ size_t len = dg->len;
+#endif
+ if (dg->peerlen > 0) {
+ n = sendto(xport->fd, dg->data, len, 0, (const struct sockaddr *) &dg->peer, dg->peerlen);
+ } else {
+ n = send(xport->fd, dg->data, len, 0);
+ }
+ if (n < 0) {
+ errno = php_openssl_xport_errno();
+ return errno == EAGAIN ? 0 : -1;
+ }
+ return 1;
+}
+
+/* Stream transport receive; returns bytes, 0 on EOF, -1 with errno (EAGAIN when it would block) */
+static ssize_t php_openssl_xport_recv(php_openssl_xport *xport, char *buf, size_t len)
+{
+ if (xport->inner) {
+ ssize_t n = php_stream_read(xport->inner, buf, len);
+ if (n < 0) {
+ errno = EIO;
+ return -1;
+ }
+ if (n == 0) {
+ if (xport->inner->eof) {
+ return 0;
+ }
+ errno = EAGAIN;
+ return -1;
+ }
+ return n;
+ }
+
+ ssize_t n;
+#ifdef PHP_WIN32
+ n = recv(xport->fd, buf, len > INT_MAX ? INT_MAX : (int) len, 0);
+#else
+ n = recv(xport->fd, buf, len, 0);
+#endif
+ if (n < 0) {
+ errno = php_openssl_xport_errno();
+ }
+ return n;
+}
+
+ssize_t php_openssl_xport_recv_dgram(php_openssl_xport *xport, char *buf, size_t len,
+ php_sockaddr_storage *peer, socklen_t *peerlen)
+{
+ *peerlen = 0;
+ if (xport->inner) {
+ ssize_t n = php_stream_read(xport->inner, buf, len);
+ if (n < 0) {
+ errno = EIO;
+ return -1;
+ }
+ if (n == 0) {
+ errno = EAGAIN;
+ return -1;
+ }
+ return n;
+ }
+
+ socklen_t sl = sizeof(*peer);
+ ssize_t n;
+#ifdef PHP_WIN32
+ n = recvfrom(xport->fd, buf, len > INT_MAX ? INT_MAX : (int) len, 0, (struct sockaddr *) peer, &sl);
+#else
+ n = recvfrom(xport->fd, buf, len, 0, (struct sockaddr *) peer, &sl);
+#endif
+ if (n < 0) {
+ errno = php_openssl_xport_errno();
+#ifdef PHP_WIN32
+ /* A datagram larger than the buffer is truncated, as POSIX does */
+ if (errno == EMSGSIZE) {
+ *peerlen = sl;
+ return len;
+ }
+#endif
+ return -1;
+ }
+ *peerlen = sl;
+ return n;
+}
+
+/* The BIO */
+
+static BIO_METHOD *php_openssl_bio_method = NULL;
+
+static int php_openssl_bio_read(BIO *bio, char *buf, int len)
+{
+ php_openssl_conn *conn = BIO_get_data(bio);
+
+ BIO_clear_retry_flags(bio);
+ if (conn == NULL || buf == NULL || len <= 0) {
+ return -1;
+ }
+
+ if (conn->dgram) {
+ php_openssl_dgram *dg = php_openssl_dgram_queue_pop(&conn->rxq);
+ if (dg != NULL) {
+ size_t n = dg->len > (size_t) len ? (size_t) len : dg->len;
+ memcpy(buf, dg->data, n);
+ pefree(dg, conn->persistent);
+ return (int) n;
+ }
+ } else {
+ size_t avail = php_openssl_bytebuf_avail(&conn->rx);
+ if (avail > 0) {
+ size_t n = avail > (size_t) len ? (size_t) len : avail;
+ memcpy(buf, conn->rx.data + conn->rx.pos, n);
+ conn->rx.pos += n;
+ return (int) n;
+ }
+ }
+
+ if (conn->rx_eof) {
+ return 0;
+ }
+ conn->rx_want = (size_t) len;
+ BIO_set_retry_read(bio);
+ return -1;
+}
+
+static int php_openssl_bio_write(BIO *bio, const char *buf, int len)
+{
+ php_openssl_conn *conn = BIO_get_data(bio);
+
+ BIO_clear_retry_flags(bio);
+ if (conn == NULL || buf == NULL || len < 0) {
+ return -1;
+ }
+
+ if (conn->dgram) {
+ if (conn->txq.count >= PHP_OPENSSL_TX_DGRAM_LIMIT) {
+ conn->tx_full = true;
+ BIO_set_retry_write(bio);
+ return -1;
+ }
+ php_openssl_dgram_queue_push(&conn->txq, php_openssl_dgram_new(buf, (size_t) len,
+ (const struct sockaddr *) &conn->peer, conn->peerlen, conn->persistent));
+ } else {
+ if (php_openssl_bytebuf_avail(&conn->tx) >= PHP_OPENSSL_TX_LIMIT) {
+ conn->tx_full = true;
+ BIO_set_retry_write(bio);
+ return -1;
+ }
+ php_openssl_bytebuf_append(&conn->tx, buf, (size_t) len, conn->persistent);
+ }
+ return len;
+}
+
+static unsigned php_openssl_bio_mtu_overhead(const php_openssl_conn *conn)
+{
+#ifdef HAVE_IPV6
+ if (conn->peerlen > 0 && conn->peer.ss_family == AF_INET6) {
+ return 48;
+ }
+ if (conn->peerlen == 0 && conn->xport && conn->xport->fd != SOCK_ERR) {
+ php_sockaddr_storage sa;
+ socklen_t sl = sizeof(sa);
+ if (getsockname(conn->xport->fd, (struct sockaddr *) &sa, &sl) == 0 && sa.ss_family == AF_INET6) {
+ return 48;
+ }
+ }
+#endif
+ return 28;
+}
+
+/* The path MTU of a connected socket when the system knows it, else the Ethernet default */
+static long php_openssl_bio_query_mtu(php_openssl_conn *conn)
+{
+ unsigned overhead = php_openssl_bio_mtu_overhead(conn);
+
+ if (conn->link_mtu) {
+ return conn->link_mtu > overhead ? (long) (conn->link_mtu - overhead) : 0;
+ }
+#if defined(__linux__) && defined(IP_MTU)
+ if (conn->peerlen == 0 && conn->xport && conn->xport->fd != SOCK_ERR) {
+ int mtu = 0;
+ socklen_t len = sizeof(mtu);
+ int level = overhead == 48 ? IPPROTO_IPV6 : IPPROTO_IP;
+#ifdef IPV6_MTU
+ int name = overhead == 48 ? IPV6_MTU : IP_MTU;
+#else
+ int name = IP_MTU;
+#endif
+ if (getsockopt(conn->xport->fd, level, name, &mtu, &len) == 0 && mtu > (int) overhead) {
+ return mtu - (long) overhead;
+ }
+ }
+#endif
+ return 1500 - (long) overhead;
+}
+
+static long php_openssl_bio_ctrl(BIO *bio, int cmd, long num, void *ptr)
+{
+ php_openssl_conn *conn = BIO_get_data(bio);
+
+ if (conn == NULL) {
+ return 0;
+ }
+ switch (cmd) {
+ case BIO_CTRL_FLUSH:
+ case BIO_CTRL_DUP:
+ return 1;
+ case BIO_CTRL_EOF:
+ return conn->rx_eof && php_openssl_conn_rx_empty(conn);
+ case BIO_CTRL_PENDING:
+ if (conn->dgram) {
+ return conn->rxq.head ? (long) conn->rxq.head->len : 0;
+ }
+ return (long) php_openssl_bytebuf_avail(&conn->rx);
+ case BIO_CTRL_WPENDING:
+ if (conn->dgram) {
+ return (long) conn->txq.count;
+ }
+ return (long) php_openssl_bytebuf_avail(&conn->tx);
+#ifdef BIO_CTRL_DGRAM_QUERY_MTU
+ case BIO_CTRL_DGRAM_QUERY_MTU:
+ return conn->dgram ? php_openssl_bio_query_mtu(conn) : 0;
+ case BIO_CTRL_DGRAM_GET_FALLBACK_MTU:
+ return php_openssl_bio_mtu_overhead(conn) == 48 ? 1280 - 48 : 576 - 28;
+ case BIO_CTRL_DGRAM_GET_MTU_OVERHEAD:
+ return (long) php_openssl_bio_mtu_overhead(conn);
+ case BIO_CTRL_DGRAM_SET_MTU:
+ conn->link_mtu = (unsigned) num + php_openssl_bio_mtu_overhead(conn);
+ return num;
+ case BIO_CTRL_DGRAM_GET_MTU:
+ return conn->link_mtu ? (long) (conn->link_mtu - php_openssl_bio_mtu_overhead(conn)) : 0;
+ case BIO_CTRL_DGRAM_MTU_EXCEEDED: {
+ long ret = conn->mtu_exceeded;
+ conn->mtu_exceeded = false;
+ return ret;
+ }
+ case BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT:
+ case BIO_CTRL_DGRAM_SET_CONNECTED:
+ return 1;
+ case BIO_CTRL_DGRAM_GET_PEER:
+ if (ptr != NULL && conn->peerlen > 0) {
+ memcpy(ptr, &conn->peer, conn->peerlen);
+ return (long) conn->peerlen;
+ }
+ return 0;
+ case BIO_CTRL_DGRAM_SET_PEER:
+ if (ptr != NULL) {
+ const struct sockaddr *sa = ptr;
+ socklen_t sl = 0;
+ if (sa->sa_family == AF_INET) {
+ sl = sizeof(struct sockaddr_in);
+ }
+#ifdef HAVE_IPV6
+ else if (sa->sa_family == AF_INET6) {
+ sl = sizeof(struct sockaddr_in6);
+ }
+#endif
+ php_openssl_conn_set_peer(conn, sa, sl);
+ return 1;
+ }
+ return 0;
+#endif
+ default:
+ return 0;
+ }
+}
+
+static int php_openssl_bio_create(BIO *bio)
+{
+ BIO_set_init(bio, 0);
+ return 1;
+}
+
+static int php_openssl_bio_destroy(BIO *bio)
+{
+ if (bio == NULL) {
+ return 0;
+ }
+ BIO_set_data(bio, NULL);
+ BIO_set_init(bio, 0);
+ return 1;
+}
+
+void php_openssl_bio_minit(void)
+{
+ BIO_METHOD *m = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, "PHP stream");
+ if (m == NULL) {
+ return;
+ }
+ BIO_meth_set_write(m, php_openssl_bio_write);
+ BIO_meth_set_read(m, php_openssl_bio_read);
+ BIO_meth_set_ctrl(m, php_openssl_bio_ctrl);
+ BIO_meth_set_create(m, php_openssl_bio_create);
+ BIO_meth_set_destroy(m, php_openssl_bio_destroy);
+ php_openssl_bio_method = m;
+}
+
+void php_openssl_bio_mshutdown(void)
+{
+ if (php_openssl_bio_method != NULL) {
+ BIO_meth_free(php_openssl_bio_method);
+ php_openssl_bio_method = NULL;
+ }
+}
+
+/* Connections */
+
+php_openssl_conn *php_openssl_conn_new(php_openssl_xport *xport, php_stream *stream, bool persistent)
+{
+ php_openssl_conn *conn = pecalloc(1, sizeof(*conn), persistent);
+ php_openssl_xport_addref(xport);
+ conn->xport = xport;
+ conn->stream = stream;
+ conn->dgram = xport->dgram;
+ conn->persistent = persistent;
+ conn->fill = php_openssl_conn_fill_own;
+ return conn;
+}
+
+void php_openssl_conn_free(php_openssl_conn *conn)
+{
+ if (conn == NULL) {
+ return;
+ }
+ php_openssl_bytebuf_free(&conn->rx, conn->persistent);
+ php_openssl_bytebuf_free(&conn->tx, conn->persistent);
+ php_openssl_dgram_queue_free(&conn->rxq, conn->persistent);
+ php_openssl_dgram_queue_free(&conn->txq, conn->persistent);
+ php_openssl_xport_release(conn->xport);
+ pefree(conn, conn->persistent);
+}
+
+zend_result php_openssl_conn_set_ssl(php_openssl_conn *conn, SSL *ssl)
+{
+ BIO *bio = php_openssl_bio_method ? BIO_new(php_openssl_bio_method) : NULL;
+ if (bio == NULL) {
+ return FAILURE;
+ }
+ BIO_set_data(bio, conn);
+ BIO_set_init(bio, 1);
+ /* One BIO for both directions: SSL_set_bio() takes one reference, the second direction
+ * shares it */
+ SSL_set_bio(ssl, bio, bio);
+ conn->ssl = ssl;
+ return SUCCESS;
+}
+
+void php_openssl_conn_set_peer(php_openssl_conn *conn, const struct sockaddr *peer, socklen_t peerlen)
+{
+ if (peer == NULL || peerlen == 0 || peerlen > sizeof(conn->peer)) {
+ conn->peerlen = 0;
+ return;
+ }
+ memcpy(&conn->peer, peer, peerlen);
+ conn->peerlen = peerlen;
+}
+
+zend_result php_openssl_conn_push_dgram(php_openssl_conn *conn, const char *data, size_t len,
+ const struct sockaddr *peer, socklen_t peerlen)
+{
+ if (!conn->dgram) {
+ return FAILURE;
+ }
+ php_openssl_dgram_queue_push(&conn->rxq, php_openssl_dgram_new(data, len, peer, peerlen,
+ conn->persistent));
+ return SUCCESS;
+}
+
+bool php_openssl_conn_rx_empty(const php_openssl_conn *conn)
+{
+ return conn->dgram ? conn->rxq.count == 0 : php_openssl_bytebuf_avail(&conn->rx) == 0;
+}
+
+bool php_openssl_conn_tx_empty(const php_openssl_conn *conn)
+{
+ return conn->dgram ? conn->txq.count == 0 : php_openssl_bytebuf_avail(&conn->tx) == 0;
+}
+
+void php_openssl_conn_timer_deadline(php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ struct timeval tv;
+
+ php_openssl_deadline_init_infinite(dl);
+#ifndef OPENSSL_NO_DTLS
+ if (conn->dgram && conn->ssl != NULL && DTLSv1_get_timeout(conn->ssl, &tv)) {
+ if (tv.tv_sec == 0 && tv.tv_usec == 0) {
+ php_openssl_deadline_init_nonblock(dl);
+ } else {
+ php_openssl_deadline_init(dl, &tv);
+ }
+ }
+#endif
+}
+
+int php_openssl_conn_handle_timeout(php_openssl_conn *conn)
+{
+#ifndef OPENSSL_NO_DTLS
+ if (conn->dgram && conn->ssl != NULL) {
+ return DTLSv1_handle_timeout(conn->ssl) > 0;
+ }
+#endif
+ return 0;
+}
+
+int php_openssl_conn_flush(php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ php_openssl_xport *xport = conn->xport;
+
+ for (;;) {
+ if (conn->dgram) {
+ php_openssl_dgram *dg = conn->txq.head;
+ if (dg == NULL) {
+ conn->tx_full = false;
+ return 1;
+ }
+ int n = php_openssl_xport_send_dgram(xport, dg);
+ if (n > 0) {
+ php_openssl_dgram_queue_pop(&conn->txq);
+ pefree(dg, conn->persistent);
+ continue;
+ }
+ if (n < 0) {
+ if (errno == EMSGSIZE) {
+ /* Too large for the path: OpenSSL retransmits a smaller one */
+ conn->mtu_exceeded = true;
+ php_openssl_dgram_queue_pop(&conn->txq);
+ pefree(dg, conn->persistent);
+ continue;
+ }
+ return -1;
+ }
+ } else {
+ size_t avail = php_openssl_bytebuf_avail(&conn->tx);
+ if (avail == 0) {
+ conn->tx_full = false;
+ php_openssl_bytebuf_compact(&conn->tx);
+ return 1;
+ }
+ ssize_t n = php_openssl_xport_send(xport, conn->tx.data + conn->tx.pos, avail);
+ if (n > 0) {
+ conn->tx.pos += (size_t) n;
+ continue;
+ }
+ if (n < 0) {
+ return -1;
+ }
+ }
+
+ /* The transport would block */
+ if (dl == NULL || xport->inner != NULL) {
+ errno = EAGAIN;
+ return 0;
+ }
+ int w = php_openssl_xport_wait(xport, POLLOUT, dl);
+ if (w < 0) {
+ errno = php_openssl_xport_errno();
+ return -1;
+ }
+ if (w == 0) {
+ errno = ETIMEDOUT;
+ return 0;
+ }
+ }
+}
+
+/* The receive of a stream transport into the connection's queue */
+static int php_openssl_conn_recv_stream(php_openssl_conn *conn)
+{
+ size_t want = conn->rx_want ? conn->rx_want : 16384;
+ if (want > 65536) {
+ want = 65536;
+ }
+ char *dst = php_openssl_bytebuf_reserve(&conn->rx, want, conn->persistent);
+ ssize_t n = php_openssl_xport_recv(conn->xport, dst, want);
+ if (n > 0) {
+ conn->rx.len += (size_t) n;
+ conn->rx_want = 0;
+ return 1;
+ }
+ if (n == 0) {
+ conn->rx_eof = true;
+ return 0;
+ }
+ return -1;
+}
+
+/* The receive of a datagram transport into the connection's queue */
+static int php_openssl_conn_recv_dgram(php_openssl_conn *conn)
+{
+ char buf[PHP_OPENSSL_MAX_DGRAM];
+ php_sockaddr_storage peer;
+ socklen_t peerlen;
+
+ for (;;) {
+ ssize_t n = php_openssl_xport_recv_dgram(conn->xport, buf, sizeof(buf), &peer, &peerlen);
+ if (n < 0) {
+ return -1;
+ }
+ /* skip empty datagram */
+ if (n == 0) {
+ continue;
+ }
+ php_openssl_conn_push_dgram(conn, buf, (size_t) n, (const struct sockaddr *) &peer, peerlen);
+ return 1;
+ }
+}
+
+int php_openssl_conn_fill_own(php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ php_openssl_xport *xport = conn->xport;
+
+ for (;;) {
+ int n = conn->dgram ? php_openssl_conn_recv_dgram(conn) : php_openssl_conn_recv_stream(conn);
+ if (n >= 0) {
+ return n;
+ }
+ if (errno != EAGAIN) {
+ return -1;
+ }
+ if (dl == NULL || xport->inner != NULL) {
+ errno = EAGAIN;
+ return -1;
+ }
+
+ /* Wait for the socket, no longer than the retransmit timer of a DTLS connection */
+ php_openssl_deadline wait = *dl;
+ php_openssl_deadline timer;
+ php_openssl_conn_timer_deadline(conn, &timer);
+ php_openssl_deadline_cap(&wait, &timer);
+
+ int w = php_openssl_xport_wait(xport, PHP_POLLREADABLE, &wait);
+ if (w < 0) {
+ errno = php_openssl_xport_errno();
+ return -1;
+ }
+ if (w == 0) {
+ if (!timer.infinite && php_openssl_deadline_expired(&timer)) {
+ if (php_openssl_conn_handle_timeout(conn) > 0) {
+ return 1;
+ }
+ }
+ if (php_openssl_deadline_expired(dl)) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ }
+ }
+}
+
+int php_openssl_conn_fill(php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ return conn->fill(conn, dl);
+}
diff --git a/ext/openssl/xp_bio.h b/ext/openssl/xp_bio.h
new file mode 100644
index 000000000000..799ea0fd64cd
--- /dev/null
+++ b/ext/openssl/xp_bio.h
@@ -0,0 +1,172 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka |
+ +----------------------------------------------------------------------+
+*/
+
+/* The ciphertext transport of the TLS and DTLS streams.
+ *
+ * OpenSSL never does IO itself: its BIO serves records from a receive queue and appends to a
+ * send queue, both owned by a connection. The stream moves ciphertext between the queues and
+ * the transport around each SSL call, so no call into OpenSSL ever waits. The transport is a
+ * socket, or any other stream (a user wrapper included). A DTLS connection queues datagrams
+ * with their peer, so one socket can carry the connections of a server. */
+
+#ifndef PHP_OPENSSL_XP_BIO_H
+#define PHP_OPENSSL_XP_BIO_H
+
+#include "php.h"
+#include "php_network.h"
+#include "zend_hrtime.h"
+
+#include
+#include
+
+/* A monotonic deadline; infinite when no timeout applies */
+typedef struct _php_openssl_deadline {
+ zend_hrtime_t at;
+ bool infinite;
+} php_openssl_deadline;
+
+/* NULL or a non-positive timeval means no timeout */
+void php_openssl_deadline_init(php_openssl_deadline *dl, const struct timeval *tv);
+void php_openssl_deadline_init_infinite(php_openssl_deadline *dl);
+/* A deadline already passed: one attempt, no wait */
+void php_openssl_deadline_init_nonblock(php_openssl_deadline *dl);
+bool php_openssl_deadline_expired(const php_openssl_deadline *dl);
+/* -1 for infinite */
+int php_openssl_deadline_remaining_ms(const php_openssl_deadline *dl);
+/* Fills tv with the time left and returns it, NULL for infinite */
+struct timeval *php_openssl_deadline_remaining_tv(const php_openssl_deadline *dl, struct timeval *tv);
+/* The earlier of the two */
+void php_openssl_deadline_cap(php_openssl_deadline *dl, const php_openssl_deadline *other);
+
+/* Bytes of a stream transport */
+typedef struct _php_openssl_bytebuf {
+ char *data;
+ size_t pos;
+ size_t len;
+ size_t cap;
+} php_openssl_bytebuf;
+
+/* One datagram with its peer; the peer is empty on a connected socket */
+typedef struct _php_openssl_dgram {
+ struct _php_openssl_dgram *next;
+ size_t len;
+ socklen_t peerlen;
+ php_sockaddr_storage peer;
+ char data[1];
+} php_openssl_dgram;
+
+typedef struct _php_openssl_dgram_queue {
+ php_openssl_dgram *head;
+ php_openssl_dgram *tail;
+ size_t count;
+ /* Memory the queued datagrams take */
+ size_t size;
+} php_openssl_dgram_queue;
+
+typedef struct _php_openssl_conn php_openssl_conn;
+
+/* Where the ciphertext goes: a socket, or an inner stream. Shared by the connections of a DTLS
+ * server through its port, hence counted. The owner closes the socket; the transport only keeps
+ * the inner stream alive. */
+typedef struct _php_openssl_xport {
+ int refcount;
+ bool persistent;
+ bool dgram;
+ php_socket_t fd;
+ php_stream *inner;
+} php_openssl_xport;
+
+php_openssl_xport *php_openssl_xport_new_fd(php_socket_t fd, bool dgram, bool persistent);
+php_openssl_xport *php_openssl_xport_new_stream(php_stream *inner, bool dgram);
+void php_openssl_xport_addref(php_openssl_xport *xport);
+void php_openssl_xport_release(php_openssl_xport *xport);
+/* Waits for readiness of the socket; 1 ready, 0 timed out, -1 error. Not for an inner stream. */
+int php_openssl_xport_wait(php_openssl_xport *xport, short events, php_openssl_deadline *dl);
+
+/* Receives ciphertext for a connection. Installed by a DTLS port, whose socket carries every
+ * connection of the server; the default receives for the connection itself. Returns like
+ * php_openssl_conn_fill(). */
+typedef int (*php_openssl_conn_fill_fn)(php_openssl_conn *conn, php_openssl_deadline *dl);
+
+/* One SSL connection's ciphertext queues: the data of its BIO */
+struct _php_openssl_conn {
+ php_openssl_xport *xport;
+ SSL *ssl;
+ /* The stream OpenSSL callbacks reach through ex_data; NULL while a DTLS server connection
+ * has no stream yet */
+ php_stream *stream;
+ bool dgram;
+ bool persistent;
+ /* Stream transport queues */
+ php_openssl_bytebuf rx;
+ php_openssl_bytebuf tx;
+ /* Datagram transport queues */
+ php_openssl_dgram_queue rxq;
+ php_openssl_dgram_queue txq;
+ /* Bytes the last read of OpenSSL asked for, the size of the next receive */
+ size_t rx_want;
+ bool rx_eof;
+ /* The queue is as full as a non-blocking transport lets it be: the BIO reports a retry */
+ bool tx_full;
+ /* The peer of a datagram connection; empty on a connected socket */
+ php_sockaddr_storage peer;
+ socklen_t peerlen;
+ /* DTLS path MTU state the BIO answers */
+ unsigned link_mtu;
+ bool mtu_exceeded;
+ php_openssl_conn_fill_fn fill;
+ void *owner;
+};
+
+void php_openssl_bio_minit(void);
+void php_openssl_bio_mshutdown(void);
+
+php_openssl_conn *php_openssl_conn_new(php_openssl_xport *xport, php_stream *stream, bool persistent);
+/* Frees the queues and the transport reference; the SSL is the caller's */
+void php_openssl_conn_free(php_openssl_conn *conn);
+/* Installs the connection as the BIO of ssl */
+zend_result php_openssl_conn_set_ssl(php_openssl_conn *conn, SSL *ssl);
+void php_openssl_conn_set_peer(php_openssl_conn *conn, const struct sockaddr *peer, socklen_t peerlen);
+/* Queues a received datagram for the connection */
+zend_result php_openssl_conn_push_dgram(php_openssl_conn *conn, const char *data, size_t len,
+ const struct sockaddr *peer, socklen_t peerlen);
+bool php_openssl_conn_rx_empty(const php_openssl_conn *conn);
+bool php_openssl_conn_tx_empty(const php_openssl_conn *conn);
+/* The retransmit timer of a DTLS connection as a deadline; infinite when none is armed */
+void php_openssl_conn_timer_deadline(php_openssl_conn *conn, php_openssl_deadline *dl);
+/* Services an expired DTLS retransmit timer; 1 when it did something */
+int php_openssl_conn_handle_timeout(php_openssl_conn *conn);
+
+/* Sends everything queued. A NULL deadline makes one attempt without waiting. Returns 1 when
+ * the queue is empty, 0 when the transport would block, -1 on an error with errno set. */
+int php_openssl_conn_flush(php_openssl_conn *conn, php_openssl_deadline *dl);
+/* Receives more ciphertext, waiting up to the deadline, or not at all when it is NULL. The DTLS
+ * retransmit timer bounds the wait and is serviced when it fires. Returns 1 when something was
+ * received or a timer fired, 0 on EOF, -1 with errno: EAGAIN when the transport would block,
+ * ETIMEDOUT when the deadline passed, otherwise the transport error. */
+int php_openssl_conn_fill(php_openssl_conn *conn, php_openssl_deadline *dl);
+/* The default fill: receives for this connection alone */
+int php_openssl_conn_fill_own(php_openssl_conn *conn, php_openssl_deadline *dl);
+/* Receives one datagram from a datagram transport into buf; -1 with errno EAGAIN when none */
+ssize_t php_openssl_xport_recv_dgram(php_openssl_xport *xport, char *buf, size_t len,
+ php_sockaddr_storage *peer, socklen_t *peerlen);
+
+/* The largest datagram a DTLS peer may send */
+#define PHP_OPENSSL_MAX_DGRAM 18432
+/* Bytes a non-blocking stream may leave unsent before the BIO reports a retry */
+#define PHP_OPENSSL_TX_LIMIT 65536
+/* Datagrams a non-blocking connection may leave unsent */
+#define PHP_OPENSSL_TX_DGRAM_LIMIT 64
+
+#endif /* PHP_OPENSSL_XP_BIO_H */
diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c
new file mode 100644
index 000000000000..abda83316210
--- /dev/null
+++ b/ext/openssl/xp_dtls.c
@@ -0,0 +1,695 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Gianfrancesco Aurecchia |
+ | Jakub Zelenka |
+ +----------------------------------------------------------------------+
+*/
+
+/* The dtls:// server port. One UDP socket carries every peer of a server: the port receives the
+ * datagrams, routes each to its connection by the peer address, runs the handshakes of new
+ * peers with a cookie exchange, and hands the connections that completed to accept() as
+ * streams. The listener stream and every accepted stream share the port and its socket. */
+
+#ifdef HAVE_CONFIG_H
+#include
+#endif
+
+#include "php.h"
+#include "php_openssl.h"
+#include "xp_ssl.h"
+
+#include
+#include
+#include
+#include
+#include
+#ifdef PHP_WIN32
+#include
+# ifndef SIO_UDP_CONNRESET
+# define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
+# endif
+#endif
+
+#ifdef HAVE_DTLS
+
+#define PHP_OPENSSL_DTLS_MAX_PENDING 256
+#define PHP_OPENSSL_DTLS_PENDING_TIMEOUT 30
+/* Memory queued for a connection nobody is reading before the port drops the next datagram */
+#define PHP_OPENSSL_DTLS_RX_LIMIT (64 * 1024)
+/* A cookie is a timestamp and an HMAC of it with the peer address */
+#define PHP_OPENSSL_DTLS_COOKIE_TS_LEN 8
+#define PHP_OPENSSL_DTLS_COOKIE_MAC_LEN 32
+#define PHP_OPENSSL_DTLS_COOKIE_LEN (PHP_OPENSSL_DTLS_COOKIE_TS_LEN + PHP_OPENSSL_DTLS_COOKIE_MAC_LEN)
+#define PHP_OPENSSL_DTLS_COOKIE_MAX_AGE 60
+
+typedef enum {
+ PHP_OPENSSL_DTLS_PEER_PENDING,
+ PHP_OPENSSL_DTLS_PEER_READY,
+ PHP_OPENSSL_DTLS_PEER_ACCEPTED,
+} php_openssl_dtls_peer_state;
+
+typedef struct _php_openssl_dtls_peer {
+ php_openssl_port *port;
+ php_openssl_conn *conn;
+ SSL *ssl;
+ zend_string *key;
+ zend_hrtime_t created;
+ php_openssl_dtls_peer_state state;
+ /* Returned a valid cookie: the peer owns its address */
+ bool verified;
+ /* The accepted stream's data, owner of ssl and conn from then on */
+ php_openssl_netstream_data_t *sslsock;
+ struct _php_openssl_dtls_peer *next;
+} php_openssl_dtls_peer;
+
+struct _php_openssl_port {
+ int refcount;
+ /* The listener while it is open */
+ php_stream *stream;
+ php_openssl_netstream_data_t *listener;
+ php_openssl_xport *xport;
+ php_socket_t fd;
+ SSL_CTX *ctx;
+ /* Every peer by address */
+ HashTable peers;
+ php_openssl_dtls_peer *pending;
+ /* Peers not accepted yet, pending or ready: they hold memory nobody else bounds */
+ size_t pending_count;
+ /* Handshakes completed, oldest first */
+ php_openssl_dtls_peer *ready;
+ php_openssl_dtls_peer *ready_tail;
+ zend_long max_pending;
+ zend_long pending_timeout;
+ unsigned link_mtu;
+ unsigned char cookie_secret[32];
+ bool closed;
+};
+
+static int php_openssl_port_fill(php_openssl_conn *conn, php_openssl_deadline *dl);
+
+/* Peers */
+
+static zend_string *php_openssl_dtls_peer_key(const struct sockaddr *peer, socklen_t peerlen)
+{
+ return zend_string_init((const char *) peer, peerlen, 0);
+}
+
+static void php_openssl_dtls_peer_unlink(php_openssl_dtls_peer **list, php_openssl_dtls_peer *peer)
+{
+ while (*list != NULL && *list != peer) {
+ list = &(*list)->next;
+ }
+ if (*list == peer) {
+ *list = peer->next;
+ }
+ peer->next = NULL;
+}
+
+/* Frees a peer the port still owns: not accepted, or accepted and now closed by its stream */
+static void php_openssl_dtls_peer_free(php_openssl_port *port, php_openssl_dtls_peer *peer)
+{
+ switch (peer->state) {
+ case PHP_OPENSSL_DTLS_PEER_PENDING:
+ php_openssl_dtls_peer_unlink(&port->pending, peer);
+ port->pending_count--;
+ break;
+ case PHP_OPENSSL_DTLS_PEER_READY:
+ php_openssl_dtls_peer_unlink(&port->ready, peer);
+ port->pending_count--;
+ if (port->ready == NULL) {
+ port->ready_tail = NULL;
+ } else if (port->ready_tail == peer) {
+ port->ready_tail = port->ready;
+ while (port->ready_tail->next) {
+ port->ready_tail = port->ready_tail->next;
+ }
+ }
+ break;
+ case PHP_OPENSSL_DTLS_PEER_ACCEPTED:
+ /* The stream owns the SSL and the connection */
+ peer->ssl = NULL;
+ peer->conn = NULL;
+ break;
+ }
+ if (peer->key) {
+ zend_hash_del(&port->peers, peer->key);
+ zend_string_release(peer->key);
+ }
+ if (peer->ssl) {
+ SSL_free(peer->ssl);
+ }
+ if (peer->conn) {
+ php_openssl_conn_free(peer->conn);
+ }
+ efree(peer);
+}
+
+static php_openssl_dtls_peer *php_openssl_dtls_peer_of(php_openssl_conn *conn)
+{
+ return (php_openssl_dtls_peer *) conn->owner;
+}
+
+/* Cookies: HMAC of a timestamp and the peer address, keyed by a secret of the port */
+
+static php_openssl_dtls_peer *php_openssl_dtls_peer_from_ssl(SSL *ssl)
+{
+ BIO *bio = SSL_get_rbio(ssl);
+ php_openssl_conn *conn = bio ? BIO_get_data(bio) : NULL;
+ return conn ? php_openssl_dtls_peer_of(conn) : NULL;
+}
+
+static void php_openssl_dtls_cookie_mac(php_openssl_port *port, const php_openssl_conn *conn,
+ const unsigned char *ts, unsigned char *mac)
+{
+ unsigned char msg[PHP_OPENSSL_DTLS_COOKIE_TS_LEN + sizeof(conn->peer)];
+ unsigned int len = 0;
+
+ memcpy(msg, ts, PHP_OPENSSL_DTLS_COOKIE_TS_LEN);
+ memcpy(msg + PHP_OPENSSL_DTLS_COOKIE_TS_LEN, &conn->peer, conn->peerlen);
+ HMAC(EVP_sha256(), port->cookie_secret, sizeof(port->cookie_secret), msg,
+ PHP_OPENSSL_DTLS_COOKIE_TS_LEN + conn->peerlen, mac, &len);
+}
+
+static int php_openssl_dtls_cookie_generate(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len)
+{
+ php_openssl_dtls_peer *peer = php_openssl_dtls_peer_from_ssl(ssl);
+ if (peer == NULL || peer->conn->peerlen == 0) {
+ return 0;
+ }
+
+ uint64_t now = (uint64_t) time(NULL);
+ for (int i = 0; i < PHP_OPENSSL_DTLS_COOKIE_TS_LEN; i++) {
+ cookie[i] = (unsigned char) (now >> (8 * (PHP_OPENSSL_DTLS_COOKIE_TS_LEN - 1 - i)));
+ }
+ php_openssl_dtls_cookie_mac(peer->port, peer->conn, cookie, cookie + PHP_OPENSSL_DTLS_COOKIE_TS_LEN);
+ *cookie_len = PHP_OPENSSL_DTLS_COOKIE_LEN;
+ return 1;
+}
+
+static int php_openssl_dtls_cookie_verify(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len)
+{
+ php_openssl_dtls_peer *peer = php_openssl_dtls_peer_from_ssl(ssl);
+ if (peer == NULL || peer->conn->peerlen == 0 || cookie_len != PHP_OPENSSL_DTLS_COOKIE_LEN) {
+ return 0;
+ }
+
+ uint64_t ts = 0;
+ for (int i = 0; i < PHP_OPENSSL_DTLS_COOKIE_TS_LEN; i++) {
+ ts = (ts << 8) | cookie[i];
+ }
+ uint64_t now = (uint64_t) time(NULL);
+ if (ts > now || now - ts > PHP_OPENSSL_DTLS_COOKIE_MAX_AGE) {
+ return 0;
+ }
+
+ unsigned char mac[PHP_OPENSSL_DTLS_COOKIE_MAC_LEN];
+ php_openssl_dtls_cookie_mac(peer->port, peer->conn, cookie, mac);
+ if (CRYPTO_memcmp(mac, cookie + PHP_OPENSSL_DTLS_COOKIE_TS_LEN, sizeof(mac)) != 0) {
+ return 0;
+ }
+ peer->verified = true;
+ return 1;
+}
+
+static int php_openssl_dtls_stateless_cookie_generate(SSL *ssl, unsigned char *cookie, size_t *cookie_len)
+{
+ unsigned int len = 0;
+ int ret = php_openssl_dtls_cookie_generate(ssl, cookie, &len);
+ *cookie_len = len;
+ return ret;
+}
+
+static int php_openssl_dtls_stateless_cookie_verify(SSL *ssl, const unsigned char *cookie, size_t cookie_len)
+{
+ return php_openssl_dtls_cookie_verify(ssl, cookie, (unsigned int) cookie_len);
+}
+
+/* The port */
+
+static void php_openssl_port_release(php_openssl_port *port)
+{
+ if (--port->refcount > 0) {
+ return;
+ }
+ ZEND_ASSERT(port->pending == NULL && port->ready == NULL);
+ zend_hash_destroy(&port->peers);
+ if (port->ctx) {
+ SSL_CTX_free(port->ctx);
+ }
+ php_openssl_xport_release(port->xport);
+ if (port->fd != SOCK_ERR) {
+ closesocket(port->fd);
+ }
+ efree(port);
+}
+
+/* Sends what a pending handshake produced; what the socket refuses waits for the next tick */
+static void php_openssl_dtls_peer_flush(php_openssl_dtls_peer *peer)
+{
+ php_openssl_conn_flush(peer->conn, NULL);
+}
+
+/* Moves a pending handshake as far as the datagrams received let it */
+static void php_openssl_dtls_peer_drive(php_openssl_port *port, php_openssl_dtls_peer *peer)
+{
+ for (;;) {
+ ERR_clear_error();
+ int n = SSL_accept(peer->ssl);
+ int err = n > 0 ? SSL_ERROR_NONE : SSL_get_error(peer->ssl, n);
+ php_openssl_dtls_peer_flush(peer);
+
+ if (n > 0) {
+ php_openssl_dtls_peer_unlink(&port->pending, peer);
+ peer->state = PHP_OPENSSL_DTLS_PEER_READY;
+ if (port->ready_tail) {
+ port->ready_tail->next = peer;
+ } else {
+ port->ready = peer;
+ }
+ port->ready_tail = peer;
+ return;
+ }
+ if (err == SSL_ERROR_WANT_READ) {
+ return;
+ }
+ if (err == SSL_ERROR_WANT_WRITE) {
+ if (php_openssl_conn_tx_empty(peer->conn)) {
+ continue;
+ }
+ return;
+ }
+ /* A failed handshake: the peer is forgotten, its next ClientHello starts over */
+ ERR_clear_error();
+ php_openssl_dtls_peer_free(port, peer);
+ return;
+ }
+}
+
+static php_openssl_dtls_peer *php_openssl_dtls_peer_new(php_openssl_port *port,
+ const struct sockaddr *addr, socklen_t addrlen, zend_string *key)
+{
+ php_openssl_dtls_peer *peer = ecalloc(1, sizeof(*peer));
+ peer->port = port;
+ peer->state = PHP_OPENSSL_DTLS_PEER_PENDING;
+ peer->created = zend_hrtime();
+
+ peer->ssl = SSL_new(port->ctx);
+ if (peer->ssl == NULL) {
+ efree(peer);
+ return NULL;
+ }
+ SSL_set_accept_state(peer->ssl);
+ /* The peer proves its address with a cookie before the handshake goes on: HelloVerifyRequest
+ * for DTLS 1.2, HelloRetryRequest for DTLS 1.3 */
+ SSL_set_options(peer->ssl, SSL_OP_COOKIE_EXCHANGE);
+ SSL_set_mode(peer->ssl, SSL_MODE_RELEASE_BUFFERS | SSL_MODE_ENABLE_PARTIAL_WRITE
+ | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+ SSL_set_ex_data(peer->ssl, php_openssl_get_ssl_stream_data_index(), port->stream);
+
+ peer->conn = php_openssl_conn_new(port->xport, port->stream, false);
+ php_openssl_conn_set_peer(peer->conn, addr, addrlen);
+ peer->conn->fill = php_openssl_port_fill;
+ peer->conn->owner = peer;
+ if (php_openssl_conn_set_ssl(peer->conn, peer->ssl) == FAILURE) {
+ SSL_free(peer->ssl);
+ php_openssl_conn_free(peer->conn);
+ efree(peer);
+ return NULL;
+ }
+ if (port->link_mtu) {
+ DTLS_set_link_mtu(peer->ssl, (long) port->link_mtu);
+ SSL_set_options(peer->ssl, SSL_OP_NO_QUERY_MTU);
+ peer->conn->link_mtu = port->link_mtu;
+ }
+
+ peer->key = zend_string_copy(key);
+ zend_hash_add_new_ptr(&port->peers, key, peer);
+ peer->next = port->pending;
+ port->pending = peer;
+ port->pending_count++;
+ return peer;
+}
+
+/* Expired retransmit timers and stale handshakes of the pending peers; true when a timer fired */
+static bool php_openssl_port_tick(php_openssl_port *port)
+{
+ bool progress = false;
+ zend_hrtime_t now = zend_hrtime();
+ zend_hrtime_t max_age = (zend_hrtime_t) port->pending_timeout * ZEND_NANO_IN_SEC;
+ php_openssl_dtls_peer *peer = port->pending;
+
+ while (peer != NULL) {
+ php_openssl_dtls_peer *next = peer->next;
+ if (now - peer->created > max_age) {
+ php_openssl_dtls_peer_free(port, peer);
+ } else {
+ php_openssl_deadline timer;
+ php_openssl_conn_timer_deadline(peer->conn, &timer);
+ if (!timer.infinite && php_openssl_deadline_expired(&timer)
+ && php_openssl_conn_handle_timeout(peer->conn) > 0) {
+ progress = true;
+ }
+ php_openssl_dtls_peer_flush(peer);
+ }
+ peer = next;
+ }
+ return progress;
+}
+
+/* The earliest retransmit timer of the pending peers */
+static void php_openssl_port_timer_deadline(php_openssl_port *port, php_openssl_deadline *dl)
+{
+ php_openssl_deadline_init_infinite(dl);
+ for (php_openssl_dtls_peer *peer = port->pending; peer != NULL; peer = peer->next) {
+ php_openssl_deadline timer;
+ php_openssl_conn_timer_deadline(peer->conn, &timer);
+ php_openssl_deadline_cap(dl, &timer);
+ }
+ if (port->pending != NULL) {
+ /* Stale handshakes are reaped at the latest when the oldest expires */
+ php_openssl_dtls_peer *oldest = port->pending;
+ while (oldest->next) {
+ oldest = oldest->next;
+ }
+ php_openssl_deadline reap;
+ reap.infinite = false;
+ reap.at = oldest->created + (zend_hrtime_t) port->pending_timeout * ZEND_NANO_IN_SEC;
+ php_openssl_deadline_cap(dl, &reap);
+ }
+}
+
+/* Receives one datagram and gives it to its peer, a new one when unknown; 1 when it did,
+ * -1 with errno EAGAIN when the socket has none */
+static int php_openssl_port_recv(php_openssl_port *port)
+{
+ char buf[PHP_OPENSSL_MAX_DGRAM];
+ php_sockaddr_storage addr;
+ socklen_t addrlen;
+
+ ssize_t n = php_openssl_xport_recv_dgram(port->xport, buf, sizeof(buf), &addr, &addrlen);
+ if (n < 0) {
+ return -1;
+ }
+ if (n == 0) {
+ /* An empty datagram is no record: queued, it would end the connection of its peer */
+ return 1;
+ }
+ if (addrlen == 0) {
+ /* No peer to route by: the transport is connected to one */
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr.ss_family);
+ }
+
+ zend_string *key = php_openssl_dtls_peer_key((const struct sockaddr *) &addr, addrlen);
+ php_openssl_dtls_peer *peer = zend_hash_find_ptr(&port->peers, key);
+ if (peer == NULL) {
+ if (port->closed) {
+ zend_string_release(key);
+ return 1;
+ }
+ if ((zend_long) port->pending_count >= port->max_pending) {
+ php_openssl_dtls_peer *oldest = NULL;
+ for (php_openssl_dtls_peer *p = port->pending; p != NULL; p = p->next) {
+ if (!p->verified) {
+ oldest = p;
+ }
+ }
+ if (oldest == NULL) {
+ zend_string_release(key);
+ return 1;
+ }
+ php_openssl_dtls_peer_free(port, oldest);
+ }
+ peer = php_openssl_dtls_peer_new(port, (const struct sockaddr *) &addr, addrlen, key);
+ if (peer == NULL) {
+ zend_string_release(key);
+ return 1;
+ }
+ }
+ zend_string_release(key);
+
+ if (peer->conn->rxq.size >= PHP_OPENSSL_DTLS_RX_LIMIT) {
+ return 1;
+ }
+ php_openssl_conn_push_dgram(peer->conn, buf, (size_t) n, (const struct sockaddr *) &addr, addrlen);
+ if (peer->state == PHP_OPENSSL_DTLS_PEER_PENDING) {
+ php_openssl_dtls_peer_drive(port, peer);
+ }
+ return 1;
+}
+
+/* Receives for the port, waiting up to the deadline, bounded by the retransmit timers of conn
+ * (when given) and of the pending peers, which it services when they fire. Returns like
+ * php_openssl_conn_fill(). */
+static int php_openssl_port_pump(php_openssl_port *port, php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ for (;;) {
+ int n = php_openssl_port_recv(port);
+ if (n > 0) {
+ return 1;
+ }
+ if (errno != EAGAIN) {
+ return -1;
+ }
+ if (dl == NULL) {
+ errno = EAGAIN;
+ return -1;
+ }
+
+ php_openssl_deadline wait = *dl;
+ php_openssl_deadline timer;
+ if (conn != NULL) {
+ php_openssl_conn_timer_deadline(conn, &timer);
+ php_openssl_deadline_cap(&wait, &timer);
+ }
+ php_openssl_port_timer_deadline(port, &timer);
+ php_openssl_deadline_cap(&wait, &timer);
+
+ int w = php_openssl_xport_wait(port->xport, PHP_POLLREADABLE, &wait);
+ if (w < 0) {
+ errno = php_socket_errno();
+ return -1;
+ }
+ if (w == 0) {
+ bool progress = php_openssl_port_tick(port);
+ if (conn != NULL) {
+ php_openssl_conn_timer_deadline(conn, &timer);
+ if (!timer.infinite && php_openssl_deadline_expired(&timer)
+ && php_openssl_conn_handle_timeout(conn) > 0) {
+ progress = true;
+ }
+ }
+ if (progress) {
+ return 1;
+ }
+ if (php_openssl_deadline_expired(dl)) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ }
+ }
+}
+
+static int php_openssl_port_fill(php_openssl_conn *conn, php_openssl_deadline *dl)
+{
+ php_openssl_dtls_peer *peer = php_openssl_dtls_peer_of(conn);
+ return php_openssl_port_pump(peer->port, conn, dl);
+}
+
+/* The stream API */
+
+int php_openssl_dtls_listen(php_stream *stream, php_openssl_netstream_data_t *sslsock)
+{
+ zval *val;
+
+ if (sslsock->port != NULL || sslsock->s.socket == SOCK_ERR) {
+ return -1;
+ }
+
+ /* The server's method */
+ sslsock->is_client = 0;
+ sslsock->method &= ~STREAM_CRYPTO_IS_CLIENT;
+ if (sslsock->ctx == NULL && php_openssl_create_server_ctx(stream, sslsock, sslsock->method) == FAILURE) {
+ return -1;
+ }
+
+ php_openssl_port *port = ecalloc(1, sizeof(*port));
+ port->refcount = 1;
+ port->stream = stream;
+ port->listener = sslsock;
+ port->fd = sslsock->s.socket;
+#ifdef PHP_WIN32
+ {
+ BOOL report = FALSE;
+ DWORD bytes;
+ WSAIoctl(port->fd, SIO_UDP_CONNRESET, &report, sizeof(report), NULL, 0, &bytes, NULL, NULL);
+ }
+#endif
+ port->max_pending = PHP_OPENSSL_DTLS_MAX_PENDING;
+ port->pending_timeout = PHP_OPENSSL_DTLS_PENDING_TIMEOUT;
+ GET_VER_OPT_LONG("dtls_max_pending", port->max_pending);
+ GET_VER_OPT_LONG("dtls_pending_timeout", port->pending_timeout);
+ if (port->max_pending < 1) {
+ port->max_pending = 1;
+ }
+ if (port->pending_timeout < 1) {
+ port->pending_timeout = 1;
+ }
+ if (GET_VER_OPT("dtls_link_mtu")) {
+ zend_long mtu = zval_get_long(val);
+ if (mtu > 0) {
+ port->link_mtu = (unsigned) mtu;
+ }
+ }
+ if (RAND_bytes(port->cookie_secret, sizeof(port->cookie_secret)) != 1) {
+ php_stream_warn(stream, CreateFailed, "DTLS cookie secret generation failure");
+ efree(port);
+ return -1;
+ }
+ zend_hash_init(&port->peers, 8, NULL, NULL, 0);
+
+ SSL_CTX_up_ref(sslsock->ctx);
+ port->ctx = sslsock->ctx;
+ SSL_CTX_set_cookie_generate_cb(port->ctx, php_openssl_dtls_cookie_generate);
+ SSL_CTX_set_cookie_verify_cb(port->ctx, php_openssl_dtls_cookie_verify);
+ SSL_CTX_set_stateless_cookie_generate_cb(port->ctx, php_openssl_dtls_stateless_cookie_generate);
+ SSL_CTX_set_stateless_cookie_verify_cb(port->ctx, php_openssl_dtls_stateless_cookie_verify);
+
+ php_set_sock_blocking(port->fd, 0);
+ php_openssl_dgram_socket_setup(port->fd);
+ port->xport = php_openssl_xport_new_fd(port->fd, true, false);
+
+ sslsock->port = port;
+ return 0;
+}
+
+int php_openssl_dtls_accept(php_stream *stream, php_openssl_netstream_data_t *sslsock,
+ php_stream_xport_param *xparam STREAMS_DC)
+{
+ php_openssl_port *port = sslsock->port;
+ php_openssl_deadline dl;
+
+ xparam->outputs.client = NULL;
+ if (port == NULL) {
+ if (xparam->want_errortext) {
+ xparam->outputs.error_text = ZSTR_INIT_LITERAL("The dtls:// stream is not a server", 0);
+ }
+ return -1;
+ }
+
+ /* No timeout waits for good, a zero one does not wait at all */
+ if (xparam->inputs.timeout != NULL && xparam->inputs.timeout->tv_sec == 0
+ && xparam->inputs.timeout->tv_usec == 0) {
+ php_openssl_deadline_init_nonblock(&dl);
+ } else {
+ php_openssl_deadline_init(&dl, xparam->inputs.timeout);
+ }
+ while (port->ready == NULL) {
+ if (php_openssl_port_pump(port, NULL, &dl) < 0) {
+ if (errno == ETIMEDOUT) {
+ xparam->outputs.error_code = ETIMEDOUT;
+ if (xparam->want_errortext) {
+ xparam->outputs.error_text = ZSTR_INIT_LITERAL("Accept timed out", 0);
+ }
+ } else {
+ xparam->outputs.error_code = errno;
+ if (xparam->want_errortext) {
+ xparam->outputs.error_text = strpprintf(0, "%s", strerror(errno));
+ }
+ }
+ return -1;
+ }
+ }
+
+ /* The oldest completed handshake becomes a stream */
+ php_openssl_dtls_peer *peer = port->ready;
+ port->ready = peer->next;
+ if (port->ready == NULL) {
+ port->ready_tail = NULL;
+ }
+ peer->next = NULL;
+ peer->state = PHP_OPENSSL_DTLS_PEER_ACCEPTED;
+ port->pending_count--;
+
+ php_openssl_netstream_data_t *clisockdata = emalloc(sizeof(*clisockdata));
+ memset(clisockdata, 0, sizeof(*clisockdata));
+ memcpy(&clisockdata->s, &sslsock->s, sizeof(clisockdata->s));
+ clisockdata->s.is_blocked = true;
+ clisockdata->connect_timeout = sslsock->connect_timeout;
+ clisockdata->method = sslsock->method;
+ clisockdata->is_client = 0;
+ clisockdata->state_set = 1;
+ clisockdata->ssl_handle = peer->ssl;
+ clisockdata->conn = peer->conn;
+ clisockdata->port = port;
+ port->refcount++;
+ php_openssl_netstream_share_ctx(clisockdata, sslsock);
+ peer->sslsock = clisockdata;
+
+ php_stream *clistream = php_stream_alloc_rel(&php_openssl_dgram_socket_ops, clisockdata, NULL, "r+");
+ clistream->ctx = stream->ctx;
+ if (stream->ctx) {
+ GC_ADDREF(stream->ctx);
+ }
+ peer->conn->stream = clistream;
+ SSL_set_ex_data(peer->ssl, php_openssl_get_ssl_stream_data_index(), clistream);
+
+ if (php_openssl_handshake_complete(clistream, clisockdata) < 0) {
+ php_stream_close(clistream);
+ if (xparam->want_errortext) {
+ xparam->outputs.error_text = ZSTR_INIT_LITERAL("Cannot enable crypto", 0);
+ }
+ return -1;
+ }
+
+ if (xparam->want_addr || xparam->want_textaddr) {
+ php_network_populate_name_from_sockaddr((struct sockaddr *) &peer->conn->peer,
+ peer->conn->peerlen,
+ xparam->want_textaddr ? &xparam->outputs.textaddr : NULL,
+ xparam->want_addr ? &xparam->outputs.addr : NULL,
+ xparam->want_addr ? &xparam->outputs.addrlen : NULL);
+ }
+ xparam->outputs.client = clistream;
+ return 0;
+}
+
+bool php_openssl_dtls_detach(php_stream *stream, php_openssl_netstream_data_t *sslsock)
+{
+ php_openssl_port *port = sslsock->port;
+
+ if (port == NULL) {
+ return false;
+ }
+ sslsock->port = NULL;
+
+ if (port->listener == sslsock) {
+ /* The listener is gone: no new peers, and the handshakes under way are dropped */
+ port->closed = true;
+ port->stream = NULL;
+ port->listener = NULL;
+ while (port->pending) {
+ php_openssl_dtls_peer_free(port, port->pending);
+ }
+ while (port->ready) {
+ php_openssl_dtls_peer_free(port, port->ready);
+ }
+ } else if (sslsock->conn != NULL) {
+ php_openssl_dtls_peer *peer = php_openssl_dtls_peer_of(sslsock->conn);
+ if (peer != NULL) {
+ php_openssl_dtls_peer_free(port, peer);
+ }
+ }
+
+ php_openssl_port_release(port);
+ return true;
+}
+
+#endif /* HAVE_DTLS */
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 0f84a4006565..70c5817c48bf 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -27,6 +27,7 @@
#include "zend_exceptions.h"
#include "php_openssl.h"
#include "php_openssl_backend.h"
+#include "xp_ssl.h"
#include "php_io.h"
#include
#include
@@ -55,77 +56,6 @@
#include
#endif
-/* Flags for determining allowed stream crypto methods */
-#define STREAM_CRYPTO_IS_CLIENT (1<<0)
-#define STREAM_CRYPTO_METHOD_SSLv2 (1<<1)
-#define STREAM_CRYPTO_METHOD_SSLv3 (1<<2)
-#define STREAM_CRYPTO_METHOD_TLSv1_0 (1<<3)
-#define STREAM_CRYPTO_METHOD_TLSv1_1 (1<<4)
-#define STREAM_CRYPTO_METHOD_TLSv1_2 (1<<5)
-#define STREAM_CRYPTO_METHOD_TLSv1_3 (1<<6)
-
-#ifndef OPENSSL_NO_TLS1_METHOD
-#define HAVE_TLS1 1
-#endif
-
-#ifndef OPENSSL_NO_TLS1_1_METHOD
-#define HAVE_TLS11 1
-#endif
-
-#ifndef OPENSSL_NO_TLS1_2_METHOD
-#define HAVE_TLS12 1
-#endif
-
-#ifndef OPENSSL_NO_TLS1_3
-#define HAVE_TLS13 1
-#endif
-
-#ifndef OPENSSL_NO_ECDH
-#define HAVE_ECDH 1
-#endif
-
-#ifndef OPENSSL_NO_TLSEXT
-#define HAVE_TLS_SNI 1
-#define HAVE_TLS_ALPN 1
-#endif
-
-#ifndef LIBRESSL_VERSION_NUMBER
-#define HAVE_SEC_LEVEL 1
-#endif
-
-#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
-#define HAVE_SSL3 1
-#define PHP_OPENSSL_MIN_PROTO_VERSION STREAM_CRYPTO_METHOD_SSLv3
-#else
-#define PHP_OPENSSL_MIN_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_0
-#endif
-#ifdef HAVE_TLS13
-#define PHP_OPENSSL_MAX_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_3
-#else
-#define PHP_OPENSSL_MAX_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_2
-#endif
-
-/* Simplify ssl context option retrieval */
-#define GET_VER_OPT(_name) \
- (PHP_STREAM_CONTEXT(stream) && (val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", _name)) != NULL)
-#define GET_VER_OPT_STRING(_name, _str) \
- do { \
- if (GET_VER_OPT(_name)) { \
- if (try_convert_to_string(val)) _str = Z_STRVAL_P(val); \
- } \
- } while (0)
-#define GET_VER_OPT_STRINGL(_name, _str, _len) \
- do { \
- if (GET_VER_OPT(_name)) { \
- if (try_convert_to_string(val)) { \
- _str = Z_STRVAL_P(val); \
- _len = Z_STRLEN_P(val); \
- } \
- } \
- } while (0)
-#define GET_VER_OPT_LONG(_name, _num) \
- if (GET_VER_OPT(_name)) _num = zval_get_long(val)
-
#ifdef HAVE_IPV6
/* Used for IPv6 Address peer verification */
#define EXPAND_IPV6_ADDRESS(_str, _bytes) \
@@ -147,112 +77,14 @@
extern php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl);
extern zend_string* php_openssl_x509_fingerprint(
X509 *peer, const char *method, bool raw, php_stream *stream);
-extern int php_openssl_get_ssl_stream_data_index(void);
-static struct timeval php_openssl_subtract_timeval(struct timeval a, struct timeval b);
-static int php_openssl_compare_timeval(struct timeval a, struct timeval b);
static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, size_t count);
static const php_stream_ops php_openssl_socket_ops;
-/* Certificate contexts used for server-side SNI selection */
-typedef struct _php_openssl_sni_cert_t {
- char *name;
- SSL_CTX *ctx;
-} php_openssl_sni_cert_t;
-
-/* Provides leaky bucket handhsake renegotiation rate-limiting */
-typedef struct _php_openssl_handshake_bucket_t {
- zend_long prev_handshake;
- zend_long limit;
- zend_long window;
- float tokens;
- unsigned should_close;
-} php_openssl_handshake_bucket_t;
-
-#ifdef HAVE_TLS_ALPN
-/* Holds the available server ALPN protocols for negotiation */
-typedef struct _php_openssl_alpn_ctx_t {
- unsigned char *data;
- unsigned short len;
-} php_openssl_alpn_ctx;
-#endif
-
/* TLS 1.3 PSK ciphersuite IDs */
static const unsigned char php_openssl_tls13_aes128gcmsha256_id[] = { 0x13, 0x01 };
static const unsigned char php_openssl_tls13_aes256gcmsha384_id[] = { 0x13, 0x02 };
-/* Holds PSK callbacks */
-typedef struct _php_openssl_psk_callbacks_t {
- int refcount;
- zend_fcall_info_cache client_cb;
- zend_fcall_info_cache server_cb;
-} php_openssl_psk_callbacks_t;
-
-#ifdef HAVE_TLS13
-/* TLS 1.3 early data (0-RTT) handshake phase */
-typedef enum {
- PHP_OPENSSL_EARLY_DATA_NONE = 0,
- PHP_OPENSSL_EARLY_DATA_ACTIVE,
- PHP_OPENSSL_EARLY_DATA_DONE,
-} php_openssl_early_data_state_t;
-
-/* Size of the buffer used to drain server-side early data chunk by chunk */
-#define PHP_OPENSSL_EARLY_DATA_CHUNK 16384
-
-/* Holds the server early data callback */
-typedef struct _php_openssl_early_data_callbacks_t {
- int refcount;
- zend_fcall_info_cache read_cb;
-} php_openssl_early_data_callbacks_t;
-#endif
-
-/* Holds session callback */
-typedef struct _php_openssl_session_callbacks_t {
- int refcount;
- zend_fcall_info_cache new_cb;
- zend_fcall_info_cache get_cb;
- zend_fcall_info_cache remove_cb;
-} php_openssl_session_callbacks_t;
-
-/* This implementation is very closely tied to the that of the native
- * sockets implemented in the core.
- * Don't try this technique in other extensions!
- * */
-typedef struct _php_openssl_netstream_data_t {
- php_netstream_data_t s;
- SSL *ssl_handle;
- SSL_CTX *ctx;
- struct timeval connect_timeout;
- int enable_on_connect;
- int is_client;
- int ssl_active;
- int last_status;
- php_stream_xport_crypt_method_t method;
- php_openssl_handshake_bucket_t *reneg;
- php_openssl_sni_cert_t *sni_certs;
- unsigned sni_cert_count;
-#ifdef HAVE_TLS_ALPN
- php_openssl_alpn_ctx alpn_ctx;
-#endif
- php_openssl_session_callbacks_t *session_callbacks;
- php_openssl_psk_callbacks_t *psk_callbacks;
- /* Identity buffer for TLS 1.3 client PSK whose lifetime outlives the
- * psk_use_session_cb call but OpenSSL doesn't free it, so we own it. */
- unsigned char *psk_identity_buf;
- size_t psk_identity_len;
-#ifdef HAVE_TLS13
- /* TLS 1.3 early data (0-RTT) */
- php_openssl_early_data_callbacks_t *early_data_callbacks;
- /* Client payload to send as early data, borrowed for the handshake */
- zend_string *early_data_send;
- size_t early_data_offset;
- php_openssl_early_data_state_t early_data_state;
-#endif
- char *url_name;
- unsigned state_set:1;
- unsigned _spare:31;
-} php_openssl_netstream_data_t;
-
/* it doesn't matter that we do some hash traversal here, since it is done only
* in an error condition arising from a network connection problem */
static int php_openssl_is_http_stream_talking_to_iis(php_stream *stream) /* {{{ */
@@ -279,9 +111,8 @@ static int php_openssl_is_http_stream_talking_to_iis(php_stream *stream) /* {{{
}
/* }}} */
-static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool is_init) /* {{{ */
+bool php_openssl_handle_ssl_error(php_stream *stream, php_openssl_netstream_data_t *sslsock, int nr_bytes)
{
- php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
int err = SSL_get_error(sslsock->ssl_handle, nr_bytes);
char esbuf[512];
smart_str ebuf = {0};
@@ -298,11 +129,8 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i
/* re-negotiation, or perhaps the SSL layer needs more
* packets: retry in next iteration */
errno = EAGAIN;
- retry = is_init ? true : sslsock->s.is_blocked;
- if (!retry) {
- sslsock->last_status = err == SSL_ERROR_WANT_READ ?
- STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE;
- }
+ sslsock->last_status = err == SSL_ERROR_WANT_READ ?
+ STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE;
break;
case SSL_ERROR_SYSCALL:
if (ERR_peek_error() == 0) {
@@ -1090,31 +918,40 @@ static zend_result php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream)
}
/* }}} */
-static inline int php_openssl_get_min_proto_version_flag(int flags) /* {{{ */
+/* The version flags of a method: the TLS range, or the DTLS range of a datagram stream */
+static inline int php_openssl_min_version_flag(bool dgram)
+{
+ return dgram ? PHP_OPENSSL_MIN_DTLS_PROTO_VERSION : PHP_OPENSSL_MIN_PROTO_VERSION;
+}
+
+static inline int php_openssl_max_version_flag(bool dgram)
+{
+ return dgram ? PHP_OPENSSL_MAX_DTLS_PROTO_VERSION : PHP_OPENSSL_MAX_PROTO_VERSION;
+}
+
+static inline int php_openssl_get_min_proto_version_flag(int flags, bool dgram)
{
int ver;
- for (ver = PHP_OPENSSL_MIN_PROTO_VERSION; ver <= PHP_OPENSSL_MAX_PROTO_VERSION; ver <<= 1) {
+ for (ver = php_openssl_min_version_flag(dgram); ver <= php_openssl_max_version_flag(dgram); ver <<= 1) {
if (flags & ver) {
return ver;
}
}
- return PHP_OPENSSL_MAX_PROTO_VERSION;
+ return php_openssl_max_version_flag(dgram);
}
-/* }}} */
-static inline int php_openssl_get_max_proto_version_flag(int flags) /* {{{ */
+static inline int php_openssl_get_max_proto_version_flag(int flags, bool dgram)
{
int ver;
- for (ver = PHP_OPENSSL_MAX_PROTO_VERSION; ver >= PHP_OPENSSL_MIN_PROTO_VERSION; ver >>= 1) {
+ for (ver = php_openssl_max_version_flag(dgram); ver >= php_openssl_min_version_flag(dgram); ver >>= 1) {
if (flags & ver) {
return ver;
}
}
- return STREAM_CRYPTO_METHOD_TLSv1_3;
+ return php_openssl_max_version_flag(dgram);
}
-/* }}} */
-static inline int php_openssl_map_proto_version(int flag) /* {{{ */
+static inline int php_openssl_map_proto_version(int flag)
{
switch (flag) {
#ifdef HAVE_TLS13
@@ -1130,37 +967,42 @@ static inline int php_openssl_map_proto_version(int flag) /* {{{ */
#ifdef HAVE_SSL3
case STREAM_CRYPTO_METHOD_SSLv3:
return SSL3_VERSION;
+#endif
+#ifdef HAVE_DTLS13
+ case STREAM_CRYPTO_METHOD_DTLSv1_3:
+ return DTLS1_3_VERSION;
+#endif
+#ifdef HAVE_DTLS
+ case STREAM_CRYPTO_METHOD_DTLSv1_2:
+ return DTLS1_2_VERSION;
#endif
default:
return TLS1_2_VERSION;
}
}
-/* }}} */
-static int php_openssl_get_min_proto_version(int flags) /* {{{ */
+static int php_openssl_get_min_proto_version(int flags, bool dgram)
{
- return php_openssl_map_proto_version(php_openssl_get_min_proto_version_flag(flags));
+ return php_openssl_map_proto_version(php_openssl_get_min_proto_version_flag(flags, dgram));
}
-/* }}} */
-static int php_openssl_get_max_proto_version(int flags) /* {{{ */
+static int php_openssl_get_max_proto_version(int flags, bool dgram)
{
- return php_openssl_map_proto_version(php_openssl_get_max_proto_version_flag(flags));
+ return php_openssl_map_proto_version(php_openssl_get_max_proto_version_flag(flags, dgram));
}
-/* }}} */
-static int php_openssl_get_proto_version_flags(int flags, int min, int max) /* {{{ */
+static int php_openssl_get_proto_version_flags(int flags, int min, int max, bool dgram)
{
int ver;
if (!min) {
- min = php_openssl_get_min_proto_version_flag(flags);
+ min = php_openssl_get_min_proto_version_flag(flags, dgram);
}
if (!max) {
- max = php_openssl_get_max_proto_version_flag(flags);
+ max = php_openssl_get_max_proto_version_flag(flags, dgram);
}
- for (ver = PHP_OPENSSL_MIN_PROTO_VERSION; ver <= PHP_OPENSSL_MAX_PROTO_VERSION; ver <<= 1) {
+ for (ver = php_openssl_min_version_flag(dgram); ver <= php_openssl_max_version_flag(dgram); ver <<= 1) {
if (ver >= min && ver <= max) {
if (!(flags & ver)) {
flags |= ver;
@@ -1172,7 +1014,6 @@ static int php_openssl_get_proto_version_flags(int flags, int min, int max) /* {
return flags;
}
-/* }}} */
static void php_openssl_limit_handshake_reneg(const SSL *ssl) /* {{{ */
{
@@ -2591,12 +2432,25 @@ static zend_result php_openssl_apply_client_session_data(php_stream *stream,
return SUCCESS;
}
-static zend_result php_openssl_create_server_ctx(php_stream *stream,
+zend_result php_openssl_create_server_ctx(php_stream *stream,
php_openssl_netstream_data_t *sslsock, int method_flags)
{
zval *val;
- const SSL_METHOD *method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method();
+ const SSL_METHOD *method;
+ bool dgram = sslsock->s.is_dgram;
+
+ if (dgram) {
+#ifdef HAVE_DTLS
+ method = sslsock->is_client ? DTLS_client_method() : DTLS_server_method();
+#else
+ php_stream_warn(stream, ProtocolUnsupported,
+ "DTLS support is not compiled into the OpenSSL library against which PHP is linked");
+ return FAILURE;
+#endif
+ } else {
+ method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method();
+ }
sslsock->ctx = SSL_CTX_new(method);
if (sslsock->ctx == NULL) {
@@ -2610,7 +2464,7 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream,
zend_long max_version = 0;
GET_VER_OPT_LONG("min_proto_version", min_version);
GET_VER_OPT_LONG("max_proto_version", max_version);
- method_flags = php_openssl_get_proto_version_flags(method_flags, min_version, max_version);
+ method_flags = php_openssl_get_proto_version_flags(method_flags, min_version, max_version, dgram);
int ssl_ctx_options = SSL_OP_ALL;
if (GET_VER_OPT("no_ticket") && zend_is_true(val)) {
@@ -2716,8 +2570,8 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream,
SSL_CTX_set_options(sslsock->ctx, ssl_ctx_options);
- SSL_CTX_set_min_proto_version(sslsock->ctx, php_openssl_get_min_proto_version(method_flags));
- SSL_CTX_set_max_proto_version(sslsock->ctx, php_openssl_get_max_proto_version(method_flags));
+ SSL_CTX_set_min_proto_version(sslsock->ctx, php_openssl_get_min_proto_version(method_flags, dgram));
+ SSL_CTX_set_max_proto_version(sslsock->ctx, php_openssl_get_max_proto_version(method_flags, dgram));
if (sslsock->is_client) {
@@ -2759,6 +2613,75 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream,
return SUCCESS;
}
+/* Sets the socket of a datagram transport up for path MTU discovery */
+void php_openssl_dgram_socket_setup(php_socket_t fd)
+{
+#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO)
+ php_sockaddr_storage sa;
+ socklen_t sl = sizeof(sa);
+
+ if (getsockname(fd, (struct sockaddr *) &sa, &sl) != 0) {
+ return;
+ }
+ if (sa.ss_family == AF_INET) {
+ int mode = IP_PMTUDISC_DO;
+ setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, (char *) &mode, sizeof(mode));
+ }
+#if defined(HAVE_IPV6) && defined(IPV6_MTU_DISCOVER) && defined(IPV6_PMTUDISC_DO)
+ else if (sa.ss_family == AF_INET6) {
+ int mode = IPV6_PMTUDISC_DO;
+ setsockopt(fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, (char *) &mode, sizeof(mode));
+ }
+#endif
+#endif
+}
+
+/* Gives the SSL handle its transport: the ciphertext queues over the stream's socket or inner
+ * stream. The socket stays non-blocking from here on; the stream's mode is emulated by waiting. */
+static zend_result php_openssl_conn_attach(php_stream *stream, php_openssl_netstream_data_t *sslsock)
+{
+ bool persistent = php_stream_is_persistent(stream);
+ zval *val;
+
+ if (sslsock->conn == NULL) {
+ php_openssl_xport *xport;
+
+ if (sslsock->inner != NULL) {
+ xport = php_openssl_xport_new_stream(sslsock->inner, sslsock->s.is_dgram);
+ } else {
+ if (sslsock->s.socket == SOCK_ERR) {
+ php_stream_warn(stream, Generic, "SSL/TLS cannot be set up on a stream without a socket");
+ return FAILURE;
+ }
+ php_set_sock_blocking(sslsock->s.socket, 0);
+ if (sslsock->s.is_dgram) {
+ php_openssl_dgram_socket_setup(sslsock->s.socket);
+ }
+ xport = php_openssl_xport_new_fd(sslsock->s.socket, sslsock->s.is_dgram, persistent);
+ }
+ sslsock->conn = php_openssl_conn_new(xport, stream, persistent);
+ php_openssl_xport_release(xport);
+ }
+
+ if (php_openssl_conn_set_ssl(sslsock->conn, sslsock->ssl_handle) == FAILURE) {
+ php_stream_warn(stream, CreateFailed, "SSL BIO creation failure");
+ return FAILURE;
+ }
+
+#ifdef HAVE_DTLS
+ if (sslsock->s.is_dgram && GET_VER_OPT("dtls_link_mtu")) {
+ zend_long mtu = zval_get_long(val);
+ if (mtu > 0) {
+ DTLS_set_link_mtu(sslsock->ssl_handle, (long) mtu);
+ SSL_set_options(sslsock->ssl_handle, SSL_OP_NO_QUERY_MTU);
+ sslsock->conn->link_mtu = (unsigned) mtu;
+ }
+ }
+#endif
+
+ return SUCCESS;
+}
+
static zend_result php_openssl_setup_crypto(php_stream *stream,
php_openssl_netstream_data_t *sslsock,
php_stream_xport_crypto_param *cparam) /* {{{ */
@@ -2783,7 +2706,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
if (cparam->inputs.session) {
php_openssl_netstream_data_t *parent_sslsock;
- if (cparam->inputs.session->ops != &php_openssl_socket_ops) {
+ if (cparam->inputs.session->ops != &php_openssl_socket_ops
+ && cparam->inputs.session->ops != &php_openssl_dgram_socket_ops) {
php_stream_warn(stream, SslNotSupported, "Supplied session stream must be an SSL enabled stream");
} else if ((parent_sslsock = cparam->inputs.session->abstract)->ctx == NULL) {
php_stream_warn(stream, SslNotSupported, "Supplied SSL session stream is not set up");
@@ -2825,8 +2749,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream);
- if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) {
- php_openssl_handle_ssl_error(stream, 0, true);
+ if (php_openssl_conn_attach(stream, sslsock) == FAILURE) {
+ return FAILURE;
}
if (sslsock->is_client) {
@@ -2861,8 +2785,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
return FAILURE;
}
- if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) {
- php_openssl_handle_ssl_error(stream, 0, true);
+ if (php_openssl_conn_attach(stream, sslsock) == FAILURE) {
+ return FAILURE;
}
return SUCCESS;
@@ -2923,15 +2847,6 @@ static int php_openssl_capture_peer_certs(php_stream *stream,
}
/* }}} */
-static zend_result php_openssl_set_blocking(php_openssl_netstream_data_t *sslsock, int block)
-{
- zend_result result = php_set_sock_blocking(sslsock->s.socket, block);
- if (EXPECTED(SUCCESS == result)) {
- sslsock->s.is_blocked = block;
- }
- return result;
-}
-
#ifdef HAVE_TLS13
/* Send pending client early data (0-RTT), then connect. Returns like SSL_connect(). */
static int php_openssl_handshake_client_early_data(php_openssl_netstream_data_t *sslsock)
@@ -3006,20 +2921,149 @@ static int php_openssl_do_handshake(php_stream *stream, php_openssl_netstream_da
: SSL_accept(sslsock->ssl_handle);
}
+typedef int (*php_openssl_ssl_op)(php_stream *stream, php_openssl_netstream_data_t *sslsock, void *arg);
+
+static void php_openssl_transport_error(php_stream *stream, const char *what)
+{
+ char *estr = php_socket_strerror(errno, NULL, 0);
+ php_stream_warn(stream, ProtocolError, "SSL: %s failed: %s", what, estr);
+ efree(estr);
+}
+
+/* Runs an SSL call over the transport until it completes: what OpenSSL produced is sent after
+ * every call, the receive queue is filled on WANT_READ and the call repeated. With a deadline
+ * the transport is waited for up to it; without one nothing waits. Returns the SSL result when
+ * positive; 0 with errno EAGAIN and last_status set when the transport would block; -1 with
+ * *timed_out when the deadline passed; otherwise the failed result after reporting the error. */
+static int php_openssl_pump(php_stream *stream, php_openssl_netstream_data_t *sslsock,
+ php_openssl_ssl_op op, void *arg, php_openssl_deadline *dl, bool *timed_out)
+{
+ php_openssl_conn *conn = sslsock->conn;
+ int n;
+
+ *timed_out = false;
+ for (;;) {
+ ERR_clear_error();
+ n = op(stream, sslsock, arg);
+ if (sslsock->reneg && sslsock->reneg->should_close) {
+ /* The renegotiation rate limit closes the connection */
+ php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR);
+ stream->eof = 1;
+ errno = 0;
+ return 0;
+ }
+ int err = n > 0 ? SSL_ERROR_NONE : SSL_get_error(sslsock->ssl_handle, n);
+
+ int flushed = php_openssl_conn_flush(conn, dl);
+ if (flushed < 0) {
+ php_openssl_transport_error(stream, "send");
+ errno = 0;
+ return -1;
+ }
+ if (flushed == 0 && errno == ETIMEDOUT) {
+ *timed_out = true;
+ return -1;
+ }
+ if (n > 0) {
+ return n;
+ }
+
+ if (err == SSL_ERROR_WANT_WRITE) {
+ if (flushed > 0) {
+ continue;
+ }
+ sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE;
+ errno = EAGAIN;
+ return 0;
+ }
+ if (err == SSL_ERROR_WANT_READ) {
+ int filled = php_openssl_conn_fill(conn, dl);
+ if (filled >= 0) {
+ /* Data, a retransmit or an EOF for OpenSSL to see */
+ continue;
+ }
+ if (errno == EAGAIN) {
+ sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ;
+ return 0;
+ }
+ if (errno == ETIMEDOUT) {
+ *timed_out = true;
+ return -1;
+ }
+ php_openssl_transport_error(stream, "recv");
+ errno = 0;
+ return -1;
+ }
+
+ php_openssl_handle_ssl_error(stream, sslsock, n);
+ errno = 0;
+ return n;
+ }
+}
+
+static int php_openssl_op_handshake(php_stream *stream, php_openssl_netstream_data_t *sslsock, void *arg)
+{
+ return php_openssl_do_handshake(stream, sslsock);
+}
+
+typedef struct _php_openssl_io_arg {
+ char *buf;
+ size_t count;
+} php_openssl_io_arg;
+
+static int php_openssl_op_read(php_stream *stream, php_openssl_netstream_data_t *sslsock, void *arg)
+{
+ php_openssl_io_arg *io = arg;
+ return SSL_read(sslsock->ssl_handle, io->buf, (int) io->count);
+}
+
+static int php_openssl_op_write(php_stream *stream, php_openssl_netstream_data_t *sslsock, void *arg)
+{
+ php_openssl_io_arg *io = arg;
+ return SSL_write(sslsock->ssl_handle, io->buf, (int) io->count);
+}
+
+static int php_openssl_op_peek(php_stream *stream, php_openssl_netstream_data_t *sslsock, void *arg)
+{
+ php_openssl_io_arg *io = arg;
+ return SSL_peek(sslsock->ssl_handle, io->buf, (int) io->count);
+}
+
+int php_openssl_handshake_complete(php_stream *stream, php_openssl_netstream_data_t *sslsock)
+{
+ int cert_captured = 0;
+ int n;
+ X509 *peer_cert = SSL_get_peer_certificate(sslsock->ssl_handle);
+
+ if (peer_cert && PHP_STREAM_CONTEXT(stream)) {
+ cert_captured = php_openssl_capture_peer_certs(stream, sslsock, peer_cert);
+ }
+
+ if (FAILURE == php_openssl_apply_peer_verification_policy(sslsock->ssl_handle, peer_cert, stream)) {
+ SSL_shutdown(sslsock->ssl_handle);
+ php_openssl_conn_flush(sslsock->conn, NULL);
+ n = -1;
+ } else {
+ sslsock->ssl_active = 1;
+ n = 1;
+ }
+
+ if (peer_cert && cert_captured == 0) {
+ X509_free(peer_cert);
+ }
+ return n;
+}
+
static int php_openssl_enable_crypto(php_stream *stream,
php_openssl_netstream_data_t *sslsock,
php_stream_xport_crypto_param *cparam) /* {{{ */
{
int n;
- int retry = 1;
- int cert_captured = 0;
- X509 *peer_cert;
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
if (cparam->inputs.activate && !sslsock->ssl_active) {
- struct timeval start_time, *timeout;
- bool blocked = sslsock->s.is_blocked, has_timeout = false;
+ bool blocked = sslsock->s.is_blocked;
if (!sslsock->state_set) {
#ifdef PHP_OPENSSL_TLS_DEBUG
@@ -3057,94 +3101,42 @@ static int php_openssl_enable_crypto(php_stream *stream,
sslsock->state_set = 1;
}
- SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS);
+ SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS
+ | SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
- if (SUCCESS == php_openssl_set_blocking(sslsock, 0)) {
- /* The following mode are added only if we are able to change socket
- * to non blocking mode which is also used for read and write */
- SSL_set_mode(sslsock->ssl_handle, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
- }
+ /* A blocking stream waits for the transport up to its timeout, a non-blocking one
+ * returns with the WANT status */
+ php_openssl_deadline dl;
+ bool timed_out;
+ php_openssl_deadline_init(&dl,
+ sslsock->is_client ? &sslsock->connect_timeout : &sslsock->s.timeout);
+ n = php_openssl_pump(stream, sslsock, php_openssl_op_handshake, NULL, blocked ? &dl : NULL,
+ &timed_out);
- timeout = sslsock->is_client ? &sslsock->connect_timeout : &sslsock->s.timeout;
- has_timeout = !sslsock->s.is_blocked && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec));
- /* gettimeofday is not monotonic; using it here is not strictly correct */
- if (has_timeout) {
- gettimeofday(&start_time, NULL);
+ if (n > 0) {
+ return php_openssl_handshake_complete(stream, sslsock);
}
-
- do {
- struct timeval cur_time, elapsed_time;
-
- ERR_clear_error();
- n = php_openssl_do_handshake(stream, sslsock);
-
- if (has_timeout) {
- gettimeofday(&cur_time, NULL);
- elapsed_time = php_openssl_subtract_timeval(cur_time, start_time);
-
- if (php_openssl_compare_timeval( elapsed_time, *timeout) > 0) {
- php_openssl_set_blocking(sslsock, blocked);
- php_stream_warn(stream, TimeOut, "SSL: Handshake timed out");
- return -1;
- }
- }
-
- if (n <= 0) {
- /* in case of SSL_ERROR_WANT_READ/WRITE, do not retry in non-blocking mode */
- retry = php_openssl_handle_ssl_error(stream, n, blocked);
- if (retry) {
- /* wait until something interesting happens in the socket. It may be a
- * timeout. Also consider the unlikely of possibility of a write block */
- int err = SSL_get_error(sslsock->ssl_handle, n);
- struct timeval left_time;
-
- if (has_timeout) {
- left_time = php_openssl_subtract_timeval(*timeout, elapsed_time);
- }
- php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
- (POLLIN|POLLPRI) : POLLOUT, has_timeout ? &left_time : NULL);
- }
- } else {
- retry = 0;
- }
- } while (retry);
-
- if (sslsock->s.is_blocked != blocked) {
- php_openssl_set_blocking(sslsock, blocked);
+ if (timed_out) {
+ php_stream_warn(stream, TimeOut, "SSL: Handshake timed out");
+ return -1;
}
-
- if (n == 1) {
- peer_cert = SSL_get_peer_certificate(sslsock->ssl_handle);
- if (peer_cert && PHP_STREAM_CONTEXT(stream)) {
- cert_captured = php_openssl_capture_peer_certs(stream, sslsock, peer_cert);
- }
-
- if (FAILURE == php_openssl_apply_peer_verification_policy(sslsock->ssl_handle, peer_cert, stream)) {
- SSL_shutdown(sslsock->ssl_handle);
- n = -1;
- } else {
- sslsock->ssl_active = 1;
- }
- } else if (errno == EAGAIN) {
- n = 0;
- } else {
- n = -1;
- /* We want to capture the peer cert even if verification fails*/
- peer_cert = SSL_get_peer_certificate(sslsock->ssl_handle);
- if (peer_cert && PHP_STREAM_CONTEXT(stream)) {
- cert_captured = php_openssl_capture_peer_certs(stream, sslsock, peer_cert);
- }
+ if (errno == EAGAIN) {
+ return 0;
}
- if (n && peer_cert && cert_captured == 0) {
- X509_free(peer_cert);
+ /* Capture the peer certificate even when the handshake failed */
+ X509 *peer_cert = SSL_get_peer_certificate(sslsock->ssl_handle);
+ if (peer_cert) {
+ if (!PHP_STREAM_CONTEXT(stream) || php_openssl_capture_peer_certs(stream, sslsock, peer_cert) == 0) {
+ X509_free(peer_cert);
+ }
}
-
- return n;
+ return -1;
} else if (!cparam->inputs.activate && sslsock->ssl_active) {
/* deactivate - common for server/client */
SSL_shutdown(sslsock->ssl_handle);
+ php_openssl_conn_flush(sslsock->conn, NULL);
sslsock->ssl_active = 0;
}
@@ -3164,223 +3156,57 @@ static ssize_t php_openssl_sockop_write(php_stream *stream, const char *buf, siz
}
/* }}} */
-/**
- * Factored out common functionality (blocking, timeout, loop management) for read and write.
- * Perform IO (read or write) to an SSL socket. If we have a timeout, we switch to non-blocking mode
- * for the duration of the operation, using select to do our waits. If we time out, or we have an error
- * report that back to PHP
- */
+/* Reads or writes through the TLS layer. A blocking stream waits for the transport up to its
+ * timeout, a non-blocking one returns with the WANT status; a read with data already buffered
+ * only takes what is there. */
static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, size_t count) /* {{{ */
{
php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
- /* Only do this if SSL is active. */
- if (sslsock->ssl_active) {
- /* We have already returned some buffered data. Don't retry and don't
- * block. We're just trying to fill the buffer more, but the stream might
- * be empty, so we don't want to wait in vain. */
- bool supplemental = stream->has_buffered_data;
- int retry = !supplemental;
- struct timeval start_time;
- struct timeval *timeout = NULL;
- bool began_blocked = sslsock->s.is_blocked;
- bool has_timeout = false;
- int nr_bytes = 0;
-
- /* prevent overflow in openssl */
- if (count > INT_MAX) {
- count = INT_MAX;
- }
-
- /* never use a timeout with non-blocking sockets */
- if (began_blocked && !supplemental) {
- timeout = &sslsock->s.timeout;
- }
-
- if (timeout || supplemental) {
- php_openssl_set_blocking(sslsock, 0);
- }
-
- if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) {
- has_timeout = true;
- /* gettimeofday is not monotonic; using it here is not strictly correct */
- gettimeofday(&start_time, NULL);
+ if (!sslsock->ssl_active) {
+ if (sslsock->inner != NULL) {
+ return read ? php_stream_read(sslsock->inner, buf, count)
+ : php_stream_write(sslsock->inner, buf, count);
}
+ return read ? php_stream_socket_ops.read(stream, buf, count)
+ : php_stream_socket_ops.write(stream, buf, count);
+ }
- /* Main IO loop. */
- do {
- struct timeval cur_time, elapsed_time, left_time;
-
- /* If we have a timeout to check, figure out how much time has elapsed since we started. */
- if (has_timeout) {
- gettimeofday(&cur_time, NULL);
-
- /* Determine how much time we've taken so far. */
- elapsed_time = php_openssl_subtract_timeval(cur_time, start_time);
-
- /* and return an error if we've taken too long. */
- if (php_openssl_compare_timeval(elapsed_time, *timeout) > 0 ) {
- /* If the socket was originally blocking, set it back. */
- if (began_blocked) {
- php_openssl_set_blocking(sslsock, 1);
- }
- sslsock->s.timeout_event = true;
- return -1;
- }
- }
-
- /* Now, do the IO operation. Don't block if we can't complete... */
- ERR_clear_error();
- sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
- if (read) {
- nr_bytes = SSL_read(sslsock->ssl_handle, buf, (int)count);
-
- if (sslsock->reneg && sslsock->reneg->should_close) {
- /* renegotiation rate limiting triggered */
- php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR);
- nr_bytes = 0;
- stream->eof = 1;
- break;
- }
- } else {
- nr_bytes = SSL_write(sslsock->ssl_handle, buf, (int)count);
- }
-
- /* Now, how much time until we time out? */
- if (has_timeout) {
- left_time = php_openssl_subtract_timeval( *timeout, elapsed_time );
- }
-
- /* If we didn't do anything on the last loop (or an error) check to see if we should retry or exit. */
- if (nr_bytes <= 0) {
-
- /* Get the error code from SSL, and check to see if it's an error or not. */
- int err = SSL_get_error(sslsock->ssl_handle, nr_bytes );
- retry = php_openssl_handle_ssl_error(stream, nr_bytes, false);
-
- /* If we get this (the above doesn't check) then we'll retry as well. */
- if (errno == EAGAIN && err == SSL_ERROR_WANT_READ && read) {
- retry = 1;
- }
- if (errno == EAGAIN && err == SSL_ERROR_WANT_WRITE && read == 0) {
- retry = 1;
- }
-
- if (read) {
- /* EOF unless the SSL layer just needs to wait. */
- stream->eof = (retry == 0
- && err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE
- && !SSL_pending(sslsock->ssl_handle));
- }
-
- /* Don't loop indefinitely in non-blocking mode if no data is available */
- if (began_blocked == 0 || supplemental) {
- break;
- }
-
- /* Now, if we have to wait some time, and we're supposed to be blocking, wait for the socket to become
- * available. Now, php_pollfd_for uses select to wait up to our time_left value only...
- */
- if (retry) {
- if (read) {
- php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ?
- (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL);
- } else {
- php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
- (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL);
- }
- }
- } else {
- /* Else, if we got bytes back, check for possible errors. */
- int err = SSL_get_error(sslsock->ssl_handle, nr_bytes);
-
- /* If we didn't get any error, then let's return it to PHP. */
- if (err == SSL_ERROR_NONE) {
- break;
- }
-
- /* Otherwise, we need to wait again (up to time_left or we get an error) */
- if (began_blocked) {
- if (read) {
- php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ?
- (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL);
- } else {
- php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ?
- (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL);
- }
- } else if (err == SSL_ERROR_WANT_READ) {
- sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ;
- } else if (err == SSL_ERROR_WANT_WRITE) {
- sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE;
- }
- }
-
- /* Finally, we keep going until we got data, and an SSL_ERROR_NONE, unless we had an error. */
- } while (retry);
+ bool supplemental = stream->has_buffered_data;
+ bool wait = sslsock->s.is_blocked && !supplemental && !sslsock->peek_only;
+ php_openssl_io_arg io = { buf, count > INT_MAX ? INT_MAX : count };
+ php_openssl_deadline dl;
+ bool timed_out;
+ int n;
- /* Tell PHP if we read / wrote bytes. */
- if (nr_bytes > 0) {
- php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), nr_bytes, 0);
- }
+ sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
+ sslsock->s.timeout_event = false;
+ php_openssl_deadline_init(&dl, &sslsock->s.timeout);
+ n = php_openssl_pump(stream, sslsock, read ? php_openssl_op_read : php_openssl_op_write, &io,
+ wait ? &dl : NULL, &timed_out);
- /* This might be a supplemental read after consuming buffered data. If
- * the read returned nothing, ignore status WANT_READ. */
- if (read &&
- supplemental &&
- nr_bytes <= 0 &&
- sslsock->last_status == STREAM_CRYPTO_STATUS_WANT_READ
- ) {
+ if (n > 0) {
+ php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), n, 0);
+ return n;
+ }
+ if (timed_out) {
+ sslsock->s.timeout_event = true;
+ return -1;
+ }
+ if (errno == EAGAIN) {
+ /* A supplemental read that found nothing is not a WANT_READ */
+ if (read && supplemental && sslsock->last_status == STREAM_CRYPTO_STATUS_WANT_READ) {
sslsock->last_status = STREAM_CRYPTO_STATUS_NONE;
}
-
- /* And if we were originally supposed to be blocking, let's reset the socket to that. */
- if (began_blocked) {
- php_openssl_set_blocking(sslsock, 1);
- }
-
- return 0 > nr_bytes ? 0 : nr_bytes;
- } else {
- size_t nr_bytes = 0;
-
- /* This block is if we had no timeout... We will just sit and wait forever on the IO operation. */
- if (read) {
- nr_bytes = php_stream_socket_ops.read(stream, buf, count);
- } else {
- nr_bytes = php_stream_socket_ops.write(stream, buf, count);
- }
-
- return nr_bytes;
+ return 0;
}
-}
-/* }}} */
-
-static struct timeval php_openssl_subtract_timeval(struct timeval a, struct timeval b) /* {{{ */
-{
- struct timeval difference;
-
- difference.tv_sec = a.tv_sec - b.tv_sec;
- difference.tv_usec = a.tv_usec - b.tv_usec;
-
- if (a.tv_usec < b.tv_usec) {
- difference.tv_sec -= 1L;
- difference.tv_usec += 1000000L;
+ if (read) {
+ stream->eof = !SSL_pending(sslsock->ssl_handle);
}
-
- return difference;
+ return 0;
}
/* }}} */
-static int php_openssl_compare_timeval( struct timeval a, struct timeval b )
-{
- if (a.tv_sec > b.tv_sec || (a.tv_sec == b.tv_sec && a.tv_usec > b.tv_usec) ) {
- return 1;
- } else if( a.tv_sec == b.tv_sec && a.tv_usec == b.tv_usec ) {
- return 0;
- } else {
- return -1;
- }
-}
-
static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ */
{
php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
@@ -3392,12 +3218,23 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{
if (close_handle) {
if (sslsock->ssl_active) {
SSL_shutdown(sslsock->ssl_handle);
+ php_openssl_conn_flush(sslsock->conn, NULL);
sslsock->ssl_active = 0;
}
if (sslsock->ssl_handle) {
SSL_free(sslsock->ssl_handle);
sslsock->ssl_handle = NULL;
}
+ }
+
+#ifdef HAVE_DTLS
+ /* The socket of a dtls:// server belongs to its port, which closes it with its last user */
+ if (php_openssl_dtls_detach(stream, sslsock)) {
+ sslsock->s.socket = SOCK_ERR;
+ }
+#endif
+
+ if (close_handle) {
if (sslsock->ctx) {
/* Clear the ctx stream pointer as the ctx may outlive this stream. */
if (SSL_CTX_get_ex_data(sslsock->ctx, php_openssl_get_ctx_stream_data_index()) == stream) {
@@ -3435,6 +3272,18 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{
}
}
+ if (sslsock->conn) {
+ php_openssl_conn_free(sslsock->conn);
+ sslsock->conn = NULL;
+ }
+ if (sslsock->inner) {
+ sslsock->inner->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE;
+ if (sslsock->inner->res) {
+ zend_list_delete(sslsock->inner->res);
+ }
+ sslsock->inner = NULL;
+ }
+
if (sslsock->sni_certs) {
for (i = 0; i < sslsock->sni_cert_count; i++) {
if (sslsock->sni_certs[i].ctx) {
@@ -3503,6 +3352,15 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{
static int php_openssl_sockop_flush(php_stream *stream) /* {{{ */
{
+ php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
+
+ if (sslsock->conn != NULL) {
+ php_openssl_conn_flush(sslsock->conn, NULL);
+ return 0;
+ }
+ if (sslsock->inner != NULL) {
+ return php_stream_flush(sslsock->inner);
+ }
return php_stream_socket_ops.flush(stream);
}
/* }}} */
@@ -3608,6 +3466,12 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
array_init(&tmp);
switch (SSL_version(sslsock->ssl_handle)) {
+#ifdef HAVE_DTLS13
+ case DTLS1_3_VERSION: proto_str = "DTLSv1.3"; break;
+#endif
+#ifdef HAVE_DTLS
+ case DTLS1_2_VERSION: proto_str = "DTLSv1.2"; break;
+#endif
#ifdef HAVE_TLS13
case TLS1_3_VERSION: proto_str = "TLSv1.3"; break;
#endif
@@ -3662,6 +3526,30 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
}
}
#endif
+ /* RFC 5705 exported keying material, requested with keying_material_label and
+ * keying_material_length */
+ {
+ zval *val;
+ char *km_label = NULL;
+ size_t km_label_len = 0;
+
+ GET_VER_OPT_STRINGL("keying_material_label", km_label, km_label_len);
+ if (km_label != NULL && GET_VER_OPT("keying_material_length")) {
+ zend_long km_len = zval_get_long(val);
+ if (km_len > 0 && km_len <= 1024) {
+ zend_string *km = zend_string_alloc((size_t) km_len, 0);
+ if (SSL_export_keying_material(sslsock->ssl_handle,
+ (unsigned char *) ZSTR_VAL(km), (size_t) km_len,
+ km_label, km_label_len, NULL, 0, 0) == 1) {
+ ZSTR_VAL(km)[km_len] = '\0';
+ add_assoc_str(&tmp, "keying_material", km);
+ } else {
+ zend_string_release(km);
+ }
+ }
+ }
+ }
+
add_assoc_zval((zval *)ptrparam, "crypto", &tmp);
}
@@ -3693,7 +3581,9 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
tv.tv_usec = 0;
}
- if (sslsock->s.socket == -1) {
+ if (sslsock->inner != NULL) {
+ alive = !sslsock->inner->eof;
+ } else if (sslsock->s.socket == -1) {
alive = 0;
} else if (
(
@@ -3707,94 +3597,19 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
/* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */
/* additionally, we don't use this optimization if SSL is active because in that case, we're not using MSG_DONTWAIT */
if (sslsock->ssl_active) {
- int retry = 1;
- struct timeval start_time;
- struct timeval *timeout = NULL;
- bool began_blocked = sslsock->s.is_blocked;
- bool has_timeout = false;
-
- /* never use a timeout with non-blocking sockets */
- if (began_blocked) {
- timeout = &tv;
- }
-
- if (timeout) {
- php_openssl_set_blocking(sslsock, 0);
- }
-
- if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) {
- has_timeout = true;
- /* gettimeofday is not monotonic; using it here is not strictly correct */
- gettimeofday(&start_time, NULL);
- }
-
- /* Main IO loop. */
- do {
- struct timeval cur_time, elapsed_time, left_time;
-
- /* If we have a timeout to check, figure out how much time has elapsed since we started. */
- if (has_timeout) {
- gettimeofday(&cur_time, NULL);
-
- /* Determine how much time we've taken so far. */
- elapsed_time = php_openssl_subtract_timeval(cur_time, start_time);
-
- /* and return an error if we've taken too long. */
- if (php_openssl_compare_timeval(elapsed_time, *timeout) > 0 ) {
- /* If the socket was originally blocking, set it back. */
- if (began_blocked) {
- php_openssl_set_blocking(sslsock, 1);
- }
- sslsock->s.timeout_event = true;
- return PHP_STREAM_OPTION_RETURN_ERR;
- }
- }
-
- int n = SSL_peek(sslsock->ssl_handle, &buf, sizeof(buf));
- /* If we didn't do anything on the last loop (or an error) check to see if we should retry or exit. */
- if (n <= 0) {
- /* Now, do the IO operation. Don't block if we can't complete... */
- int err = SSL_get_error(sslsock->ssl_handle, n);
- switch (err) {
- case SSL_ERROR_SYSCALL:
- retry = php_socket_errno() == EAGAIN;
- break;
- case SSL_ERROR_WANT_READ:
- case SSL_ERROR_WANT_WRITE:
- retry = 1;
- break;
- default:
- /* any other problem is a fatal error */
- retry = 0;
- }
-
- /* Don't loop indefinitely in non-blocking mode if no data is available */
- if (!began_blocked || !has_timeout) {
- alive = retry;
- break;
- }
-
- /* Now, if we have to wait some time, and we're supposed to be blocking, wait for the socket to become
- * available. Now, php_pollfd_for uses select to wait up to our time_left value only...
- */
- if (retry) {
- /* Now, how much time until we time out? */
- left_time = php_openssl_subtract_timeval(*timeout, elapsed_time);
- if (php_pollfd_for(sslsock->s.socket, PHP_POLLREADABLE|POLLPRI|POLLOUT, has_timeout ? &left_time : NULL) <= 0) {
- retry = 0;
- alive = 0;
- };
- }
- } else {
- retry = 0;
- alive = 1;
- }
- /* Finally, we keep going until there are any data or there is no time to wait. */
- } while (retry);
-
- if (began_blocked && !sslsock->s.is_blocked) {
- // Set it back to blocking
- php_openssl_set_blocking(sslsock, 1);
+ /* Peek through the TLS layer, waiting up to the timeout on a blocking
+ * stream; a transport that would block means the peer is there */
+ php_openssl_io_arg io = { &buf, 1 };
+ php_openssl_deadline dl;
+ bool timed_out;
+
+ /* A zero timeout asks without waiting, as the poll above did */
+ bool wait = sslsock->s.is_blocked && (tv.tv_sec > 0 || tv.tv_usec > 0);
+ php_openssl_deadline_init(&dl, &tv);
+ int n = php_openssl_pump(stream, sslsock, php_openssl_op_peek, &io,
+ wait ? &dl : NULL, &timed_out);
+ if (n <= 0) {
+ alive = !timed_out && errno == EAGAIN;
}
} else {
#ifdef PHP_WIN32
@@ -3819,6 +3634,22 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
return alive ? PHP_STREAM_OPTION_RETURN_OK : PHP_STREAM_OPTION_RETURN_ERR;
}
+ case PHP_STREAM_OPTION_BLOCKING:
+ if (sslsock->conn != NULL || sslsock->inner != NULL) {
+ /* The socket stays non-blocking under the transport, the mode is the stream's */
+ int oldmode = sslsock->s.is_blocked;
+ sslsock->s.is_blocked = value;
+ return oldmode;
+ }
+ break;
+
+ case PHP_STREAM_OPTION_READ_TIMEOUT:
+ if (sslsock->inner != NULL) {
+ sslsock->s.timeout = *(struct timeval*)ptrparam;
+ return php_stream_set_option(sslsock->inner, option, value, ptrparam);
+ }
+ break;
+
case PHP_STREAM_OPTION_CRYPTO_API:
switch(cparam->op) {
@@ -3846,7 +3677,12 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
case STREAM_XPORT_OP_CONNECT_ASYNC:
/* TODO: Async connects need to check the enable_on_connect option when
* we notice that the connect has actually been established */
- php_stream_socket_ops.set_option(stream, option, value, ptrparam);
+ if (sslsock->inner != NULL) {
+ /* The inner stream is connected already */
+ xparam->outputs.returncode = 0;
+ } else {
+ php_stream_socket_ops.set_option(stream, option, value, ptrparam);
+ }
if ((sslsock->enable_on_connect) &&
((xparam->outputs.returncode == 0) ||
@@ -3860,15 +3696,31 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
php_stream_from_zval_no_verify(session_stream, val);
}
- if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 ||
- php_stream_xport_crypto_enable(stream, 1) < 0) {
+ int enabled = php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0
+ ? -1 : php_stream_xport_crypto_enable(stream, 1);
+ if (enabled < 0 || (enabled == 0 && xparam->op != STREAM_XPORT_OP_CONNECT_ASYNC)) {
php_stream_warn(stream, ProtocolError, "Failed to enable crypto");
xparam->outputs.returncode = -1;
}
}
return PHP_STREAM_OPTION_RETURN_OK;
+#ifdef HAVE_DTLS
+ case STREAM_XPORT_OP_BIND:
+ php_stream_socket_ops.set_option(stream, option, value, ptrparam);
+ if (xparam->outputs.returncode == 0 && sslsock->s.is_dgram && sslsock->enable_on_connect) {
+ /* A dtls:// server: the port demultiplexes its peers from here on */
+ xparam->outputs.returncode = php_openssl_dtls_listen(stream, sslsock);
+ }
+ return PHP_STREAM_OPTION_RETURN_OK;
+#endif
+
case STREAM_XPORT_OP_LISTEN:
+ if (sslsock->s.is_dgram) {
+ /* A datagram socket has no backlog */
+ xparam->outputs.returncode = 0;
+ return PHP_STREAM_OPTION_RETURN_OK;
+ }
/* Do normal listen first */
xparam->outputs.returncode = php_stream_socket_ops.set_option(
stream, option, value, ptrparam);
@@ -3887,7 +3739,35 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
}
return PHP_STREAM_OPTION_RETURN_OK;
+#ifdef HAVE_DTLS
+ case STREAM_XPORT_OP_GET_PEER_NAME:
+ if (sslsock->port != NULL && sslsock->conn != NULL && sslsock->conn->peerlen > 0) {
+ php_network_populate_name_from_sockaddr((struct sockaddr *) &sslsock->conn->peer,
+ sslsock->conn->peerlen,
+ xparam->want_textaddr ? &xparam->outputs.textaddr : NULL,
+ xparam->want_addr ? &xparam->outputs.addr : NULL,
+ xparam->want_addr ? &xparam->outputs.addrlen : NULL);
+ xparam->outputs.returncode = 0;
+ return PHP_STREAM_OPTION_RETURN_OK;
+ }
+ break;
+
+ case STREAM_XPORT_OP_SHUTDOWN:
+ if (sslsock->port != NULL) {
+ /* The socket carries the other peers of the port */
+ xparam->outputs.returncode = 0;
+ return PHP_STREAM_OPTION_RETURN_OK;
+ }
+ break;
+#endif
+
case STREAM_XPORT_OP_ACCEPT:
+#ifdef HAVE_DTLS
+ if (sslsock->s.is_dgram) {
+ xparam->outputs.returncode = php_openssl_dtls_accept(stream, sslsock, xparam STREAMS_CC);
+ return PHP_STREAM_OPTION_RETURN_OK;
+ }
+#endif
/* we need to copy the additional fields that the underlying tcp transport
* doesn't know about */
xparam->outputs.returncode = php_openssl_tcp_sockop_accept(stream, sslsock, xparam STREAMS_CC);
@@ -3901,6 +3781,11 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
}
}
+ if (sslsock->inner != NULL && option == PHP_STREAM_OPTION_XPORT_API) {
+ /* Names, shutdown and the like belong to the inner stream */
+ return php_stream_set_option(sslsock->inner, option, value, ptrparam);
+ }
+
return php_stream_socket_ops.set_option(stream, option, value, ptrparam);
}
/* }}} */
@@ -3909,6 +3794,11 @@ static int php_openssl_sockop_cast(php_stream *stream, int castas, void **ret)
{
php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;
+ if (sslsock->inner != NULL && castas != PHP_STREAM_AS_FD_FOR_SELECT
+ && castas != PHP_STREAM_AS_FD_FOR_POLL) {
+ return FAILURE;
+ }
+
switch(castas) {
case PHP_STREAM_AS_STDIO:
if (sslsock->ssl_active) {
@@ -3925,21 +3815,31 @@ static int php_openssl_sockop_cast(php_stream *stream, int castas, void **ret)
case PHP_STREAM_AS_FD_FOR_SELECT:
if (ret) {
- size_t pending;
- if (stream->writepos == stream->readpos
- && sslsock->ssl_active
- && (pending = (size_t)SSL_pending(sslsock->ssl_handle)) > 0) {
- php_stream_fill_read_buffer(stream, pending < stream->chunk_size
+ if (stream->writepos == stream->readpos && sslsock->ssl_active) {
+ /* Bytes OpenSSL decrypted, and ciphertext queued below it, are not visible
+ * on the descriptor: move what forms a record into the stream buffer */
+ size_t pending = (size_t)SSL_pending(sslsock->ssl_handle);
+ if (pending > 0 || !php_openssl_conn_rx_empty(sslsock->conn)) {
+ sslsock->peek_only = 1;
+ php_stream_fill_read_buffer(stream, pending > 0 && pending < stream->chunk_size
? pending
: stream->chunk_size);
+ sslsock->peek_only = 0;
+ }
}
+ if (sslsock->inner != NULL) {
+ return php_stream_cast(sslsock->inner, castas, ret, 0);
+ }
*(php_socket_t *)ret = sslsock->s.socket;
}
return SUCCESS;
case PHP_STREAM_AS_FD_FOR_POLL:
/* Descriptor only, OpenSSL pending bytes stay put */
+ if (sslsock->inner != NULL) {
+ return php_stream_cast(sslsock->inner, castas, ret, 0);
+ }
if (ret) {
*(php_socket_t *)ret = sslsock->s.socket;
}
@@ -3982,6 +3882,38 @@ static const php_stream_ops php_openssl_socket_ops = {
php_openssl_sockop_set_option,
};
+const php_stream_ops php_openssl_dgram_socket_ops = {
+ php_openssl_sockop_write, php_openssl_sockop_read,
+ php_openssl_sockop_close, php_openssl_sockop_flush,
+ "udp_socket/dtls",
+ NULL, /* seek */
+ php_openssl_sockop_cast,
+ php_openssl_sockop_stat,
+ php_openssl_sockop_set_option,
+};
+
+void php_openssl_netstream_share_ctx(php_openssl_netstream_data_t *dst, const php_openssl_netstream_data_t *src)
+{
+ if (src->ctx) {
+ SSL_CTX_up_ref(src->ctx);
+ dst->ctx = src->ctx;
+ }
+ if (src->session_callbacks) {
+ src->session_callbacks->refcount++;
+ dst->session_callbacks = src->session_callbacks;
+ }
+ if (src->psk_callbacks) {
+ src->psk_callbacks->refcount++;
+ dst->psk_callbacks = src->psk_callbacks;
+ }
+#ifdef HAVE_TLS13
+ if (src->early_data_callbacks) {
+ src->early_data_callbacks->refcount++;
+ dst->early_data_callbacks = src->early_data_callbacks;
+ }
+#endif
+}
+
static zend_long php_openssl_get_crypto_method(
php_stream_context *ctx, zend_long crypto_method) /* {{{ */
{
@@ -4070,7 +4002,11 @@ php_stream *php_openssl_ssl_socket_factory(const char *proto, size_t protolen,
/* Initialize context as NULL */
sslsock->ctx = NULL;
- stream = php_stream_alloc_rel(&php_openssl_socket_ops, sslsock, persistent_id, "r+");
+ bool dgram = strncmp(proto, "udp", protolen) == 0 || strncmp(proto, "dtls", 4) == 0;
+ sslsock->s.is_dgram = dgram;
+
+ stream = php_stream_alloc_rel(dgram ? &php_openssl_dgram_socket_ops : &php_openssl_socket_ops,
+ sslsock, persistent_id, "r+");
if (stream == NULL) {
pefree(sslsock, persistent_id ? 1 : 0);
@@ -4130,6 +4066,71 @@ php_stream *php_openssl_ssl_socket_factory(const char *proto, size_t protolen,
php_stream_close(stream);
return NULL;
#endif
+ } else if (strncmp(proto, "dtls", protolen) == 0) {
+#ifdef HAVE_DTLS
+ sslsock->enable_on_connect = 1;
+ sslsock->method = php_openssl_get_crypto_method(context, STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT);
+#else
+ php_stream_warn(stream, ProtocolUnsupported,
+ "DTLS support is not compiled into the OpenSSL library against which PHP is linked");
+ php_stream_close(stream);
+ return NULL;
+#endif
+ } else if (strncmp(proto, "dtlsv1.2", protolen) == 0) {
+#ifdef HAVE_DTLS
+ sslsock->enable_on_connect = 1;
+ sslsock->method = STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT;
+#else
+ php_stream_warn(stream, ProtocolUnsupported,
+ "DTLS support is not compiled into the OpenSSL library against which PHP is linked");
+ php_stream_close(stream);
+ return NULL;
+#endif
+ } else if (strncmp(proto, "dtlsv1.3", protolen) == 0) {
+#ifdef HAVE_DTLS13
+ sslsock->enable_on_connect = 1;
+ sslsock->method = STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT;
+#else
+ php_stream_warn(stream, ProtocolUnsupported,
+ "DTLSv1.3 support is not compiled into the OpenSSL library against which PHP is linked");
+ php_stream_close(stream);
+ return NULL;
+#endif
+ }
+
+ /* The ciphertext of a crypto scheme may go through another stream instead of a socket of our
+ * own. The plain tcp:// and udp:// schemes always connect themselves. */
+ if (context && sslsock->enable_on_connect) {
+ zval *val = php_stream_context_get_option(context, "ssl", "inner_stream");
+ if (val != NULL && Z_TYPE_P(val) != IS_NULL) {
+ php_stream *inner = NULL;
+ if (Z_TYPE_P(val) == IS_RESOURCE) {
+ php_stream_from_zval_no_verify(inner, val);
+ }
+ if (inner == NULL || inner == stream) {
+ php_stream_warn(stream, Generic, "inner_stream must be an open stream");
+ php_stream_close(stream);
+ return NULL;
+ }
+ if (persistent_id) {
+ php_stream_warn(stream, PersistentNotSupported,
+ "inner_stream is not supported for persistent streams");
+ php_stream_close(stream);
+ return NULL;
+ }
+ /* One stream at a time: the flag below is the mark of the stream that holds it */
+ if (inner->flags & PHP_STREAM_FLAG_NO_FCLOSE) {
+ php_stream_warn(stream, Generic, "inner_stream is already used by another stream");
+ php_stream_close(stream);
+ return NULL;
+ }
+ if (inner->res) {
+ GC_ADDREF(inner->res);
+ }
+ /* Not from under the stream that carries its ciphertext through it */
+ inner->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
+ sslsock->inner = inner;
+ }
}
sslsock->url_name = php_openssl_get_url_name(resourcename, resourcenamelen, !!persistent_id, context);
diff --git a/ext/openssl/xp_ssl.h b/ext/openssl/xp_ssl.h
new file mode 100644
index 000000000000..7166607dcadf
--- /dev/null
+++ b/ext/openssl/xp_ssl.h
@@ -0,0 +1,263 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Wez Furlong |
+ | Daniel Lowrey |
+ | Chris Wright |
+ | Jakub Zelenka |
+ +----------------------------------------------------------------------+
+*/
+
+/* Shared between the TLS stream implementation (xp_ssl.c) and the DTLS server port (xp_dtls.c) */
+
+#ifndef PHP_OPENSSL_XP_SSL_H
+#define PHP_OPENSSL_XP_SSL_H
+
+#include "php.h"
+#include "php_network.h"
+#include "php_streams.h"
+#include "ext/standard/file.h"
+#include "streams/php_streams_int.h"
+#include "xp_bio.h"
+
+#include
+
+/* Flags for determining allowed stream crypto methods */
+#define STREAM_CRYPTO_IS_CLIENT (1<<0)
+#define STREAM_CRYPTO_METHOD_SSLv2 (1<<1)
+#define STREAM_CRYPTO_METHOD_SSLv3 (1<<2)
+#define STREAM_CRYPTO_METHOD_TLSv1_0 (1<<3)
+#define STREAM_CRYPTO_METHOD_TLSv1_1 (1<<4)
+#define STREAM_CRYPTO_METHOD_TLSv1_2 (1<<5)
+#define STREAM_CRYPTO_METHOD_TLSv1_3 (1<<6)
+#define STREAM_CRYPTO_METHOD_DTLSv1_2 (1<<7)
+#define STREAM_CRYPTO_METHOD_DTLSv1_3 (1<<8)
+
+#ifndef OPENSSL_NO_TLS1_METHOD
+#define HAVE_TLS1 1
+#endif
+
+#ifndef OPENSSL_NO_TLS1_1_METHOD
+#define HAVE_TLS11 1
+#endif
+
+#ifndef OPENSSL_NO_TLS1_2_METHOD
+#define HAVE_TLS12 1
+#endif
+
+#ifndef OPENSSL_NO_TLS1_3
+#define HAVE_TLS13 1
+#endif
+
+/* DTLS 1.0 is deprecated (RFC 8996) and never offered: the streams need DTLS 1.2 */
+#if !defined(OPENSSL_NO_DTLS) && !defined(OPENSSL_NO_DTLS1_2)
+#define HAVE_DTLS 1
+#endif
+
+/* The version constant is defined even when the protocol is disabled */
+#if defined(HAVE_DTLS) && defined(DTLS1_3_VERSION) && !defined(OPENSSL_NO_DTLS1_3)
+#define HAVE_DTLS13 1
+#endif
+
+#ifndef OPENSSL_NO_ECDH
+#define HAVE_ECDH 1
+#endif
+
+#ifndef OPENSSL_NO_TLSEXT
+#define HAVE_TLS_SNI 1
+#define HAVE_TLS_ALPN 1
+#endif
+
+#ifndef LIBRESSL_VERSION_NUMBER
+#define HAVE_SEC_LEVEL 1
+#endif
+
+#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
+#define HAVE_SSL3 1
+#define PHP_OPENSSL_MIN_PROTO_VERSION STREAM_CRYPTO_METHOD_SSLv3
+#else
+#define PHP_OPENSSL_MIN_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_0
+#endif
+#ifdef HAVE_TLS13
+#define PHP_OPENSSL_MAX_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_3
+#else
+#define PHP_OPENSSL_MAX_PROTO_VERSION STREAM_CRYPTO_METHOD_TLSv1_2
+#endif
+
+#define PHP_OPENSSL_MIN_DTLS_PROTO_VERSION STREAM_CRYPTO_METHOD_DTLSv1_2
+#ifdef HAVE_DTLS13
+#define PHP_OPENSSL_MAX_DTLS_PROTO_VERSION STREAM_CRYPTO_METHOD_DTLSv1_3
+#else
+#define PHP_OPENSSL_MAX_DTLS_PROTO_VERSION STREAM_CRYPTO_METHOD_DTLSv1_2
+#endif
+
+/* Simplify ssl context option retrieval */
+#define GET_VER_OPT(_name) \
+ (PHP_STREAM_CONTEXT(stream) && (val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", _name)) != NULL)
+#define GET_VER_OPT_STRING(_name, _str) \
+ do { \
+ if (GET_VER_OPT(_name)) { \
+ if (try_convert_to_string(val)) _str = Z_STRVAL_P(val); \
+ } \
+ } while (0)
+#define GET_VER_OPT_STRINGL(_name, _str, _len) \
+ do { \
+ if (GET_VER_OPT(_name)) { \
+ if (try_convert_to_string(val)) { \
+ _str = Z_STRVAL_P(val); \
+ _len = Z_STRLEN_P(val); \
+ } \
+ } \
+ } while (0)
+#define GET_VER_OPT_LONG(_name, _num) \
+ do { \
+ if (GET_VER_OPT(_name)) { \
+ _num = zval_get_long(val); \
+ } \
+ } while (0)
+
+/* Certificate contexts used for server-side SNI selection */
+typedef struct _php_openssl_sni_cert_t {
+ char *name;
+ SSL_CTX *ctx;
+} php_openssl_sni_cert_t;
+
+/* Provides leaky bucket handhsake renegotiation rate-limiting */
+typedef struct _php_openssl_handshake_bucket_t {
+ zend_long prev_handshake;
+ zend_long limit;
+ zend_long window;
+ float tokens;
+ unsigned should_close;
+} php_openssl_handshake_bucket_t;
+
+#ifdef HAVE_TLS_ALPN
+/* Holds the available server ALPN protocols for negotiation */
+typedef struct _php_openssl_alpn_ctx_t {
+ unsigned char *data;
+ unsigned short len;
+} php_openssl_alpn_ctx;
+#endif
+
+/* Holds PSK callbacks */
+typedef struct _php_openssl_psk_callbacks_t {
+ int refcount;
+ zend_fcall_info_cache client_cb;
+ zend_fcall_info_cache server_cb;
+} php_openssl_psk_callbacks_t;
+
+#ifdef HAVE_TLS13
+/* TLS 1.3 early data (0-RTT) handshake phase */
+typedef enum {
+ PHP_OPENSSL_EARLY_DATA_NONE = 0,
+ PHP_OPENSSL_EARLY_DATA_ACTIVE,
+ PHP_OPENSSL_EARLY_DATA_DONE,
+} php_openssl_early_data_state_t;
+
+/* Size of the buffer used to drain server-side early data chunk by chunk */
+#define PHP_OPENSSL_EARLY_DATA_CHUNK 16384
+
+/* Holds the server early data callback */
+typedef struct _php_openssl_early_data_callbacks_t {
+ int refcount;
+ zend_fcall_info_cache read_cb;
+} php_openssl_early_data_callbacks_t;
+#endif
+
+/* Holds session callback */
+typedef struct _php_openssl_session_callbacks_t {
+ int refcount;
+ zend_fcall_info_cache new_cb;
+ zend_fcall_info_cache get_cb;
+ zend_fcall_info_cache remove_cb;
+} php_openssl_session_callbacks_t;
+
+/* The DTLS server port: the socket, the demultiplexer and the connections of a dtls:// server */
+typedef struct _php_openssl_port php_openssl_port;
+
+/* This implementation is very closely tied to the that of the native
+ * sockets implemented in the core.
+ * Don't try this technique in other extensions!
+ * */
+typedef struct _php_openssl_netstream_data_t {
+ php_netstream_data_t s;
+ SSL *ssl_handle;
+ SSL_CTX *ctx;
+ /* The ciphertext queues and transport, once crypto is set up */
+ php_openssl_conn *conn;
+ /* The inner stream carrying the ciphertext instead of a socket of our own */
+ php_stream *inner;
+ /* The DTLS server port of a listener or of a connection it accepted */
+ php_openssl_port *port;
+ struct timeval connect_timeout;
+ int enable_on_connect;
+ int is_client;
+ int ssl_active;
+ int last_status;
+ php_stream_xport_crypt_method_t method;
+ php_openssl_handshake_bucket_t *reneg;
+ php_openssl_sni_cert_t *sni_certs;
+ unsigned sni_cert_count;
+#ifdef HAVE_TLS_ALPN
+ php_openssl_alpn_ctx alpn_ctx;
+#endif
+ php_openssl_session_callbacks_t *session_callbacks;
+ php_openssl_psk_callbacks_t *psk_callbacks;
+ /* Identity buffer for TLS 1.3 client PSK whose lifetime outlives the
+ * psk_use_session_cb call but OpenSSL doesn't free it, so we own it. */
+ unsigned char *psk_identity_buf;
+ size_t psk_identity_len;
+#ifdef HAVE_TLS13
+ /* TLS 1.3 early data (0-RTT) */
+ php_openssl_early_data_callbacks_t *early_data_callbacks;
+ /* Client payload to send as early data, borrowed for the handshake */
+ zend_string *early_data_send;
+ size_t early_data_offset;
+ php_openssl_early_data_state_t early_data_state;
+#endif
+ char *url_name;
+ unsigned state_set:1;
+ /* The next IO call must not wait: it only moves what is already there */
+ unsigned peek_only:1;
+ unsigned _spare:30;
+} php_openssl_netstream_data_t;
+
+/* xp_ssl.c, for the DTLS port */
+
+extern const php_stream_ops php_openssl_dgram_socket_ops;
+
+int php_openssl_get_ssl_stream_data_index(void);
+/* Builds the SSL_CTX of the stream from its context options */
+zend_result php_openssl_create_server_ctx(php_stream *stream, php_openssl_netstream_data_t *sslsock,
+ int method_flags);
+/* Path MTU discovery on a datagram socket */
+void php_openssl_dgram_socket_setup(php_socket_t fd);
+/* Shares the context and the callbacks of src with dst, counting the references */
+void php_openssl_netstream_share_ctx(php_openssl_netstream_data_t *dst, const php_openssl_netstream_data_t *src);
+/* Captures and verifies the peer certificate after a handshake; 1 when the connection may be
+ * used, -1 when it was rejected */
+int php_openssl_handshake_complete(php_stream *stream, php_openssl_netstream_data_t *sslsock);
+/* Logs an SSL error of the connection; true when the call should be retried */
+bool php_openssl_handle_ssl_error(php_stream *stream, php_openssl_netstream_data_t *sslsock, int nr_bytes);
+
+/* xp_dtls.c */
+
+#ifdef HAVE_DTLS
+/* Sets up the port of a bound dtls:// server stream; -1 on failure */
+int php_openssl_dtls_listen(php_stream *stream, php_openssl_netstream_data_t *sslsock);
+/* Accepts the next connection of the port as a new stream in xparam->outputs.client */
+int php_openssl_dtls_accept(php_stream *stream, php_openssl_netstream_data_t *sslsock,
+ php_stream_xport_param *xparam STREAMS_DC);
+/* Detaches a listener or an accepted connection from its port; true when the socket is the
+ * port's and must not be closed by the stream */
+bool php_openssl_dtls_detach(php_stream *stream, php_openssl_netstream_data_t *sslsock);
+#endif
+
+#endif /* PHP_OPENSSL_XP_SSL_H */
diff --git a/ext/sockets/tests/bug_export_stream_type.phpt b/ext/sockets/tests/bug_export_stream_type.phpt
index b9dcbf63ce77..7612db52d125 100644
--- a/ext/sockets/tests/bug_export_stream_type.phpt
+++ b/ext/sockets/tests/bug_export_stream_type.phpt
@@ -6,7 +6,7 @@ sockets
--EXPECT--
udp_socket
diff --git a/ext/standard/file.stub.php b/ext/standard/file.stub.php
index d7b1fef17cdc..6314658b7999 100644
--- a/ext/standard/file.stub.php
+++ b/ext/standard/file.stub.php
@@ -184,6 +184,21 @@
* @cvalue STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT
*/
const STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT
+ */
+const STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT
+ */
+const STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT
+ */
+const STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT = UNKNOWN;
/**
* @var int
* @cvalue STREAM_CRYPTO_METHOD_ANY_SERVER
@@ -229,6 +244,21 @@
* @cvalue STREAM_CRYPTO_METHOD_TLSv1_3_SERVER
*/
const STREAM_CRYPTO_METHOD_TLSv1_3_SERVER = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER
+ */
+const STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER
+ */
+const STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER = UNKNOWN;
+/**
+ * @var int
+ * @cvalue STREAM_CRYPTO_METHOD_DTLS_ANY_SERVER
+ */
+const STREAM_CRYPTO_METHOD_DTLS_ANY_SERVER = UNKNOWN;
/**
* @var int
diff --git a/ext/standard/file_arginfo.h b/ext/standard/file_arginfo.h
index dd8a64910be8..e21fbafaae4d 100644
--- a/ext/standard/file_arginfo.h
+++ b/ext/standard/file_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit file.stub.php instead.
- * Stub hash: 0c62c6fb217a87010a9e2e63d4b104cde0138655 */
+ * Stub hash: e04b5fa35afddb17121b8c10713c619b0b2fb23a */
#include "zend_attributes.h"
#include "zend_constants.h"
@@ -41,6 +41,9 @@ static void register_file_symbols(int module_number)
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT", STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT", STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT", STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT", STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT", STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT", STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_ANY_SERVER", STREAM_CRYPTO_METHOD_ANY_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_SSLv2_SERVER", STREAM_CRYPTO_METHOD_SSLv2_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_SSLv3_SERVER", STREAM_CRYPTO_METHOD_SSLv3_SERVER, CONST_PERSISTENT);
@@ -50,6 +53,9 @@ static void register_file_symbols(int module_number)
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_1_SERVER", STREAM_CRYPTO_METHOD_TLSv1_1_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_2_SERVER", STREAM_CRYPTO_METHOD_TLSv1_2_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_TLSv1_3_SERVER", STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER", STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER", STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER, CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("STREAM_CRYPTO_METHOD_DTLS_ANY_SERVER", STREAM_CRYPTO_METHOD_DTLS_ANY_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_SSLv3", STREAM_CRYPTO_METHOD_SSLv3_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_TLSv1_0", STREAM_CRYPTO_METHOD_TLSv1_0_SERVER, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("STREAM_CRYPTO_PROTO_TLSv1_1", STREAM_CRYPTO_METHOD_TLSv1_1_SERVER, CONST_PERSISTENT);
diff --git a/main/php_network.h b/main/php_network.h
index 6904f7603951..401299295478 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -525,6 +525,8 @@ struct _php_netstream_data_t {
bool timeout_event;
struct timeval timeout;
size_t ownsize;
+ /* Datagram socket transport: the generic ops connect/bind with SOCK_DGRAM. */
+ bool is_dgram;
};
typedef struct _php_netstream_data_t php_netstream_data_t;
PHPAPI extern const php_stream_ops php_stream_socket_ops;
diff --git a/main/streams/php_stream_transport.h b/main/streams/php_stream_transport.h
index 1ee6a840cfd1..6c6d9b7087bf 100644
--- a/main/streams/php_stream_transport.h
+++ b/main/streams/php_stream_transport.h
@@ -172,6 +172,9 @@ typedef enum {
STREAM_CRYPTO_METHOD_TLS_CLIENT = ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | 1),
STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT = ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | 1),
STREAM_CRYPTO_METHOD_ANY_CLIENT = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | 1),
+ STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT = (1 << 7 | 1),
+ STREAM_CRYPTO_METHOD_DTLSv1_3_CLIENT = (1 << 8 | 1),
+ STREAM_CRYPTO_METHOD_DTLS_ANY_CLIENT = ((1 << 7) | (1 << 8) | 1),
STREAM_CRYPTO_METHOD_SSLv2_SERVER = (1 << 1),
STREAM_CRYPTO_METHOD_SSLv3_SERVER = (1 << 2),
/* v23 no longer negotiates SSL2 or SSL3 */
@@ -183,7 +186,10 @@ typedef enum {
/* TLS equates to TLS_ANY as of PHP 7.2 */
STREAM_CRYPTO_METHOD_TLS_SERVER = ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 6)),
STREAM_CRYPTO_METHOD_TLS_ANY_SERVER = ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 6)),
- STREAM_CRYPTO_METHOD_ANY_SERVER = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6))
+ STREAM_CRYPTO_METHOD_ANY_SERVER = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6)),
+ STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = (1 << 7),
+ STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER = (1 << 8),
+ STREAM_CRYPTO_METHOD_DTLS_ANY_SERVER = ((1 << 7) | (1 << 8))
} php_stream_xport_crypt_method_t;
/* Flags for crypto status */
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index 221552056420..b0b5a53bced3 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -56,7 +56,9 @@ static const php_stream_ops php_stream_unixdg_socket_ops;
#define PHP_STREAM_XPORT_IS_UNIX_ST(stream) false
#define PHP_STREAM_XPORT_IS_UNIX(stream) false
#endif
-#define PHP_STREAM_XPORT_IS_UDP(stream) (php_stream_is(stream, &php_stream_udp_socket_ops))
+#define PHP_STREAM_XPORT_IS_UDP(stream) \
+ (php_stream_is(stream, &php_stream_udp_socket_ops) \
+ || ((php_netstream_data_t *)(stream)->abstract)->is_dgram)
#define PHP_STREAM_XPORT_IS_TCP(stream) (!PHP_STREAM_XPORT_IS_UNIX(stream) && !PHP_STREAM_XPORT_IS_UDP(stream))
static int php_tcp_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam);