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