Skip to content

Commit 6cdf46f

Browse files
committed
openssl: Introduce TLS PSK support
Add stream context options psk_client_cb and psk_server_cb that let clients and servers negotiate pre-shared key authentication on both TLS 1.2 and TLS 1.3. Callbacks return an Openssl\Psk instance carrying the key and, on clients, the identity, or null to refuse PSK. A new final Openssl\Psk class is added for that purpose, with readonly $psk and $identity properties and MAX_PSK_LEN / MAX_IDENTITY_LEN constants. Closes GH-22057
1 parent d8a5aec commit 6cdf46f

16 files changed

Lines changed: 1039 additions & 5 deletions

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ PHP NEWS
107107
openssl_x509_parse() output). (StephenWall)
108108
. Added TLS session resumption support for streams with new context options
109109
and Openssl\Session class. (Jakub Zelenka)
110+
. Added TLS external PSK support for streams with new context options and
111+
Openssl\Psk class. (Jakub Zelenka)
110112

111113
- PCNTL:
112114
. pcntl_exec() now throws a ValueError if the $args array is not a list

UPGRADING

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ PHP 8.6 UPGRADE NOTES
196196
requests, implementing custom server-side session storage, and controlling
197197
session cache behavior.
198198
RFC: https://wiki.php.net/rfc/tls_session_resumption
199+
. Added TLS external PSK support for streams with new strem context options:
200+
psk_client_cb and psk_server_cb. This allows setting and receiving PSK.
199201

200202
- Phar:
201203
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
@@ -317,6 +319,7 @@ PHP 8.6 UPGRADE NOTES
317319
. Openssl\OpensslException
318320
. Openssl\Session
319321
RFC: https://wiki.php.net/rfc/tls_session_resumption
322+
. Openssl\Psk
320323

321324
- Standard:
322325
. enum SortDirection

ext/openssl/openssl.c

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,76 @@ static void php_openssl_pkey_free_obj(zend_object *object)
169169
zend_object_std_dtor(&key_object->std);
170170
}
171171

172-
/* OpenSSLSession class */
172+
/* Openssl\Psk class */
173+
174+
zend_class_entry *php_openssl_psk_ce;
175+
176+
static zend_object_handlers php_openssl_psk_object_handlers;
177+
178+
bool php_openssl_is_psk_ce(zval *val)
179+
{
180+
return Z_TYPE_P(val) == IS_OBJECT && Z_OBJCE_P(val) == php_openssl_psk_ce;
181+
}
182+
183+
zend_string *php_openssl_psk_get_psk(zval *psk_zv)
184+
{
185+
zval rv;
186+
zval *prop = zend_read_property(php_openssl_psk_ce, Z_OBJ_P(psk_zv), ZEND_STRL("psk"), 0, &rv);
187+
if (UNEXPECTED(Z_TYPE_P(prop) != IS_STRING)) {
188+
return NULL;
189+
}
190+
return Z_STR_P(prop);
191+
}
192+
193+
zend_string *php_openssl_psk_get_identity(zval *psk_zv)
194+
{
195+
zval rv;
196+
zval *prop = zend_read_property(php_openssl_psk_ce, Z_OBJ_P(psk_zv),
197+
ZEND_STRL("identity"), 0, &rv);
198+
if (Z_TYPE_P(prop) == IS_NULL) {
199+
return NULL;
200+
}
201+
if (UNEXPECTED(Z_TYPE_P(prop) != IS_STRING)) {
202+
return NULL;
203+
}
204+
return Z_STR_P(prop);
205+
}
206+
207+
PHP_METHOD(Openssl_Psk, __construct)
208+
{
209+
zend_string *psk;
210+
zend_string *identity = NULL;
211+
212+
ZEND_PARSE_PARAMETERS_START(1, 2)
213+
Z_PARAM_STR(psk)
214+
Z_PARAM_OPTIONAL
215+
Z_PARAM_STR_OR_NULL(identity)
216+
ZEND_PARSE_PARAMETERS_END();
217+
218+
if (ZSTR_LEN(psk) == 0) {
219+
zend_argument_value_error(1, "must not be empty");
220+
RETURN_THROWS();
221+
}
222+
if (ZSTR_LEN(psk) > PHP_OPENSSL_PSK_MAX_PSK_LEN) {
223+
zend_argument_value_error(1, "must not exceed %d bytes", PHP_OPENSSL_PSK_MAX_PSK_LEN);
224+
RETURN_THROWS();
225+
}
226+
if (identity != NULL && ZSTR_LEN(identity) > PHP_OPENSSL_PSK_MAX_IDENTITY_LEN) {
227+
zend_argument_value_error(2, "must not exceed %d bytes", PHP_OPENSSL_PSK_MAX_IDENTITY_LEN);
228+
RETURN_THROWS();
229+
}
230+
231+
zend_update_property_str(php_openssl_psk_ce, Z_OBJ_P(ZEND_THIS), ZEND_STRL("psk"), psk);
232+
233+
if (identity != NULL) {
234+
zend_update_property_str(php_openssl_psk_ce, Z_OBJ_P(ZEND_THIS),
235+
ZEND_STRL("identity"), identity);
236+
} else {
237+
zend_update_property_null(php_openssl_psk_ce, Z_OBJ_P(ZEND_THIS), ZEND_STRL("identity"));
238+
}
239+
}
240+
241+
/* Openssl\Session class */
173242

