]> granicus.if.org Git - php/commitdiff
Added client-side Server Name Indication (SNI) support in OpenSSL extension.
authorArnaud Le Blanc <lbarnaud@php.net>
Wed, 21 Oct 2009 16:10:19 +0000 (16:10 +0000)
committerArnaud Le Blanc <lbarnaud@php.net>
Wed, 21 Oct 2009 16:10:19 +0000 (16:10 +0000)
#
# [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).

ext/openssl/openssl.c
ext/openssl/tests/sni_001.phpt [new file with mode: 0644]
ext/openssl/xp_ssl.c

index 764b6df594723794100bc91c58c63e6df313acc6..b1306a99e9216286b65370fbdc9db055b70ecad1 100644 (file)
@@ -1036,6 +1036,11 @@ PHP_MINIT_FUNCTION(openssl)
        REGISTER_LONG_CONSTANT("OPENSSL_KEYTYPE_EC", OPENSSL_KEYTYPE_EC, CONST_CS|CONST_PERSISTENT);
 #endif
 
+#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
+       /* SNI support included in OpenSSL >= 0.9.8j */
+       REGISTER_LONG_CONSTANT("OPENSSL_TLSEXT_SERVER_NAME", 1, CONST_CS|CONST_PERSISTENT);
+#endif
+
        /* Determine default SSL configuration file */
        config_filename = getenv("OPENSSL_CONF");
        if (config_filename == NULL) {
diff --git a/ext/openssl/tests/sni_001.phpt b/ext/openssl/tests/sni_001.phpt
new file mode 100644 (file)
index 0000000..3d7798c
--- /dev/null
@@ -0,0 +1,178 @@
+--TEST--
+SNI 001
+--SKIPIF--
+<?php
+       if (!extension_loaded('openssl')) die("skip openssl extension not available");
+       if (!getenv('SNI_TESTS')) die("skip Set SNI_TESTS to enable this test (uses remote resources)");
+?>
+--FILE--
+<?php
+/* Server Name Indication (SNI) tests
+ * 
+ * This test relies on https://sni.velox.ch/ and thus is disabled by default.
+ *
+ * sni.velox.ch uses 3 certificates :
+ * - CN=alice.sni.velox.ch (sent in response to server_name = alice.sni.velox.ch or not set)
+ * - CN=bob.sni.velox.ch (sent in response to server_name = bob.sni.velox.ch)
+ * - CN=*.sni.velox.ch (sent in response to server_name = mallory.sni.velox.ch or *.sni.velox.ch or sni.velox.ch)
+ *
+ * The test sends requests to the server, sending different names, and checks which certificate
+ * the server returned.
+ */
+
+function context() {
+       return stream_context_create(array(
+               'ssl' => array(
+                       'capture_peer_cert' => true,
+               ),
+       ));
+}
+
+function get_CN($context) {
+
+       $ary = stream_context_get_options($context);
+       assert($ary);
+
+       $cert = $ary['ssl']['peer_certificate'];
+       assert($cert);
+
+       $cert_ary = openssl_x509_parse($cert);
+       return $cert_ary['subject']['CN'];
+}
+
+function do_http_test($url, $context) {
+
+       $fh = fopen($url, 'r', false, $context);
+       assert($fh);
+
+       var_dump(get_CN($context));
+}
+
+function do_ssl_test($url, $context) {
+
+       $fh = stream_socket_client($url, $errno, $errstr, 
+                       ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);
+       assert($fh);
+
+       var_dump(get_CN($context));
+}
+
+function do_enable_crypto_test($url, $context) {
+
+       $fh = stream_socket_client($url, $errno, $errstr,
+               ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);
+       assert($fh);
+
+       $r = stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+       assert($r);
+
+       var_dump(get_CN($context));
+}
+
+/* Test https:// streams */
+
+echo "-- auto host name (1) --\n";
+do_http_test('https://alice.sni.velox.ch/', context());
+
+echo "-- auto host name (2) --\n";
+do_http_test('https://bob.sni.velox.ch/', context());
+
+echo "-- auto host name (3) --\n";
+do_http_test('https://bob.sni.velox.ch./', context());
+
+echo "-- user supplied server name --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
+stream_context_set_option($context, 'http', 'header', b'Host: bob.sni.velox.ch');
+do_http_test('https://alice.sni.velox.ch/', $context);
+
+echo "-- sni disabled --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
+do_http_test('https://bob.sni.velox.ch/', $context);
+
+/* Test ssl:// socket streams */
+
+echo "-- raw SSL stream (1) --\n";
+do_ssl_test('ssl://bob.sni.velox.ch:443', context());
+
+echo "-- raw SSL stream (2) --\n";
+do_ssl_test('ssl://mallory.sni.velox.ch:443', context());
+
+echo "-- raw SSL stream with user supplied sni --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
+
+do_ssl_test('ssl://mallory.sni.velox.ch:443', $context);
+
+echo "-- raw SSL stream with sni disabled --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
+
+do_ssl_test('ssl://mallory.sni.velox.ch:443', $context);
+
+/* Test tcp:// socket streams with SSL enabled */
+
+echo "-- stream_socket_enable_crypto (1) --\n";
+
+do_enable_crypto_test('tcp://bob.sni.velox.ch:443', context());
+
+echo "-- stream_socket_enable_crypto (2) --\n";
+
+do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', context());
+
+echo "-- stream_socket_enable_crypto with user supplied sni --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch');
+
+do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
+
+echo "-- stream_socket_enable_crypto with sni disabled --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_enabled', false);
+
+do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
+
+echo "-- stream_socket_enable_crypto with long name --\n";
+
+$context = context();
+stream_context_set_option($context, 'ssl', 'SNI_server_name', str_repeat('a.', 500) . '.sni.velox.ch');
+
+do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context);
+
+?>
+--EXPECTF--
+-- auto host name (1) --
+%unicode|string%(18) "alice.sni.velox.ch"
+-- auto host name (2) --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- auto host name (3) --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- user supplied server name --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- sni disabled --
+%unicode|string%(18) "alice.sni.velox.ch"
+-- raw SSL stream (1) --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- raw SSL stream (2) --
+%unicode|string%(14) "*.sni.velox.ch"
+-- raw SSL stream with user supplied sni --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- raw SSL stream with sni disabled --
+%unicode|string%(18) "alice.sni.velox.ch"
+-- stream_socket_enable_crypto (1) --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- stream_socket_enable_crypto (2) --
+%unicode|string%(14) "*.sni.velox.ch"
+-- stream_socket_enable_crypto with user supplied sni --
+%unicode|string%(16) "bob.sni.velox.ch"
+-- stream_socket_enable_crypto with sni disabled --
+%unicode|string%(18) "alice.sni.velox.ch"
+-- stream_socket_enable_crypto with long name --
+%unicode|string%(18) "alice.sni.velox.ch"
index 3d122d6e2da8fb8da9d660dbcd0ff77b8f0f8a32..1a49aca4d9cc5e547c7e0a15b0c5d572f6d3fc44 100644 (file)
@@ -20,6 +20,7 @@
 
 #include "php.h"
 #include "ext/standard/file.h"
