Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1f8915b
openssl: Add skeleton dtls:// stream transport and tests
GianfriAur Jun 30, 2026
4be6e60
openssl: Connect the dtls:// client and set up the DTLS objects
GianfriAur Jun 30, 2026
0d75bd8
openssl: Drive the dtls:// client handshake and datagram I/O
GianfriAur Jun 30, 2026
89aa780
openssl: Expose underlying fd via dtls:// cast op
GianfriAur Jun 30, 2026
9b11a26
openssl: Reuse base socket data and improve dtls:// diagnostics
GianfriAur Jun 30, 2026
f9308ef
openssl: Apply ssl context options to the dtls:// client
GianfriAur Jun 30, 2026
32fa44f
openssl: Add a dtls:// server with cookie exchange
GianfriAur Jul 1, 2026
a46ed3c
openssl: Enable DTLS over udp:// and harden the dtls:// server
GianfriAur Jul 1, 2026
6d6adf7
openssl: Handle DTLS path MTU for dtls://
GianfriAur Jul 1, 2026
3d44d67
openssl: Harden dtls:// cookie handling and accept
GianfriAur Jul 2, 2026
8265766
openssl: drop udp:// override,
GianfriAur Jul 5, 2026
5bf9389
openssl: Add dtls:// client session resumption
GianfriAur Jul 5, 2026
15a489c
openssl: Add dtls:// server session resumption
GianfriAur Jul 8, 2026
ef07fe1
openssl: Fix dtls:// accept build with --enable-debug
GianfriAur Jul 8, 2026
2fd0641
openssl: Fix dtls:// portability on macOS, Windows and FreeBSD
GianfriAur Jul 8, 2026
9ab6444
openssl: build dtls:// on the udp transport via enable_crypto
GianfriAur Jul 13, 2026
52cab11
streams: add php_netstream_data_t.is_dgram for datagram transports
GianfriAur Jul 13, 2026
afa54fe
sockets: fix test bug_export_stream_type
GianfriAur Jul 13, 2026
bba43ef
openssl: wip xp_common
GianfriAur Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/openssl/config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") {
var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL);

