From 5603f5be27b1183cf211d2f60686dcedf23c4365 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 6 Nov 2026 12:39:59 +0100 Subject: [PATCH 1/8] Add DTLS stream transport to ext/openssl This adds the dtls://, dtlsv1.2:// and dtlsv1.3:// stream transports (client and server) and DTLS over udp:// through stream_socket_enable_crypto(). The ssl context options, Openssl\Session and the stream API work for DTLS as they do for tls://. The implementation is split by layer so that TLS and DTLS share the option handling and the IO loops: - xp_bio.c is the ciphertext transport. The BIO serves records from a receive queue and appends to a send queue owned by the connection, and the stream moves ciphertext between the queues and the transport around each SSL call. The transport is the socket of the stream or any other stream given by the new inner_stream context option. - xp_ssl.c is one stream implementation for TLS and DTLS. The datagram schemes select the DTLS methods and version range, everything else is shared. - xp_dtls.c is the server port. One UDP socket carries every peer of a server. The port routes the datagrams by peer address, runs the handshakes of new peers with a cookie exchange and hands the completed connections to stream_socket_accept(). It uses neither DTLSv1_listen() nor the OpenSSL listener API so it works with all supported OpenSSL versions. New ssl context options are inner_stream, dtls_link_mtu, dtls_max_pending, dtls_pending_timeout, keying_material_label and keying_material_length. New constants are STREAM_CRYPTO_METHOD_DTLSv1_2_*, STREAM_CRYPTO_METHOD_DTLSv1_3_* and STREAM_CRYPTO_METHOD_DTLS_ANY_*. Closes GH-22638 Co-authored-by: Gianfrancesco --- ext/openssl/config.w32 | 2 +- ext/openssl/config0.m4 | 2 +- ext/openssl/openssl.c | 26 +- ext/openssl/php_openssl.h | 2 + ext/openssl/tests/dtls13_basic.phpt | 51 + ext/openssl/tests/dtls13_session_resume.phpt | 87 ++ ext/openssl/tests/dtls_client_basic.phpt | 110 ++ ext/openssl/tests/dtls_client_cert.phpt | 86 ++ .../tests/dtls_client_fingerprint.phpt | 88 ++ .../tests/dtls_client_nonblocking.phpt | 64 + ext/openssl/tests/dtls_client_timeout.phpt | 27 + ext/openssl/tests/dtls_client_verify.phpt | 91 ++ ext/openssl/tests/dtls_keying_material.phpt | 57 + .../tests/dtls_local_pk_passphrase.phpt | 97 ++ ext/openssl/tests/dtls_mtu.phpt | 54 + .../tests/dtls_peer_fingerprint_invalid.phpt | 88 ++ ext/openssl/tests/dtls_peer_name.phpt | 86 ++ ext/openssl/tests/dtls_server.phpt | 52 + ext/openssl/tests/dtls_server_multi_peer.phpt | 74 + ext/openssl/tests/dtls_server_robustness.phpt | 60 + ext/openssl/tests/dtls_server_verify.phpt | 73 + ext/openssl/tests/dtls_session_resume.phpt | 82 ++ .../tests/dtls_session_resume_server.phpt | 95 ++ ext/openssl/tests/dtls_skipif.inc | 23 + ext/openssl/tests/dtls_udp_enable_crypto.phpt | 52 + ext/openssl/tests/tls_inner_stream.phpt | 68 + .../tests/tls_inner_stream_shared.phpt | 74 + .../tests/tls_inner_stream_user_wrapper.phpt | 82 ++ ext/openssl/xp_bio.c | 868 ++++++++++++ ext/openssl/xp_bio.h | 170 +++ ext/openssl/xp_dtls.c | 657 +++++++++ ext/openssl/xp_ssl.c | 1192 ++++++++--------- ext/openssl/xp_ssl.h | 263 ++++ ext/sockets/tests/bug_export_stream_type.phpt | 2 +- ext/standard/file.stub.php | 30 + ext/standard/file_arginfo.h | 8 +- main/php_network.h | 2 + main/streams/php_stream_transport.h | 8 +- main/streams/xp_socket.c | 4 +- 39 files changed, 4354 insertions(+), 603 deletions(-) create mode 100644 ext/openssl/tests/dtls13_basic.phpt create mode 100644 ext/openssl/tests/dtls13_session_resume.phpt create mode 100644 ext/openssl/tests/dtls_client_basic.phpt create mode 100644 ext/openssl/tests/dtls_client_cert.phpt create mode 100644 ext/openssl/tests/dtls_client_fingerprint.phpt create mode 100644 ext/openssl/tests/dtls_client_nonblocking.phpt create mode 100644 ext/openssl/tests/dtls_client_timeout.phpt create mode 100644 ext/openssl/tests/dtls_client_verify.phpt create mode 100644 ext/openssl/tests/dtls_keying_material.phpt create mode 100644 ext/openssl/tests/dtls_local_pk_passphrase.phpt create mode 100644 ext/openssl/tests/dtls_mtu.phpt create mode 100644 ext/openssl/tests/dtls_peer_fingerprint_invalid.phpt create mode 100644 ext/openssl/tests/dtls_peer_name.phpt create mode 100644 ext/openssl/tests/dtls_server.phpt create mode 100644 ext/openssl/tests/dtls_server_multi_peer.phpt create mode 100644 ext/openssl/tests/dtls_server_robustness.phpt create mode 100644 ext/openssl/tests/dtls_server_verify.phpt create mode 100644 ext/openssl/tests/dtls_session_resume.phpt create mode 100644 ext/openssl/tests/dtls_session_resume_server.phpt create mode 100644 ext/openssl/tests/dtls_skipif.inc create mode 100644 ext/openssl/tests/dtls_udp_enable_crypto.phpt create mode 100644 ext/openssl/tests/tls_inner_stream.phpt create mode 100644 ext/openssl/tests/tls_inner_stream_shared.phpt create mode 100644 ext/openssl/tests/tls_inner_stream_user_wrapper.phpt create mode 100644 ext/openssl/xp_bio.c create mode 100644 ext/openssl/xp_bio.h create mode 100644 ext/openssl/xp_dtls.c create mode 100644 ext/openssl/xp_ssl.h 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_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_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_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/xp_bio.c b/ext/openssl/xp_bio.c new file mode 100644 index 000000000000..16e98eb8051e --- /dev/null +++ b/ext/openssl/xp_bio.c @@ -0,0 +1,868 @@ +/* + +----------------------------------------------------------------------+ + | 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++; +} + +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--; + } + 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; + } +#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; + + ssize_t n = php_openssl_xport_recv_dgram(conn->xport, buf, sizeof(buf), &peer, &peerlen); + if (n < 0) { + return -1; + } + 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..d967e00cdbe1 --- /dev/null +++ b/ext/openssl/xp_bio.h @@ -0,0 +1,170 @@ +/* + +----------------------------------------------------------------------+ + | 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; +} 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..a88e4b9f76b2 --- /dev/null +++ b/ext/openssl/xp_dtls.c @@ -0,0 +1,657 @@ +/* + +----------------------------------------------------------------------+ + | 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 HAVE_DTLS + +#define PHP_OPENSSL_DTLS_MAX_PENDING 256 +#define PHP_OPENSSL_DTLS_PENDING_TIMEOUT 30 +/* Datagrams queued for a connection nobody is reading before the port drops the next */ +#define PHP_OPENSSL_DTLS_RX_LIMIT 256 +/* 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; + /* 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; + 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); + 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); + return CRYPTO_memcmp(mac, cookie + PHP_OPENSSL_DTLS_COOKIE_TS_LEN, sizeof(mac)) == 0; +} + +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); + port->pending_count--; + 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 (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_long) port->pending_count >= port->max_pending) { + zend_string_release(key); + return 1; + } + 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.count >= 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; + 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; + + 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..bba37d43a3b4 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) || @@ -3868,7 +3704,22 @@ 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_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 +3738,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 +3780,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 +3793,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 +3814,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 +3881,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 +4001,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 +4065,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); From fab9e726e65328a782e55bf5e92e3b2ef1f61c8a Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 12:26:15 +0100 Subject: [PATCH 2/8] Ignore empty datagrams in the DTLS transport A zero-length datagram reached openssl as a 0-byte read, which it takes as eof: anyone able to send from the peer's address could end a dtls:// connection with one. Drop them on receive, on the client connection and on the server port, as rfc9147 4.5.2 asks for invalid records. --- ext/openssl/tests/dtls_empty_datagram.phpt | 55 ++++++++++++++++++++++ ext/openssl/xp_bio.c | 16 +++++-- ext/openssl/xp_dtls.c | 4 ++ 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 ext/openssl/tests/dtls_empty_datagram.phpt 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/xp_bio.c b/ext/openssl/xp_bio.c index 16e98eb8051e..9a46c07b9515 100644 --- a/ext/openssl/xp_bio.c +++ b/ext/openssl/xp_bio.c @@ -812,12 +812,18 @@ static int php_openssl_conn_recv_dgram(php_openssl_conn *conn) php_sockaddr_storage peer; socklen_t peerlen; - ssize_t n = php_openssl_xport_recv_dgram(conn->xport, buf, sizeof(buf), &peer, &peerlen); - if (n < 0) { - return -1; + 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; } - 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) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index a88e4b9f76b2..bcc3b6bbf1b4 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -385,6 +385,10 @@ static int php_openssl_port_recv(php_openssl_port *port) 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)); From c1f46d7a573c2adcb0d2f351d6a83bc1d3c071ff Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 13:15:10 +0100 Subject: [PATCH 3/8] Bound the memory DTLS peers that are not accepted can take Handshakes that completed but were not accepted were not counted in dtls_max_pending and never expired, and each peer queued up to 256 datagrams of 18 KB: a remote client could open peers from many source ports until the server ran out of memory. Count them until they are accepted and bound the receive queue by its size instead. --- .../tests/dtls_server_unaccepted_memory.phpt | 71 +++++++++++++++++++ ext/openssl/xp_bio.c | 2 + ext/openssl/xp_bio.h | 2 + ext/openssl/xp_dtls.c | 10 +-- 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 ext/openssl/tests/dtls_server_unaccepted_memory.phpt 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..529fa26f9547 --- /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', '16M'); + $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', 18432); + $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/xp_bio.c b/ext/openssl/xp_bio.c index 9a46c07b9515..eea7e15ec368 100644 --- a/ext/openssl/xp_bio.c +++ b/ext/openssl/xp_bio.c @@ -171,6 +171,7 @@ static void php_openssl_dgram_queue_push(php_openssl_dgram_queue *q, php_openssl } 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) @@ -182,6 +183,7 @@ static php_openssl_dgram *php_openssl_dgram_queue_pop(php_openssl_dgram_queue *q q->tail = NULL; } q->count--; + q->size -= sizeof(*dg) + dg->len; } return dg; } diff --git a/ext/openssl/xp_bio.h b/ext/openssl/xp_bio.h index d967e00cdbe1..799ea0fd64cd 100644 --- a/ext/openssl/xp_bio.h +++ b/ext/openssl/xp_bio.h @@ -70,6 +70,8 @@ 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; diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index bcc3b6bbf1b4..3377d4ddfde3 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -36,8 +36,8 @@ #define PHP_OPENSSL_DTLS_MAX_PENDING 256 #define PHP_OPENSSL_DTLS_PENDING_TIMEOUT 30 -/* Datagrams queued for a connection nobody is reading before the port drops the next */ -#define PHP_OPENSSL_DTLS_RX_LIMIT 256 +/* 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 @@ -73,6 +73,7 @@ struct _php_openssl_port { /* 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; @@ -114,6 +115,7 @@ static void php_openssl_dtls_peer_free(php_openssl_port *port, php_openssl_dtls_ 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) { @@ -254,7 +256,6 @@ static void php_openssl_dtls_peer_drive(php_openssl_port *port, php_openssl_dtls if (n > 0) { php_openssl_dtls_peer_unlink(&port->pending, peer); - port->pending_count--; peer->state = PHP_OPENSSL_DTLS_PEER_READY; if (port->ready_tail) { port->ready_tail->next = peer; @@ -410,7 +411,7 @@ static int php_openssl_port_recv(php_openssl_port *port) } zend_string_release(key); - if (peer->conn->rxq.count >= PHP_OPENSSL_DTLS_RX_LIMIT) { + 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); @@ -584,6 +585,7 @@ int php_openssl_dtls_accept(php_stream *stream, php_openssl_netstream_data_t *ss } 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)); From 642ec03851877e28fd5ab176dace757798f32ef2 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 14:41:57 +0100 Subject: [PATCH 4/8] Fix test Bound the memory DTLS peers that are not accepted can take in macOS e FreeBSD max udp datagram size is 9216 byte --- ext/openssl/tests/dtls_server_unaccepted_memory.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/openssl/tests/dtls_server_unaccepted_memory.phpt b/ext/openssl/tests/dtls_server_unaccepted_memory.phpt index 529fa26f9547..f82f61d65a15 100644 --- a/ext/openssl/tests/dtls_server_unaccepted_memory.phpt +++ b/ext/openssl/tests/dtls_server_unaccepted_memory.phpt @@ -14,7 +14,7 @@ include 'CertificateGenerator.inc'; (new CertificateGenerator())->saveNewCertAsFileWithKey('dtls-server', $certFile); $serverCode = <<<'CODE' - ini_set('memory_limit', '16M'); + 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); @@ -33,7 +33,7 @@ $clientCode = <<<'CODE' // 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', 18432); + $big = str_repeat('A', 8192); $flood = []; $handshakes = 0; for ($i = 0; $i < 8; $i++) { From e4f2a7153e9298403d6f1b9f7c6de7d1c21ccc85 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 14:47:26 +0100 Subject: [PATCH 5/8] Fail a TLS connect over an inner stream that would block The handshake doesn't wait for an internal stream, so an operation with no data to read leaves it unfinished, and the connection returns a stream with encryption disabled: read and write operations are then performed via the cleartext internal stream. A non-asynchronous connection now fails as if the handshake failed --- .../tests/tls_inner_stream_would_block.phpt | 107 ++++++++++++++++++ ext/openssl/xp_ssl.c | 5 +- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 ext/openssl/tests/tls_inner_stream_would_block.phpt 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..7bd0b73a8799 --- /dev/null +++ b/ext/openssl/tests/tls_inner_stream_would_block.phpt @@ -0,0 +1,107 @@ +--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); +for ($i = 0; $i < 200; $i++) { + $client = stream_socket_enable_crypto($tls, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + $srv = stream_socket_enable_crypto($peer, true, STREAM_CRYPTO_METHOD_TLS_SERVER); + if ($client === true && $srv === true) { + break; + } + 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_ssl.c b/ext/openssl/xp_ssl.c index bba37d43a3b4..70c5817c48bf 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3696,8 +3696,9 @@ 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; } From 4128cd125cbf27dadad906fcc126c64de0b3b7c8 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 16:12:18 +0100 Subject: [PATCH 6/8] Retry ENOTCONN while an asynchronous connect is in progress macOS fails to send on a socket still connected with ENOTCONN, which the transport interpreted as an error: the asynchronous TLS connection handshake failed when it was performed before the connection was complete, as in bug48182.phpt. The OpenSSL socket BIO retries, as it does here. --- ext/openssl/tests/tls_inner_stream_would_block.phpt | 12 +++++++----- ext/openssl/xp_bio.c | 10 ++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ext/openssl/tests/tls_inner_stream_would_block.phpt b/ext/openssl/tests/tls_inner_stream_would_block.phpt index 7bd0b73a8799..30a3fec2c46f 100644 --- a/ext/openssl/tests/tls_inner_stream_would_block.phpt +++ b/ext/openssl/tests/tls_inner_stream_would_block.phpt @@ -73,11 +73,13 @@ stream_set_blocking($tls, false); $peer = stream_socket_accept($server); stream_set_blocking($peer, false); stream_context_set_option($peer, 'ssl', 'local_cert', $certFile); -for ($i = 0; $i < 200; $i++) { - $client = stream_socket_enable_crypto($tls, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); - $srv = stream_socket_enable_crypto($peer, true, STREAM_CRYPTO_METHOD_TLS_SERVER); - if ($client === true && $srv === true) { - break; +$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); } diff --git a/ext/openssl/xp_bio.c b/ext/openssl/xp_bio.c index eea7e15ec368..86a570997cc8 100644 --- a/ext/openssl/xp_bio.c +++ b/ext/openssl/xp_bio.c @@ -271,6 +271,16 @@ static int php_openssl_xport_errno(void) 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; From a98fee34b25e2158c7754249f74abafc5b206267 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 16:57:35 +0100 Subject: [PATCH 7/8] Do not let unverified DTLS peers hold every pending slot A peer occupied a pending slot on its first ClientHello before returning the cookie and held it until the dtls_pending_timeout: a ClientHello per slot every 30 seconds, from spoofed addresses, prevented all other clients from connecting. When the slots are full, the oldest peer that hasn't yet returned the cookie makes room for the new one. --- .../tests/dtls_server_pending_cookie.phpt | 69 +++++++++++++++++++ ext/openssl/xp_dtls.c | 23 ++++++- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 ext/openssl/tests/dtls_server_pending_cookie.phpt 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/xp_dtls.c b/ext/openssl/xp_dtls.c index 3377d4ddfde3..4e179cc8f5d7 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -57,6 +57,8 @@ typedef struct _php_openssl_dtls_peer { 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; @@ -204,7 +206,11 @@ static int php_openssl_dtls_cookie_verify(SSL *ssl, const unsigned char *cookie, unsigned char mac[PHP_OPENSSL_DTLS_COOKIE_MAC_LEN]; php_openssl_dtls_cookie_mac(peer->port, peer->conn, cookie, mac); - return CRYPTO_memcmp(mac, cookie + PHP_OPENSSL_DTLS_COOKIE_TS_LEN, sizeof(mac)) == 0; + 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) @@ -399,10 +405,23 @@ static int php_openssl_port_recv(php_openssl_port *port) 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_long) port->pending_count >= port->max_pending) { + 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); From 17e9629ea4dc1eba87e560650949a7ffe270dc61 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 9 Nov 2026 18:13:42 +0100 Subject: [PATCH 8/8] Ignore ICMP port unreachable on the DTLS server socket on Windows Windows fails to execute the next recvfrom() call on a UDP socket with WSAECONNRESET after a sent datagram is returned as an ICMP port unreachable, such as a close notification to a client that has already disconnected. The socket is the port of every peer, so accept() fails for the next client. Disabled with SIO_UDP_CONNRESET, since Linux never reports this on an unconnected socket. --- ext/openssl/xp_dtls.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 4e179cc8f5d7..abda83316210 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -31,6 +31,12 @@ #include #include #include +#ifdef PHP_WIN32 +#include +# ifndef SIO_UDP_CONNRESET +# define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12) +# endif +#endif #ifdef HAVE_DTLS @@ -520,6 +526,13 @@ int php_openssl_dtls_listen(php_stream *stream, php_openssl_netstream_data_t *ss 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);