diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 index 4b7f4b8b8565..7fd8cc93c345 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_common.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..1f0b72f9575b 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_common.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 1e63eb1381f6..83e95863832b 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -830,6 +830,12 @@ 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); +#ifndef OPENSSL_NO_DTLS + php_stream_xport_register("dtls", php_openssl_dtls_socket_factory); + php_stream_xport_register("dtlsv1.2", php_openssl_dtls_socket_factory); + php_stream_xport_register("udp", php_openssl_dtls_socket_factory); +#endif + /* override the default tcp socket provider */ php_stream_xport_register("tcp", php_openssl_ssl_socket_factory); @@ -904,6 +910,13 @@ PHP_MSHUTDOWN_FUNCTION(openssl) php_stream_xport_unregister("tlsv1.2"); php_stream_xport_unregister("tlsv1.3"); +#ifndef OPENSSL_NO_DTLS + php_stream_xport_unregister("dtls"); + php_stream_xport_unregister("dtlsv1.2"); + /* reinstate the default udp handler */ + php_stream_xport_register("udp", php_stream_generic_socket_factory); +#endif + /* reinstate the default tcp handler */ php_stream_xport_register("tcp", php_stream_generic_socket_factory); @@ -2755,7 +2768,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 b88a6c59e4e9..76dbc9b94875 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -97,6 +97,7 @@ ZEND_TSRMLS_CACHE_EXTERN(); #endif php_stream_transport_factory_func php_openssl_ssl_socket_factory; +php_stream_transport_factory_func php_openssl_dtls_socket_factory; void php_openssl_store_errors(void); void php_openssl_errors_set_mark(void); diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt new file mode 100644 index 000000000000..5d5459abc41c --- /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]]); +$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..35a69e81b116 --- /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, + '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]]); +$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..bd9a15d9fad9 --- /dev/null +++ b/ext/openssl/tests/dtls_client_fingerprint.phpt @@ -0,0 +1,81 @@ +--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' => [ + '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' => [ + 'peer_fingerprint' => ['sha256' => str_repeat('00', 32)], +]]); +$client = @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx); +var_dump($client === false); +var_dump(str_contains($errstr, 'fingerprint')); +stop_server($proc, $pipes); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/ext/openssl/tests/dtls_client_timeout.phpt b/ext/openssl/tests/dtls_client_timeout.phpt new file mode 100644 index 000000000000..66ba1735dce5 --- /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]])); +$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..32c27f2e02f9 --- /dev/null +++ b/ext/openssl/tests/dtls_client_verify.phpt @@ -0,0 +1,88 @@ +--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, +]]); +$client = @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx); +var_dump($client === false); +var_dump(str_contains($errstr, 'verify')); +stop_server($proc, $pipes); + +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/ext/openssl/tests/dtls_keying_material.phpt b/ext/openssl/tests/dtls_keying_material.phpt new file mode 100644 index 000000000000..87d1c22c202b --- /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, + '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..65134e8412c2 --- /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, + '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..fb53e8822daa --- /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, '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..33c617f1bbd1 --- /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, + '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..9a65a8f6a717 --- /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]]); + $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_robustness.phpt b/ext/openssl/tests/dtls_server_robustness.phpt new file mode 100644 index 000000000000..8b7ded9b839e --- /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 DTLSv1_listen loop 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]]); + $client = stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $ctx); + var_dump($client !== false); + fwrite($client, "ping"); + var_dump(fread($client, 8192)); + fclose($client); +CODE; + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +string(4) "PING" diff --git a/ext/openssl/tests/dtls_server_verify.phpt b/ext/openssl/tests/dtls_server_verify.phpt new file mode 100644 index 000000000000..c2177d6c121a --- /dev/null +++ b/ext/openssl/tests/dtls_server_verify.phpt @@ -0,0 +1,71 @@ +--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]]); + $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 handshake is rejected. +$clientNoCert = <<<'CODE' + $ctx = stream_context_create(['ssl' => ['verify_peer' => false]]); + $client = @stream_socket_client('dtls://{{ ADDR }}', $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $ctx); + var_dump($client === false); +CODE; +ServerClientTestCase::getInstance()->run($clientNoCert, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +string(8) "verified" +bool(true) diff --git a/ext/openssl/tests/dtls_session_resume.phpt b/ext/openssl/tests/dtls_session_resume.phpt new file mode 100644 index 000000000000..861b83fbaa4f --- /dev/null +++ b/ext/openssl/tests/dtls_session_resume.phpt @@ -0,0 +1,78 @@ +--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); +$ctx = stream_context_create(['ssl' => ['verify_peer' => false]]); +$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($crypto['session'] instanceof Openssl\Session); +var_dump($crypto['session']->isResumable()); +var_dump(strlen($crypto['session']->export()) > 0); +$session = $crypto['session']; +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, '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..84ea4f80f0e5 --- /dev/null +++ b/ext/openssl/tests/dtls_session_resume_server.phpt @@ -0,0 +1,97 @@ +--TEST-- +dtls:// server: full session resumption via a userland cache (session_new_cb/get_cb) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- +saveNewCertAsFileWithKey('dtls-server', $certFile); + +// A single-peer dtls:// server serves one connection per bind, so it cannot keep +// an internal session cache across connections. The session_new_cb/session_get_cb +// options let userland hold the cache (keyed by session id) so it survives across +// binds. Here one server process serves two connections on the same port and +// resumes the second from the session stored during the first. +$serverCode = <<<'CODE' + $store = []; + $mkctx = function () use (&$store) { + return stream_context_create(['ssl' => [ + 'local_cert' => '%s', + '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, $mkctx()); + $port = substr(strrchr(stream_socket_get_name($server, false), ':'), 1); + 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); + fclose($server); + + // Second connection on the same port: session_get_cb resumes. + $server2 = stream_socket_server("dtls://127.0.0.1:$port", $errno, $errstr, $flags, $mkctx()); + phpt_notify(); + $peer2 = stream_socket_accept($server2, 10); + $reused = stream_get_meta_data($peer2)['crypto']['session_reused']; + fwrite($peer2, strtoupper(fread($peer2, 64))); + fread($peer2, 1); + fclose($peer2); + fclose($server2); + + phpt_notify(message: "reused=" . ($reused ? "yes" : "no") . " stored=" . count($store)); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + // First connection: capture the negotiated session. + $ctx = stream_context_create(['ssl' => ['verify_peer' => false]]); + $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=" . ($meta['session'] instanceof Openssl\Session ? "yes" : "no") . "\n"; + echo "first_reused=" . ($meta['session_reused'] ? "yes" : "no") . "\n"; + $session = $meta['session']; + fread($c1, 64); + fclose($c1); + + // Wait for the server to re-bind, then reconnect presenting the session. + phpt_wait(); + $ctx2 = stream_context_create(['ssl' => ['verify_peer' => 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/xp_common.c b/ext/openssl/xp_common.c new file mode 100644 index 000000000000..716836ebf8b7 --- /dev/null +++ b/ext/openssl/xp_common.c @@ -0,0 +1,231 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "php.h" +#include "php_openssl.h" +#include "php_openssl_backend.h" +#include "xp_common.h" +#include "ext/uri/php_uri.h" + +bool php_openssl_x509_fingerprint_is_equal(php_stream *stream, X509 *peer, + const char *method, const zend_string *expected) +{ + bool is_equal = false; + zend_string *fingerprint = php_openssl_x509_fingerprint(peer, method, false, stream); + if (fingerprint) { + is_equal = zend_string_equals_ci(fingerprint, expected); + zend_string_release_ex(fingerprint, false); + } + + return is_equal; +} + +bool php_openssl_x509_fingerprint_match(php_stream *stream, X509 *peer, const zval *val) +{ + if (Z_TYPE_P(val) == IS_STRING) { + const char *method = NULL; + + switch (Z_STRLEN_P(val)) { + case 32: + method = "md5"; + break; + + case 40: + method = "sha1"; + break; + } + + if (UNEXPECTED(method == NULL)) { + php_stream_warn(stream, AuthFailed, "peer_fingerprint length doesn't match a md5 or sha1 hash"); + return false; + } + if (!php_openssl_x509_fingerprint_is_equal(stream, peer, method, Z_STR_P(val))) { + php_stream_warn(stream, AuthFailed, "peer_fingerprint match failure"); + return false; + } + return true; + } else if (Z_TYPE_P(val) == IS_ARRAY) { + zval *current; + zend_string *key; + + if (!zend_hash_num_elements(Z_ARRVAL_P(val))) { + php_stream_warn(stream, Generic, "Invalid peer_fingerprint array; [algo => fingerprint] form required"); + return false; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(val), key, current) { + if (key == NULL || Z_TYPE_P(current) != IS_STRING) { + php_stream_warn(stream, Generic, "Invalid peer_fingerprint array; [algo => fingerprint] form required"); + return false; + } + if (!php_openssl_x509_fingerprint_is_equal(stream, peer, ZSTR_VAL(key), Z_STR_P(current))) { + php_stream_warn(stream, AuthFailed, "peer_fingerprint match failure"); + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; + } else { + php_stream_warn(stream, Generic, + "Invalid peer_fingerprint value; fingerprint string or array of the form [algo => fingerprint] required"); + } + + return false; +} + +char *php_openssl_get_url_name(const char *resourcename, size_t resourcenamelen, + int is_persistent, php_stream_context *context) +{ + if (!resourcename) { + return NULL; + } + + const php_uri_parser *uri_parser = php_stream_context_get_uri_parser("ssl", context); + if (uri_parser == NULL) { + zend_value_error("%s(): Provided stream context has invalid value for the \"uri_parser_class\" option", get_active_function_name()); + return NULL; + } + + php_uri_internal *internal_uri = php_uri_parse(uri_parser, resourcename, resourcenamelen, true); + if (internal_uri == NULL) { + return NULL; + } + + char * url_name = NULL; + zval host_zv; + zend_result result = php_uri_get_host(internal_uri, PHP_URI_COMPONENT_READ_MODE_RAW, &host_zv); + if (result == SUCCESS && Z_TYPE(host_zv) == IS_STRING) { + const char * host = Z_STRVAL(host_zv); + size_t len = Z_STRLEN(host_zv); + + /* skip trailing dots */ + while (len && host[len-1] == '.') { + --len; + } + + if (len) { + url_name = pestrndup(host, len, is_persistent); + } + } + + php_uri_free(internal_uri); + zval_ptr_dtor(&host_zv); + + return url_name; +} + +int php_openssl_passwd_callback(char *buf, int num, int verify, void *data) +{ + php_stream *stream = (php_stream *)data; + zval *val; + + /* TODO: could expand this to make a callback into PHP user-space */ + if (PHP_STREAM_CONTEXT(stream) == NULL) { + return 0; + } + + val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", "passphrase"); + if (val == NULL || !try_convert_to_string(val)) { + return 0; + } + + if (Z_STRLEN_P(val) < (size_t)num - 1) { + memcpy(buf, Z_STRVAL_P(val), Z_STRLEN_P(val) + 1); + return (int)Z_STRLEN_P(val); + } + + return 0; +} + +zend_result php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream) +{ + zval *val; + char *certfile = NULL; + size_t certfile_len = 0; + + if (PHP_STREAM_CONTEXT(stream) != NULL + && (val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", "local_cert")) != NULL + && try_convert_to_string(val)) { + certfile = Z_STRVAL_P(val); + certfile_len = Z_STRLEN_P(val); + } + + if (certfile) { + char resolved_path_buff[MAXPATHLEN]; + char *private_key = NULL; + size_t private_key_len = 0; + + if (!php_openssl_check_path_ex(certfile, certfile_len, resolved_path_buff, 0, false, false, + "local_cert in ssl stream context", stream)) { + php_stream_warn(stream, NotFound, "Unable to get real path of certificate file `%s'", certfile); + return FAILURE; + } + /* a certificate to use for authentication */ + if (SSL_CTX_use_certificate_chain_file(ctx, resolved_path_buff) != 1) { + php_stream_warn(stream, WriteFailed, + "Unable to set local cert chain file `%s'; Check that your cafile/capath " + "settings include details of your certificate and its issuer", + certfile); + return FAILURE; + } + + if (PHP_STREAM_CONTEXT(stream) != NULL + && (val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", "local_pk")) != NULL + && try_convert_to_string(val)) { + private_key = Z_STRVAL_P(val); + private_key_len = Z_STRLEN_P(val); + } + if (private_key && !php_openssl_check_path_ex(private_key, private_key_len, resolved_path_buff, 0, false, false, + "local_pk in ssl stream context", stream)) { + php_stream_warn(stream, NotFound, "Unable to get real path of private key file `%s'", private_key); + return FAILURE; + } + if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff, SSL_FILETYPE_PEM) != 1) { + php_stream_warn(stream, WriteFailed, "Unable to set private key file `%s'", resolved_path_buff); + return FAILURE; + } + if (!SSL_CTX_check_private_key(ctx)) { + php_stream_warn(stream, PermissionDenied, "Private key does not match certificate!"); + } + } + + return SUCCESS; +} + +int php_openssl_setup_crypto_on_connect(php_stream *stream, + php_stream_xport_crypt_method_t method) +{ + zval *val; + php_stream *session_stream = NULL; + + if (PHP_STREAM_CONTEXT(stream) && + (val = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "ssl", "session_stream")) != NULL) { + php_stream_from_zval_no_verify(session_stream, val); + } + + if (php_stream_xport_crypto_setup(stream, method, session_stream) < 0 || + php_stream_xport_crypto_enable(stream, 1) < 0) { + php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + return -1; + } + + return 0; +} diff --git a/ext/openssl/xp_common.h b/ext/openssl/xp_common.h new file mode 100644 index 000000000000..b0a865b84dbd --- /dev/null +++ b/ext/openssl/xp_common.h @@ -0,0 +1,49 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +/* Code shared between the tls:// (xp_ssl.c) and dtls:// (xp_dtls.c) transports. */ + +#ifndef PHP_OPENSSL_XP_COMMON_H +#define PHP_OPENSSL_XP_COMMON_H + +#include "php.h" +#include "php_network.h" + +#include +#include + +/* Match the peer certificate against the peer_fingerprint context option. */ +bool php_openssl_x509_fingerprint_is_equal(php_stream *stream, X509 *peer, + const char *method, const zend_string *expected); +bool php_openssl_x509_fingerprint_match(php_stream *stream, X509 *peer, const zval *val); + +/* Host from the stream URL, used as the default peer name for verification. */ +char *php_openssl_get_url_name(const char *resourcename, size_t resourcenamelen, + int is_persistent, php_stream_context *context); + +/* Turn on crypto right after the base transport connected (enable_on_connect). */ +int php_openssl_setup_crypto_on_connect(php_stream *stream, + php_stream_xport_crypt_method_t method); + +/* SSL_CTX default passphrase callback: supplies the "passphrase" context option. */ +int php_openssl_passwd_callback(char *buf, int num, int verify, void *data); + +/* Load the local_cert chain and local_pk private key onto the SSL_CTX. The caller + * must have installed the passphrase callback for an encrypted key. */ +zend_result php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream); + +#endif /* PHP_OPENSSL_XP_COMMON_H */ diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c new file mode 100644 index 000000000000..8c2c1cab8581 --- /dev/null +++ b/ext/openssl/xp_dtls.c @@ -0,0 +1,1316 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "php.h" +#include "ext/standard/file.h" +#include "streams/php_streams_int.h" +#include "php_openssl.h" +#include "php_openssl_backend.h" +#include "php_network.h" +#include "xp_common.h" +#ifdef PHP_WIN32 +# include "win32/time.h" +#endif +#include +#include +#include +#include +#include +#include + +#ifndef OPENSSL_NO_DTLS + +/* Index of the php_stream pointer stored on an SSL, so a callback can recover it. */ +extern int php_openssl_get_ssl_stream_data_index(void); + +/* Holds the session cache callbacks */ +typedef struct _php_openssl_dtls_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_dtls_session_callbacks_t; + +/* The base socket data is embedded first so the generic socket option handlers + * can be reused; the datagram BIO is owned by ssl_handle (freed with it). */ +typedef struct _php_openssl_dtls_data_t { + php_netstream_data_t s; + SSL_CTX *ctx; + SSL *ssl_handle; + bool is_server; + bool ssl_active; + bool enable_on_connect; + php_stream_xport_crypt_method_t method; + struct timeval connect_timeout; + char *url_name; + php_openssl_dtls_session_callbacks_t *session_callbacks; +} php_openssl_dtls_data_t; + +/* Index of the php_stream pointer stored on an SSL_CTX, for the remove callback + * which is only passed the context. */ +static int php_openssl_dtls_ctx_stream_index = -1; +static int php_openssl_dtls_get_ctx_stream_index(void) +{ + if (php_openssl_dtls_ctx_stream_index < 0) { + php_openssl_dtls_ctx_stream_index = + SSL_CTX_get_ex_new_index(0, "PHP dtls ctx stream index", NULL, NULL, NULL); + } + return php_openssl_dtls_ctx_stream_index; +} + +static const php_stream_ops php_openssl_dtls_socket_ops; + +/* Read an option from the "ssl" stream context into the local `val`. */ +#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) + +/* Datagram semantics: like udp:// (and tls://) the stream is buffered, so a read + * yields the data of a single DTLS record and a write emits one record (atomic, + * no partial writes). EOF is the peer's close_notify, which UDP need not deliver, + * so it is best-effort. + * + * The socket is non-blocking, so on WANT_* we poll (up to the read timeout when + * blocking) and retry. */ +static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, size_t count) +{ + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + + /* Plain udp:// (no crypto): read/write the datagram socket directly. */ + if (!dtlssock->ssl_active) { + return read + ? php_stream_socket_ops.read(stream, buf, count) + : php_stream_socket_ops.write(stream, buf, count); + } + + SSL *ssl = dtlssock->ssl_handle; + if (ssl == NULL) { + return -1; + } + + /* OpenSSL takes an int length. */ + if (count > INT_MAX) { + count = INT_MAX; + } + + dtlssock->s.timeout_event = false; + + /* Bound the total time across retries, not each individual poll. */ + struct timeval deadline; + bool has_deadline = false; + if (dtlssock->s.is_blocked && (dtlssock->s.timeout.tv_sec > 0 || dtlssock->s.timeout.tv_usec > 0)) { + gettimeofday(&deadline, NULL); + deadline.tv_sec += dtlssock->s.timeout.tv_sec; + deadline.tv_usec += dtlssock->s.timeout.tv_usec; + if (deadline.tv_usec >= 1000000) { + deadline.tv_sec++; + deadline.tv_usec -= 1000000; + } + has_deadline = true; + } + + for (;;) { + int events; + + ERR_clear_error(); + int n = read ? SSL_read(ssl, buf, (int)count) : SSL_write(ssl, buf, (int)count); + if (n > 0) { + php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), n, 0); + return n; + } + switch (SSL_get_error(ssl, n)) { + case SSL_ERROR_WANT_READ: + events = POLLIN; + break; + case SSL_ERROR_WANT_WRITE: + events = POLLOUT; + break; + case SSL_ERROR_ZERO_RETURN: + /* Peer sent close_notify. */ + stream->eof = 1; + return 0; + default: + if (read) { + stream->eof = 1; + } + return -1; + } + + /* Non-blocking, or a zero read timeout: don't wait, report would-block. */ + if (!dtlssock->s.is_blocked + || (dtlssock->s.timeout.tv_sec == 0 && dtlssock->s.timeout.tv_usec == 0)) { + return 0; + } + + int wait_ms = -1; + if (has_deadline) { + struct timeval now; + gettimeofday(&now, NULL); + long remaining = (deadline.tv_sec - now.tv_sec) * 1000L + + (deadline.tv_usec - now.tv_usec) / 1000; + if (remaining <= 0) { + dtlssock->s.timeout_event = true; + return -1; + } + wait_ms = remaining > INT_MAX ? INT_MAX : (int)remaining; + } + + int ready = php_pollfd_for_ms(dtlssock->s.socket, events, wait_ms); + if (ready == 0) { + dtlssock->s.timeout_event = true; + return -1; + } + if (ready < 0) { + return -1; + } + } +} + +/* Send one datagram of application data. */ +static ssize_t php_openssl_dtls_sockop_write(php_stream *stream, const char *buf, size_t count) +{ + return php_openssl_dtls_io(false, stream, (char *)buf, count); +} + +/* Receive one datagram of application data. */ +static ssize_t php_openssl_dtls_sockop_read(php_stream *stream, char *buf, size_t count) +{ + return php_openssl_dtls_io(true, stream, buf, count); +} + +/* Free the DTLS objects and close the socket. */ +static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) +{ + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + + if (dtlssock == NULL) { + return 0; + } + + /* SSL_free also frees the BIO (created BIO_NOCLOSE), so the socket is closed + * separately below. */ + if (dtlssock->ssl_handle != NULL) { + /* Shut down cleanly so a session captured for resumption stays usable; + * an SSL_free on an unfinished exchange marks the session non-resumable. */ + if (SSL_is_init_finished(dtlssock->ssl_handle)) { + SSL_shutdown(dtlssock->ssl_handle); + } + SSL_free(dtlssock->ssl_handle); + dtlssock->ssl_handle = NULL; + } + if (dtlssock->ctx != NULL) { + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + } + + if (close_handle && dtlssock->s.socket != SOCK_ERR) { + closesocket(dtlssock->s.socket); + dtlssock->s.socket = SOCK_ERR; + } + + /* Shared between the listener and its connections; free on the last one. */ + if (dtlssock->session_callbacks && --dtlssock->session_callbacks->refcount == 0) { + if (ZEND_FCC_INITIALIZED(dtlssock->session_callbacks->new_cb)) { + zend_fcc_dtor(&dtlssock->session_callbacks->new_cb); + } + if (ZEND_FCC_INITIALIZED(dtlssock->session_callbacks->get_cb)) { + zend_fcc_dtor(&dtlssock->session_callbacks->get_cb); + } + if (ZEND_FCC_INITIALIZED(dtlssock->session_callbacks->remove_cb)) { + zend_fcc_dtor(&dtlssock->session_callbacks->remove_cb); + } + pefree(dtlssock->session_callbacks, 0); + } + dtlssock->session_callbacks = NULL; + + if (dtlssock->url_name != NULL) { + pefree(dtlssock->url_name, php_stream_is_persistent(stream)); + } + + pefree(dtlssock, php_stream_is_persistent(stream)); + stream->abstract = NULL; + + return 0; +} + +/* Apply the "ssl" context options to the SSL_CTX: peer verification, ciphers and + * the local certificate. Set on the context so SSL_new() inherits them. */ +static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +{ + SSL_CTX *ctx = dtlssock->ctx; + char *cafile = NULL, *capath = NULL, *cipherlist = NULL; + zval *val; + + /* DTLS 1.0 is deprecated; require DTLS 1.2 or higher. */ + SSL_CTX_set_min_proto_version(ctx, DTLS1_2_VERSION); + + /* Clients verify the server by default; a server does not request a client + * certificate unless verify_peer is set explicitly. A peer_fingerprint + * authenticates the peer by itself (checked after the handshake), so it + * overrides CA verification. */ + bool is_server = dtlssock->is_server; + bool verify_peer = GET_VER_OPT("verify_peer") ? zend_is_true(val) : !is_server; + bool has_fingerprint = GET_VER_OPT("peer_fingerprint"); + if (!verify_peer || has_fingerprint) { + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); + } else { + GET_VER_OPT_STRING("cafile", cafile); + GET_VER_OPT_STRING("capath", capath); + if (cafile != NULL || capath != NULL) { + if (SSL_CTX_load_verify_locations(ctx, cafile, capath) != 1) { + php_stream_warn(stream, CreateFailed, "Failed to load the CA verify locations"); + return -1; + } + } else if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + php_stream_warn(stream, CreateFailed, "Failed to set the default CA verify paths"); + return -1; + } + int verify_mode = SSL_VERIFY_PEER; + if (is_server) { + /* A server that verifies peers must require the client certificate. */ + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + } + SSL_CTX_set_verify(ctx, verify_mode, NULL); + } + + GET_VER_OPT_STRING("ciphers", cipherlist); + if (cipherlist != NULL && SSL_CTX_set_cipher_list(ctx, cipherlist) != 1) { + php_stream_warn(stream, CreateFailed, "Failed to set the cipher list"); + return -1; + } + + /* A passphrase for an encrypted private key (used by set_local_cert below). */ + if (GET_VER_OPT("passphrase")) { + SSL_CTX_set_default_passwd_cb_userdata(ctx, stream); + SSL_CTX_set_default_passwd_cb(ctx, php_openssl_passwd_callback); + } + + /* Local certificate chain and private key as file paths. */ + if (php_openssl_set_local_cert(ctx, stream) != SUCCESS) { + return -1; + } + + return 0; +} + +/* Keep the handshake within the path MTU: enable path-MTU discovery so the + * kernel drops-and-signals oversized datagrams (OpenSSL then shrinks its DTLS + * MTU and retransmits), and honour an explicit dtls_link_mtu context option. */ +static void php_openssl_dtls_configure_mtu(php_stream *stream, SSL *ssl, php_socket_t fd, int family) +{ + zval *val; + +#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO) + if (family == AF_INET) { + int mode = IP_PMTUDISC_DO; + setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, (char *)&mode, sizeof(mode)); + } +#endif +#if defined(IPV6_MTU_DISCOVER) && defined(IPV6_PMTUDISC_DO) + if (family == AF_INET6) { + int mode = IPV6_PMTUDISC_DO; + setsockopt(fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, (char *)&mode, sizeof(mode)); + } +#endif + + if (GET_VER_OPT("dtls_link_mtu")) { + zend_long mtu = zval_get_long(val); + if (mtu > 0) { + DTLS_set_link_mtu(ssl, mtu); + SSL_set_options(ssl, SSL_OP_NO_QUERY_MTU); + } + } +} + +/* Create the DTLS context, SSL object and datagram BIO. */ +static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + const char *peer_host) +{ + BIO *bio; + zval *val; + + /* The DTLS I/O loop emulates blocking with poll, so the fd stays non-blocking. */ + php_set_sock_blocking(dtlssock->s.socket, 0); + + dtlssock->ctx = SSL_CTX_new(DTLS_client_method()); + if (dtlssock->ctx == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS context creation failure"); + return -1; + } + + if (php_openssl_dtls_apply_context(stream, dtlssock) != 0) { + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + return -1; + } + + dtlssock->ssl_handle = SSL_new(dtlssock->ctx); + if (dtlssock->ssl_handle == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS handle creation failure"); + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + return -1; + } + + /* Resume a previous session (abbreviated handshake) if session_data holds an + * Openssl\Session; SSL_set_session takes its own reference. The client cache + * must be enabled for the session to be offered in the ClientHello. */ + if (GET_VER_OPT("session_data") && php_openssl_is_session_ce(val)) { + SSL_SESSION *session = php_openssl_session_from_zval(val); + if (session != NULL) { + SSL_CTX_set_session_cache_mode(dtlssock->ctx, + SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL); + SSL_set_session(dtlssock->ssl_handle, session); + } + } + + /* Hostname verification needs the SSL object; the verify mode is inherited + * from the context. */ + bool verify_peer = !(GET_VER_OPT("verify_peer") && !zend_is_true(val)); + bool verify_name = !(GET_VER_OPT("verify_peer_name") && !zend_is_true(val)); + if (verify_peer && verify_name) { + const char *name = peer_host ? peer_host : dtlssock->url_name; + GET_VER_OPT_STRING("peer_name", name); + if (name != NULL) { + /* An IP literal needs IP-address matching, not DNS-name matching. */ + X509_VERIFY_PARAM *param = SSL_get0_param(dtlssock->ssl_handle); + if (X509_VERIFY_PARAM_set1_ip_asc(param, name) != 1) { + X509_VERIFY_PARAM_set1_host(param, name, 0); + } + } + } + + bio = BIO_new_dgram(dtlssock->s.socket, BIO_NOCLOSE); + if (bio == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS datagram BIO creation failure"); + SSL_free(dtlssock->ssl_handle); + dtlssock->ssl_handle = NULL; + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + return -1; + } + + /* A datagram BIO defaults to sendto() with an empty peer, which fails with + * EINVAL on a connected socket; mark it connected so it uses send()/recv(). */ + { + struct sockaddr_storage peer; + socklen_t peerlen = sizeof(peer); + if (getpeername(dtlssock->s.socket, (struct sockaddr *)&peer, &peerlen) == 0) { + BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); + php_openssl_dtls_configure_mtu(stream, dtlssock->ssl_handle, dtlssock->s.socket, + peer.ss_family); + } + } + + SSL_set_bio(dtlssock->ssl_handle, bio, bio); + SSL_set_connect_state(dtlssock->ssl_handle); + + return 0; +} + +/* The socket is non-blocking, so poll between flights and resend on the + * retransmission timeout. */ +static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + struct timeval *timeout, zend_string **error_text) +{ + SSL *ssl = dtlssock->ssl_handle; + + /* Always bound the handshake so a silent peer can't make OpenSSL's DTLS timer + * retransmit forever: use the connect timeout, else the default socket timeout. */ + struct timeval *tmo = timeout; + struct timeval deadline; + gettimeofday(&deadline, NULL); + if (tmo != NULL && (tmo->tv_sec > 0 || tmo->tv_usec > 0)) { + deadline.tv_sec += tmo->tv_sec; + deadline.tv_usec += tmo->tv_usec; + if (deadline.tv_usec >= 1000000) { + deadline.tv_sec++; + deadline.tv_usec -= 1000000; + } + } else { + deadline.tv_sec += (time_t) FG(default_socket_timeout); + } + + for (;;) { + ERR_clear_error(); + int n = SSL_do_handshake(ssl); + if (n == 1) { + return 0; + } + int saved_errno = errno; + + int events; + switch (SSL_get_error(ssl, n)) { + case SSL_ERROR_WANT_READ: + events = POLLIN; + break; + case SSL_ERROR_WANT_WRITE: + events = POLLOUT; + break; + default: { + char buf[256] = ""; + unsigned long ecode = ERR_get_error(); + if (ecode != 0) { + ERR_error_string_n(ecode, buf, sizeof(buf)); + } else if (saved_errno != 0) { + /* SSL_ERROR_SYSCALL with an empty queue: report the syscall. */ + snprintf(buf, sizeof(buf), "%s", strerror(saved_errno)); + } + if (error_text != NULL) { + *error_text = strpprintf(0, "DTLS handshake failed: %s", + buf[0] != '\0' ? buf : "unexpected error"); + } + return -1; + } + } + + struct timeval tv; + int wait_ms = DTLSv1_get_timeout(ssl, &tv) + ? (int)(tv.tv_sec * 1000 + tv.tv_usec / 1000) + : -1; + + struct timeval now; + gettimeofday(&now, NULL); + long remaining = (deadline.tv_sec - now.tv_sec) * 1000L + + (deadline.tv_usec - now.tv_usec) / 1000; + if (remaining <= 0) { + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); + } + return -1; + } + if (wait_ms < 0 || wait_ms > remaining) { + wait_ms = (int)remaining; + } + + int ready = php_pollfd_for_ms(dtlssock->s.socket, events, wait_ms); + if (ready == 0) { + /* Timer fired: let OpenSSL resend the last flight. */ + if (DTLSv1_handle_timeout(ssl) < 0) { + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); + } + return -1; + } + } else if (ready < 0) { + if (error_text != NULL) { + *error_text = + ZSTR_INIT_LITERAL("DTLS handshake failed while waiting for the socket", 0); + } + return -1; + } + } +} + +/* Verify the peer certificate against the peer_fingerprint option, if set. */ +static int php_openssl_dtls_check_fingerprint(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + zend_string **error_text) +{ + zval *val; + if (!GET_VER_OPT("peer_fingerprint")) { + return 0; + } + + X509 *peer = SSL_get_peer_certificate(dtlssock->ssl_handle); + bool match = peer != NULL && php_openssl_x509_fingerprint_match(stream, peer, val); + if (peer != NULL) { + X509_free(peer); + } + + if (!match) { + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("peer_fingerprint match failure", 0); + } + return -1; + } + + return 0; +} + +/* Per-process secret for the DTLSv1_listen cookie (an HMAC of the peer address, + * so the server stays stateless during the HelloVerifyRequest exchange). */ +#define PHP_OPENSSL_DTLS_COOKIE_SECRET_LEN 16 +static unsigned char php_openssl_dtls_cookie_secret[PHP_OPENSSL_DTLS_COOKIE_SECRET_LEN]; +static bool php_openssl_dtls_cookie_secret_ready = false; + +static bool php_openssl_dtls_peer_addr(SSL *ssl, struct sockaddr_storage *peer, socklen_t *peerlen) +{ + memset(peer, 0, sizeof(*peer)); + if (BIO_dgram_get_peer(SSL_get_rbio(ssl), peer) <= 0) { + return false; + } + *peerlen = (peer->ss_family == AF_INET6) ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in); + return true; +} + +static int php_openssl_dtls_cookie_generate(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len) +{ + struct sockaddr_storage peer; + socklen_t peerlen; + + if (!php_openssl_dtls_cookie_secret_ready) { + if (RAND_bytes(php_openssl_dtls_cookie_secret, sizeof(php_openssl_dtls_cookie_secret)) != 1) { + return 0; + } + php_openssl_dtls_cookie_secret_ready = true; + } + if (!php_openssl_dtls_peer_addr(ssl, &peer, &peerlen)) { + return 0; + } + + unsigned int len = 0; + HMAC(EVP_sha256(), php_openssl_dtls_cookie_secret, sizeof(php_openssl_dtls_cookie_secret), + (const unsigned char *)&peer, peerlen, cookie, &len); + *cookie_len = len; + return 1; +} + +static int php_openssl_dtls_cookie_verify(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len) +{ + unsigned char expected[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + struct sockaddr_storage peer; + socklen_t peerlen; + + if (!php_openssl_dtls_cookie_secret_ready || !php_openssl_dtls_peer_addr(ssl, &peer, &peerlen)) { + return 0; + } + HMAC(EVP_sha256(), php_openssl_dtls_cookie_secret, sizeof(php_openssl_dtls_cookie_secret), + (const unsigned char *)&peer, peerlen, expected, &len); + /* Constant-time compare: the cookie is derived from a secret HMAC. */ + return (cookie_len == len && CRYPTO_memcmp(cookie, expected, len) == 0) ? 1 : 0; +} + +/* The session_new_cb / session_get_cb / session_remove_cb options let userland + * keep the session cache. A single-peer server uses a new context per accepted + * connection, so the built-in cache cannot resume across connections. */ + +enum php_openssl_dtls_session_cb_type { + PHP_OPENSSL_DTLS_NEW_CB, + PHP_OPENSSL_DTLS_GET_CB, + PHP_OPENSSL_DTLS_REMOVE_CB, +}; + +/* Called when a new session is established. */ +static int php_openssl_dtls_session_new_cb(SSL *ssl, SSL_SESSION *session) +{ + php_stream *stream = (php_stream *)SSL_get_ex_data(ssl, php_openssl_get_ssl_stream_data_index()); + if (!stream) { + return 0; + } + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + if (!dtlssock || !dtlssock->session_callbacks) { + return 0; + } + + /* The PHP object takes its own reference. */ + SSL_SESSION_up_ref(session); + + zval args[2]; + ZVAL_RES(&args[0], stream->res); + php_openssl_session_object_init(&args[1], session); + zend_call_known_fcc(&dtlssock->session_callbacks->new_cb, NULL, 2, args, NULL); + zval_ptr_dtor(&args[1]); + + return 0; +} + +/* Called when the server looks up a session by id. */ +static SSL_SESSION *php_openssl_dtls_session_get_cb(SSL *ssl, const unsigned char *session_id, + int session_id_len, int *copy) +{ + *copy = 0; + php_stream *stream = (php_stream *)SSL_get_ex_data(ssl, php_openssl_get_ssl_stream_data_index()); + if (!stream) { + return NULL; + } + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + if (!dtlssock || !dtlssock->session_callbacks) { + return NULL; + } + + zval args[2]; + zval retval; + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); + + SSL_SESSION *session = NULL; + zend_call_known_fcc(&dtlssock->session_callbacks->get_cb, &retval, 2, args, NULL); + zval_ptr_dtor(&args[1]); + + if (php_openssl_is_session_ce(&retval)) { + SSL_SESSION *found = php_openssl_session_from_zval(&retval); + if (found != NULL) { + /* OpenSSL takes ownership of the returned session. */ + SSL_SESSION_up_ref(found); + session = found; + } + } else if (Z_TYPE(retval) != IS_NULL) { + zend_type_error("session_get_cb return type must be null or Openssl\\Session"); + } + zval_ptr_dtor(&retval); + + return session; +} + +/* Called when a session is removed from the cache. */ +static void php_openssl_dtls_session_remove_cb(SSL_CTX *ctx, SSL_SESSION *session) +{ + php_stream *stream = (php_stream *)SSL_CTX_get_ex_data(ctx, php_openssl_dtls_get_ctx_stream_index()); + if (!stream) { + return; + } + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + if (!dtlssock || !dtlssock->session_callbacks) { + return; + } + + unsigned int session_id_len = 0; + const unsigned char *session_id = SSL_SESSION_get_id(session, &session_id_len); + + zval args[2]; + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); + zend_call_known_fcc(&dtlssock->session_callbacks->remove_cb, NULL, 2, args, NULL); + zval_ptr_dtor(&args[1]); +} + +/* Validate a session callback option and store it, allocating the struct once. */ +static zend_result php_openssl_dtls_store_session_cb(php_stream *stream, + php_openssl_dtls_data_t *dtlssock, const zval *callable, + enum php_openssl_dtls_session_cb_type cb_type) +{ + const char *name = cb_type == PHP_OPENSSL_DTLS_NEW_CB ? "session_new_cb" + : cb_type == PHP_OPENSSL_DTLS_GET_CB ? "session_get_cb" : "session_remove_cb"; + + char *is_callable_error = NULL; + zend_fcall_info_cache fcc; + if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (is_callable_error) { + zend_type_error("%s must be a valid callback, %s", name, is_callable_error); + efree(is_callable_error); + } else { + zend_type_error("%s must be a valid callback", name); + } + return FAILURE; + } + + if (!dtlssock->session_callbacks) { + dtlssock->session_callbacks = pecalloc(1, sizeof(*dtlssock->session_callbacks), 0); + dtlssock->session_callbacks->refcount = 1; + } + + zend_fcc_addref(&fcc); + switch (cb_type) { + case PHP_OPENSSL_DTLS_NEW_CB: dtlssock->session_callbacks->new_cb = fcc; break; + case PHP_OPENSSL_DTLS_GET_CB: dtlssock->session_callbacks->get_cb = fcc; break; + case PHP_OPENSSL_DTLS_REMOVE_CB: dtlssock->session_callbacks->remove_cb = fcc; break; + } + return SUCCESS; +} + +/* Configure server-side session resumption from the "ssl" context options. */ +static zend_result php_openssl_dtls_setup_server_session(php_stream *stream, + php_openssl_dtls_data_t *dtlssock) +{ + zval *val; + bool has_get_cb = false, has_new_cb = false, has_session_id_context = false; + + if (php_stream_is_persistent(stream) && + (GET_VER_OPT("session_new_cb") || GET_VER_OPT("session_get_cb") + || GET_VER_OPT("session_remove_cb"))) { + php_stream_warn(stream, PersistentNotSupported, + "session callbacks are not supported for persistent dtls:// streams"); + return FAILURE; + } + + if (GET_VER_OPT("session_get_cb")) { + if (php_openssl_dtls_store_session_cb(stream, dtlssock, val, PHP_OPENSSL_DTLS_GET_CB) == FAILURE) { + return FAILURE; + } + has_get_cb = true; + } + + if (GET_VER_OPT("session_id_context")) { + if (Z_TYPE_P(val) != IS_STRING || Z_STRLEN_P(val) == 0) { + zend_type_error("session_id_context must be a non empty string"); + return FAILURE; + } + SSL_CTX_set_session_id_context(dtlssock->ctx, + (const unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val)); + has_session_id_context = true; + } + + if (GET_VER_OPT("session_new_cb")) { + if (php_openssl_dtls_store_session_cb(stream, dtlssock, val, PHP_OPENSSL_DTLS_NEW_CB) == FAILURE) { + return FAILURE; + } + has_new_cb = true; + } + + if (has_get_cb && !has_new_cb) { + zend_value_error("session_new_cb is required when session_get_cb is provided"); + return FAILURE; + } + /* Server-side resumption needs a session id context. */ + if (has_get_cb && !has_session_id_context) { + zend_value_error("session_id_context must be set when session_get_cb is provided"); + return FAILURE; + } + + if (GET_VER_OPT("session_remove_cb")) { + if (php_openssl_dtls_store_session_cb(stream, dtlssock, val, PHP_OPENSSL_DTLS_REMOVE_CB) == FAILURE) { + return FAILURE; + } + } + + if (has_get_cb) { + /* External cache mode - the callbacks hold the sessions. */ + SSL_CTX_set_ex_data(dtlssock->ctx, php_openssl_dtls_get_ctx_stream_index(), stream); + SSL_CTX_set_session_cache_mode(dtlssock->ctx, + SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL); + SSL_CTX_sess_set_new_cb(dtlssock->ctx, php_openssl_dtls_session_new_cb); + SSL_CTX_sess_set_get_cb(dtlssock->ctx, php_openssl_dtls_session_get_cb); + if (dtlssock->session_callbacks + && ZEND_FCC_INITIALIZED(dtlssock->session_callbacks->remove_cb)) { + SSL_CTX_sess_set_remove_cb(dtlssock->ctx, php_openssl_dtls_session_remove_cb); + } + /* Tickets bypass the id-based cache, so disable them here. */ + SSL_CTX_set_options(dtlssock->ctx, SSL_OP_NO_TICKET); + if (GET_VER_OPT("no_ticket") && !zend_is_true(val)) { + zend_value_error("Session tickets cannot be enabled when session_get_cb is set"); + return FAILURE; + } + + if (GET_VER_OPT("session_timeout")) { + zend_long timeout = zval_get_long(val); + if (timeout <= 0) { + zend_value_error("session_timeout must be positive"); + return FAILURE; + } + SSL_CTX_set_timeout(dtlssock->ctx, timeout); + } + } else { + SSL_CTX_set_session_cache_mode(dtlssock->ctx, SSL_SESS_CACHE_OFF); + } + + return SUCCESS; +} + +/* Set up the server SSL_CTX (cert/verify options plus the cookie callbacks that + * DTLSv1_listen needs for the stateless HelloVerifyRequest exchange). */ +static int php_openssl_dtls_server_ctx(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +{ + dtlssock->ctx = SSL_CTX_new(DTLS_server_method()); + if (dtlssock->ctx == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS context creation failure"); + return -1; + } + if (php_openssl_dtls_apply_context(stream, dtlssock) != 0) { + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + return -1; + } + SSL_CTX_set_cookie_generate_cb(dtlssock->ctx, php_openssl_dtls_cookie_generate); + SSL_CTX_set_cookie_verify_cb(dtlssock->ctx, php_openssl_dtls_cookie_verify); + if (php_openssl_dtls_setup_server_session(stream, dtlssock) == FAILURE) { + SSL_CTX_free(dtlssock->ctx); + dtlssock->ctx = NULL; + return -1; + } + return 0; +} + +/* Accept one peer: run the cookie exchange with DTLSv1_listen, connect the + * listening socket to that peer, and finish the handshake on it. */ +static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t *listen, + php_stream_xport_param *xparam STREAMS_DC) +{ + if (listen->s.socket == SOCK_ERR) { + /* The listening socket is handed to the first accepted peer. */ + php_error_docref(NULL, E_WARNING, "This dtls:// server has already accepted its peer"); + return -1; + } + + SSL *ssl = SSL_new(listen->ctx); + if (ssl == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS handle creation failure"); + return -1; + } + BIO *bio = BIO_new_dgram(listen->s.socket, BIO_NOCLOSE); + if (bio == NULL) { + SSL_free(ssl); + return -1; + } + SSL_set_bio(ssl, bio, bio); + + /* Bound the whole accept by a deadline (not each poll), so a bogus-ClientHello + * flood can't keep DTLSv1_listen() spinning past the timeout; with no timeout + * we block. */ + struct timeval *tmo = xparam->inputs.timeout; + struct timeval deadline; + bool has_deadline = tmo != NULL && (tmo->tv_sec > 0 || tmo->tv_usec > 0); + if (has_deadline) { + gettimeofday(&deadline, NULL); + deadline.tv_sec += tmo->tv_sec; + deadline.tv_usec += tmo->tv_usec; + if (deadline.tv_usec >= 1000000) { + deadline.tv_sec++; + deadline.tv_usec -= 1000000; + } + } + + BIO_ADDR *client_addr = BIO_ADDR_new(); + if (client_addr == NULL) { + SSL_free(ssl); + return -1; + } + for (;;) { + int ret = DTLSv1_listen(ssl, client_addr); + if (ret > 0) { + break; + } + if (ret < 0) { + BIO_ADDR_free(client_addr); + SSL_free(ssl); + return -1; + } + + int wait_ms = -1; + if (has_deadline) { + struct timeval now; + gettimeofday(&now, NULL); + long remaining = (deadline.tv_sec - now.tv_sec) * 1000L + + (deadline.tv_usec - now.tv_usec) / 1000; + if (remaining <= 0) { + BIO_ADDR_free(client_addr); + SSL_free(ssl); + return -1; + } + wait_ms = remaining > INT_MAX ? INT_MAX : (int)remaining; + } + if (php_pollfd_for_ms(listen->s.socket, POLLIN, wait_ms) <= 0) { + BIO_ADDR_free(client_addr); + SSL_free(ssl); + return -1; + } + } + + /* Turn the BIO_ADDR the cookie exchange gave us into a sockaddr. */ + struct sockaddr_storage peer; + socklen_t peerlen = 0; + int family = BIO_ADDR_family(client_addr); + memset(&peer, 0, sizeof(peer)); + if (family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)&peer; + size_t addrlen = sizeof(sin->sin_addr); + sin->sin_family = AF_INET; + sin->sin_port = BIO_ADDR_rawport(client_addr); + BIO_ADDR_rawaddress(client_addr, &sin->sin_addr, &addrlen); + peerlen = sizeof(struct sockaddr_in); +#ifdef HAVE_STRUCT_SOCKADDR_SA_LEN + sin->sin_len = sizeof(struct sockaddr_in); +#endif + } +#ifdef HAVE_IPV6 + else if (family == AF_INET6) { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&peer; + size_t addrlen = sizeof(sin6->sin6_addr); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = BIO_ADDR_rawport(client_addr); + BIO_ADDR_rawaddress(client_addr, &sin6->sin6_addr, &addrlen); + peerlen = sizeof(struct sockaddr_in6); +#ifdef HAVE_STRUCT_SOCKADDR_SA_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + } +#endif + BIO_ADDR_free(client_addr); + if (peerlen == 0) { + SSL_free(ssl); + return -1; + } + + /* Connect the listening socket to this peer and hand it to the accepted + * stream. Serving one peer per server stream avoids SO_REUSEPORT (which + * would let another process bind the same port and steal datagrams). */ + if (connect(listen->s.socket, (struct sockaddr *)&peer, peerlen) != 0) { + SSL_free(ssl); + return -1; + } + BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); + php_openssl_dtls_configure_mtu(stream, ssl, listen->s.socket, peer.ss_family); + + php_openssl_dtls_data_t *clisock = pemalloc(sizeof(*clisock), 0); + memset(clisock, 0, sizeof(*clisock)); + clisock->s.socket = listen->s.socket; + clisock->s.is_blocked = true; + clisock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); + clisock->ssl_handle = ssl; + clisock->is_server = true; + + /* The socket now belongs to the accepted stream. */ + listen->s.socket = SOCK_ERR; + + php_stream *clistream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, clisock, NULL, "r+"); + if (clistream == NULL) { + SSL_free(ssl); + closesocket(clisock->s.socket); + pefree(clisock, 0); + return -1; + } + + /* The accepted stream inherits the listener's context (cert options, + * keying material requests, ...). */ + clistream->ctx = stream->ctx; + if (stream->ctx) { + GC_ADDREF(stream->ctx); + } + + /* The connection shares the listener's session callbacks, and the callbacks + * find its stream through the SSL during the handshake. */ + clisock->session_callbacks = listen->session_callbacks; + if (clisock->session_callbacks) { + clisock->session_callbacks->refcount++; + } + SSL_set_ex_data(ssl, php_openssl_get_ssl_stream_data_index(), clistream); + + /* Finish the handshake (SSL_do_handshake performs the accept). */ + if (php_openssl_dtls_handshake(clistream, clisock, + xparam->inputs.timeout, + xparam->want_errortext ? &xparam->outputs.error_text : NULL) != 0) { + php_stream_close(clistream); + return -1; + } + clisock->ssl_active = true; + + xparam->outputs.client = clistream; + return 0; +} + +/* Connect the datagram socket through the generic transport, then run DTLS on + * top, the way xp_ssl.c layers TLS on a connected tcp:// stream. */ +static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + php_stream_xport_param *xparam) +{ + /* The generic ops open and connect the datagram socket (recognised as UDP by + * the "udp_socket" ops label). */ + php_stream_socket_ops.set_option(stream, PHP_STREAM_OPTION_XPORT_API, 0, xparam); + if (xparam->outputs.returncode != 0 || !dtlssock->enable_on_connect) { + return xparam->outputs.returncode; + } + + /* Run the handshake here rather than via the crypto ops so the connect timeout + * and the OpenSSL error text still reach the caller. */ + zend_string **err_text = xparam->want_errortext ? &xparam->outputs.error_text : NULL; + if (php_openssl_dtls_setup_crypto(stream, dtlssock, NULL) != 0 + || php_openssl_dtls_handshake(stream, dtlssock, &dtlssock->connect_timeout, err_text) != 0 + || php_openssl_dtls_check_fingerprint(stream, dtlssock, err_text) != 0) { + xparam->outputs.returncode = -1; + return -1; + } + + dtlssock->ssl_active = true; + return 0; +} + +/* Run or tear down the DTLS handshake for stream_socket_enable_crypto(). */ +static int php_openssl_dtls_enable_crypto(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + php_stream_xport_crypto_param *cparam) +{ + if (cparam->inputs.activate && !dtlssock->ssl_active) { + if (dtlssock->ssl_handle == NULL) { + php_stream_warn(stream, Generic, "Crypto has not been set up; the setup op must run first"); + return -1; + } + if (php_openssl_dtls_handshake(stream, dtlssock, &dtlssock->s.timeout, NULL) != 0) { + return -1; + } + if (php_openssl_dtls_check_fingerprint(stream, dtlssock, NULL) != 0) { + return -1; + } + dtlssock->ssl_active = true; + /* 1 (not 0) so stream_socket_enable_crypto() reports success, as xp_ssl.c does. */ + return 1; + } else if (!cparam->inputs.activate && dtlssock->ssl_active) { + SSL_shutdown(dtlssock->ssl_handle); + dtlssock->ssl_active = false; + return 1; + } + + return -1; +} + +/* Expose the fd for stream_select(); the raw fd is not handed out otherwise, + * since DTLS is always encrypted. */ +static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **ret) +{ + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + + switch (castas) { + case PHP_STREAM_AS_FD_FOR_SELECT: + if (ret != NULL) { + /* Decrypted data buffered in OpenSSL is invisible to select(), so push + * it into the read buffer. + * TODO: same idiom as php_openssl_sockop_cast() in xp_ssl.c. */ + size_t pending; + if (stream->writepos == stream->readpos + && dtlssock->ssl_handle != NULL + && (pending = (size_t)SSL_pending(dtlssock->ssl_handle)) > 0) { + php_stream_fill_read_buffer(stream, pending < stream->chunk_size + ? pending + : stream->chunk_size); + } + *(php_socket_t *)ret = dtlssock->s.socket; + } + return SUCCESS; + + default: + /* Plain udp:// (no crypto): let the base hand out the fd/stdio. */ + if (!dtlssock->ssl_active) { + return php_stream_socket_ops.cast(stream, castas, ret); + } + return FAILURE; + } +} + +/* Handle transport and stream options. */ +static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam) +{ + php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; + php_stream_xport_param *xparam; + + switch (option) { + case PHP_STREAM_OPTION_META_DATA_API: { + if (dtlssock->ssl_handle != NULL) { + zval crypto; + char *proto_str; + const SSL_CIPHER *cipher; + zval *val; + + array_init(&crypto); + switch (SSL_version(dtlssock->ssl_handle)) { + case DTLS1_2_VERSION: proto_str = "DTLSv1.2"; break; + case DTLS1_VERSION: proto_str = "DTLSv1.0"; break; + default: proto_str = "UNKNOWN"; break; + } + add_assoc_string(&crypto, "protocol", proto_str); + + cipher = SSL_get_current_cipher(dtlssock->ssl_handle); + if (cipher != NULL) { + add_assoc_string(&crypto, "cipher_name", (char *) SSL_CIPHER_get_name(cipher)); + add_assoc_long(&crypto, "cipher_bits", SSL_CIPHER_get_bits(cipher, NULL)); + add_assoc_string(&crypto, "cipher_version", (char *) SSL_CIPHER_get_version(cipher)); + } + + /* RFC 5705 exported keying material (e.g. DTLS-SRTP keys), + * requested via the keying_material_label/length context options. */ + 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(dtlssock->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(&crypto, "keying_material", km); + } else { + zend_string_release(km); + } + } + } + + /* Expose the negotiated session so the caller can resume it later + * (session_data), and whether this handshake was resumed. */ + SSL_SESSION *session = SSL_get1_session(dtlssock->ssl_handle); + if (session != NULL) { + zval zsession; + php_openssl_session_object_init(&zsession, session); + add_assoc_zval(&crypto, "session", &zsession); + } + add_assoc_bool(&crypto, "session_reused", + SSL_session_reused(dtlssock->ssl_handle) == 1); + + add_assoc_zval((zval *)ptrparam, "crypto", &crypto); + } + add_assoc_bool((zval *)ptrparam, "timed_out", dtlssock->s.timeout_event); + add_assoc_bool((zval *)ptrparam, "blocked", dtlssock->s.is_blocked); + add_assoc_bool((zval *)ptrparam, "eof", stream->eof); + return PHP_STREAM_OPTION_RETURN_OK; + } + + case PHP_STREAM_OPTION_CRYPTO_API: { + php_stream_xport_crypto_param *cparam = (php_stream_xport_crypto_param *)ptrparam; + + switch (cparam->op) { + case STREAM_XPORT_CRYPTO_OP_SETUP: + /* DTLS 1.3 is a defined method but not implemented yet. */ + if (cparam->inputs.method & STREAM_CRYPTO_METHOD_DTLSv1_3_SERVER) { + php_stream_warn(stream, Generic, "DTLS 1.3 is not supported yet"); + cparam->outputs.returncode = -1; + return PHP_STREAM_OPTION_RETURN_OK; + } + cparam->outputs.returncode = + php_openssl_dtls_setup_crypto(stream, dtlssock, NULL); + return PHP_STREAM_OPTION_RETURN_OK; + + case STREAM_XPORT_CRYPTO_OP_ENABLE: + cparam->outputs.returncode = + php_openssl_dtls_enable_crypto(stream, dtlssock, cparam); + return PHP_STREAM_OPTION_RETURN_OK; + + case STREAM_XPORT_CRYPTO_OP_GET_STATUS: + cparam->outputs.returncode = dtlssock->ssl_active; + return PHP_STREAM_OPTION_RETURN_OK; + + default: + return PHP_STREAM_OPTION_RETURN_ERR; + } + } + + case PHP_STREAM_OPTION_XPORT_API: + xparam = (php_stream_xport_param *)ptrparam; + + switch (xparam->op) { + case STREAM_XPORT_OP_CONNECT: + case STREAM_XPORT_OP_CONNECT_ASYNC: + xparam->outputs.returncode = + php_openssl_dtls_connect(stream, dtlssock, xparam); + return PHP_STREAM_OPTION_RETURN_OK; + + case STREAM_XPORT_OP_BIND: { + /* s.socktype makes the generic ops bind a SOCK_DGRAM socket. */ + php_stream_socket_ops.set_option(stream, option, value, ptrparam); + if (xparam->outputs.returncode != 0 || !dtlssock->enable_on_connect) { + return PHP_STREAM_OPTION_RETURN_OK; + } + /* dtls:// server: keep the fd non-blocking for the accept timers + * and set up the cookie/handshake context. */ + php_set_sock_blocking(dtlssock->s.socket, 0); + dtlssock->is_server = true; + xparam->outputs.returncode = php_openssl_dtls_server_ctx(stream, dtlssock); + if (xparam->outputs.returncode != 0) { + closesocket(dtlssock->s.socket); + dtlssock->s.socket = SOCK_ERR; + } + return PHP_STREAM_OPTION_RETURN_OK; + } + + case STREAM_XPORT_OP_LISTEN: + /* DTLS has no socket-level listen; accept uses DTLSv1_listen. */ + xparam->outputs.returncode = 0; + return PHP_STREAM_OPTION_RETURN_OK; + + case STREAM_XPORT_OP_ACCEPT: + xparam->outputs.returncode = php_openssl_dtls_accept(stream, dtlssock, xparam STREAMS_CC); + return PHP_STREAM_OPTION_RETURN_OK; + + default: + /* Local/peer name, shutdown, etc. operate on the socket. */ + return php_stream_socket_ops.set_option(stream, option, value, ptrparam); + } + + case PHP_STREAM_OPTION_BLOCKING: { + if (!dtlssock->ssl_active) { + /* Plain udp:// socket: let the base handler flip the fd. */ + return php_stream_socket_ops.set_option(stream, option, value, ptrparam); + } + /* The fd must stay non-blocking for the DTLS timers, so only track the + * stream-level mode (the base handler would flip the fd too). */ + int old = dtlssock->s.is_blocked; + dtlssock->s.is_blocked = value; + return old; + } + } + + /* Read timeout, liveness check and the rest operate on the embedded base + * socket data, so defer to the generic socket handler. */ + return php_stream_socket_ops.set_option(stream, option, value, ptrparam); +} + +static const php_stream_ops php_openssl_dtls_socket_ops = { + php_openssl_dtls_sockop_write, php_openssl_dtls_sockop_read, + php_openssl_dtls_sockop_close, NULL, /* flush */ + "udp_socket/dtls", + NULL, /* seek */ + php_openssl_dtls_sockop_cast, + NULL, /* stat */ + php_openssl_dtls_sockop_set_option, +}; + +/* Allocate a dtls:// stream. */ +php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, + const char *resourcename, size_t resourcenamelen, + const char *persistent_id, int options, int flags, + struct timeval *timeout, + php_stream_context *context STREAMS_DC) +{ + php_openssl_dtls_data_t *dtlssock; + php_stream *stream; + + dtlssock = pemalloc(sizeof(*dtlssock), persistent_id ? 1 : 0); + memset(dtlssock, 0, sizeof(*dtlssock)); + dtlssock->s.socket = -1; + dtlssock->s.is_blocked = true; + dtlssock->s.is_dgram = true; + dtlssock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); + dtlssock->s.timeout.tv_usec = 0; + if (timeout != NULL) { + dtlssock->connect_timeout = *timeout; + } else { + dtlssock->connect_timeout = dtlssock->s.timeout; + } + + if (strncmp(proto, "udp", protolen) == 0) { + /* Plain udp://: stays a datagram socket until stream_socket_enable_crypto(). */ + dtlssock->enable_on_connect = 0; + } else { + /* dtls:// / dtlsv1.2://: run DTLS on connect. */ + dtlssock->enable_on_connect = 1; + dtlssock->method = STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT; + } + + stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); + if (stream == NULL) { + pefree(dtlssock, persistent_id ? 1 : 0); + return NULL; + } + + dtlssock->url_name = php_openssl_get_url_name(resourcename, resourcenamelen, persistent_id != NULL, context); + + return stream; +} + +#endif /* OPENSSL_NO_DTLS */ diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index bc483df617b1..39f91e375043 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -28,6 +28,7 @@ #include "php_openssl.h" #include "php_openssl_backend.h" #include "php_network.h" +#include "xp_common.h" #include #include #include @@ -378,73 +379,8 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) /* {{{ */ } /* }}} */ -static bool php_openssl_x509_fingerprint_is_equal(php_stream *stream, X509 *peer, const char *method, const zend_string *expected) -{ - bool is_equal = false; - zend_string *fingerprint = php_openssl_x509_fingerprint(peer, method, false, stream); - if (fingerprint) { - is_equal = zend_string_equals_ci(fingerprint, expected); - zend_string_release_ex(fingerprint, false); - } - - return is_equal; -} - -static bool php_openssl_x509_fingerprint_match(php_stream *stream, X509 *peer, const zval *val) -{ - if (Z_TYPE_P(val) == IS_STRING) { - const char *method = NULL; - - switch (Z_STRLEN_P(val)) { - case 32: - method = "md5"; - break; - - case 40: - method = "sha1"; - break; - } - - if (UNEXPECTED(method == NULL)) { - php_stream_warn(stream, AuthFailed, "peer_fingerprint length doesn't match a md5 or sha1 hash"); - return false; - } - if (!php_openssl_x509_fingerprint_is_equal(stream, peer, method, Z_STR_P(val))) { - php_stream_warn(stream, AuthFailed, "peer_fingerprint match failure"); - return false; - } - return true; - } else if (Z_TYPE_P(val) == IS_ARRAY) { - zval *current; - zend_string *key; - - if (!zend_hash_num_elements(Z_ARRVAL_P(val))) { - // TODO: Should this be a ValueError (also must not be empty error)? - php_stream_warn(stream, Generic, "Invalid peer_fingerprint array; [algo => fingerprint] form required"); - return false; - } - - ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(val), key, current) { - if (key == NULL || Z_TYPE_P(current) != IS_STRING) { - // TODO: Should this be a ValueError? - php_stream_warn(stream, Generic, "Invalid peer_fingerprint array; [algo => fingerprint] form required"); - return false; - } - if (!php_openssl_x509_fingerprint_is_equal(stream, peer, ZSTR_VAL(key), Z_STR_P(current))) { - php_stream_warn(stream, AuthFailed, "peer_fingerprint match failure"); - return false; - } - } ZEND_HASH_FOREACH_END(); - - return true; - } else { - // TODO: Should this be a TypeError? - php_stream_warn(stream, Generic, - "Invalid peer_fingerprint value; fingerprint string or array of the form [algo => fingerprint] required"); - } - - return false; -} +/* php_openssl_x509_fingerprint_is_equal() and php_openssl_x509_fingerprint_match() + * are shared with the dtls:// transport; see xp_common.c. */ static bool php_openssl_matches_wildcard_name(const char *subjectname, const char *certname) /* {{{ */ { @@ -681,24 +617,7 @@ static zend_result php_openssl_apply_peer_verification_policy(SSL *ssl, X509 *pe } /* }}} */ -static int php_openssl_passwd_callback(char *buf, int num, int verify, void *data) /* {{{ */ -{ - php_stream *stream = (php_stream *)data; - zval *val = NULL; - char *passphrase = NULL; - /* TODO: could expand this to make a callback into PHP user-space */ - - GET_VER_OPT_STRING("passphrase", passphrase); - - if (passphrase) { - if (Z_STRLEN_P(val) < (size_t)num - 1) { - memcpy(buf, Z_STRVAL_P(val), Z_STRLEN_P(val)+1); - return (int)Z_STRLEN_P(val); - } - } - return 0; -} -/* }}} */ +/* php_openssl_passwd_callback() is shared with the dtls:// transport; see xp_common.c. */ #ifdef PHP_WIN32 #define RETURN_CERT_VERIFY_FAILURE(code) X509_STORE_CTX_set_error(x509_store_ctx, code); return 0; @@ -972,53 +891,7 @@ static void php_openssl_disable_peer_verification(SSL_CTX *ctx, php_stream *stre } /* }}} */ -static zend_result php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream) /* {{{ */ -{ - zval *val = NULL; - char *certfile = NULL; - size_t certfile_len; - - GET_VER_OPT_STRINGL("local_cert", certfile, certfile_len); - - if (certfile) { - char resolved_path_buff[MAXPATHLEN]; - const char *private_key = NULL; - size_t private_key_len; - - if (!php_openssl_check_path_ex( - certfile, certfile_len, resolved_path_buff, 0, false, false, - "local_cert in ssl stream context", stream)) { - php_stream_warn(stream, NotFound, "Unable to get real path of certificate file `%s'", certfile); - return FAILURE; - } - /* a certificate to use for authentication */ - if (SSL_CTX_use_certificate_chain_file(ctx, resolved_path_buff) != 1) { - php_stream_warn(stream, WriteFailed, - "Unable to set local cert chain file `%s'; Check that your cafile/capath " - "settings include details of your certificate and its issuer", - certfile); - return FAILURE; - } - - GET_VER_OPT_STRINGL("local_pk", private_key, private_key_len); - if (private_key && !php_openssl_check_path_ex( - private_key, private_key_len, resolved_path_buff, 0, false, false, - "local_pk in ssl stream context", stream)) { - php_stream_warn(stream, NotFound, "Unable to get real path of private key file `%s'", private_key); - return FAILURE; - } - if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff, SSL_FILETYPE_PEM) != 1) { - php_stream_warn(stream, WriteFailed, "Unable to set private key file `%s'", resolved_path_buff); - return FAILURE; - } - if (!SSL_CTX_check_private_key(ctx)) { - php_stream_warn(stream, PermissionDenied, "Private key does not match certificate!"); - } - } - - return SUCCESS; -} -/* }}} */ +/* php_openssl_set_local_cert() is shared with the dtls:// transport; see xp_common.c. */ static inline int php_openssl_get_min_proto_version_flag(int flags) /* {{{ */ { @@ -3499,16 +3372,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val (xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC && xparam->outputs.returncode == 1 && xparam->outputs.error_code == EINPROGRESS))) { - zval *val; - php_stream *session_stream = NULL; - - if (GET_VER_OPT("session_stream")) { - 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) { - php_stream_warn(stream, ProtocolError, "Failed to enable crypto"); + if (php_openssl_setup_crypto_on_connect(stream, sslsock->method) < 0) { xparam->outputs.returncode = -1; } } @@ -3619,47 +3483,7 @@ static zend_long php_openssl_get_crypto_method( } /* }}} */ -static char *php_openssl_get_url_name(const char *resourcename, - size_t resourcenamelen, int is_persistent, php_stream_context *context) /* {{{ */ -{ - if (!resourcename) { - return NULL; - } - - const php_uri_parser *uri_parser = php_stream_context_get_uri_parser("ssl", context); - if (uri_parser == NULL) { - zend_value_error("%s(): Provided stream context has invalid value for the \"uri_parser_class\" option", get_active_function_name()); - return NULL; - } - - php_uri_internal *internal_uri = php_uri_parse(uri_parser, resourcename, resourcenamelen, true); - if (internal_uri == NULL) { - return NULL; - } - - char * url_name = NULL; - zval host_zv; - zend_result result = php_uri_get_host(internal_uri, PHP_URI_COMPONENT_READ_MODE_RAW, &host_zv); - if (result == SUCCESS && Z_TYPE(host_zv) == IS_STRING) { - const char * host = Z_STRVAL(host_zv); - size_t len = Z_STRLEN(host_zv); - - /* skip trailing dots */ - while (len && host[len-1] == '.') { - --len; - } - - if (len) { - url_name = pestrndup(host, len, is_persistent); - } - } - - php_uri_free(internal_uri); - zval_ptr_dtor(&host_zv); - - return url_name; -} -/* }}} */ +/* php_openssl_get_url_name() is shared with the dtls:// transport; see xp_common.c. */ php_stream *php_openssl_ssl_socket_factory(const char *proto, size_t protolen, const char *resourcename, size_t resourcenamelen, 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 24e3722cd86e..dee62529bc2c 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 */ static void register_file_symbols(int module_number) { @@ -38,6 +38,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); @@ -47,6 +50,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 e6d3009a6c82..44625ed7092c 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -357,6 +357,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 60bea8e9e1fc..33552e5a8933 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), 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) } 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 d18b0e901957..89e4168e54cb 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_STD(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);