Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ PHP NEWS
and Openssl\Session class. (Jakub Zelenka)
. Added TLS external PSK support for streams with new context options and
Openssl\Psk class. (Jakub Zelenka)
. Added TLS 1.3 early data (0-RTT) support for streams with new context
options early_data, max_early_data and early_data_cb. (Jakub Zelenka)
. Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE.
(Jakub Zelenka)

Expand Down
5 changes: 5 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ PHP 8.6 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/tls_session_resumption
. Added TLS external PSK support for streams with new strem context options:
psk_client_cb and psk_server_cb. This allows setting and receiving PSK.
. Added TLS 1.3 early data (0-RTT) support for streams. Clients send early
data with the early_data context option; servers accept it with
max_early_data and receive it through the early_data_cb callback. The
outcome is reported as 'accepted', 'rejected' or 'not_sent' in the
early_data key of the crypto stream_get_meta_data() array.

- Phar:
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
Expand Down
89 changes: 89 additions & 0 deletions ext/openssl/tests/tls_early_data_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
--TEST--
TLS 1.3 early data (0-RTT) basic client/server round-trip
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
?>
--FILE--
<?php
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_basic.pem.tmp';

$serverCode = <<<'CODE'
$early = '';
$ctx = stream_context_create(['ssl' => [
'local_cert' => '%s',
'session_id_context' => 'early-data-test',
'max_early_data' => 16384,
'early_data_cb' => function ($stream, string $data) use (&$early) {
$early .= $data;
},
]]);

$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
phpt_notify_server_start($server);

for ($i = 0; $i < 2; $i++) {
$early = '';
$client = @stream_socket_accept($server, 30);
if ($client) {
$meta = stream_get_meta_data($client);
$status = $meta['crypto']['early_data'] ?? 'none';
fwrite($client, "early=[$early] status=$status\n");
fclose($client);
}
}
CODE;
$serverCode = sprintf($serverCode, $certFile);

$clientCode = <<<'CODE'
$sessionData = null;

$ctx = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
$sessionData = $session;
},
]]);

/* First connection: full handshake, capture the resumable session */
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx);
echo trim(fgets($client1)) . "\n";
fclose($client1);

/* Second connection: resume and send 0-RTT early data */
$ctx2 = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_data' => $sessionData,
'early_data' => 'ping-0rtt',
]]);

$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx2);
echo trim(fgets($client2)) . "\n";
$meta2 = stream_get_meta_data($client2);
echo "client early_data: " . ($meta2['crypto']['early_data'] ?? 'none') . "\n";
fclose($client2);
CODE;

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_basic', $certFile);

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_basic.pem.tmp');
?>
--EXPECT--
early=[] status=not_sent
early=[ping-0rtt] status=accepted
client early_data: accepted
50 changes: 50 additions & 0 deletions ext/openssl/tests/tls_early_data_invalid.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
TLS 1.3 early data with an invalid early_data option raises TypeError
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
?>
--FILE--
<?php
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_invalid.pem.tmp';

$serverCode = <<<'CODE'
$ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
phpt_notify_server_start($server);
@stream_socket_accept($server, 3);
CODE;
$serverCode = sprintf($serverCode, $certFile);

$clientCode = <<<'CODE'
$ctx = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'early_data' => 123,
]]);
try {
@stream_socket_client('tls://{{ ADDR }}', $errno, $errstr,
5, STREAM_CLIENT_CONNECT, $ctx);
echo "no exception\n";
} catch (TypeError $e) {
echo "caught: ", $e->getMessage(), "\n";
}
CODE;

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_invalid', $certFile);

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_invalid.pem.tmp');
?>
--EXPECT--
caught: early_data must be a string
80 changes: 80 additions & 0 deletions ext/openssl/tests/tls_early_data_large.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
--TEST--
TLS 1.3 early data (0-RTT) larger than one read chunk
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
?>
--FILE--
<?php
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_large.pem.tmp';

$serverCode = <<<'CODE'
$early = '';
$ctx = stream_context_create(['ssl' => [
'local_cert' => '%s',
'session_id_context' => 'early-data-large',
'max_early_data' => 65536,
'early_data_cb' => function ($stream, string $data) use (&$early) {
$early .= $data;
},
]]);
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
phpt_notify_server_start($server);
for ($i = 0; $i < 2; $i++) {
$early = '';
$client = @stream_socket_accept($server, 30);
if ($client) {
$meta = stream_get_meta_data($client);
$status = $meta['crypto']['early_data'] ?? 'none';
fwrite($client, "len=" . strlen($early) . " ok=" .
(int) ($early === str_repeat('A', 40000)) . " status=$status\n");
fclose($client);
}
}
CODE;
$serverCode = sprintf($serverCode, $certFile);

$clientCode = <<<'CODE'
$sessionData = null;
$ctx = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
$sessionData = $session;
},
]]);
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx);
echo trim(fgets($client1)) . "\n";
fclose($client1);

$ctx2 = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_data' => $sessionData,
'early_data' => str_repeat('A', 40000),
]]);
$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx2);
echo trim(fgets($client2)) . "\n";
fclose($client2);
CODE;

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_large', $certFile);

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_large.pem.tmp');
?>
--EXPECT--
len=0 ok=0 status=not_sent
len=40000 ok=1 status=accepted
79 changes: 79 additions & 0 deletions ext/openssl/tests/tls_early_data_unsupported.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
--TEST--
TLS 1.3 early data option is ignored when the session does not allow 0-RTT
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
?>
--FILE--
<?php
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_unsupported.pem.tmp';

/* Server does NOT enable early data, so a resumed session must not carry a
* 0-RTT limit and the client must fall back to a normal handshake. */
$serverCode = <<<'CODE'
$ctx = stream_context_create(['ssl' => [
'local_cert' => '%s',
'session_cache' => true,
'session_id_context' => 'early-data-off',
]]);
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
phpt_notify_server_start($server);
for ($i = 0; $i < 2; $i++) {
$client = @stream_socket_accept($server, 30);
if ($client) {
fwrite($client, "hello\n");
fclose($client);
}
}
CODE;
$serverCode = sprintf($serverCode, $certFile);

$clientCode = <<<'CODE'
$sessionData = null;
$ctx = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
$sessionData = $session;
},
]]);
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx);
echo trim(fgets($client1)) . "\n";
fclose($client1);

$ctx2 = stream_context_create(['ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'session_data' => $sessionData,
'early_data' => 'ping-0rtt',
]]);
$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
30, STREAM_CLIENT_CONNECT, $ctx2);
echo trim(fgets($client2)) . "\n";
$meta2 = stream_get_meta_data($client2);
echo "reused: " . ($meta2['crypto']['session_reused'] ? 'yes' : 'no') . "\n";
echo "early_data key present: " . (array_key_exists('early_data', $meta2['crypto']) ? 'yes' : 'no') . "\n";
fclose($client2);
CODE;

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_unsupported', $certFile);

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_unsupported.pem.tmp');
?>
--EXPECT--
hello
hello
reused: yes
early_data key present: no
Loading
Loading