Skip to content

Commit c29b5be

Browse files
committed
Added client-side Server Name Indication (SNI) support in OpenSSL extension.
# # [DOC] # # New SSL context options : # # - SNI_enabled : Set to FALSE to disable SNI support (enabled by default) # - SNI_server_name : If not set, the server name will be guessed from the # stream URL (e.g. https://example.com/ will use example.com as hostname.), # else the given name will be used. # # SNI is to SSL/TLS what the Host header is for HTTP : it allows multiple # certificates on the same IP address. # # As for HTTP virtual hosts, this should be totaly transparent in most cases. # # Context options allows more control, e.g. : # # $context = stream_context_create(array( # 'ssl' => array('SNI_server_name' => 'foo.example.com'), # 'http' => array('header' => 'Host: foo.example.com'), # )); # file_get_contents('https://127.0.0.1/', false, $context); # # OpenSSL >= 0.9.8j supports SNI (by default since OpenSSL 0.9.8k).
1 parent dd60a6e commit c29b5be

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed

ext/openssl/openssl.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,11 @@ PHP_MINIT_FUNCTION(openssl)
10361036
REGISTER_LONG_CONSTANT("OPENSSL_KEYTYPE_EC", OPENSSL_KEYTYPE_EC, CONST_CS|CONST_PERSISTENT);
10371037
#endif
10381038