174243
zend_class_entry *php_openssl_session_ce;
175244

@@ -716,6 +785,11 @@ PHP_MINIT_FUNCTION(openssl)
716785
php_openssl_pkey_object_handlers.clone_obj = NULL;
717786
php_openssl_pkey_object_handlers.compare = zend_objects_not_comparable;
718787

788+
php_openssl_psk_ce = register_class_Openssl_Psk();
789+
php_openssl_psk_ce->default_object_handlers = &php_openssl_psk_object_handlers;
790+
791+
memcpy(&php_openssl_psk_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
792+
719793
php_openssl_session_ce = register_class_Openssl_Session();
720794
php_openssl_session_ce->create_object = php_openssl_session_create_object;
721795
php_openssl_session_ce->default_object_handlers = &php_openssl_session_object_handlers;

ext/openssl/openssl.stub.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ class OpensslException extends Exception
88
{
99
}
1010

11+
/**
12+
* @strict-properties
13+
*/
14+
final class Psk
15+
{
16+
/**
17+
* @cvalue PHP_OPENSSL_PSK_MAX_PSK_LEN
18+
*/
19+
public const int MAX_PSK_LEN = UNKNOWN;
20+
21+
/**
22+
* @cvalue PHP_OPENSSL_PSK_MAX_IDENTITY_LEN
23+
*/
24+
public const int MAX_IDENTITY_LEN = UNKNOWN;
25+
26+
public readonly string $psk;
27+
public readonly ?string $identity;
28+
29+
public function __construct(string $psk, ?string $identity = null) {}
30+
}
31+
1132
/**
1233
* @strict-properties
1334
*/

ext/openssl/openssl_arginfo.h

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/openssl/php_openssl.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,19 @@ static inline php_openssl_pkey_object *php_openssl_pkey_from_obj(zend_object *ob
203203
bool php_openssl_is_pkey_ce(zval *val);
204204
void php_openssl_pkey_object_init(zval *zv, EVP_PKEY *pkey, bool is_private);
205205

206-
/* OpenSSLSession class */
206+
/* Openssl\Psk class */
207+
208+
/* Matches OpenSSL's PSK_MAX_PSK_LEN and PSK_MAX_IDENTITY_LEN */
209+
#define PHP_OPENSSL_PSK_MAX_PSK_LEN 256
210+
#define PHP_OPENSSL_PSK_MAX_IDENTITY_LEN 128
211+
212+
extern zend_class_entry *php_openssl_psk_ce;
213+
214+
bool php_openssl_is_psk_ce(zval *val);
215+
zend_string *php_openssl_psk_get_psk(zval *psk_zv);
216+
zend_string *php_openssl_psk_get_identity(zval *psk_zv);
217+
218+
/* Openssl\Session class */
207219

208220
#include <openssl/ssl.h>
209221

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
TLS PSK callback option must be a valid callable
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
?>
9+
--FILE--
10+
<?php
11+
$serverCode = <<<'CODE'
12+
$serverCtx = stream_context_create(['ssl' => [
13+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER,
14+
'ciphers' => 'PSK',
15+
'psk_server_cb' => function ($stream, string $identity): ?Openssl\Psk {
16+
return new Openssl\Psk("k", "id");
17+
},
18+
]]);
19+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
20+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx);
21+
phpt_notify_server_start($server);
22+
@stream_socket_accept($server, 3);
23+
CODE;
24+
25+
$clientCode = <<<'CODE'
26+
$clientCtx = stream_context_create(['ssl' => [
27+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
28+
'ciphers' => 'PSK',
29+
'verify_peer' => false,
30+
'psk_client_cb' => 'php_openssl_no_such_function',
31+
]]);
32+
try {
33+
@stream_socket_client('tls://{{ ADDR }}', $errno, $errstr,
34+
5, STREAM_CLIENT_CONNECT, $clientCtx);
35+
echo "no exception\n";
36+
} catch (TypeError $e) {
37+
echo "caught: ", $e->getMessage(), "\n";
38+
}
39+
CODE;
40+
41+
include __DIR__ . '/ServerClientTestCase.inc';
42+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
43+
?>
44+
--EXPECTF--
45+
caught: psk_client_cb must be a valid callback, %s
46+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
TLS PSK client callback returning wrong type raises TypeError
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
?>
9+
--FILE--
10+
<?php
11+
$serverCode = <<<'CODE'
12+
$serverCtx = stream_context_create(['ssl' => [
13+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER,
14+
'ciphers' => 'PSK',
15+
'psk_server_cb' => function ($stream, string $identity): ?Openssl\Psk {
16+
return new Openssl\Psk("k", "id");
17+
},
18+
]]);
19+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
20+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx);
21+
phpt_notify_server_start($server);
22+
@stream_socket_accept($server, 3);
23+
CODE;
24+
25+
$clientCode = <<<'CODE'
26+
$clientCtx = stream_context_create(['ssl' => [
27+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
28+
'ciphers' => 'PSK',
29+
'verify_peer' => false,
30+
'psk_client_cb' => function ($stream) {
31+
/* Returning a string instead of Openssl\Psk|null. */
32+
return "not a Psk object";
33+
},
34+
]]);
35+
try {
36+
@stream_socket_client('tls://{{ ADDR }}', $errno, $errstr,
37+
5, STREAM_CLIENT_CONNECT, $clientCtx);
38+
echo "no exception\n";
39+
} catch (TypeError $e) {
40+
echo "caught: ", $e->getMessage(), "\n";
41+
}
42+
CODE;
43+
44+
include __DIR__ . '/ServerClientTestCase.inc';
45+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
46+
?>
47+
--EXPECT--
48+
caught: PSK callback must return Openssl\Psk or null
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
TLS 1.2 PSK: client callback returning null aborts handshake (no shared cipher)
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
?>
9+
--FILE--
10+
<?php
11+
$serverCode = <<<'CODE'
12+
$serverCtx = stream_context_create(['ssl' => [
13+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER,
14+
'ciphers' => 'PSK',
15+
'psk_server_cb' => function ($stream, string $identity): ?Openssl\Psk {
16+
return new Openssl\Psk("doesnotmatter", null);
17+
},
18+
]]);
19+
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr,
20+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx);
21+
phpt_notify_server_start($server);
22+
@stream_socket_accept($server, 3);
23+
CODE;
24+
25+
$clientCode = <<<'CODE'
26+
$clientCtx = stream_context_create(['ssl' => [
27+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
28+
'ciphers' => 'PSK',
29+
'verify_peer' => false,
30+
'psk_client_cb' => function ($stream): ?Openssl\Psk {
31+
/* Reject: no PSK to use, no other ciphers offered. */
32+
return null;
33+
},
34+
]]);
35+
$client = @stream_socket_client('tls://{{ ADDR }}', $errno, $errstr,
36+
5, STREAM_CLIENT_CONNECT, $clientCtx);
37+
var_dump($client);
38+
CODE;
39+
40+
include __DIR__ . '/ServerClientTestCase.inc';
41+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
42+
?>
43+
--EXPECT--
44+
bool(false)
45+

0 commit comments

Comments
 (0)