if (ret >= 2) {
EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c");
EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_common.c xp_ssl.c xp_dtls.c");
AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available.");
if (PHP_OPENSSL_LEGACY_PROVIDER != "no") {
AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider.");
Expand Down
2 changes: 1 addition & 1 deletion ext/openssl/config0.m4
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ PHP_ARG_WITH([openssl-argon2],

if test "$PHP_OPENSSL" != "no"; then
PHP_NEW_EXTENSION([openssl],
[openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c],
[openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_common.c xp_ssl.c xp_dtls.c],
[$ext_shared])
PHP_SUBST([OPENSSL_SHARED_LIBADD])
PHP_SETUP_OPENSSL([OPENSSL_SHARED_LIBADD],
Expand Down
15 changes: 14 additions & 1 deletion ext/openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,12 @@ PHP_MINIT_FUNCTION(openssl)
php_stream_xport_register("tlsv1.2", php_openssl_ssl_socket_factory);
php_stream_xport_register("tlsv1.3", php_openssl_ssl_socket_factory);

#ifndef OPENSSL_NO_DTLS
php_stream_xport_register("dtls", php_openssl_dtls_socket_factory);
php_stream_xport_register("dtlsv1.2", php_openssl_dtls_socket_factory);
php_stream_xport_register("udp", php_openssl_dtls_socket_factory);
#endif

/* override the default tcp socket provider */
php_stream_xport_register("tcp", php_openssl_ssl_socket_factory);

Expand Down Expand Up @@ -904,6 +910,13 @@ PHP_MSHUTDOWN_FUNCTION(openssl)
php_stream_xport_unregister("tlsv1.2");
php_stream_xport_unregister("tlsv1.3");

#ifndef OPENSSL_NO_DTLS
php_stream_xport_unregister("dtls");
php_stream_xport_unregister("dtlsv1.2");
/* reinstate the default udp handler */
php_stream_xport_register("udp", php_stream_generic_socket_factory);
#endif

/* reinstate the default tcp handler */
php_stream_xport_register("tcp", php_stream_generic_socket_factory);

Expand Down Expand Up @@ -2755,7 +2768,7 @@ PHP_FUNCTION(openssl_pkey_get_details)
array_init(return_value);
add_assoc_long(return_value, "bits", EVP_PKEY_bits(pkey));
add_assoc_stringl(return_value, "key", pbio, pbio_len);

zend_long ktype = php_openssl_pkey_get_details(return_value, pkey);

add_assoc_long(return_value, "type", ktype);
Expand Down
1 change: 1 addition & 0 deletions ext/openssl/php_openssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
110 changes: 110 additions & 0 deletions ext/openssl/tests/dtls_client_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
--TEST--
dtls:// client: DTLS 1.2 handshake and data round trip against openssl s_server
--EXTENSIONS--
openssl
--SKIPIF--
<?php
require __DIR__ . '/dtls_skipif.inc';
if (!function_exists('proc_open')) die('skip proc_open() not available');
if (substr(PHP_OS, 0, 3) === 'WIN') die('skip not for Windows');
exec('openssl version', $out, $code);
if ($code !== 0) die('skip openssl binary not available');
?>
--FILE--
<?php
include 'CertificateGenerator.inc';

$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_basic.pem.tmp';
(new CertificateGenerator())->saveNewCertAsFileWithKey('dtls-server', $certFile);

// Grab a free UDP port, then hand it to s_server.
$probe = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
$port = (int) substr(strrchr(stream_socket_get_name($probe, false), ':'), 1);
fclose($probe);

// Plain relay mode: data from the client is printed to s_server's stdout, and
// s_server's stdin is sent to the client.
$cmd = ['openssl', 's_server', '-dtls1_2', '-accept', (string) $port,
'-cert', $certFile, '-key', $certFile];
$descriptors = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']];
$server = proc_open($cmd, $descriptors, $pipes);
if (!is_resource($server)) {
die("skip-like: could not start s_server\n");
}

// s_server prints "ACCEPT" once it is bound and listening; wait for it so the
// client does not fire its ClientHello at an unbound port.
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$ready = false;
$deadline = microtime(true) + 10;
$buf = '';
while (microtime(true) < $deadline) {
$r = [$pipes[1], $pipes[2]]; $w = $e = [];
if (stream_select($r, $w, $e, 1)) {
foreach ($r as $pipe) {
$buf .= (string) fread($pipe, 8192);
}
if (strpos($buf, 'ACCEPT') !== false) { $ready = true; break; }
}
}

// Self-signed s_server cert: skip verification for this round-trip scenario.
$ctx = stream_context_create(['ssl' => ['verify_peer' => false]]);
$client = $ready
? stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx)
: false;
var_dump($client !== false);

if ($client !== false) {
var_dump(stream_get_meta_data($client)['crypto']['protocol'] === 'DTLSv1.2');
stream_set_timeout($client, 10);

// client -> server: our datagram should show up on s_server's stdout.
fwrite($client, "PING\n");
// server -> client: feed s_server's stdin, it relays to the client.
fwrite($pipes[0], "PONG\n");

// stream_select() on the dtls:// stream exercises the transport cast op.
$r = [$client]; $w = $e = [];
var_dump(stream_select($r, $w, $e, 10) === 1);
var_dump(rtrim((string) fread($client, 8192)) === 'PONG');

$srvOut = '';
$deadline = microtime(true) + 5;
while (microtime(true) < $deadline && strpos($srvOut, 'PING') === false) {
$srvOut .= (string) fread($pipes[1], 8192);
usleep(50000);
}
var_dump(strpos($srvOut, 'PING') !== false);

// A read with nothing pending times out; the metadata reflects it.
stream_set_timeout($client, 0, 200000);
fread($client, 8192);
var_dump(stream_get_meta_data($client)['timed_out']);

// A non-blocking read with nothing pending returns '' (would block), not false.
stream_set_blocking($client, false);
var_dump(fread($client, 8192));

fclose($client);
}

proc_terminate($server);
foreach ($pipes as $pipe) {
if (is_resource($pipe)) fclose($pipe);
}
proc_close($server);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_basic.pem.tmp');
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
string(0) ""
86 changes: 86 additions & 0 deletions ext/openssl/tests/dtls_client_cert.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
--TEST--
dtls:// client: client certificate for mutual authentication (local_cert)
--EXTENSIONS--
openssl
--SKIPIF--
<?php
require __DIR__ . '/dtls_skipif.inc';
if (!function_exists('proc_open')) die('skip proc_open() not available');
if (substr(PHP_OS, 0, 3) === 'WIN') die('skip not for Windows');
exec('openssl version', $out, $code);
if ($code !== 0) die('skip openssl binary not available');
?>
--FILE--
<?php
include 'CertificateGenerator.inc';

$dir = __DIR__ . DIRECTORY_SEPARATOR;
$caFile = $dir . 'dtls_client_cert-ca.pem.tmp';
$serverCert = $dir . 'dtls_client_cert-server.pem.tmp';
$clientCert = $dir . 'dtls_client_cert-client.pem.tmp';
$gen = new CertificateGenerator();
$gen->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--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_cert-ca.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_cert-server.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_cert-client.pem.tmp');
?>
--EXPECT--
bool(true)
bool(true)
81 changes: 81 additions & 0 deletions ext/openssl/tests/dtls_client_fingerprint.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
--TEST--
dtls:// client: peer authentication by certificate fingerprint (peer_fingerprint)
--EXTENSIONS--
openssl
--SKIPIF--
<?php
require __DIR__ . '/dtls_skipif.inc';
if (!function_exists('proc_open')) die('skip proc_open() not available');
if (substr(PHP_OS, 0, 3) === 'WIN') die('skip not for Windows');
exec('openssl version', $out, $code);
if ($code !== 0) die('skip openssl binary not available');
?>
--FILE--
<?php
include 'CertificateGenerator.inc';

$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_fingerprint.pem.tmp';
$gen = new CertificateGenerator();
$gen->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--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'dtls_client_fingerprint.pem.tmp');
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
27 changes: 27 additions & 0 deletions ext/openssl/tests/dtls_client_timeout.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
dtls:// client: the handshake is bounded by the connect timeout
--EXTENSIONS--
openssl
--SKIPIF--
<?php require __DIR__ . '/dtls_skipif.inc'; ?>
--FILE--
<?php
// A bound UDP socket that never speaks DTLS: the ClientHello is retransmitted but
// never answered, so the handshake must give up at the connect timeout instead of
// retransmitting forever.
$sink = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
$port = (int) substr(strrchr(stream_socket_get_name($sink, false), ':'), 1);

$start = microtime(true);
$client = @stream_socket_client("dtls://127.0.0.1:$port", $errno, $errstr, 2,
STREAM_CLIENT_CONNECT, stream_context_create(['ssl' => ['verify_peer' => false]]));
$elapsed = microtime(true) - $start;

var_dump($client === false);
var_dump($elapsed >= 1 && $elapsed < 5);

fclose($sink);
?>
--EXPECT--
bool(true)
bool(true)
Loading
Loading