+#include "ext/standard/url.h"
 #include "streams/php_streams_int.h"
 #include "ext/standard/php_smart_str.h"
 #include "php_network.h"
@@ -54,6 +55,7 @@ typedef struct _php_openssl_netstream_data_t {
        int is_client;
        int ssl_active;
        php_stream_xport_crypt_method_t method;
+       char *sni;
        unsigned state_set:1;
        unsigned _spare:31;
 } php_openssl_netstream_data_t;
@@ -283,6 +285,9 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_
                }
        }
 
+       if (sslsock->sni) {
+               pefree(sslsock->sni, php_stream_is_persistent(stream));
+       }
        pefree(sslsock, php_stream_is_persistent(stream));
        
        return 0;
@@ -393,6 +398,12 @@ static inline int php_openssl_enable_crypto(php_stream *stream,
                float timeout = sslsock->connect_timeout.tv_sec + sslsock->connect_timeout.tv_usec / 1000000;
                int blocked = sslsock->s.is_blocked;
 
+#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
+               if (sslsock->is_client && sslsock->sni) {
+                       SSL_set_tlsext_host_name(sslsock->ssl_handle, sslsock->sni);
+               }
+#endif
+
                if (!sslsock->state_set) {
                        if (sslsock->is_client) {
                                SSL_set_connect_state(sslsock->ssl_handle);
@@ -758,6 +769,52 @@ php_stream_ops php_openssl_socket_ops = {
        php_openssl_sockop_set_option,
 };
 
+static char * get_sni(php_stream_context *ctx, char *resourcename, long resourcenamelen, int is_persistent) {
+
+       php_url *url;
+
+       if (ctx) {
+               zval **val = NULL;
+
+               if (php_stream_context_get_option(ctx, "ssl", "SNI_enabled", &val) == SUCCESS && !zend_is_true(*val)) {
+                       return NULL;
+               }
+               if (php_stream_context_get_option(ctx, "ssl", "SNI_server_name", &val) == SUCCESS) {
+                       convert_to_string_with_converter_ex(val, UG(utf8_conv));
+                       return pestrdup(Z_STRVAL_PP(val), is_persistent);
+               }
+       }
+
+       if (!resourcename) {
+               return NULL;
+       }
+
+       url = php_url_parse_ex(resourcename, resourcenamelen);
+       if (!url) {
+               return NULL;
+       }
+
+       if (url->host) {
+               const char * host = url->host;
+               char * sni = NULL;
+               size_t len = strlen(host);
+
+               /* skip trailing dots */
+               while (len && host[len-1] == '.') {
+                       --len;
+               }
+
+               if (len) {
+                       sni = pestrndup(host, len, is_persistent);
+               }
+
+               php_url_free(url);
+               return sni;
+       }
+
+       php_url_free(url);
+       return NULL;
+}
 
 php_stream *php_openssl_ssl_socket_factory(const char *proto, long protolen,
                char *resourcename, long resourcenamelen,
@@ -794,6 +851,8 @@ php_stream *php_openssl_ssl_socket_factory(const char *proto, long protolen,
                return NULL;
        }
 
+       sslsock->sni = get_sni(context, resourcename, resourcenamelen, !!persistent_id);
+       
        if (strncmp(proto, "ssl", protolen) == 0) {
                sslsock->enable_on_connect = 1;
                sslsock->method = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;