1039+
#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
1040+
/* SNI support included in OpenSSL >= 0.9.8j */
1041+
REGISTER_LONG_CONSTANT("OPENSSL_TLSEXT_SERVER_NAME", 1, CONST_CS|CONST_PERSISTENT);
1042+
#endif
1043+
10391044
/* Determine default SSL configuration file */
10401045
config_filename = getenv("OPENSSL_CONF");
10411046
if (config_filename == NULL) {

ext/openssl/tests/sni_001.phpt

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
--TEST--
2+
SNI 001
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('openssl')) die("skip openssl extension not available");
6+
if (!getenv('SNI_TESTS')) die("skip Set SNI_TESTS to enable this test (uses remote resources)");
7+
?>
8+
--FILE--
9+
<?php
10+
/* Server Name Indication (SNI) tests
11+
*
12+
* This test relies on https://sni.velox.ch/ and thus is disabled by default.
13+
*
14+
* sni.velox.ch uses 3 certificates :
15+
* - CN=alice.sni.velox.ch (sent in response to server_name = alice.sni.velox.ch or not set)
16+
* - CN=bob.sni.velox.ch (sent in response to server_name = bob.sni.velox.ch)
17+
* - CN=*.sni.velox.ch (sent in response to server_name = mallory.sni.velox.ch or *.sni.velox.ch or sni.velox.ch)
18+
*
19+
* The test sends requests to the server, sending different names, and checks which certificate
20+
* the server returned.
21+
*/
22+
23+
function context() {
24+
return stream_context_create(array(
25+
'ssl' => array(
26+
'capture_peer_cert' => true,
27+
),
28+
));
29+
}
30+
31+
function get_CN($context) {
32+
33+
$ary = stream_context_get_options($context);
34+
assert($ary);
35+
36+
$cert = $ary['ssl']['peer_certificate'];
37+
assert($cert);
38+
39+
$cert_ary = openssl_x509_parse($cert);
40+
return $cert_ary['subject']['CN'];
41+
}
42+
43+
function do_http_test($url, $context) {
44+
45+
$fh = fopen($url, 'r', false, $context);
46+
assert($fh);
47+
48+
var_dump(get_CN($context));
49+
}
50+
51+
function do_ssl_test($url, $context) {
52+
53+
$fh = stream_socket_client($url, $errno, $errstr,
54+
ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);
55+
assert($fh);
56+
57+
var_dump(get_CN($context));
58+
}
59+
60+
function do_enable_crypto_test($url, $context) {
61+
62+
$fh = stream_socket_client($url, $errno, $errstr,
63+
ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);
64+
assert($fh);
65+
66+
$r = stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
67+
assert($r);
68+
69+
var_dump(get_CN($context));
70+
}
71+
72+
/* Test https:// streams */
73+
74+
echo "-- auto host name (1) --\n";
75+
do_http_test('https://alice.sni.velox.ch/', context());
76+
77+
echo "-- auto host name (2) --\n";
78+
do_http_test('https://bob.sni.velox.ch/', context());
79+
80+
echo "-- auto host name (3) --\n";
81+
do_http_test('https://bob.sni.velox.ch./', context());
82+
83+
echo "-- user supplied server name --\n";
84+
85+
$context = context();
86+
stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
87+
stream_context_set_option($context, 'http', 'header', b'Host: bob.sni.velox.ch');
88+
do_http_test('https://alice.sni.velox.ch/', $context);
89+
90+
echo "-- sni disabled --\n";
91+
92+
$context = context();
93+
stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
94+
do_http_test('https://bob.sni.velox.ch/', $context);
95+
96+
/* Test ssl:// socket streams */
97+
98+
echo "-- raw SSL stream (1) --\n";
99+
do_ssl_test('ssl://bob.sni.velox.ch:443', context());
100+
101+
echo "-- raw SSL stream (2) --\n";
102+
do_ssl_test('ssl://mallory.sni.velox.ch:443', context());
103+
104+
echo "-- raw SSL stream with user supplied sni --\n";
105+
106+
$context = context();
107+
stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
108+
109+
do_ssl_test('ssl://mallory.sni.velox.ch:443', $context);
110+
111+
echo "-- raw SSL stream with sni disabled --\n";
112+
113+
$context = context();
114+
stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
115+
116+
do_ssl_test('ssl://mallory.sni.velox.ch:443', $context);
117+
118+
/* Test tcp:// socket streams with SSL enabled */
119+
120+
echo "-- stream_socket_enable_crypto (1) --\n";
121+
122+
do_enable_crypto_test('tcp://bob.sni.velox.ch:443', context());
123+
124+
echo "-- stream_socket_enable_crypto (2) --\n";
125+
126+
do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', context());
127+
128+
echo "-- stream_socket_enable_crypto with user supplied sni --\n";
129+
130+
$context = context();
131+
stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
132+
133+
do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
134+
135+
echo "-- stream_socket_enable_crypto with sni disabled --\n";
136+
137+
$context = context();
138+
stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
139+
140+
do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
141+
142+
echo "-- stream_socket_enable_crypto with long name --\n";
143+
144+
$context = context();
145+
stream_context_set_option($context, 'ssl', 'SNI_server_name', str_repeat('a.', 500) . '.sni.velox.ch');
146+
147+
do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
148+
149+
?>
150+
--EXPECTF--
151+
-- auto host name (1) --
152+
%unicode|string%(18) "alice.sni.velox.ch"
153+
-- auto host name (2) --
154+
%unicode|string%(16) "bob.sni.velox.ch"
155+
-- auto host name (3) --
156+
%unicode|string%(16) "bob.sni.velox.ch"
157+
-- user supplied server name --
158+
%unicode|string%(16) "bob.sni.velox.ch"
159+
-- sni disabled --
160+
%unicode|string%(18) "alice.sni.velox.ch"
161+
-- raw SSL stream (1) --
162+
%unicode|string%(16) "bob.sni.velox.ch"
163+
-- raw SSL stream (2) --
164+
%unicode|string%(14) "*.sni.velox.ch"
165+
-- raw SSL stream with user supplied sni --
166+
%unicode|string%(16) "bob.sni.velox.ch"
167+
-- raw SSL stream with sni disabled --
168+
%unicode|string%(18) "alice.sni.velox.ch"
169+
-- stream_socket_enable_crypto (1) --
170+
%unicode|string%(16) "bob.sni.velox.ch"
171+
-- stream_socket_enable_crypto (2) --
172+
%unicode|string%(14) "*.sni.velox.ch"
173+
-- stream_socket_enable_crypto with user supplied sni --
174+
%unicode|string%(16) "bob.sni.velox.ch"
175+
-- stream_socket_enable_crypto with sni disabled --
176+
%unicode|string%(18) "alice.sni.velox.ch"
177+
-- stream_socket_enable_crypto with long name --
178+
%unicode|string%(18) "alice.sni.velox.ch"

