Skip to content

Commit f465d35

Browse files
authored
Add OpenSSL TLS 1.3 early data (0-RTT) support (#22802)
This builds on the session resumption support to let clients send and servers accept TLS 1.3 early data through the stream SSL context. Clients set the early_data context option (a string) to send 0-RTT data, which is only sent when the resumed session allows it. Servers enable acceptance with max_early_data and receive the data through the early_data_cb callback, so nothing is buffered on the stream. The outcome is reported as 'accepted', 'rejected' or 'not_sent' in the early_data key of the crypto stream_get_meta_data() array. Enabling early data also shares the server SSL_CTX across accepted connections and keeps the session cache on, both of which 0-RTT resumption requires.
1 parent 387a3af commit f465d35

7 files changed

Lines changed: 576 additions & 7 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ PHP NEWS
366366
and Openssl\Session class. (Jakub Zelenka)
367367
. Added TLS external PSK support for streams with new context options and
368368
Openssl\Psk class. (Jakub Zelenka)
369+
. Added TLS 1.3 early data (0-RTT) support for streams with new context
370+
options early_data, max_early_data and early_data_cb. (Jakub Zelenka)
369371
. Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE.
370372
(Jakub Zelenka)
371373

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ PHP 8.6 UPGRADE NOTES
299299
RFC: https://wiki.php.net/rfc/tls_session_resumption
300300
. Added TLS external PSK support for streams with new stream context options:
301301
psk_client_cb and psk_server_cb. This allows setting and receiving PSK.
302+
. Added TLS 1.3 early data (0-RTT) support for streams. Clients send early
303+
data with the early_data context option; servers accept it with
304+
max_early_data and receive it through the early_data_cb callback. The
305+
outcome is reported as 'accepted', 'rejected' or 'not_sent' in the
306+
early_data key of the crypto stream_get_meta_data() array.
302307

303308
- Phar:
304309
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
--TEST--
2+
TLS 1.3 early data (0-RTT) basic client/server round-trip
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
9+
?>
10+
--FILE--
11+
<?php
12+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_basic.pem.tmp';
13+
14+
$serverCode = <<<'CODE'
15+
$early = '';
16+
$ctx = stream_context_create(['ssl' => [
17+
'local_cert' => '%s',
18+
'session_id_context' => 'early-data-test',
19+
'max_early_data' => 16384,
20+
'early_data_cb' => function ($stream, string $data) use (&$early) {
21+
$early .= $data;
22+
},
23+
]]);
24+
25+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
26+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
27+
phpt_notify_server_start($server);
28+
29+
for ($i = 0; $i < 2; $i++) {
30+
$early = '';
31+
$client = @stream_socket_accept($server, 30);
32+
if ($client) {
33+
$meta = stream_get_meta_data($client);
34+
$status = $meta['crypto']['early_data'] ?? 'none';
35+
fwrite($client, "early=[$early] status=$status\n");
36+
fclose($client);
37+
}
38+
}
39+
CODE;
40+
$serverCode = sprintf($serverCode, $certFile);
41+
42+
$clientCode = <<<'CODE'
43+
$sessionData = null;
44+
45+
$ctx = stream_context_create(['ssl' => [
46+
'verify_peer' => false,
47+
'verify_peer_name' => false,
48+
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
49+
$sessionData = $session;
50+
},
51+
]]);
52+
53+
/* First connection: full handshake, capture the resumable session */
54+
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
55+
30, STREAM_CLIENT_CONNECT, $ctx);
56+
echo trim(fgets($client1)) . "\n";
57+
fclose($client1);
58+
59+
/* Second connection: resume and send 0-RTT early data */
60+
$ctx2 = stream_context_create(['ssl' => [
61+
'verify_peer' => false,
62+
'verify_peer_name' => false,
63+
'session_data' => $sessionData,
64+
'early_data' => 'ping-0rtt',
65+
]]);
66+
67+
$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
68+
30, STREAM_CLIENT_CONNECT, $ctx2);
69+
echo trim(fgets($client2)) . "\n";
70+
$meta2 = stream_get_meta_data($client2);
71+
echo "client early_data: " . ($meta2['crypto']['early_data'] ?? 'none') . "\n";
72+
fclose($client2);
73+
CODE;
74+
75+
include 'CertificateGenerator.inc';
76+
$certificateGenerator = new CertificateGenerator();
77+
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_basic', $certFile);
78+
79+
include 'ServerClientTestCase.inc';
80+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
81+
?>
82+
--CLEAN--
83+
<?php
84+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_basic.pem.tmp');
85+
?>
86+
--EXPECT--
87+
early=[] status=not_sent
88+
early=[ping-0rtt] status=accepted
89+
client early_data: accepted
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
TLS 1.3 early data with an invalid early_data option raises TypeError
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
9+
?>
10+
--FILE--
11+
<?php
12+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_invalid.pem.tmp';
13+
14+
$serverCode = <<<'CODE'
15+
$ctx = stream_context_create(['ssl' => ['local_cert' => '%s']]);
16+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
17+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
18+
phpt_notify_server_start($server);
19+
@stream_socket_accept($server, 3);
20+
CODE;
21+
$serverCode = sprintf($serverCode, $certFile);
22+
23+
$clientCode = <<<'CODE'
24+
$ctx = stream_context_create(['ssl' => [
25+
'verify_peer' => false,
26+
'verify_peer_name' => false,
27+
'early_data' => 123,
28+
]]);
29+
try {
30+
@stream_socket_client('tls://{{ ADDR }}', $errno, $errstr,
31+
5, STREAM_CLIENT_CONNECT, $ctx);
32+
echo "no exception\n";
33+
} catch (TypeError $e) {
34+
echo "caught: ", $e->getMessage(), "\n";
35+
}
36+
CODE;
37+
38+
include 'CertificateGenerator.inc';
39+
$certificateGenerator = new CertificateGenerator();
40+
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_invalid', $certFile);
41+
42+
include 'ServerClientTestCase.inc';
43+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
44+
?>
45+
--CLEAN--
46+
<?php
47+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_invalid.pem.tmp');
48+
?>
49+
--EXPECT--
50+
caught: early_data must be a string
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
--TEST--
2+
TLS 1.3 early data (0-RTT) larger than one read chunk
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
9+
?>
10+
--FILE--
11+
<?php
12+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_large.pem.tmp';
13+
14+
$serverCode = <<<'CODE'
15+
$early = '';
16+
$ctx = stream_context_create(['ssl' => [
17+
'local_cert' => '%s',
18+
'session_id_context' => 'early-data-large',
19+
'max_early_data' => 65536,
20+
'early_data_cb' => function ($stream, string $data) use (&$early) {
21+
$early .= $data;
22+
},
23+
]]);
24+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
25+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
26+
phpt_notify_server_start($server);
27+
for ($i = 0; $i < 2; $i++) {
28+
$early = '';
29+
$client = @stream_socket_accept($server, 30);
30+
if ($client) {
31+
$meta = stream_get_meta_data($client);
32+
$status = $meta['crypto']['early_data'] ?? 'none';
33+
fwrite($client, "len=" . strlen($early) . " ok=" .
34+
(int) ($early === str_repeat('A', 40000)) . " status=$status\n");
35+
fclose($client);
36+
}
37+
}
38+
CODE;
39+
$serverCode = sprintf($serverCode, $certFile);
40+
41+
$clientCode = <<<'CODE'
42+
$sessionData = null;
43+
$ctx = stream_context_create(['ssl' => [
44+
'verify_peer' => false,
45+
'verify_peer_name' => false,
46+
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
47+
$sessionData = $session;
48+
},
49+
]]);
50+
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
51+
30, STREAM_CLIENT_CONNECT, $ctx);
52+
echo trim(fgets($client1)) . "\n";
53+
fclose($client1);
54+
55+
$ctx2 = stream_context_create(['ssl' => [
56+
'verify_peer' => false,
57+
'verify_peer_name' => false,
58+
'session_data' => $sessionData,
59+
'early_data' => str_repeat('A', 40000),
60+
]]);
61+
$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
62+
30, STREAM_CLIENT_CONNECT, $ctx2);
63+
echo trim(fgets($client2)) . "\n";
64+
fclose($client2);
65+
CODE;
66+
67+
include 'CertificateGenerator.inc';
68+
$certificateGenerator = new CertificateGenerator();
69+
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_large', $certFile);
70+
71+
include 'ServerClientTestCase.inc';
72+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
73+
?>
74+
--CLEAN--
75+
<?php
76+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_large.pem.tmp');
77+
?>
78+
--EXPECT--
79+
len=0 ok=0 status=not_sent
80+
len=40000 ok=1 status=accepted
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
--TEST--
2+
TLS 1.3 early data option is ignored when the session does not allow 0-RTT
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
if (!defined('STREAM_CRYPTO_METHOD_TLSv1_3_SERVER')) die("skip TLS 1.3 not available");
9+
?>
10+
--FILE--
11+
<?php
12+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_unsupported.pem.tmp';
13+
14+
/* Server does NOT enable early data, so a resumed session must not carry a
15+
* 0-RTT limit and the client must fall back to a normal handshake. */
16+
$serverCode = <<<'CODE'
17+
$ctx = stream_context_create(['ssl' => [
18+
'local_cert' => '%s',
19+
'session_cache' => true,
20+
'session_id_context' => 'early-data-off',
21+
]]);
22+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
23+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
24+
phpt_notify_server_start($server);
25+
for ($i = 0; $i < 2; $i++) {
26+
$client = @stream_socket_accept($server, 30);
27+
if ($client) {
28+
fwrite($client, "hello\n");
29+
fclose($client);
30+
}
31+
}
32+
CODE;
33+
$serverCode = sprintf($serverCode, $certFile);
34+
35+
$clientCode = <<<'CODE'
36+
$sessionData = null;
37+
$ctx = stream_context_create(['ssl' => [
38+
'verify_peer' => false,
39+
'verify_peer_name' => false,
40+
'session_new_cb' => function ($stream, $session) use (&$sessionData) {
41+
$sessionData = $session;
42+
},
43+
]]);
44+
$client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
45+
30, STREAM_CLIENT_CONNECT, $ctx);
46+
echo trim(fgets($client1)) . "\n";
47+
fclose($client1);
48+
49+
$ctx2 = stream_context_create(['ssl' => [
50+
'verify_peer' => false,
51+
'verify_peer_name' => false,
52+
'session_data' => $sessionData,
53+
'early_data' => 'ping-0rtt',
54+
]]);
55+
$client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr,
56+
30, STREAM_CLIENT_CONNECT, $ctx2);
57+
echo trim(fgets($client2)) . "\n";
58+
$meta2 = stream_get_meta_data($client2);
59+
echo "reused: " . ($meta2['crypto']['session_reused'] ? 'yes' : 'no') . "\n";
60+
echo "early_data key present: " . (array_key_exists('early_data', $meta2['crypto']) ? 'yes' : 'no') . "\n";
61+
fclose($client2);
62+
CODE;
63+
64+
include 'CertificateGenerator.inc';
65+
$certificateGenerator = new CertificateGenerator();
66+
$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_unsupported', $certFile);
67+
68+
include 'ServerClientTestCase.inc';
69+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
70+
?>
71+
--CLEAN--
72+
<?php
73+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'tls_early_data_unsupported.pem.tmp');
74+
?>
75+
--EXPECT--
76+
hello
77+
hello
78+
reused: yes
79+
early_data key present: no

0 commit comments

Comments
 (0)