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);