ext/openssl/xp_ssl.c

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include "php.h"
2222
#include "ext/standard/file.h"
23+
#include "ext/standard/url.h"
2324
#include "streams/php_streams_int.h"
2425
#include "ext/standard/php_smart_str.h"
2526
#include "php_network.h"
@@ -54,6 +55,7 @@ typedef struct _php_openssl_netstream_data_t {
5455
int is_client;
5556
int ssl_active;
5657
php_stream_xport_crypt_method_t method;
58+
char *sni;
5759
unsigned state_set:1;
5860
unsigned _spare:31;
5961
} php_openssl_netstream_data_t;
@@ -283,6 +285,9 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_
283285
}
284286
}
285287

288+
if (sslsock->sni) {
289+
pefree(sslsock->sni, php_stream_is_persistent(stream));
290+
}
286291
pefree(sslsock, php_stream_is_persistent(stream));
287292

288293
return 0;
@@ -393,6 +398,12 @@ static inline int php_openssl_enable_crypto(php_stream *stream,
393398
float timeout = sslsock->connect_timeout.tv_sec + sslsock->connect_timeout.tv_usec / 1000000;
394399
int blocked = sslsock->s.is_blocked;
395400

401+
#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
402+
if (sslsock->is_client && sslsock->sni) {
403+
SSL_set_tlsext_host_name(sslsock->ssl_handle, sslsock->sni);
404+
}
405+
#endif
406+
396407
if (!sslsock->state_set) {
397408
if (sslsock->is_client) {
398409
SSL_set_connect_state(sslsock->ssl_handle);
@@ -758,6 +769,52 @@ php_stream_ops php_openssl_socket_ops = {
758769
php_openssl_sockop_set_option,
759770
};
760771

772+
static char * get_sni(php_stream_context *ctx, char *resourcename, long resourcenamelen, int is_persistent) {
773+
774+
php_url *url;
775+
776+
if (ctx) {
777+
zval **val = NULL;
778+
779+
if (php_stream_context_get_option(ctx, "ssl", "SNI_enabled", &val) == SUCCESS && !zend_is_true(*val)) {
780+
return NULL;
781+
}
782+
if (php_stream_context_get_option(ctx, "ssl", "SNI_server_name", &val) == SUCCESS) {
783+
convert_to_string_with_converter_ex(val, UG(utf8_conv));
784+
return pestrdup(Z_STRVAL_PP(val), is_persistent);
785+
}
786+
}
787+
788+
if (!resourcename) {
789+
return NULL;
790+
}
791+
792+
url = php_url_parse_ex(resourcename, resourcenamelen);
793+
if (!url) {
794+
return NULL;
795+
}
796+
797+
if (url->host) {
798+
const char * host = url->host;
799+
char * sni = NULL;
800+
size_t len = strlen(host);
801+
802+
/* skip trailing dots */
803+
while (len && host[len-1] == '.') {
804+
--len;
805+
}
806+
807+
if (len) {
808+
sni = pestrndup(host, len, is_persistent);
809+
}
810+
811+
php_url_free(url);
812+
return sni;
813+
}
814+
815+
php_url_free(url);
816+
return NULL;
817+
}
761818

762819
php_stream *php_openssl_ssl_socket_factory(const char *proto, long protolen,
763820
char *resourcename, long resourcenamelen,
@@ -794,6 +851,8 @@ php_stream *php_openssl_ssl_socket_factory(const char *proto, long protolen,
794851
return NULL;
795852
}
796853

854+
sslsock->sni = get_sni(context, resourcename, resourcenamelen, !!persistent_id);
855+
797856
if (strncmp(proto, "ssl", protolen) == 0) {
798857
sslsock->enable_on_connect = 1;
799858
sslsock->method = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;

0 commit comments

Comments
 (0)