From 1f8915b5be565d63255c23d479ff604923b2ee7c Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 12:15:15 +0200 Subject: [PATCH 01/19] openssl: Add skeleton dtls:// stream transport and tests --- ext/openssl/config.w32 | 2 +- ext/openssl/config0.m4 | 2 +- ext/openssl/openssl.c | 8 ++ ext/openssl/php_openssl.h | 1 + .../tests/dtls_transport_registered.phpt | 10 +++ ext/openssl/xp_dtls.c | 89 +++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 ext/openssl/tests/dtls_transport_registered.phpt create mode 100644 ext/openssl/xp_dtls.c diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 index 4b7f4b8b8565..8016f8f97ef4 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_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..95eed7c559d4 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_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..7a36a11fa01b 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -830,6 +830,10 @@ 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); +#endif + /* override the default tcp socket provider */ php_stream_xport_register("tcp", php_openssl_ssl_socket_factory); @@ -904,6 +908,10 @@ 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"); +#endif + /* reinstate the default tcp handler */ php_stream_xport_register("tcp", php_stream_generic_socket_factory); 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_transport_registered.phpt b/ext/openssl/tests/dtls_transport_registered.phpt new file mode 100644 index 000000000000..937e6fdd2bea --- /dev/null +++ b/ext/openssl/tests/dtls_transport_registered.phpt @@ -0,0 +1,10 @@ +--TEST-- +dtls:// stream transport is registered +--EXTENSIONS-- +openssl +--FILE-- + +--EXPECT-- +bool(true) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c new file mode 100644 index 000000000000..0c9d8e124b6e --- /dev/null +++ b/ext/openssl/xp_dtls.c @@ -0,0 +1,89 @@ +/* + +----------------------------------------------------------------------+ + | 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 "streams/php_streams_int.h" +#include "php_openssl.h" +#include "php_network.h" +#include + +#ifndef OPENSSL_NO_DTLS + +/* Stream data backing a dtls:// transport. The datagram BIO is owned by + * ssl_handle (attached with SSL_set_bio) and freed with it. */ +typedef struct _php_openssl_dtls_data_t { + php_socket_t socket; +} php_openssl_dtls_data_t; + +/* TODO: datagram read/write */ +static ssize_t php_openssl_dtls_sockop_write(php_stream *stream, const char *buf, size_t count) +{ + return -1; +} + +static ssize_t php_openssl_dtls_sockop_read(php_stream *stream, char *buf, size_t count) +{ + return -1; +} + +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; + } + + pefree(dtlssock, php_stream_is_persistent(stream)); + stream->abstract = NULL; + + return 0; +} + +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 */ + NULL, /* cast */ + NULL, /* stat */ + NULL, /* set_option */ +}; + +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->socket = -1; + + stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); + if (stream == NULL) { + pefree(dtlssock, persistent_id ? 1 : 0); + } + + return stream; +} + +#endif /* OPENSSL_NO_DTLS */ From 4be6e60585927e0cfe79a15c55d796feb22fe486 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 13:05:58 +0200 Subject: [PATCH 02/19] openssl: Connect the dtls:// client and set up the DTLS objects --- ext/openssl/tests/dtls_client_basic.phpt | 21 +++ ext/openssl/tests/dtls_skipif.inc | 23 +++ .../tests/dtls_transport_registered.phpt | 10 -- ext/openssl/xp_dtls.c | 157 +++++++++++++++++- 4 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 ext/openssl/tests/dtls_client_basic.phpt create mode 100644 ext/openssl/tests/dtls_skipif.inc delete mode 100644 ext/openssl/tests/dtls_transport_registered.phpt diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt new file mode 100644 index 000000000000..2f3243951277 --- /dev/null +++ b/ext/openssl/tests/dtls_client_basic.phpt @@ -0,0 +1,21 @@ +--TEST-- +dtls:// client: transport, UDP socket and DTLS setup +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +int(0) diff --git a/ext/openssl/tests/dtls_skipif.inc b/ext/openssl/tests/dtls_skipif.inc new file mode 100644 index 000000000000..2973e4557f8d --- /dev/null +++ b/ext/openssl/tests/dtls_skipif.inc @@ -0,0 +1,23 @@ + + * + * Skips when the openssl extension does not expose a usable dtls:// transport, + * for example an OpenSSL build with OPENSSL_NO_DTLS or a php-src that does not + * register the transport yet. Tests that also spawn a peer process should add + * their own proc_open() check on top of this one. + */ + +if (!extension_loaded('openssl')) { + die('skip openssl extension not available'); +} + +if (!in_array('dtls', stream_get_transports(), true)) { + die('skip dtls:// stream transport not available'); +} diff --git a/ext/openssl/tests/dtls_transport_registered.phpt b/ext/openssl/tests/dtls_transport_registered.phpt deleted file mode 100644 index 937e6fdd2bea..000000000000 --- a/ext/openssl/tests/dtls_transport_registered.phpt +++ /dev/null @@ -1,10 +0,0 @@ ---TEST-- -dtls:// stream transport is registered ---EXTENSIONS-- -openssl ---FILE-- - ---EXPECT-- -bool(true) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 0c9d8e124b6e..50953507b883 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -21,6 +21,7 @@ #include "php_openssl.h" #include "php_network.h" #include +#include #ifndef OPENSSL_NO_DTLS @@ -28,6 +29,8 @@ * ssl_handle (attached with SSL_set_bio) and freed with it. */ typedef struct _php_openssl_dtls_data_t { php_socket_t socket; + SSL_CTX *ctx; + SSL *ssl_handle; } php_openssl_dtls_data_t; /* TODO: datagram read/write */ @@ -49,12 +52,164 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) return 0; } + /* SSL_free also frees the datagram BIO attached with SSL_set_bio; the BIO was + * created BIO_NOCLOSE, so the socket itself is closed separately below. */ + if (dtlssock->ssl_handle != NULL) { + 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->socket != SOCK_ERR) { + closesocket(dtlssock->socket); + dtlssock->socket = SOCK_ERR; + } + pefree(dtlssock, php_stream_is_persistent(stream)); stream->abstract = NULL; return 0; } +/* Split "host:port" (or "[ipv6]:port") from the resource name into host and + * port. Returns an emalloc'd host the caller must efree, or NULL on a parse + * error, with xparam->outputs.error_text set when requested. */ +static char *php_openssl_dtls_parse_ip_address(php_stream_xport_param *xparam, int *portno) +{ + const char *str = xparam->inputs.name; + size_t str_len = xparam->inputs.namelen; + const char *colon; + + if (str == NULL || memchr(str, '\0', str_len)) { + if (xparam->want_errortext) { + xparam->outputs.error_text = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); + } + return NULL; + } + +#ifdef HAVE_IPV6 + if (*str == '[' && str_len > 1) { + /* [ipv6]:port notation, e.g. [fe80::1]:443 */ + const char *p = memchr(str + 1, ']', str_len - 2); + if (p == NULL || *(p + 1) != ':') { + if (xparam->want_errortext) { + xparam->outputs.error_text = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); + } + return NULL; + } + *portno = atoi(p + 2); + return estrndup(str + 1, p - str - 1); + } +#endif + + colon = str_len ? memchr(str, ':', str_len - 1) : NULL; + if (colon == NULL) { + if (xparam->want_errortext) { + xparam->outputs.error_text = strpprintf(0, "Failed to parse address \"%s\"", str); + } + return NULL; + } + + *portno = atoi(colon + 1); + return estrndup(str, colon - str); +} + +/* Create the DTLS context and SSL object and bind a datagram BIO to the + * connected UDP socket. */ +static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +{ + BIO *bio; + + dtlssock->ctx = SSL_CTX_new(DTLS_client_method()); + if (dtlssock->ctx == NULL) { + php_stream_warn(stream, CreateFailed, "DTLS context creation failure"); + 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; + } + + bio = BIO_new_dgram(dtlssock->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; + } + + /* The UDP socket is already connected, so one datagram BIO serves both + * reads and writes. */ + SSL_set_bio(dtlssock->ssl_handle, bio, bio); + SSL_set_connect_state(dtlssock->ssl_handle); + + return 0; +} + +/* Create the UDP socket, connect it to the peer and attach the DTLS objects. */ +static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + php_stream_xport_param *xparam) +{ + char *host; + int portno = 0; + int err = 0; + + host = php_openssl_dtls_parse_ip_address(xparam, &portno); + if (host == NULL) { + return -1; + } + + dtlssock->socket = php_network_connect_socket_to_host(host, (unsigned short)portno, + SOCK_DGRAM, + xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, + xparam->inputs.timeout, + xparam->want_errortext ? &xparam->outputs.error_text : NULL, + &err, NULL, 0, STREAM_SOCKOP_NONE); + + xparam->outputs.error_code = err; + efree(host); + + if (dtlssock->socket == SOCK_ERR) { + return -1; + } + + return php_openssl_dtls_setup_crypto(stream, dtlssock); +} + +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_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; + + default: + return PHP_STREAM_OPTION_RETURN_NOTIMPL; + } + + default: + return PHP_STREAM_OPTION_RETURN_NOTIMPL; + } +} + 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 */ @@ -62,7 +217,7 @@ static const php_stream_ops php_openssl_dtls_socket_ops = { NULL, /* seek */ NULL, /* cast */ NULL, /* stat */ - NULL, /* set_option */ + php_openssl_dtls_sockop_set_option, }; php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, From 0d75bd81ce53d0cdb5ec273356225b646f4b03c0 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 14:54:54 +0200 Subject: [PATCH 03/19] openssl: Drive the dtls:// client handshake and datagram I/O --- ext/openssl/tests/dtls_client_basic.phpt | 85 ++++++++++-- ext/openssl/xp_dtls.c | 164 ++++++++++++++++++++++- 2 files changed, 233 insertions(+), 16 deletions(-) diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt index 2f3243951277..dbff3a6d1fe4 100644 --- a/ext/openssl/tests/dtls_client_basic.phpt +++ b/ext/openssl/tests/dtls_client_basic.phpt @@ -1,21 +1,86 @@ --TEST-- -dtls:// client: transport, UDP socket and DTLS setup +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; } + } +} + +$client = $ready ? stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10) : false; var_dump($client !== false); -var_dump($errno); + if ($client !== false) { + 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"); + + 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); + fclose($client); } + +proc_terminate($server); +foreach ($pipes as $pipe) { + if (is_resource($pipe)) fclose($pipe); +} +proc_close($server); +@unlink($certFile); ?> --EXPECT-- bool(true) -int(0) +bool(true) +bool(true) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 50953507b883..72ddedb0dc74 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -17,31 +17,96 @@ #endif #include "php.h" +#include "ext/standard/file.h" #include "streams/php_streams_int.h" #include "php_openssl.h" #include "php_network.h" #include #include +#include #ifndef OPENSSL_NO_DTLS /* Stream data backing a dtls:// transport. The datagram BIO is owned by - * ssl_handle (attached with SSL_set_bio) and freed with it. */ + * ssl_handle (attached with SSL_set_bio) and freed with it. The socket stays + * non-blocking for the DTLS timers, so is_blocked tracks the stream-level mode + * and timeout is the read timeout, both honoured by the I/O loop. */ typedef struct _php_openssl_dtls_data_t { php_socket_t socket; SSL_CTX *ctx; SSL *ssl_handle; + bool is_blocked; + struct timeval timeout; } php_openssl_dtls_data_t; -/* TODO: datagram read/write */ +/* Datagram I/O: each SSL_read/SSL_write maps to one DTLS record. The socket is + * non-blocking, so when OpenSSL wants more I/O we poll (up to the read timeout + * for a blocking stream) 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; + SSL *ssl = dtlssock->ssl_handle; + + if (ssl == NULL) { + return -1; + } + + /* OpenSSL takes an int length. */ + if (count > INT_MAX) { + count = INT_MAX; + } + + int timeout_ms = -1; + if (dtlssock->is_blocked && (dtlssock->timeout.tv_sec > 0 || dtlssock->timeout.tv_usec > 0)) { + timeout_ms = (int)(dtlssock->timeout.tv_sec * 1000 + dtlssock->timeout.tv_usec / 1000); + } + + for (;;) { + ERR_clear_error(); + int n = read ? SSL_read(ssl, buf, (int)count) : SSL_write(ssl, buf, (int)count); + if (n > 0) { + return n; + } + + int events; + 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: + /* The peer sent a close_notify alert. */ + stream->eof = 1; + return 0; + default: + if (read) { + stream->eof = 1; + } + return -1; + } + + /* Non-blocking stream: report that no data is available right now. */ + if (!dtlssock->is_blocked) { + return -1; + } + + if (php_pollfd_for_ms(dtlssock->socket, events, timeout_ms) <= 0) { + /* Timed out or poll error. */ + return -1; + } + } +} + static ssize_t php_openssl_dtls_sockop_write(php_stream *stream, const char *buf, size_t count) { - return -1; + return php_openssl_dtls_io(false, stream, (char *)buf, count); } static ssize_t php_openssl_dtls_sockop_read(php_stream *stream, char *buf, size_t count) { - return -1; + return php_openssl_dtls_io(true, stream, buf, count); } static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) @@ -147,6 +212,17 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return -1; } + /* A datagram BIO defaults to sendto() with an empty peer address, which + * fails with EINVAL on our connected socket. Tell it the socket is connected + * (and to whom) so it uses send()/recv() instead. */ + { + struct sockaddr_storage peer; + socklen_t peerlen = sizeof(peer); + if (getpeername(dtlssock->socket, (struct sockaddr *)&peer, &peerlen) == 0) { + BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); + } + } + /* The UDP socket is already connected, so one datagram BIO serves both * reads and writes. */ SSL_set_bio(dtlssock->ssl_handle, bio, bio); @@ -155,7 +231,60 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return 0; } -/* Create the UDP socket, connect it to the peer and attach the DTLS objects. */ +/* Drive the DTLS handshake to completion. The socket is non-blocking, so the + * loop waits for readability or for the next retransmission timeout, after + * which OpenSSL resends the last flight. */ +static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +{ + SSL *ssl = dtlssock->ssl_handle; + + for (;;) { + int n = SSL_do_handshake(ssl); + if (n == 1) { + return 0; + } + + 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)); + } + php_stream_warn(stream, ProtocolError, "DTLS handshake failed: %s", + buf[0] != '\0' ? buf : "unexpected error"); + return -1; + } + } + + struct timeval tv; + int timeout = DTLSv1_get_timeout(ssl, &tv) + ? (int)(tv.tv_sec * 1000 + tv.tv_usec / 1000) + : -1; + + int ready = php_pollfd_for_ms(dtlssock->socket, events, timeout); + if (ready == 0) { + /* The retransmission timer fired: let OpenSSL resend the last flight. */ + if (DTLSv1_handle_timeout(ssl) < 0) { + php_stream_warn(stream, ProtocolError, "DTLS handshake timed out"); + return -1; + } + } else if (ready < 0) { + php_stream_warn(stream, ProtocolError, "DTLS handshake failed while waiting for the socket"); + return -1; + } + } +} + +/* Create the UDP socket, connect it to the peer, attach the DTLS objects and + * run the handshake. */ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, php_stream_xport_param *xparam) { @@ -182,7 +311,15 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t return -1; } - return php_openssl_dtls_setup_crypto(stream, dtlssock); + /* DTLS drives its own retransmission timers, so the socket must be + * non-blocking for the handshake and datagram I/O loops. */ + php_set_sock_blocking(dtlssock->socket, 0); + + if (php_openssl_dtls_setup_crypto(stream, dtlssock) != 0) { + return -1; + } + + return php_openssl_dtls_handshake(stream, dtlssock); } static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam) @@ -205,6 +342,18 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in return PHP_STREAM_OPTION_RETURN_NOTIMPL; } + case PHP_STREAM_OPTION_BLOCKING: { + /* The fd stays non-blocking for the DTLS timers; the stream-level + * blocking mode is emulated with poll in the I/O loop. */ + int old = dtlssock->is_blocked; + dtlssock->is_blocked = value; + return old; + } + + case PHP_STREAM_OPTION_READ_TIMEOUT: + dtlssock->timeout = *(struct timeval *)ptrparam; + return PHP_STREAM_OPTION_RETURN_OK; + default: return PHP_STREAM_OPTION_RETURN_NOTIMPL; } @@ -232,6 +381,9 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, dtlssock = pemalloc(sizeof(*dtlssock), persistent_id ? 1 : 0); memset(dtlssock, 0, sizeof(*dtlssock)); dtlssock->socket = -1; + dtlssock->is_blocked = true; + dtlssock->timeout.tv_sec = (time_t)FG(default_socket_timeout); + dtlssock->timeout.tv_usec = 0; stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); if (stream == NULL) { From 89aa7805d57506cc44d9969ec9564a8313a5ad27 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 15:32:50 +0200 Subject: [PATCH 04/19] openssl: Expose underlying fd via dtls:// cast op --- ext/openssl/tests/dtls_client_basic.phpt | 4 +++ ext/openssl/xp_dtls.c | 31 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt index dbff3a6d1fe4..e0a703ae00ed 100644 --- a/ext/openssl/tests/dtls_client_basic.phpt +++ b/ext/openssl/tests/dtls_client_basic.phpt @@ -60,6 +60,9 @@ if ($client !== false) { // 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 = ''; @@ -84,3 +87,4 @@ proc_close($server); bool(true) bool(true) bool(true) +bool(true) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 72ddedb0dc74..1250363311af 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -322,6 +322,35 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t return php_openssl_dtls_handshake(stream, dtlssock); } +/* Expose the underlying fd so stream_select() and friends can wait on the + * dtls:// stream. The raw fd/FILE* is never 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) { + /* Data already decrypted and buffered in OpenSSL would not show up + * in a select() on the socket, so surface it into the read buffer. */ + 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->socket; + } + return SUCCESS; + + default: + return FAILURE; + } +} + 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; @@ -364,7 +393,7 @@ static const php_stream_ops php_openssl_dtls_socket_ops = { php_openssl_dtls_sockop_close, NULL, /* flush */ "udp_socket/dtls", NULL, /* seek */ - NULL, /* cast */ + php_openssl_dtls_sockop_cast, NULL, /* stat */ php_openssl_dtls_sockop_set_option, }; From 9b11a268e02e1e8b72e0ce5161af5d8e85b0e387 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 16:43:21 +0200 Subject: [PATCH 05/19] openssl: Reuse base socket data and improve dtls:// diagnostics --- ext/openssl/tests/dtls_client_basic.phpt | 6 + ext/openssl/xp_dtls.c | 170 ++++++++++++++--------- 2 files changed, 108 insertions(+), 68 deletions(-) diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt index e0a703ae00ed..3cf78d6db381 100644 --- a/ext/openssl/tests/dtls_client_basic.phpt +++ b/ext/openssl/tests/dtls_client_basic.phpt @@ -73,6 +73,11 @@ if ($client !== false) { } 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']); + fclose($client); } @@ -88,3 +93,4 @@ bool(true) bool(true) bool(true) bool(true) +bool(true) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 1250363311af..85e2f2589d20 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -27,21 +27,16 @@ #ifndef OPENSSL_NO_DTLS -/* Stream data backing a dtls:// transport. The datagram BIO is owned by - * ssl_handle (attached with SSL_set_bio) and freed with it. The socket stays - * non-blocking for the DTLS timers, so is_blocked tracks the stream-level mode - * and timeout is the read timeout, both honoured by the I/O loop. */ +/* 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_socket_t socket; + php_netstream_data_t s; SSL_CTX *ctx; SSL *ssl_handle; - bool is_blocked; - struct timeval timeout; } php_openssl_dtls_data_t; -/* Datagram I/O: each SSL_read/SSL_write maps to one DTLS record. The socket is - * non-blocking, so when OpenSSL wants more I/O we poll (up to the read timeout - * for a blocking stream) and retry. */ +/* 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; @@ -56,9 +51,20 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz count = INT_MAX; } - int timeout_ms = -1; - if (dtlssock->is_blocked && (dtlssock->timeout.tv_sec > 0 || dtlssock->timeout.tv_usec > 0)) { - timeout_ms = (int)(dtlssock->timeout.tv_sec * 1000 + dtlssock->timeout.tv_usec / 1000); + 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 (;;) { @@ -77,7 +83,7 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz events = POLLOUT; break; case SSL_ERROR_ZERO_RETURN: - /* The peer sent a close_notify alert. */ + /* Peer sent close_notify. */ stream->eof = 1; return 0; default: @@ -87,28 +93,48 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz return -1; } - /* Non-blocking stream: report that no data is available right now. */ - if (!dtlssock->is_blocked) { + /* Non-blocking: nothing available right now. */ + if (!dtlssock->s.is_blocked) { return -1; } - if (php_pollfd_for_ms(dtlssock->socket, events, timeout_ms) <= 0) { - /* Timed out or poll error. */ + 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; @@ -117,8 +143,8 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) return 0; } - /* SSL_free also frees the datagram BIO attached with SSL_set_bio; the BIO was - * created BIO_NOCLOSE, so the socket itself is closed separately below. */ + /* SSL_free also frees the BIO (created BIO_NOCLOSE), so the socket is closed + * separately below. */ if (dtlssock->ssl_handle != NULL) { SSL_free(dtlssock->ssl_handle); dtlssock->ssl_handle = NULL; @@ -128,9 +154,9 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) dtlssock->ctx = NULL; } - if (close_handle && dtlssock->socket != SOCK_ERR) { - closesocket(dtlssock->socket); - dtlssock->socket = SOCK_ERR; + if (close_handle && dtlssock->s.socket != SOCK_ERR) { + closesocket(dtlssock->s.socket); + dtlssock->s.socket = SOCK_ERR; } pefree(dtlssock, php_stream_is_persistent(stream)); @@ -139,9 +165,9 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) return 0; } -/* Split "host:port" (or "[ipv6]:port") from the resource name into host and - * port. Returns an emalloc'd host the caller must efree, or NULL on a parse - * error, with xparam->outputs.error_text set when requested. */ +/* Returns an emalloc'd host (caller frees) and a port, or NULL on error. + * + * TODO: duplicates the static parse_ip_address_ex() in xp_socket.c. */ static char *php_openssl_dtls_parse_ip_address(php_stream_xport_param *xparam, int *portno) { const char *str = xparam->inputs.name; @@ -182,8 +208,7 @@ static char *php_openssl_dtls_parse_ip_address(php_stream_xport_param *xparam, i return estrndup(str, colon - str); } -/* Create the DTLS context and SSL object and bind a datagram BIO to the - * connected UDP socket. */ +/* 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) { BIO *bio; @@ -202,7 +227,7 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return -1; } - bio = BIO_new_dgram(dtlssock->socket, BIO_NOCLOSE); + 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); @@ -212,29 +237,26 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return -1; } - /* A datagram BIO defaults to sendto() with an empty peer address, which - * fails with EINVAL on our connected socket. Tell it the socket is connected - * (and to whom) so it uses send()/recv() instead. */ + /* 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->socket, (struct sockaddr *)&peer, &peerlen) == 0) { + if (getpeername(dtlssock->s.socket, (struct sockaddr *)&peer, &peerlen) == 0) { BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); } } - /* The UDP socket is already connected, so one datagram BIO serves both - * reads and writes. */ SSL_set_bio(dtlssock->ssl_handle, bio, bio); SSL_set_connect_state(dtlssock->ssl_handle); return 0; } -/* Drive the DTLS handshake to completion. The socket is non-blocking, so the - * loop waits for readability or for the next retransmission timeout, after - * which OpenSSL resends the last flight. */ -static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +/* 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, + php_stream_xport_param *xparam) { SSL *ssl = dtlssock->ssl_handle; @@ -243,6 +265,7 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ if (n == 1) { return 0; } + int saved_errno = errno; int events; switch (SSL_get_error(ssl, n)) { @@ -257,9 +280,14 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ 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 (xparam->want_errortext) { + xparam->outputs.error_text = strpprintf(0, "DTLS handshake failed: %s", + buf[0] != '\0' ? buf : "unexpected error"); } - php_stream_warn(stream, ProtocolError, "DTLS handshake failed: %s", - buf[0] != '\0' ? buf : "unexpected error"); return -1; } } @@ -269,22 +297,26 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ ? (int)(tv.tv_sec * 1000 + tv.tv_usec / 1000) : -1; - int ready = php_pollfd_for_ms(dtlssock->socket, events, timeout); + int ready = php_pollfd_for_ms(dtlssock->s.socket, events, timeout); if (ready == 0) { - /* The retransmission timer fired: let OpenSSL resend the last flight. */ + /* Timer fired: let OpenSSL resend the last flight. */ if (DTLSv1_handle_timeout(ssl) < 0) { - php_stream_warn(stream, ProtocolError, "DTLS handshake timed out"); + if (xparam->want_errortext) { + xparam->outputs.error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); + } return -1; } } else if (ready < 0) { - php_stream_warn(stream, ProtocolError, "DTLS handshake failed while waiting for the socket"); + if (xparam->want_errortext) { + xparam->outputs.error_text = + ZSTR_INIT_LITERAL("DTLS handshake failed while waiting for the socket", 0); + } return -1; } } } -/* Create the UDP socket, connect it to the peer, attach the DTLS objects and - * run the handshake. */ +/* Connect the UDP socket and run the DTLS handshake. */ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, php_stream_xport_param *xparam) { @@ -297,7 +329,7 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t return -1; } - dtlssock->socket = php_network_connect_socket_to_host(host, (unsigned short)portno, + dtlssock->s.socket = php_network_connect_socket_to_host(host, (unsigned short)portno, SOCK_DGRAM, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, @@ -307,24 +339,23 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t xparam->outputs.error_code = err; efree(host); - if (dtlssock->socket == SOCK_ERR) { + if (dtlssock->s.socket == SOCK_ERR) { return -1; } /* DTLS drives its own retransmission timers, so the socket must be - * non-blocking for the handshake and datagram I/O loops. */ - php_set_sock_blocking(dtlssock->socket, 0); + * non-blocking for the handshake and I/O loops. */ + php_set_sock_blocking(dtlssock->s.socket, 0); if (php_openssl_dtls_setup_crypto(stream, dtlssock) != 0) { return -1; } - return php_openssl_dtls_handshake(stream, dtlssock); + return php_openssl_dtls_handshake(stream, dtlssock, xparam); } -/* Expose the underlying fd so stream_select() and friends can wait on the - * dtls:// stream. The raw fd/FILE* is never handed out otherwise, since DTLS is - * always encrypted. */ +/* 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; @@ -332,8 +363,8 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r switch (castas) { case PHP_STREAM_AS_FD_FOR_SELECT: if (ret != NULL) { - /* Data already decrypted and buffered in OpenSSL would not show up - * in a select() on the socket, so surface it into the read buffer. */ + /* Decrypted data buffered in OpenSSL is invisible to select(), so push + * it into the read buffer. TODO: same idiom as xp_ssl.c. */ size_t pending; if (stream->writepos == stream->readpos && dtlssock->ssl_handle != NULL @@ -342,7 +373,7 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r ? pending : stream->chunk_size); } - *(php_socket_t *)ret = dtlssock->socket; + *(php_socket_t *)ret = dtlssock->s.socket; } return SUCCESS; @@ -351,6 +382,7 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r } } +/* 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; @@ -372,16 +404,17 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in } case PHP_STREAM_OPTION_BLOCKING: { - /* The fd stays non-blocking for the DTLS timers; the stream-level - * blocking mode is emulated with poll in the I/O loop. */ - int old = dtlssock->is_blocked; - dtlssock->is_blocked = value; + /* 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; } + case PHP_STREAM_OPTION_META_DATA_API: case PHP_STREAM_OPTION_READ_TIMEOUT: - dtlssock->timeout = *(struct timeval *)ptrparam; - return PHP_STREAM_OPTION_RETURN_OK; + /* The embedded base socket data is what these operate on. */ + return php_stream_socket_ops.set_option(stream, option, value, ptrparam); default: return PHP_STREAM_OPTION_RETURN_NOTIMPL; @@ -398,6 +431,7 @@ static const php_stream_ops php_openssl_dtls_socket_ops = { 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, @@ -409,10 +443,10 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, dtlssock = pemalloc(sizeof(*dtlssock), persistent_id ? 1 : 0); memset(dtlssock, 0, sizeof(*dtlssock)); - dtlssock->socket = -1; - dtlssock->is_blocked = true; - dtlssock->timeout.tv_sec = (time_t)FG(default_socket_timeout); - dtlssock->timeout.tv_usec = 0; + dtlssock->s.socket = -1; + dtlssock->s.is_blocked = true; + dtlssock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); + dtlssock->s.timeout.tv_usec = 0; stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); if (stream == NULL) { From f9308ef29f7650a8977ba5a5930743006407bc24 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 30 Jun 2026 17:15:39 +0200 Subject: [PATCH 06/19] openssl: Apply ssl context options to the dtls:// client openssl: Add DTLS version floor and peer_fingerprint to dtls:// openssl: Validate peer_fingerprint openssl: Fix dtls:// non-blocking I/O, handshake timeout and tests --- ext/openssl/tests/dtls_client_basic.phpt | 18 +- ext/openssl/tests/dtls_client_cert.phpt | 86 +++++ .../tests/dtls_client_fingerprint.phpt | 81 +++++ ext/openssl/tests/dtls_client_timeout.phpt | 27 ++ ext/openssl/tests/dtls_client_verify.phpt | 88 +++++ .../tests/dtls_local_pk_passphrase.phpt | 97 ++++++ .../tests/dtls_peer_fingerprint_invalid.phpt | 88 +++++ ext/openssl/tests/dtls_peer_name.phpt | 86 +++++ ext/openssl/xp_dtls.c | 321 +++++++++++++++++- 9 files changed, 872 insertions(+), 20 deletions(-) create mode 100644 ext/openssl/tests/dtls_client_cert.phpt create mode 100644 ext/openssl/tests/dtls_client_fingerprint.phpt create mode 100644 ext/openssl/tests/dtls_client_timeout.phpt create mode 100644 ext/openssl/tests/dtls_client_verify.phpt create mode 100644 ext/openssl/tests/dtls_local_pk_passphrase.phpt create mode 100644 ext/openssl/tests/dtls_peer_fingerprint_invalid.phpt create mode 100644 ext/openssl/tests/dtls_peer_name.phpt diff --git a/ext/openssl/tests/dtls_client_basic.phpt b/ext/openssl/tests/dtls_client_basic.phpt index 3cf78d6db381..5d5459abc41c 100644 --- a/ext/openssl/tests/dtls_client_basic.phpt +++ b/ext/openssl/tests/dtls_client_basic.phpt @@ -49,10 +49,15 @@ while (microtime(true) < $deadline) { } } -$client = $ready ? stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10) : false; +// 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. @@ -78,6 +83,10 @@ if ($client !== false) { 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); } @@ -86,7 +95,10 @@ foreach ($pipes as $pipe) { if (is_resource($pipe)) fclose($pipe); } proc_close($server); -@unlink($certFile); +?> +--CLEAN-- + --EXPECT-- bool(true) @@ -94,3 +106,5 @@ 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_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_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/xp_dtls.c b/ext/openssl/xp_dtls.c index 85e2f2589d20..b419965886f6 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -20,6 +20,7 @@ #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 #include @@ -35,6 +36,25 @@ typedef struct _php_openssl_dtls_data_t { SSL *ssl_handle; } php_openssl_dtls_data_t; +/* 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) + /* 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) @@ -71,6 +91,7 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz 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; } @@ -93,9 +114,10 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz return -1; } - /* Non-blocking: nothing available right now. */ - if (!dtlssock->s.is_blocked) { - 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; @@ -208,10 +230,107 @@ static char *php_openssl_dtls_parse_ip_address(php_stream_xport_param *xparam, i return estrndup(str, colon - str); } +/* Supply the passphrase for an encrypted local_pk from the "passphrase" option. */ +static int php_openssl_dtls_passwd_callback(char *buf, int num, int verify, void *data) +{ + php_stream *stream = (php_stream *)data; + zval *val = NULL; + char *passphrase = NULL; + + GET_VER_OPT_STRING("passphrase", passphrase); + + if (passphrase != NULL && 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; +} + +/* 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, *certfile = NULL; + size_t certfile_len = 0; + zval *val; + + /* DTLS 1.0 is deprecated; require DTLS 1.2 or higher. */ + SSL_CTX_set_min_proto_version(ctx, DTLS1_2_VERSION); + + /* Peer verification, on by default. A peer_fingerprint authenticates the peer + * by itself (checked after the handshake), so it overrides CA verification. */ + bool verify_peer = !(GET_VER_OPT("verify_peer") && !zend_is_true(val)); + 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; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, 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; + } + + /* Local certificate and private key as file paths. */ + GET_VER_OPT_STRINGL("local_cert", certfile, certfile_len); + if (certfile != NULL) { + char resolved[MAXPATHLEN]; + char *private_key = NULL; + size_t private_key_len = 0; + + if (!php_openssl_check_path_ex(certfile, certfile_len, resolved, 0, false, false, + "local_cert in ssl stream context", stream)) { + return -1; + } + if (SSL_CTX_use_certificate_chain_file(ctx, resolved) != 1) { + php_stream_warn(stream, WriteFailed, "Unable to set local cert chain file `%s'", certfile); + return -1; + } + + /* A passphrase for an encrypted private key. */ + if (GET_VER_OPT("passphrase")) { + SSL_CTX_set_default_passwd_cb_userdata(ctx, stream); + SSL_CTX_set_default_passwd_cb(ctx, php_openssl_dtls_passwd_callback); + } + + /* local_pk defaults to the certificate file (combined PEM). */ + GET_VER_OPT_STRINGL("local_pk", private_key, private_key_len); + if (private_key != NULL && !php_openssl_check_path_ex(private_key, private_key_len, resolved, 0, + false, false, "local_pk in ssl stream context", stream)) { + return -1; + } + if (SSL_CTX_use_PrivateKey_file(ctx, resolved, SSL_FILETYPE_PEM) != 1) { + php_stream_warn(stream, WriteFailed, "Unable to set private key file `%s'", resolved); + return -1; + } + if (SSL_CTX_check_private_key(ctx) != 1) { + php_stream_warn(stream, PermissionDenied, "Private key does not match certificate!"); + } + } + + return 0; +} + /* 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) +static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + const char *peer_host) { BIO *bio; + zval *val; dtlssock->ctx = SSL_CTX_new(DTLS_client_method()); if (dtlssock->ctx == NULL) { @@ -219,6 +338,12 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da 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"); @@ -227,6 +352,22 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return -1; } + /* 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; + 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) { + SSL_set1_host(dtlssock->ssl_handle, name); + } + } + } + bio = BIO_new_dgram(dtlssock->s.socket, BIO_NOCLOSE); if (bio == NULL) { php_stream_warn(stream, CreateFailed, "DTLS datagram BIO creation failure"); @@ -260,7 +401,24 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ { 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 = xparam->inputs.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; @@ -293,11 +451,25 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ } struct timeval tv; - int timeout = DTLSv1_get_timeout(ssl, &tv) + int wait_ms = DTLSv1_get_timeout(ssl, &tv) ? (int)(tv.tv_sec * 1000 + tv.tv_usec / 1000) : -1; - int ready = php_pollfd_for_ms(dtlssock->s.socket, events, timeout); + 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 (xparam->want_errortext) { + xparam->outputs.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) { @@ -316,6 +488,87 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ } } +static bool php_openssl_dtls_fingerprint_is_equal(php_stream *stream, X509 *peer, const char *method, + zend_string *expected) +{ + zend_string *fingerprint = php_openssl_x509_fingerprint(peer, method, false, stream); + bool is_equal = false; + + if (fingerprint != NULL) { + is_equal = zend_string_equals_ci(fingerprint, expected); + zend_string_release_ex(fingerprint, false); + } + + return is_equal; +} + +/* Match the peer certificate against a md5/sha1 hash (by length) or an + * [algo => hash] map. */ +static bool php_openssl_dtls_fingerprint_match(php_stream *stream, X509 *peer, 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 (method == NULL) { + php_stream_warn(stream, AuthFailed, "peer_fingerprint length doesn't match a md5 or sha1 hash"); + return false; + } + return php_openssl_dtls_fingerprint_is_equal(stream, peer, method, Z_STR_P(val)); + } + + if (Z_TYPE_P(val) == IS_ARRAY) { + zval *current; + zend_string *key; + + if (zend_hash_num_elements(Z_ARRVAL_P(val)) == 0) { + 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_dtls_fingerprint_is_equal(stream, peer, ZSTR_VAL(key), Z_STR_P(current))) { + return false; + } + } ZEND_HASH_FOREACH_END(); + return true; + } + + php_stream_warn(stream, Generic, "Invalid peer_fingerprint; a string or [algo => fingerprint] array required"); + return false; +} + +/* Verify the peer certificate against the peer_fingerprint option, if set. This + * lets the caller authenticate the peer by fingerprint instead of a CA chain. */ +static int php_openssl_dtls_check_fingerprint(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + php_stream_xport_param *xparam) +{ + 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_dtls_fingerprint_match(stream, peer, val); + if (peer != NULL) { + X509_free(peer); + } + + if (!match) { + if (xparam->want_errortext) { + xparam->outputs.error_text = ZSTR_INIT_LITERAL("peer_fingerprint match failure", 0); + } + return -1; + } + + return 0; +} + /* Connect the UDP socket and run the DTLS handshake. */ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, php_stream_xport_param *xparam) @@ -337,9 +590,9 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t &err, NULL, 0, STREAM_SOCKOP_NONE); xparam->outputs.error_code = err; - efree(host); if (dtlssock->s.socket == SOCK_ERR) { + efree(host); return -1; } @@ -347,11 +600,18 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t * non-blocking for the handshake and I/O loops. */ php_set_sock_blocking(dtlssock->s.socket, 0); - if (php_openssl_dtls_setup_crypto(stream, dtlssock) != 0) { + if (php_openssl_dtls_setup_crypto(stream, dtlssock, host) != 0) { + efree(host); + return -1; + } + + efree(host); + + if (php_openssl_dtls_handshake(stream, dtlssock, xparam) != 0) { return -1; } - return php_openssl_dtls_handshake(stream, dtlssock, xparam); + return php_openssl_dtls_check_fingerprint(stream, dtlssock, xparam); } /* Expose the fd for stream_select(); the raw fd is not handed out otherwise, @@ -364,7 +624,8 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r 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 xp_ssl.c. */ + * 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 @@ -389,6 +650,34 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in 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; + + 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)); + } + 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_XPORT_API: xparam = (php_stream_xport_param *)ptrparam; @@ -410,15 +699,11 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in dtlssock->s.is_blocked = value; return old; } - - case PHP_STREAM_OPTION_META_DATA_API: - case PHP_STREAM_OPTION_READ_TIMEOUT: - /* The embedded base socket data is what these operate on. */ - return php_stream_socket_ops.set_option(stream, option, value, ptrparam); - - default: - return PHP_STREAM_OPTION_RETURN_NOTIMPL; } + + /* 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 = { From 32fa44f8d3cf9c83dc9b72b6e6fa98c7907f0b51 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 1 Jul 2026 12:04:46 +0200 Subject: [PATCH 07/19] openssl: Add a dtls:// server with cookie exchange --- ext/openssl/openssl.c | 2 + ext/openssl/tests/dtls_server.phpt | 52 +++++ ext/openssl/xp_dtls.c | 323 +++++++++++++++++++++++++--- ext/standard/file.stub.php | 20 ++ ext/standard/file_arginfo.h | 6 +- main/streams/php_stream_transport.h | 7 +- 6 files changed, 380 insertions(+), 30 deletions(-) create mode 100644 ext/openssl/tests/dtls_server.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 7a36a11fa01b..e9af428ebb40 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -832,6 +832,7 @@ PHP_MINIT_FUNCTION(openssl) #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); #endif /* override the default tcp socket provider */ @@ -910,6 +911,7 @@ PHP_MSHUTDOWN_FUNCTION(openssl) #ifndef OPENSSL_NO_DTLS php_stream_xport_unregister("dtls"); + php_stream_xport_unregister("dtlsv1.2"); #endif /* reinstate the default tcp handler */ 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/xp_dtls.c b/ext/openssl/xp_dtls.c index b419965886f6..bf6ee6673853 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -25,6 +25,8 @@ #include #include #include +#include +#include #ifndef OPENSSL_NO_DTLS @@ -34,8 +36,12 @@ typedef struct _php_openssl_dtls_data_t { php_netstream_data_t s; SSL_CTX *ctx; SSL *ssl_handle; + int method; /* STREAM_CRYPTO_METHOD_DTLS_* */ + int enable_on_connect; /* dtls:// scheme: run the handshake on connect */ } php_openssl_dtls_data_t; +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) @@ -62,10 +68,6 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; SSL *ssl = dtlssock->ssl_handle; - if (ssl == NULL) { - return -1; - } - /* OpenSSL takes an int length. */ if (count > INT_MAX) { count = INT_MAX; @@ -88,30 +90,53 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz } for (;;) { - 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; - } - int events; - 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) { + + if (ssl != NULL) { + 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; + } + } else { + /* Plain UDP: crypto not enabled (e.g. a udp:// stream before + * stream_socket_enable_crypto()). */ + ssize_t n = read + ? recv(dtlssock->s.socket, buf, count, 0) + : send(dtlssock->s.socket, buf, count, 0); + if (n > 0) { + php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), (size_t)n, 0); + return n; + } + if (n == 0) { + return 0; + } + if (errno != EAGAIN +#if EAGAIN != EWOULDBLOCK + && errno != EWOULDBLOCK +#endif + ) { return -1; + } + events = read ? POLLIN : POLLOUT; } /* Non-blocking, or a zero read timeout: don't wait, report would-block. */ @@ -569,6 +594,198 @@ static int php_openssl_dtls_check_fingerprint(php_stream *stream, php_openssl_dt 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); + return (cookie_len == len && memcmp(cookie, expected, len) == 0) ? 1 : 0; +} + +/* 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); + return 0; +} + +/* Accept one peer: run the cookie exchange with DTLSv1_listen, move to a socket + * connected to that peer (sharing the local port), and finish the handshake. */ +static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t *listen, + php_stream_xport_param *xparam) +{ + 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); + + int timeout_ms = xparam->inputs.timeout != NULL + ? (int)(xparam->inputs.timeout->tv_sec * 1000 + xparam->inputs.timeout->tv_usec / 1000) + : -1; + + 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 || php_pollfd_for_ms(listen->s.socket, POLLIN, timeout_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_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); + } +#endif + BIO_ADDR_free(client_addr); + if (peerlen == 0) { + SSL_free(ssl); + return -1; + } + + /* A fresh socket bound to the same local address and connected to the peer + * keeps the listening socket free for the next accept. */ + struct sockaddr_storage local; + socklen_t locallen = sizeof(local); + php_socket_t clifd; + int on = 1; + + if (getsockname(listen->s.socket, (struct sockaddr *)&local, &locallen) != 0 + || (clifd = socket(peer.ss_family, SOCK_DGRAM, 0)) == SOCK_ERR) { + SSL_free(ssl); + return -1; + } + setsockopt(clifd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); +#ifdef SO_REUSEPORT + setsockopt(clifd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on)); +#endif + if (bind(clifd, (struct sockaddr *)&local, locallen) != 0 + || connect(clifd, (struct sockaddr *)&peer, peerlen) != 0) { + closesocket(clifd); + SSL_free(ssl); + return -1; + } + php_set_sock_blocking(clifd, 0); + + BIO_set_fd(bio, clifd, BIO_NOCLOSE); + BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); + + php_openssl_dtls_data_t *clisock = pemalloc(sizeof(*clisock), 0); + memset(clisock, 0, sizeof(*clisock)); + clisock->s.socket = clifd; + clisock->s.is_blocked = true; + clisock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); + clisock->ssl_handle = ssl; + clisock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; + + php_stream *clistream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, clisock, NULL, "r+"); + if (clistream == NULL) { + SSL_free(ssl); + closesocket(clifd); + pefree(clisock, 0); + return -1; + } + + /* Finish the handshake (SSL_do_handshake performs the accept). */ + if (php_openssl_dtls_handshake(clistream, clisock, xparam) != 0) { + php_stream_close(clistream); + return -1; + } + + xparam->outputs.client = clistream; + return 0; +} + /* Connect the UDP socket and run the DTLS handshake. */ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t *dtlssock, php_stream_xport_param *xparam) @@ -596,10 +813,15 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t return -1; } - /* DTLS drives its own retransmission timers, so the socket must be - * non-blocking for the handshake and I/O loops. */ + /* The I/O loop emulates blocking with poll, so the fd stays non-blocking. */ php_set_sock_blocking(dtlssock->s.socket, 0); + if (!dtlssock->enable_on_connect) { + /* Plain udp:// stream; DTLS is enabled later via stream_socket_enable_crypto(). */ + efree(host); + return 0; + } + if (php_openssl_dtls_setup_crypto(stream, dtlssock, host) != 0) { efree(host); return -1; @@ -688,8 +910,46 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in php_openssl_dtls_connect(stream, dtlssock, xparam); return PHP_STREAM_OPTION_RETURN_OK; + case STREAM_XPORT_OP_BIND: { + int portno = 0, err = 0; + char *host = php_openssl_dtls_parse_ip_address(xparam, &portno); + if (host == NULL) { + xparam->outputs.returncode = -1; + return PHP_STREAM_OPTION_RETURN_OK; + } + /* REUSEADDR+REUSEPORT so each accepted peer can bind a fresh + * socket on this same local address. */ + dtlssock->s.socket = php_network_bind_socket_to_local_addr(host, portno, SOCK_DGRAM, + STREAM_SOCKOP_SO_REUSEADDR | STREAM_SOCKOP_SO_REUSEPORT, + xparam->want_errortext ? &xparam->outputs.error_text : NULL, &err); + efree(host); + xparam->outputs.error_code = err; + if (dtlssock->s.socket == SOCK_ERR) { + xparam->outputs.returncode = -1; + return PHP_STREAM_OPTION_RETURN_OK; + } + php_set_sock_blocking(dtlssock->s.socket, 0); + dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; + 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); + return PHP_STREAM_OPTION_RETURN_OK; + default: - return PHP_STREAM_OPTION_RETURN_NOTIMPL; + /* 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: { @@ -732,6 +992,13 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, dtlssock->s.is_blocked = true; dtlssock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); dtlssock->s.timeout.tv_usec = 0; + dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_CLIENT; + + /* dtls:// runs the handshake on connect; a plain udp:// enables it on demand + * (stream_socket_enable_crypto). */ + if (protolen != strlen("udp") || strncmp(proto, "udp", protolen) != 0) { + dtlssock->enable_on_connect = 1; + } stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); if (stream == NULL) { diff --git a/ext/standard/file.stub.php b/ext/standard/file.stub.php index d7b1fef17cdc..28bdc3646dc4 100644 --- a/ext/standard/file.stub.php +++ b/ext/standard/file.stub.php @@ -229,6 +229,26 @@ * @cvalue STREAM_CRYPTO_METHOD_TLSv1_3_SERVER */ const STREAM_CRYPTO_METHOD_TLSv1_3_SERVER = UNKNOWN; +/** + * @var int + * @cvalue STREAM_CRYPTO_METHOD_DTLS_CLIENT + */ +const STREAM_CRYPTO_METHOD_DTLS_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_DTLS_SERVER + */ +const STREAM_CRYPTO_METHOD_DTLS_SERVER = UNKNOWN; +/** + * @var int + * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER + */ +const STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = UNKNOWN; /** * @var int diff --git a/ext/standard/file_arginfo.h b/ext/standard/file_arginfo.h index 24e3722cd86e..40e3aec980e1 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: c11e1ab842c8c617d5f155e393b21a1ee4bb7335 */ static void register_file_symbols(int module_number) { @@ -47,6 +47,10 @@ 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_DTLS_CLIENT", STREAM_CRYPTO_METHOD_DTLS_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_DTLS_SERVER", STREAM_CRYPTO_METHOD_DTLS_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_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/streams/php_stream_transport.h b/main/streams/php_stream_transport.h index 60bea8e9e1fc..e7ad79e2b31d 100644 --- a/main/streams/php_stream_transport.h +++ b/main/streams/php_stream_transport.h @@ -183,7 +183,12 @@ 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)), + /* DTLS 1.2 (DTLS 1.0 is deprecated and not offered) */ + STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT = (1 << 7 | 1), + STREAM_CRYPTO_METHOD_DTLS_CLIENT = (1 << 7 | 1), + STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = (1 << 7), + STREAM_CRYPTO_METHOD_DTLS_SERVER = (1 << 7) } php_stream_xport_crypt_method_t; /* Flags for crypto status */ From a46ed3ca6dd37a73558d4e2932b8667d35b404fc Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 1 Jul 2026 13:06:01 +0200 Subject: [PATCH 08/19] openssl: Enable DTLS over udp:// and harden the dtls:// server --- ext/openssl/openssl.c | 7 +- ext/openssl/tests/dtls_enable_crypto.phpt | 54 ++++++ ext/openssl/tests/dtls_keying_material.phpt | 57 +++++++ ext/openssl/tests/dtls_server_verify.phpt | 71 ++++++++ ext/openssl/xp_dtls.c | 173 +++++++++++++++----- 5 files changed, 321 insertions(+), 41 deletions(-) create mode 100644 ext/openssl/tests/dtls_enable_crypto.phpt create mode 100644 ext/openssl/tests/dtls_keying_material.phpt create mode 100644 ext/openssl/tests/dtls_server_verify.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index e9af428ebb40..06a7f29fb98f 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -831,8 +831,11 @@ PHP_MINIT_FUNCTION(openssl) php_stream_xport_register("tlsv1.3", php_openssl_ssl_socket_factory); #ifndef OPENSSL_NO_DTLS + /* override the default udp socket provider so udp:// can enable DTLS */ + php_stream_xport_register("udp", php_openssl_dtls_socket_factory); php_stream_xport_register("dtls", php_openssl_dtls_socket_factory); php_stream_xport_register("dtlsv1.2", php_openssl_dtls_socket_factory); + #endif /* override the default tcp socket provider */ @@ -912,6 +915,8 @@ PHP_MSHUTDOWN_FUNCTION(openssl) #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 */ @@ -2765,7 +2770,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/tests/dtls_enable_crypto.phpt b/ext/openssl/tests/dtls_enable_crypto.phpt new file mode 100644 index 000000000000..31fd4b617c61 --- /dev/null +++ b/ext/openssl/tests/dtls_enable_crypto.phpt @@ -0,0 +1,54 @@ +--TEST-- +udp:// + stream_socket_enable_crypto(): DTLS over an existing udp:// socket +--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('udp://{{ ADDR }}', $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $ctx); + var_dump($client !== false); + var_dump(stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_DTLS_CLIENT)); + fwrite($client, "ping"); + var_dump(fread($client, 8192)); + fclose($client); +CODE; + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +string(4) "PING" diff --git a/ext/openssl/tests/dtls_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_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/xp_dtls.c b/ext/openssl/xp_dtls.c index bf6ee6673853..3739cb4aaef3 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -37,7 +37,7 @@ typedef struct _php_openssl_dtls_data_t { SSL_CTX *ctx; SSL *ssl_handle; int method; /* STREAM_CRYPTO_METHOD_DTLS_* */ - int enable_on_connect; /* dtls:// scheme: run the handshake on connect */ + int enable_on_connect; /* dtls:// scheme (vs plain udp://): set up DTLS on connect/bind */ } php_openssl_dtls_data_t; static const php_stream_ops php_openssl_dtls_socket_ops; @@ -61,7 +61,12 @@ static const php_stream_ops php_openssl_dtls_socket_ops; } \ } while (0) -/* The socket is non-blocking, so on WANT_* we poll (up to the read timeout when +/* 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) { @@ -283,9 +288,12 @@ static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_d /* DTLS 1.0 is deprecated; require DTLS 1.2 or higher. */ SSL_CTX_set_min_proto_version(ctx, DTLS1_2_VERSION); - /* Peer verification, on by default. A peer_fingerprint authenticates the peer - * by itself (checked after the handshake), so it overrides CA verification. */ - bool verify_peer = !(GET_VER_OPT("verify_peer") && !zend_is_true(val)); + /* 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->method & 1); + 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); @@ -301,7 +309,12 @@ static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_d php_stream_warn(stream, CreateFailed, "Failed to set the default CA verify paths"); return -1; } - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); + 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); @@ -666,11 +679,17 @@ static int php_openssl_dtls_server_ctx(php_stream *stream, php_openssl_dtls_data return 0; } -/* Accept one peer: run the cookie exchange with DTLSv1_listen, move to a socket - * connected to that peer (sharing the local port), and finish the handshake. */ +/* 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) { + 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"); @@ -733,49 +752,41 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * return -1; } - /* A fresh socket bound to the same local address and connected to the peer - * keeps the listening socket free for the next accept. */ - struct sockaddr_storage local; - socklen_t locallen = sizeof(local); - php_socket_t clifd; - int on = 1; - - if (getsockname(listen->s.socket, (struct sockaddr *)&local, &locallen) != 0 - || (clifd = socket(peer.ss_family, SOCK_DGRAM, 0)) == SOCK_ERR) { - SSL_free(ssl); - return -1; - } - setsockopt(clifd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); -#ifdef SO_REUSEPORT - setsockopt(clifd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on)); -#endif - if (bind(clifd, (struct sockaddr *)&local, locallen) != 0 - || connect(clifd, (struct sockaddr *)&peer, peerlen) != 0) { - closesocket(clifd); + /* 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; } - php_set_sock_blocking(clifd, 0); - - BIO_set_fd(bio, clifd, BIO_NOCLOSE); BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &peer); php_openssl_dtls_data_t *clisock = pemalloc(sizeof(*clisock), 0); memset(clisock, 0, sizeof(*clisock)); - clisock->s.socket = clifd; + 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->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; + /* 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(clifd); + 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); + } + /* Finish the handshake (SSL_do_handshake performs the accept). */ if (php_openssl_dtls_handshake(clistream, clisock, xparam) != 0) { php_stream_close(clistream); @@ -865,6 +876,45 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r } } +/* stream_socket_enable_crypto() on a udp:// stream: set up the DTLS client + * handshake over the already-connected socket. Servers use the dtls:// scheme. */ +static int php_openssl_dtls_crypto_setup(php_stream *stream, php_openssl_dtls_data_t *dtlssock, + php_stream_xport_crypto_param *cparam) +{ + if (dtlssock->ssl_handle != NULL) { + php_error_docref(NULL, E_WARNING, "DTLS is already active on this stream"); + return -1; + } + /* The LSB of the crypto method marks a client; only client crypto can be + * layered onto an existing socket this way. */ + if (!(cparam->inputs.method & 1)) { + php_error_docref(NULL, E_WARNING, + "Only DTLS client crypto can be enabled on a udp:// stream; use the dtls:// server transport instead"); + return -1; + } + dtlssock->method = cparam->inputs.method; + return php_openssl_dtls_setup_crypto(stream, dtlssock, NULL); +} + +static int php_openssl_dtls_crypto_enable(php_stream *stream, php_openssl_dtls_data_t *dtlssock) +{ + if (dtlssock->ssl_handle == NULL) { + return -1; + } + + php_stream_xport_param p; + memset(&p, 0, sizeof(p)); + if (php_openssl_dtls_handshake(stream, dtlssock, &p) != 0 + || php_openssl_dtls_check_fingerprint(stream, dtlssock, &p) != 0) { + SSL_free(dtlssock->ssl_handle); + dtlssock->ssl_handle = NULL; + return -1; + } + /* >0 tells stream_socket_enable_crypto() the handshake finished (0 means + * "in progress", -1 means failure). */ + return 1; +} + /* Handle transport and stream options. */ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam) { @@ -877,6 +927,7 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in zval crypto; char *proto_str; const SSL_CIPHER *cipher; + zval *val; array_init(&crypto); switch (SSL_version(dtlssock->ssl_handle)) { @@ -892,6 +943,27 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in 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); + } + } + } + add_assoc_zval((zval *)ptrparam, "crypto", &crypto); } add_assoc_bool((zval *)ptrparam, "timed_out", dtlssock->s.timeout_event); @@ -900,6 +972,23 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in 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: + cparam->outputs.returncode = + php_openssl_dtls_crypto_setup(stream, dtlssock, cparam); + return PHP_STREAM_OPTION_RETURN_OK; + case STREAM_XPORT_CRYPTO_OP_ENABLE: + cparam->outputs.returncode = + php_openssl_dtls_crypto_enable(stream, dtlssock); + return PHP_STREAM_OPTION_RETURN_OK; + default: + return PHP_STREAM_OPTION_RETURN_NOTIMPL; + } + } + case PHP_STREAM_OPTION_XPORT_API: xparam = (php_stream_xport_param *)ptrparam; @@ -917,10 +1006,8 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in xparam->outputs.returncode = -1; return PHP_STREAM_OPTION_RETURN_OK; } - /* REUSEADDR+REUSEPORT so each accepted peer can bind a fresh - * socket on this same local address. */ dtlssock->s.socket = php_network_bind_socket_to_local_addr(host, portno, SOCK_DGRAM, - STREAM_SOCKOP_SO_REUSEADDR | STREAM_SOCKOP_SO_REUSEPORT, + STREAM_SOCKOP_SO_REUSEADDR, xparam->want_errortext ? &xparam->outputs.error_text : NULL, &err); efree(host); xparam->outputs.error_code = err; @@ -929,11 +1016,17 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in return PHP_STREAM_OPTION_RETURN_OK; } php_set_sock_blocking(dtlssock->s.socket, 0); - dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; - xparam->outputs.returncode = php_openssl_dtls_server_ctx(stream, dtlssock); - if (xparam->outputs.returncode != 0) { - closesocket(dtlssock->s.socket); - dtlssock->s.socket = SOCK_ERR; + if (dtlssock->enable_on_connect) { + /* dtls:// server: set up the DTLS context and cookies now. */ + dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; + xparam->outputs.returncode = php_openssl_dtls_server_ctx(stream, dtlssock); + if (xparam->outputs.returncode != 0) { + closesocket(dtlssock->s.socket); + dtlssock->s.socket = SOCK_ERR; + } + } else { + /* Plain udp:// bind; DTLS is enabled later, if at all. */ + xparam->outputs.returncode = 0; } return PHP_STREAM_OPTION_RETURN_OK; } From 6d6adf739f7735a96d5ce5797072f61e3516672a Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 1 Jul 2026 14:50:02 +0200 Subject: [PATCH 09/19] openssl: Handle DTLS path MTU for dtls:// --- ext/openssl/tests/dtls_mtu.phpt | 54 +++++++++++++++++++++++++++++++++ ext/openssl/xp_dtls.c | 32 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 ext/openssl/tests/dtls_mtu.phpt 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/xp_dtls.c b/ext/openssl/xp_dtls.c index 3739cb4aaef3..faee69d28091 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -363,6 +363,35 @@ static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_d 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) @@ -423,6 +452,8 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da 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); } } @@ -760,6 +791,7 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * 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)); From 3d44d676e9bdc3ddfac92f6df8f3b652990fc77a Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Thu, 2 Jul 2026 15:33:31 +0200 Subject: [PATCH 10/19] openssl: Harden dtls:// cookie handling and accept --- ext/openssl/tests/dtls_server_robustness.phpt | 60 +++++++++++++++++++ ext/openssl/xp_dtls.c | 43 +++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 ext/openssl/tests/dtls_server_robustness.phpt 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/xp_dtls.c b/ext/openssl/xp_dtls.c index faee69d28091..5029efd6403f 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -27,6 +27,7 @@ #include #include #include +#include #ifndef OPENSSL_NO_DTLS @@ -688,7 +689,8 @@ static int php_openssl_dtls_cookie_verify(SSL *ssl, const unsigned char *cookie, } HMAC(EVP_sha256(), php_openssl_dtls_cookie_secret, sizeof(php_openssl_dtls_cookie_secret), (const unsigned char *)&peer, peerlen, expected, &len); - return (cookie_len == len && memcmp(cookie, expected, len) == 0) ? 1 : 0; + /* Constant-time compare: the cookie is derived from a secret HMAC. */ + return (cookie_len == len && CRYPTO_memcmp(cookie, expected, len) == 0) ? 1 : 0; } /* Set up the server SSL_CTX (cert/verify options plus the cookie callbacks that @@ -733,9 +735,21 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * } SSL_set_bio(ssl, bio, bio); - int timeout_ms = xparam->inputs.timeout != NULL - ? (int)(xparam->inputs.timeout->tv_sec * 1000 + xparam->inputs.timeout->tv_usec / 1000) - : -1; + /* 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) { @@ -747,7 +761,26 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * if (ret > 0) { break; } - if (ret < 0 || php_pollfd_for_ms(listen->s.socket, POLLIN, timeout_ms) <= 0) { + 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; From 8265766dd70dbafeea503df8c946924e2cdba1e0 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Sun, 5 Jul 2026 11:15:49 +0200 Subject: [PATCH 11/19] openssl: drop udp:// override, --- ext/openssl/openssl.c | 4 - ext/openssl/tests/dtls_enable_crypto.phpt | 54 -------- ext/openssl/xp_dtls.c | 159 +++++----------------- ext/standard/file.stub.php | 20 --- ext/standard/file_arginfo.h | 6 +- main/streams/php_stream_transport.h | 7 +- 6 files changed, 34 insertions(+), 216 deletions(-) delete mode 100644 ext/openssl/tests/dtls_enable_crypto.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 06a7f29fb98f..5cd191644c07 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -831,8 +831,6 @@ PHP_MINIT_FUNCTION(openssl) php_stream_xport_register("tlsv1.3", php_openssl_ssl_socket_factory); #ifndef OPENSSL_NO_DTLS - /* override the default udp socket provider so udp:// can enable DTLS */ - php_stream_xport_register("udp", php_openssl_dtls_socket_factory); php_stream_xport_register("dtls", php_openssl_dtls_socket_factory); php_stream_xport_register("dtlsv1.2", php_openssl_dtls_socket_factory); @@ -915,8 +913,6 @@ PHP_MSHUTDOWN_FUNCTION(openssl) #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 */ diff --git a/ext/openssl/tests/dtls_enable_crypto.phpt b/ext/openssl/tests/dtls_enable_crypto.phpt deleted file mode 100644 index 31fd4b617c61..000000000000 --- a/ext/openssl/tests/dtls_enable_crypto.phpt +++ /dev/null @@ -1,54 +0,0 @@ ---TEST-- -udp:// + stream_socket_enable_crypto(): DTLS over an existing udp:// socket ---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('udp://{{ ADDR }}', $errno, $errstr, 5, - STREAM_CLIENT_CONNECT, $ctx); - var_dump($client !== false); - var_dump(stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_DTLS_CLIENT)); - fwrite($client, "ping"); - var_dump(fread($client, 8192)); - fclose($client); -CODE; - -include 'ServerClientTestCase.inc'; -ServerClientTestCase::getInstance()->run($clientCode, $serverCode); -?> ---CLEAN-- - ---EXPECT-- -bool(true) -bool(true) -string(4) "PING" diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 5029efd6403f..0a1d2d1e6f8f 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -37,8 +37,7 @@ typedef struct _php_openssl_dtls_data_t { php_netstream_data_t s; SSL_CTX *ctx; SSL *ssl_handle; - int method; /* STREAM_CRYPTO_METHOD_DTLS_* */ - int enable_on_connect; /* dtls:// scheme (vs plain udp://): set up DTLS on connect/bind */ + bool is_server; } php_openssl_dtls_data_t; static const php_stream_ops php_openssl_dtls_socket_ops; @@ -74,6 +73,10 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz php_openssl_dtls_data_t *dtlssock = (php_openssl_dtls_data_t *)stream->abstract; SSL *ssl = dtlssock->ssl_handle; + if (ssl == NULL) { + return -1; + } + /* OpenSSL takes an int length. */ if (count > INT_MAX) { count = INT_MAX; @@ -98,51 +101,28 @@ static ssize_t php_openssl_dtls_io(bool read, php_stream *stream, char *buf, siz for (;;) { int events; - if (ssl != NULL) { - 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; - } - } else { - /* Plain UDP: crypto not enabled (e.g. a udp:// stream before - * stream_socket_enable_crypto()). */ - ssize_t n = read - ? recv(dtlssock->s.socket, buf, count, 0) - : send(dtlssock->s.socket, buf, count, 0); - if (n > 0) { - php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), (size_t)n, 0); - return n; - } - if (n == 0) { + 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; - } - if (errno != EAGAIN -#if EAGAIN != EWOULDBLOCK - && errno != EWOULDBLOCK -#endif - ) { + default: + if (read) { + stream->eof = 1; + } return -1; - } - events = read ? POLLIN : POLLOUT; } /* Non-blocking, or a zero read timeout: don't wait, report would-block. */ @@ -293,7 +273,7 @@ static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_d * 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->method & 1); + 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) { @@ -832,7 +812,7 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * clisock->s.is_blocked = true; clisock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); clisock->ssl_handle = ssl; - clisock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; + clisock->is_server = true; /* The socket now belongs to the accepted stream. */ listen->s.socket = SOCK_ERR; @@ -892,12 +872,6 @@ static int php_openssl_dtls_connect(php_stream *stream, php_openssl_dtls_data_t /* The I/O loop emulates blocking with poll, so the fd stays non-blocking. */ php_set_sock_blocking(dtlssock->s.socket, 0); - if (!dtlssock->enable_on_connect) { - /* Plain udp:// stream; DTLS is enabled later via stream_socket_enable_crypto(). */ - efree(host); - return 0; - } - if (php_openssl_dtls_setup_crypto(stream, dtlssock, host) != 0) { efree(host); return -1; @@ -941,45 +915,6 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r } } -/* stream_socket_enable_crypto() on a udp:// stream: set up the DTLS client - * handshake over the already-connected socket. Servers use the dtls:// scheme. */ -static int php_openssl_dtls_crypto_setup(php_stream *stream, php_openssl_dtls_data_t *dtlssock, - php_stream_xport_crypto_param *cparam) -{ - if (dtlssock->ssl_handle != NULL) { - php_error_docref(NULL, E_WARNING, "DTLS is already active on this stream"); - return -1; - } - /* The LSB of the crypto method marks a client; only client crypto can be - * layered onto an existing socket this way. */ - if (!(cparam->inputs.method & 1)) { - php_error_docref(NULL, E_WARNING, - "Only DTLS client crypto can be enabled on a udp:// stream; use the dtls:// server transport instead"); - return -1; - } - dtlssock->method = cparam->inputs.method; - return php_openssl_dtls_setup_crypto(stream, dtlssock, NULL); -} - -static int php_openssl_dtls_crypto_enable(php_stream *stream, php_openssl_dtls_data_t *dtlssock) -{ - if (dtlssock->ssl_handle == NULL) { - return -1; - } - - php_stream_xport_param p; - memset(&p, 0, sizeof(p)); - if (php_openssl_dtls_handshake(stream, dtlssock, &p) != 0 - || php_openssl_dtls_check_fingerprint(stream, dtlssock, &p) != 0) { - SSL_free(dtlssock->ssl_handle); - dtlssock->ssl_handle = NULL; - return -1; - } - /* >0 tells stream_socket_enable_crypto() the handshake finished (0 means - * "in progress", -1 means failure). */ - return 1; -} - /* Handle transport and stream options. */ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam) { @@ -1037,23 +972,6 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in 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: - cparam->outputs.returncode = - php_openssl_dtls_crypto_setup(stream, dtlssock, cparam); - return PHP_STREAM_OPTION_RETURN_OK; - case STREAM_XPORT_CRYPTO_OP_ENABLE: - cparam->outputs.returncode = - php_openssl_dtls_crypto_enable(stream, dtlssock); - return PHP_STREAM_OPTION_RETURN_OK; - default: - return PHP_STREAM_OPTION_RETURN_NOTIMPL; - } - } - case PHP_STREAM_OPTION_XPORT_API: xparam = (php_stream_xport_param *)ptrparam; @@ -1081,17 +999,11 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in return PHP_STREAM_OPTION_RETURN_OK; } php_set_sock_blocking(dtlssock->s.socket, 0); - if (dtlssock->enable_on_connect) { - /* dtls:// server: set up the DTLS context and cookies now. */ - dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_SERVER; - xparam->outputs.returncode = php_openssl_dtls_server_ctx(stream, dtlssock); - if (xparam->outputs.returncode != 0) { - closesocket(dtlssock->s.socket); - dtlssock->s.socket = SOCK_ERR; - } - } else { - /* Plain udp:// bind; DTLS is enabled later, if at all. */ - xparam->outputs.returncode = 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; } @@ -1150,13 +1062,6 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, dtlssock->s.is_blocked = true; dtlssock->s.timeout.tv_sec = (time_t)FG(default_socket_timeout); dtlssock->s.timeout.tv_usec = 0; - dtlssock->method = STREAM_CRYPTO_METHOD_DTLS_CLIENT; - - /* dtls:// runs the handshake on connect; a plain udp:// enables it on demand - * (stream_socket_enable_crypto). */ - if (protolen != strlen("udp") || strncmp(proto, "udp", protolen) != 0) { - dtlssock->enable_on_connect = 1; - } stream = php_stream_alloc_rel(&php_openssl_dtls_socket_ops, dtlssock, persistent_id, "r+"); if (stream == NULL) { diff --git a/ext/standard/file.stub.php b/ext/standard/file.stub.php index 28bdc3646dc4..d7b1fef17cdc 100644 --- a/ext/standard/file.stub.php +++ b/ext/standard/file.stub.php @@ -229,26 +229,6 @@ * @cvalue STREAM_CRYPTO_METHOD_TLSv1_3_SERVER */ const STREAM_CRYPTO_METHOD_TLSv1_3_SERVER = UNKNOWN; -/** - * @var int - * @cvalue STREAM_CRYPTO_METHOD_DTLS_CLIENT - */ -const STREAM_CRYPTO_METHOD_DTLS_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_DTLS_SERVER - */ -const STREAM_CRYPTO_METHOD_DTLS_SERVER = UNKNOWN; -/** - * @var int - * @cvalue STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER - */ -const STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = UNKNOWN; /** * @var int diff --git a/ext/standard/file_arginfo.h b/ext/standard/file_arginfo.h index 40e3aec980e1..24e3722cd86e 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: c11e1ab842c8c617d5f155e393b21a1ee4bb7335 */ + * Stub hash: 0c62c6fb217a87010a9e2e63d4b104cde0138655 */ static void register_file_symbols(int module_number) { @@ -47,10 +47,6 @@ 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_DTLS_CLIENT", STREAM_CRYPTO_METHOD_DTLS_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_DTLS_SERVER", STREAM_CRYPTO_METHOD_DTLS_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_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/streams/php_stream_transport.h b/main/streams/php_stream_transport.h index e7ad79e2b31d..60bea8e9e1fc 100644 --- a/main/streams/php_stream_transport.h +++ b/main/streams/php_stream_transport.h @@ -183,12 +183,7 @@ 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)), - /* DTLS 1.2 (DTLS 1.0 is deprecated and not offered) */ - STREAM_CRYPTO_METHOD_DTLSv1_2_CLIENT = (1 << 7 | 1), - STREAM_CRYPTO_METHOD_DTLS_CLIENT = (1 << 7 | 1), - STREAM_CRYPTO_METHOD_DTLSv1_2_SERVER = (1 << 7), - STREAM_CRYPTO_METHOD_DTLS_SERVER = (1 << 7) + STREAM_CRYPTO_METHOD_ANY_SERVER = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6)) } php_stream_xport_crypt_method_t; /* Flags for crypto status */ From 5bf9389639473d28218cae4b342c9315a088f8e2 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Sun, 5 Jul 2026 14:27:57 +0200 Subject: [PATCH 12/19] openssl: Add dtls:// client session resumption --- ext/openssl/tests/dtls_session_resume.phpt | 78 ++++++++++++++++++++++ ext/openssl/xp_dtls.c | 20 ++++++ 2 files changed, 98 insertions(+) create mode 100644 ext/openssl/tests/dtls_session_resume.phpt 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/xp_dtls.c b/ext/openssl/xp_dtls.c index 0a1d2d1e6f8f..eab8700df078 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -400,6 +400,15 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da return -1; } + /* Resume a previous session (abbreviated handshake) if session_data holds an + * OpenSSLSession; SSL_set_session takes its own reference. */ + 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_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)); @@ -964,6 +973,17 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in } } + /* 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); From 15a489c43993754b474b6b6730c34e062893e073 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 8 Jul 2026 11:56:29 +0200 Subject: [PATCH 13/19] openssl: Add dtls:// server session resumption --- .../tests/dtls_session_resume_server.phpt | 97 ++++++ ext/openssl/xp_dtls.c | 278 +++++++++++++++++- 2 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 ext/openssl/tests/dtls_session_resume_server.phpt 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/xp_dtls.c b/ext/openssl/xp_dtls.c index eab8700df078..cacb2d40c5e6 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -31,6 +31,17 @@ #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 { @@ -38,8 +49,21 @@ typedef struct _php_openssl_dtls_data_t { SSL_CTX *ctx; SSL *ssl_handle; bool is_server; + 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`. */ @@ -179,6 +203,11 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) /* 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; } @@ -192,6 +221,21 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) 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; + pefree(dtlssock, php_stream_is_persistent(stream)); stream->abstract = NULL; @@ -401,10 +445,13 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da } /* Resume a previous session (abbreviated handshake) if session_data holds an - * OpenSSLSession; SSL_set_session takes its own reference. */ + * 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); } } @@ -682,6 +729,222 @@ static int php_openssl_dtls_cookie_verify(SSL *ssl, const unsigned char *cookie, 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) @@ -698,6 +961,11 @@ static int php_openssl_dtls_server_ctx(php_stream *stream, php_openssl_dtls_data } 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; } @@ -841,6 +1109,14 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * 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) != 0) { php_stream_close(clistream); From ef07fe1f357062a816d5f172637ca82cac721f1f Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 8 Jul 2026 14:45:35 +0200 Subject: [PATCH 14/19] openssl: Fix dtls:// accept build with --enable-debug --- ext/openssl/xp_dtls.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index cacb2d40c5e6..116335736efb 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -972,7 +972,7 @@ static int php_openssl_dtls_server_ctx(php_stream *stream, php_openssl_dtls_data /* 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) + php_stream_xport_param *xparam STREAMS_DC) { if (listen->s.socket == SOCK_ERR) { /* The listening socket is handed to the first accepted peer. */ @@ -1310,7 +1310,7 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in return PHP_STREAM_OPTION_RETURN_OK; case STREAM_XPORT_OP_ACCEPT: - xparam->outputs.returncode = php_openssl_dtls_accept(stream, dtlssock, xparam); + xparam->outputs.returncode = php_openssl_dtls_accept(stream, dtlssock, xparam STREAMS_CC); return PHP_STREAM_OPTION_RETURN_OK; default: From 2fd06416f38326df66e700a5afdaea6a2ef9fd8e Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Wed, 8 Jul 2026 17:56:20 +0200 Subject: [PATCH 15/19] openssl: Fix dtls:// portability on macOS, Windows and FreeBSD --- ext/openssl/xp_dtls.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 116335736efb..362826ad1816 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -22,6 +22,9 @@ #include "php_openssl.h" #include "php_openssl_backend.h" #include "php_network.h" +#ifdef PHP_WIN32 +# include "win32/time.h" +#endif #include #include #include @@ -467,7 +470,7 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da /* 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) { - SSL_set1_host(dtlssock->ssl_handle, name); + X509_VERIFY_PARAM_set1_host(param, name, 0); } } } @@ -1056,6 +1059,9 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * 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) { @@ -1065,6 +1071,9 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * 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); From 9ab6444bc75872535d9634ee65983a00babe1eb3 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 13 Jul 2026 18:55:45 +0200 Subject: [PATCH 16/19] openssl: build dtls:// on the udp transport via enable_crypto --- ext/openssl/config.w32 | 2 +- ext/openssl/config0.m4 | 2 +- ext/openssl/openssl.c | 4 +- ext/openssl/xp_common.c | 153 ++++++++++++++ ext/openssl/xp_common.h | 41 ++++ ext/openssl/xp_dtls.c | 306 +++++++++++++--------------- ext/openssl/xp_ssl.c | 123 +---------- ext/standard/file.stub.php | 30 +++ ext/standard/file_arginfo.h | 6 + main/php_streams.h | 3 + main/streams/php_stream_transport.h | 8 +- main/streams/xp_socket.c | 4 +- 12 files changed, 396 insertions(+), 286 deletions(-) create mode 100644 ext/openssl/xp_common.c create mode 100644 ext/openssl/xp_common.h diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 index 8016f8f97ef4..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 xp_dtls.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 95eed7c559d4..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 xp_dtls.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 5cd191644c07..83e95863832b 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -833,7 +833,7 @@ PHP_MINIT_FUNCTION(openssl) #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 */ @@ -913,6 +913,8 @@ PHP_MSHUTDOWN_FUNCTION(openssl) #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 */ diff --git a/ext/openssl/xp_common.c b/ext/openssl/xp_common.c new file mode 100644 index 000000000000..76f0fdda2733 --- /dev/null +++ b/ext/openssl/xp_common.c @@ -0,0 +1,153 @@ +/* + +----------------------------------------------------------------------+ + | 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_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..00affc5d729b --- /dev/null +++ b/ext/openssl/xp_common.h @@ -0,0 +1,41 @@ +/* + +----------------------------------------------------------------------+ + | 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 + +/* 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); + +#endif /* PHP_OPENSSL_XP_COMMON_H */ diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 362826ad1816..47e4a2e8d955 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -22,6 +22,7 @@ #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 @@ -52,6 +53,11 @@ typedef struct _php_openssl_dtls_data_t { 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; @@ -98,8 +104,15 @@ static const php_stream_ops php_openssl_dtls_socket_ops; 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; - SSL *ssl = dtlssock->ssl_handle; + /* 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; } @@ -239,55 +252,16 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) } 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; } -/* Returns an emalloc'd host (caller frees) and a port, or NULL on error. - * - * TODO: duplicates the static parse_ip_address_ex() in xp_socket.c. */ -static char *php_openssl_dtls_parse_ip_address(php_stream_xport_param *xparam, int *portno) -{ - const char *str = xparam->inputs.name; - size_t str_len = xparam->inputs.namelen; - const char *colon; - - if (str == NULL || memchr(str, '\0', str_len)) { - if (xparam->want_errortext) { - xparam->outputs.error_text = ZSTR_INIT_LITERAL("The hostname must not contain null bytes", 0); - } - return NULL; - } - -#ifdef HAVE_IPV6 - if (*str == '[' && str_len > 1) { - /* [ipv6]:port notation, e.g. [fe80::1]:443 */ - const char *p = memchr(str + 1, ']', str_len - 2); - if (p == NULL || *(p + 1) != ':') { - if (xparam->want_errortext) { - xparam->outputs.error_text = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); - } - return NULL; - } - *portno = atoi(p + 2); - return estrndup(str + 1, p - str - 1); - } -#endif - - colon = str_len ? memchr(str, ':', str_len - 1) : NULL; - if (colon == NULL) { - if (xparam->want_errortext) { - xparam->outputs.error_text = strpprintf(0, "Failed to parse address \"%s\"", str); - } - return NULL; - } - - *portno = atoi(colon + 1); - return estrndup(str, colon - str); -} - /* Supply the passphrase for an encrypted local_pk from the "passphrase" option. */ static int php_openssl_dtls_passwd_callback(char *buf, int num, int verify, void *data) { @@ -427,6 +401,9 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da 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"); @@ -464,7 +441,7 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da 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; + 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. */ @@ -506,13 +483,13 @@ static int php_openssl_dtls_setup_crypto(php_stream *stream, php_openssl_dtls_da /* 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, - php_stream_xport_param *xparam) + 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 = xparam->inputs.timeout; + struct timeval *tmo = timeout; struct timeval deadline; gettimeofday(&deadline, NULL); if (tmo != NULL && (tmo->tv_sec > 0 || tmo->tv_usec > 0)) { @@ -551,8 +528,8 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ /* SSL_ERROR_SYSCALL with an empty queue: report the syscall. */ snprintf(buf, sizeof(buf), "%s", strerror(saved_errno)); } - if (xparam->want_errortext) { - xparam->outputs.error_text = strpprintf(0, "DTLS handshake failed: %s", + if (error_text != NULL) { + *error_text = strpprintf(0, "DTLS handshake failed: %s", buf[0] != '\0' ? buf : "unexpected error"); } return -1; @@ -569,8 +546,8 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ long remaining = (deadline.tv_sec - now.tv_sec) * 1000L + (deadline.tv_usec - now.tv_usec) / 1000; if (remaining <= 0) { - if (xparam->want_errortext) { - xparam->outputs.error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); } return -1; } @@ -582,14 +559,14 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ if (ready == 0) { /* Timer fired: let OpenSSL resend the last flight. */ if (DTLSv1_handle_timeout(ssl) < 0) { - if (xparam->want_errortext) { - xparam->outputs.error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("DTLS handshake timed out", 0); } return -1; } } else if (ready < 0) { - if (xparam->want_errortext) { - xparam->outputs.error_text = + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("DTLS handshake failed while waiting for the socket", 0); } return -1; @@ -597,65 +574,9 @@ static int php_openssl_dtls_handshake(php_stream *stream, php_openssl_dtls_data_ } } -static bool php_openssl_dtls_fingerprint_is_equal(php_stream *stream, X509 *peer, const char *method, - zend_string *expected) -{ - zend_string *fingerprint = php_openssl_x509_fingerprint(peer, method, false, stream); - bool is_equal = false; - - if (fingerprint != NULL) { - is_equal = zend_string_equals_ci(fingerprint, expected); - zend_string_release_ex(fingerprint, false); - } - - return is_equal; -} - -/* Match the peer certificate against a md5/sha1 hash (by length) or an - * [algo => hash] map. */ -static bool php_openssl_dtls_fingerprint_match(php_stream *stream, X509 *peer, 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 (method == NULL) { - php_stream_warn(stream, AuthFailed, "peer_fingerprint length doesn't match a md5 or sha1 hash"); - return false; - } - return php_openssl_dtls_fingerprint_is_equal(stream, peer, method, Z_STR_P(val)); - } - - if (Z_TYPE_P(val) == IS_ARRAY) { - zval *current; - zend_string *key; - - if (zend_hash_num_elements(Z_ARRVAL_P(val)) == 0) { - 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_dtls_fingerprint_is_equal(stream, peer, ZSTR_VAL(key), Z_STR_P(current))) { - return false; - } - } ZEND_HASH_FOREACH_END(); - return true; - } - - php_stream_warn(stream, Generic, "Invalid peer_fingerprint; a string or [algo => fingerprint] array required"); - return false; -} - -/* Verify the peer certificate against the peer_fingerprint option, if set. This - * lets the caller authenticate the peer by fingerprint instead of a CA chain. */ +/* 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, - php_stream_xport_param *xparam) + zend_string **error_text) { zval *val; if (!GET_VER_OPT("peer_fingerprint")) { @@ -663,14 +584,14 @@ static int php_openssl_dtls_check_fingerprint(php_stream *stream, php_openssl_dt } X509 *peer = SSL_get_peer_certificate(dtlssock->ssl_handle); - bool match = peer != NULL && php_openssl_dtls_fingerprint_match(stream, peer, val); + bool match = peer != NULL && php_openssl_x509_fingerprint_match(stream, peer, val); if (peer != NULL) { X509_free(peer); } if (!match) { - if (xparam->want_errortext) { - xparam->outputs.error_text = ZSTR_INIT_LITERAL("peer_fingerprint match failure", 0); + if (error_text != NULL) { + *error_text = ZSTR_INIT_LITERAL("peer_fingerprint match failure", 0); } return -1; } @@ -1127,57 +1048,69 @@ static int php_openssl_dtls_accept(php_stream *stream, php_openssl_dtls_data_t * 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) != 0) { + 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 UDP socket and run the DTLS handshake. */ +/* 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) { - char *host; - int portno = 0; - int err = 0; - - host = php_openssl_dtls_parse_ip_address(xparam, &portno); - if (host == NULL) { - return -1; - } - - dtlssock->s.socket = php_network_connect_socket_to_host(host, (unsigned short)portno, - SOCK_DGRAM, - xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, - xparam->inputs.timeout, - xparam->want_errortext ? &xparam->outputs.error_text : NULL, - &err, NULL, 0, STREAM_SOCKOP_NONE); - - xparam->outputs.error_code = err; - - if (dtlssock->s.socket == SOCK_ERR) { - efree(host); - return -1; - } - - /* The I/O loop emulates blocking with poll, so the fd stays non-blocking. */ - php_set_sock_blocking(dtlssock->s.socket, 0); - - if (php_openssl_dtls_setup_crypto(stream, dtlssock, host) != 0) { - efree(host); + /* 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; } - efree(host); + dtlssock->ssl_active = true; + return 0; +} - if (php_openssl_dtls_handshake(stream, dtlssock, xparam) != 0) { - return -1; +/* 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 php_openssl_dtls_check_fingerprint(stream, dtlssock, xparam); + return -1; } /* Expose the fd for stream_select(); the raw fd is not handed out otherwise, @@ -1205,6 +1138,10 @@ static int php_openssl_dtls_sockop_cast(php_stream *stream, int castas, void **r 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; } } @@ -1277,6 +1214,35 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in 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; @@ -1288,21 +1254,13 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in return PHP_STREAM_OPTION_RETURN_OK; case STREAM_XPORT_OP_BIND: { - int portno = 0, err = 0; - char *host = php_openssl_dtls_parse_ip_address(xparam, &portno); - if (host == NULL) { - xparam->outputs.returncode = -1; - return PHP_STREAM_OPTION_RETURN_OK; - } - dtlssock->s.socket = php_network_bind_socket_to_local_addr(host, portno, SOCK_DGRAM, - STREAM_SOCKOP_SO_REUSEADDR, - xparam->want_errortext ? &xparam->outputs.error_text : NULL, &err); - efree(host); - xparam->outputs.error_code = err; - if (dtlssock->s.socket == SOCK_ERR) { - xparam->outputs.returncode = -1; + /* 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); @@ -1328,6 +1286,10 @@ static int php_openssl_dtls_sockop_set_option(php_stream *stream, int option, in } 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; @@ -1349,6 +1311,7 @@ static const php_stream_ops php_openssl_dtls_socket_ops = { php_openssl_dtls_sockop_cast, NULL, /* stat */ php_openssl_dtls_sockop_set_option, + true, /* is_dgram */ }; /* Allocate a dtls:// stream. */ @@ -1367,12 +1330,29 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, dtlssock->s.is_blocked = 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; } diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index bc483df617b1..9442e335bad5 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) /* {{{ */ { @@ -3499,16 +3435,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 +3546,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/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..bb373020866f 100644 --- a/ext/standard/file_arginfo.h +++ b/ext/standard/file_arginfo.h @@ -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_streams.h b/main/php_streams.h index 7622a7295af3..8e0a7f194678 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -124,6 +124,9 @@ typedef struct _php_stream_ops { int (*cast)(php_stream *stream, int castas, void **ret); int (*stat)(php_stream *stream, php_stream_statbuf *ssb); int (*set_option)(php_stream *stream, int option, int value, void *ptrparam); + + /* Datagram socket transport: the generic socket ops connect/bind with SOCK_DGRAM. */ + bool is_dgram; } php_stream_ops; typedef struct _php_stream_wrapper_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..fbe4e00e56ae 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -56,7 +56,8 @@ 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) || (stream)->ops->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); @@ -564,6 +565,7 @@ static const php_stream_ops php_stream_udp_socket_ops = { php_sockop_cast, php_sockop_stat, php_tcp_sockop_set_option, + true, /* is_dgram */ }; #ifdef AF_UNIX From 52cab11f42e0a778a11f8fce15c905d7a4fc62ce Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 13 Jul 2026 19:30:39 +0200 Subject: [PATCH 17/19] streams: add php_netstream_data_t.is_dgram for datagram transports --- ext/openssl/xp_dtls.c | 2 +- main/php_network.h | 2 ++ main/php_streams.h | 3 --- main/streams/xp_socket.c | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ext/openssl/xp_dtls.c b/ext/openssl/xp_dtls.c index 47e4a2e8d955..bf1bf4bfbb17 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -1311,7 +1311,6 @@ static const php_stream_ops php_openssl_dtls_socket_ops = { php_openssl_dtls_sockop_cast, NULL, /* stat */ php_openssl_dtls_sockop_set_option, - true, /* is_dgram */ }; /* Allocate a dtls:// stream. */ @@ -1328,6 +1327,7 @@ php_stream *php_openssl_dtls_socket_factory(const char *proto, size_t protolen, 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) { 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/php_streams.h b/main/php_streams.h index 8e0a7f194678..7622a7295af3 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -124,9 +124,6 @@ typedef struct _php_stream_ops { int (*cast)(php_stream *stream, int castas, void **ret); int (*stat)(php_stream *stream, php_stream_statbuf *ssb); int (*set_option)(php_stream *stream, int option, int value, void *ptrparam); - - /* Datagram socket transport: the generic socket ops connect/bind with SOCK_DGRAM. */ - bool is_dgram; } php_stream_ops; typedef struct _php_stream_wrapper_ops { diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index fbe4e00e56ae..89e4168e54cb 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -57,7 +57,8 @@ static const php_stream_ops php_stream_unixdg_socket_ops; #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) || (stream)->ops->is_dgram) + (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); @@ -565,7 +566,6 @@ static const php_stream_ops php_stream_udp_socket_ops = { php_sockop_cast, php_sockop_stat, php_tcp_sockop_set_option, - true, /* is_dgram */ }; #ifdef AF_UNIX From afa54feef801ce58d48940dada34a9908a8bf917 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Mon, 13 Jul 2026 20:19:06 +0200 Subject: [PATCH 18/19] sockets: fix test bug_export_stream_type --- ext/sockets/tests/bug_export_stream_type.phpt | 2 +- ext/standard/file_arginfo.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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_arginfo.h b/ext/standard/file_arginfo.h index bb373020866f..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) { From bba43efd86a69e222ee5cf8460d2cc16e8319621 Mon Sep 17 00:00:00 2001 From: Gianfrancesco Date: Tue, 14 Jul 2026 00:40:12 +0200 Subject: [PATCH 19/19] openssl: wip xp_common --- ext/openssl/xp_common.c | 78 +++++++++++++++++++++++++++++++++++++++++ ext/openssl/xp_common.h | 8 +++++ ext/openssl/xp_dtls.c | 61 +++++--------------------------- ext/openssl/xp_ssl.c | 67 ++--------------------------------- 4 files changed, 97 insertions(+), 117 deletions(-) diff --git a/ext/openssl/xp_common.c b/ext/openssl/xp_common.c index 76f0fdda2733..716836ebf8b7 100644 --- a/ext/openssl/xp_common.c +++ b/ext/openssl/xp_common.c @@ -132,6 +132,84 @@ char *php_openssl_get_url_name(const char *resourcename, size_t resourcenamelen, 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) { diff --git a/ext/openssl/xp_common.h b/ext/openssl/xp_common.h index 00affc5d729b..b0a865b84dbd 100644 --- a/ext/openssl/xp_common.h +++ b/ext/openssl/xp_common.h @@ -23,6 +23,7 @@ #include "php.h" #include "php_network.h" +#include #include /* Match the peer certificate against the peer_fingerprint context option. */ @@ -38,4 +39,11 @@ char *php_openssl_get_url_name(const char *resourcename, size_t resourcenamelen, 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 index bf1bf4bfbb17..8c2c1cab8581 100644 --- a/ext/openssl/xp_dtls.c +++ b/ext/openssl/xp_dtls.c @@ -262,29 +262,12 @@ static int php_openssl_dtls_sockop_close(php_stream *stream, int close_handle) return 0; } -/* Supply the passphrase for an encrypted local_pk from the "passphrase" option. */ -static int php_openssl_dtls_passwd_callback(char *buf, int num, int verify, void *data) -{ - php_stream *stream = (php_stream *)data; - zval *val = NULL; - char *passphrase = NULL; - - GET_VER_OPT_STRING("passphrase", passphrase); - - if (passphrase != NULL && 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; -} - /* 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, *certfile = NULL; - size_t certfile_len = 0; + char *cafile = NULL, *capath = NULL, *cipherlist = NULL; zval *val; /* DTLS 1.0 is deprecated; require DTLS 1.2 or higher. */ @@ -325,41 +308,15 @@ static int php_openssl_dtls_apply_context(php_stream *stream, php_openssl_dtls_d return -1; } - /* Local certificate and private key as file paths. */ - GET_VER_OPT_STRINGL("local_cert", certfile, certfile_len); - if (certfile != NULL) { - char resolved[MAXPATHLEN]; - char *private_key = NULL; - size_t private_key_len = 0; - - if (!php_openssl_check_path_ex(certfile, certfile_len, resolved, 0, false, false, - "local_cert in ssl stream context", stream)) { - return -1; - } - if (SSL_CTX_use_certificate_chain_file(ctx, resolved) != 1) { - php_stream_warn(stream, WriteFailed, "Unable to set local cert chain file `%s'", certfile); - return -1; - } - - /* A passphrase for an encrypted private key. */ - if (GET_VER_OPT("passphrase")) { - SSL_CTX_set_default_passwd_cb_userdata(ctx, stream); - SSL_CTX_set_default_passwd_cb(ctx, php_openssl_dtls_passwd_callback); - } + /* 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_pk defaults to the certificate file (combined PEM). */ - GET_VER_OPT_STRINGL("local_pk", private_key, private_key_len); - if (private_key != NULL && !php_openssl_check_path_ex(private_key, private_key_len, resolved, 0, - false, false, "local_pk in ssl stream context", stream)) { - return -1; - } - if (SSL_CTX_use_PrivateKey_file(ctx, resolved, SSL_FILETYPE_PEM) != 1) { - php_stream_warn(stream, WriteFailed, "Unable to set private key file `%s'", resolved); - return -1; - } - if (SSL_CTX_check_private_key(ctx) != 1) { - php_stream_warn(stream, PermissionDenied, "Private key does not match certificate!"); - } + /* Local certificate chain and private key as file paths. */ + if (php_openssl_set_local_cert(ctx, stream) != SUCCESS) { + return -1; } return 0; diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 9442e335bad5..39f91e375043 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -617,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; @@ -908,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) /* {{{ */ {