]> granicus.if.org Git - php/commitdiff
Fix the lack of SSL certificate verification support for ssl:// sockets and
authorWez Furlong <wez@php.net>
Sat, 26 Apr 2003 21:34:49 +0000 (21:34 +0000)
committerWez Furlong <wez@php.net>
Sat, 26 Apr 2003 21:34:49 +0000 (21:34 +0000)
https:// streams.

This code is essential for people writing secure applications in order to avoid
man-in-the-middle attacks, and is thus regarded as a bug fix.

It is, however, optional; you need to explicitly turn on the verification
functionality, as it depends on you to specify your trusted certificate chain.

This sample demonstrates a secured https:// request, making use of the CA
bundle provided by curl:

<?php
$ctx = stream_context_create();
// Turn on verification
stream_context_set_option($ctx, "ssl", "verify_peer", true);
// Set the CA bundle (trusted certificate chain)
stream_context_set_option($ctx, "ssl", "cafile",
"/usr/local/share/curl/curl-ca-bundle.crt");
$fp = fopen("https://www.zend.com", "rb", false, $ctx);
?>

This sample demonstrates how to roll your own https:// request, and specify a
certificate to use for authentication; the local_cert and passphrase options
will also work for fopen().

<?php
$ctx = stream_context_create();
stream_context_set_option($ctx, "ssl", "verify_peer", true);
stream_context_set_option($ctx, "ssl", "cafile",
"/usr/local/share/curl/curl-ca-bundle.crt");

// set local cert.  it MUST be a PEM encoded file containing the certificate
// AND your private key.  It can also contain the certificate chain of issuers.
stream_context_set_option($ctx, "ssl", "local_cert", "/path/to/my/cert.pem");
stream_context_set_option($ctx, "ssl", "passphrase", "secret!");

// Set the common name that we are expecting; PHP will perform limited wildcard
// matching.  If the CN does not match this, the connection attempt will fail.
// The value to specify will always be the same as the Host: header you specify.
stream_context_set_option($ctx, "ssl", "CN_match", "secure.sample.domain");

$ssl = fsockopen("ssl://secure.sample.domain", 443, $errno, $errstr, 10, $ctx);

if ($ssl) {
fwrite($ssl, "GET / HTTP/1.0\r\nHost: secure.sample.domain\r\n\r\n");
fpassthru($ssl);
}

?>

ext/openssl/openssl.c
ext/openssl/php_openssl.h
ext/standard/file.c
ext/standard/file.h
ext/standard/fsock.c
ext/standard/http_fopen_wrapper.c
main/network.c

index cabe01fa20e9465888b463bcf060fd80bc52827e..974ff7c3610fc5ea70477e78e6f1d732a971a1a5 100644 (file)
@@ -135,6 +135,8 @@ static int le_key;
 static int le_x509;
 static int le_csr;
 
+static int ssl_stream_data_index;
+
 /* {{{ resource destructors */
 static void php_pkey_free(zend_rsrc_list_entry *rsrc TSRMLS_DC)
 {
@@ -539,9 +541,14 @@ PHP_MINIT_FUNCTION(openssl)
        OpenSSL_add_all_algorithms();
 
        ERR_load_ERR_strings();
+       ERR_load_SSL_strings();
        ERR_load_crypto_strings();
        ERR_load_EVP_strings();
 
+       /* register a resource id number with openSSL so that we can map SSL -> stream structures in
+        * openSSL callbacks */
+       ssl_stream_data_index = SSL_get_ex_new_index(0, "PHP stream index", NULL, NULL, NULL);
+       
        /* purposes for cert purpose checking */
        REGISTER_LONG_CONSTANT("X509_PURPOSE_SSL_CLIENT", X509_PURPOSE_SSL_CLIENT, CONST_CS|CONST_PERSISTENT);
        REGISTER_LONG_CONSTANT("X509_PURPOSE_SSL_SERVER", X509_PURPOSE_SSL_SERVER, CONST_CS|CONST_PERSISTENT);
@@ -2960,6 +2967,226 @@ PHP_FUNCTION(openssl_open)
 }
 /* }}} */
 
+/* SSL verification functions */
+
+#define GET_VER_OPT(name)                              SUCCESS == php_stream_context_get_option(stream->context, "ssl", name, &val)
+#define GET_VER_OPT_STRING(name, str)  if (GET_VER_OPT(name)) { convert_to_string_ex(val); str = Z_STRVAL_PP(val); }
+
+static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
+{
+       php_stream *stream;
+       SSL *ssl;
+       X509 *err_cert;
+       int err, depth, ret;
+       zval **val;
+       TSRMLS_FETCH();
+
+       ret = preverify_ok;
+       
+       /* determine the status for the current cert */
+       err_cert = X509_STORE_CTX_get_current_cert(ctx);
+       err = X509_STORE_CTX_get_error(ctx);
+       depth = X509_STORE_CTX_get_error_depth(ctx);
+
+       /* conjure the stream & context to use */
+       ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
+       stream = (php_stream*)SSL_get_ex_data(ssl, ssl_stream_data_index);
+
+       /* if allow_self_signed is set, make sure that verification succeeds */
+       if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && GET_VER_OPT("allow_self_signed") && zval_is_true(*val)) {
+               ret = 1;
+       }
+       
+       /* check the depth */
+       if (GET_VER_OPT("verify_depth")) {
+               convert_to_long_ex(val);
+
+               if (depth > Z_LVAL_PP(val)) {
+                       ret = 0;
+                       X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_CHAIN_TOO_LONG);
+               }
+       }
+       
+       return ret;
+       
+}
+
+PHPAPI int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC)
+{
+       zval **val = NULL;
+       char *cnmatch = NULL;
+       X509_NAME *name;
+       char buf[1024];
+       int err;
+       
+       /* verification is turned off */
+       if (!(GET_VER_OPT("verify_peer") && zval_is_true(*val))) {
+               return SUCCESS;
+       }
+
+       if (peer == NULL) {
+               php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not get peer certificate");
+               return FAILURE;
+       }
+
+       err = SSL_get_verify_result(ssl);
+       switch (err) {
+               case X509_V_OK:
+                       /* fine */
+                       break;
+               case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
+                       if (GET_VER_OPT("allow_self_signed") && zval_is_true(*val)) {
+                               /* allowed */
+                               break;
+                       }
+                       /* not allowed, so fall through */
+               default:
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not verify peer: code:%d %s", err, X509_verify_cert_error_string(err));
+                       return FAILURE;
+       }
+
+       /* if the cert passed the usual checks, apply our own local policies now */
+       
+       name = X509_get_subject_name(peer);
+
+       /* Does the common name match ? (used primarily for https://) */
+       GET_VER_OPT_STRING("CN_match", cnmatch);
+       if (cnmatch) {
+               int match = 0;
+               
+               X509_NAME_get_text_by_NID(name, NID_commonName, buf, sizeof(buf));
+
+               match = strcmp(cnmatch, buf) == 0;
+
+               if (!match && strlen(buf) > 3 && buf[0] == '*' && buf[1] == '.') {
+                       /* Try wildcard */
+
+                       if (strchr(buf+2, '.')) {
+                               char *tmp = strstr(cnmatch, buf+1);
+
+                               match = tmp && strcmp(tmp, buf+2) && tmp == strchr(cnmatch, '.');
+                       }
+               }
+
+               if (!match) {
+                       /* didn't match */
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING,
+                                       "Peer certificate CN=`%s' did not match expected CN=`%s'",
+                                       buf, cnmatch);
+
+                       return FAILURE;
+               }
+       }
+
+       return SUCCESS;
+}
+
+static int passwd_callback(char *buf, int num, int verify, void *data)
+{
+       php_stream *stream = (php_stream *)data;
+       zval **val = NULL;
+       char *passphrase = NULL;
+       /* TODO: could expand this to make a callback into PHP user-space */
+
+       GET_VER_OPT_STRING("passphrase", passphrase);
+
+       if (passphrase) {
+               if (Z_STRLEN_PP(val) < num - 1) {
+                       memcpy(buf, Z_STRVAL_PP(val), Z_STRLEN_PP(val)+1);
+                       return Z_STRLEN_PP(val);
+               }
+       }
+       return 0;
+}
+
+PHPAPI SSL *php_SSL_new_from_context(SSL_CTX *ctx, php_stream *stream TSRMLS_DC)
+{
+       zval **val = NULL;
+       char *cafile = NULL;
+       char *capath = NULL;
+       char *certfile = NULL;
+       int ok = 1;
+       
+       
+       /* look at context options in the stream and set appropriate verification flags */
+       if (GET_VER_OPT("verify_peer") && zval_is_true(*val)) {
+               
+               /* turn on verification callback */
+               SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
+
+               /* CA stuff */
+               GET_VER_OPT_STRING("cafile", cafile);
+               GET_VER_OPT_STRING("capath", capath);
+
+               if (cafile || capath) {
+                       if (!SSL_CTX_load_verify_locations(ctx, cafile, capath)) {
+                               php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set verify locations `%s' `%s'\n", cafile, capath);
+                               return NULL;
+                       }
+               }
+
+               if (GET_VER_OPT("verify_depth")) {
+                       convert_to_long_ex(val);
+                       SSL_CTX_set_verify_depth(ctx, Z_LVAL_PP(val));
+               }
+
+       } else {
+               SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
+       }
+
+       /* callback for the passphrase (for localcert) */
+       if (GET_VER_OPT("passphrase")) {
+               SSL_CTX_set_default_passwd_cb_userdata(ctx, stream);
+               SSL_CTX_set_default_passwd_cb(ctx, passwd_callback);
+       }
+       
+       GET_VER_OPT_STRING("local_cert", certfile);
+       if (certfile) {
+               X509 *cert = NULL;
+               EVP_PKEY *key = NULL;
+               SSL *tmpssl;
+
+               /* a certificate to use for authentication */
+               if (SSL_CTX_use_certificate_chain_file(ctx, certfile) != 1) {
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set local cert chain file `%s'; Check that your cafile/capath settings include details of your certificate and its issuer", certfile);
+                       return NULL;
+               }
+
+               if (SSL_CTX_use_PrivateKey_file(ctx, certfile, SSL_FILETYPE_PEM) != 1) {
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set private key file `%s'", certfile);
+                       return NULL;
+               }
+
+               tmpssl = SSL_new(ctx);
+               cert = SSL_get_certificate(tmpssl);
+
+               if (cert) {
+                       key = X509_get_pubkey(cert);
+                       EVP_PKEY_copy_parameters(key, SSL_get_privatekey(tmpssl));
+                       EVP_PKEY_free(key);
+               }
+               SSL_free(tmpssl);
+
+               if (!SSL_CTX_check_private_key(ctx)) {
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING, "Private key does not match certificate!");
+               }
+       } 
+
+       if (ok) {
+               SSL *ssl = SSL_new(ctx);
+
+               if (ssl) {
+                       /* map SSL => stream */
+                       SSL_set_ex_data(ssl, ssl_stream_data_index, stream);
+               }
+               return ssl;
+       }
+       
+       return NULL;
+}
+
+
+
 /*
  * Local variables:
  * tab-width: 8
index 89da3167e18c0e6b1692d9fdf36594b519b68c9c..bf26c2bcb12a627c4f254c6035e98ebdabbff9bb 100644 (file)
@@ -65,6 +65,10 @@ PHP_FUNCTION(openssl_csr_export);
 PHP_FUNCTION(openssl_csr_export_to_file);
 PHP_FUNCTION(openssl_csr_sign);
 
+#include <openssl/ssl.h>
+PHPAPI int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC);
+PHPAPI SSL *php_SSL_new_from_context(SSL_CTX *ctx, php_stream *stream TSRMLS_DC);
+
 #else
 
 #define phpext_openssl_ptr NULL
index 2a930c577461d63142a5909e1681209a8f8c30c9..c82424a7d6b34e801291db06eceae15be2e01be6 100644 (file)
@@ -124,6 +124,11 @@ php_file_globals file_globals;
 /* sharing globals is *evil* */
 static int le_stream_context = FAILURE;
 
+PHPAPI int php_le_stream_context(void)
+{
+       return le_stream_context;
+}
+
 /* }}} */
 /* {{{ Module-Stuff */
 
@@ -493,7 +498,7 @@ PHP_FUNCTION(file)
        
                do {
                        p++;
-                       parse_eol:
+parse_eol:
                        if (PG(magic_quotes_runtime)) {
                                /* s is in target_buf which is freed at the end of the function */
                                slashed = php_addslashes(s, (p-s), &len, 0 TSRMLS_CC);
index 1458cf5edd68b0f5d90c0a009a7e18d29ce0135b..7b1c8a77c0bb9f80465fba38cb472e4c7b0a8767 100644 (file)
@@ -126,6 +126,7 @@ extern int file_globals_id;
 extern php_file_globals file_globals;
 #endif
 
+PHPAPI int php_le_stream_context(void);
 
 #endif /* FILE_H */
 
index c73ff481b5e8f4c4ed3493e087cad31d2ee0a8ff..f6e47a0059b12bf5ed819f691441870a906a8fce 100644 (file)
@@ -137,20 +137,23 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
        char *host;
        int host_len;
        long port = -1;
-       zval *zerrno = NULL, *zerrstr = NULL;
+       zval *zerrno = NULL, *zerrstr = NULL, *zcontext = NULL;
        double timeout = FG(default_socket_timeout);
        unsigned long conv;
        struct timeval tv;
        char *hashkey = NULL;
        php_stream *stream = NULL;
+       php_stream_context *context = NULL;
        int err;
 
        RETVAL_FALSE;
        
-       if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|lzzd", &host, &host_len, &port, &zerrno, &zerrstr, &timeout) == FAILURE) {
+       if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|lzzdr", &host, &host_len, &port, &zerrno, &zerrstr, &timeout, &zcontext) == FAILURE) {
                RETURN_FALSE;
        }
-
+       if (zcontext) {
+               ZEND_FETCH_RESOURCE(context, php_stream_context*, &zcontext, -1, "stream-context", php_le_stream_context());
+       }
 
        if (persistent) {
                spprintf(&hashkey, 0, "pfsockopen__%s:%d", host, port);
@@ -222,6 +225,8 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
 
                if (stream == NULL) {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "unable to connect to %s:%d", host, port);
+               } else if (context) {
+                       php_stream_context_set(stream, context);
                }
                
 #if HAVE_OPENSSL_EXT
@@ -238,8 +243,11 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
                                        /* unknown ?? */
                                        break;
                        }
-                       if (ssl_ret == FAILURE)
+                       if (ssl_ret == FAILURE) {
                                php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to activate SSL mode %d", ssl_flags);
+                               php_stream_close(stream);
+                               stream = NULL;
+                       }
                }
 #endif
                
@@ -272,14 +280,14 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
 
 /* }}} */
 
-/* {{{ proto int fsockopen(string hostname, int port [, int errno [, string errstr [, float timeout]]])
+/* {{{ proto int fsockopen(string hostname, int port [, int errno [, string errstr [, float timeout [, resource context]]]])
    Open Internet or Unix domain socket connection */
 PHP_FUNCTION(fsockopen)
 {
        php_fsockopen_stream(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
 }
 /* }}} */
-/* {{{ proto int pfsockopen(string hostname, int port [, int errno [, string errstr [, float timeout]]])
+/* {{{ proto int pfsockopen(string hostname, int port [, int errno [, string errstr [, float timeout [, resource context]]]])
    Open persistent Internet or Unix domain socket connection */
 PHP_FUNCTION(pfsockopen)
 {
index 8a0bcf81d1c4bdf6322fc14075e264e861dbb2be..7b2d9add22896b416611adf75eafe572069ba8c9 100644 (file)
@@ -153,6 +153,18 @@ php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, char *path,
        
 #if HAVE_OPENSSL_EXT
        if (use_ssl)    {
+
+               if (context) {
+                       /* set the CN we expect to be on the remote cert.
+                        * You still need to have enabled verification (verify_peer) in the context for
+                        * this to have an effect */
+                       zval *cn;
+
+                       ALLOC_INIT_ZVAL(cn);
+                       ZVAL_STRING(cn, resource->host, 1);
+                       php_stream_context_set_option(context, "ssl", "CN_match", cn);
+               }
+               
                if (php_stream_sock_ssl_activate(stream, 1) == FAILURE) {
                        php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, "Unable to activate SSL mode");
                        php_stream_close(stream);
index 5084fbd14ccbbd996bf11ba31428c10a2697c5a8..ec83f9e10110063fb870ab1f6431e1ed00d9a1e5 100644 (file)
@@ -51,6 +51,7 @@
 
 #ifdef HAVE_OPENSSL_EXT
 #include <openssl/err.h>
+#include "ext/openssl/php_openssl.h"
 #endif
 
 #ifdef HAVE_SYS_SELECT_H
@@ -705,22 +706,26 @@ PHPAPI int php_stream_sock_ssl_activate_with_method(php_stream *stream, int acti
                        return FAILURE;
                }
 
-               sock->ssl_handle = SSL_new(ctx);
+               sock->ssl_handle = php_SSL_new_from_context(ctx, stream TSRMLS_CC);
+
                if (sock->ssl_handle == NULL)   {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "php_stream_sock_ssl_activate_with_method: failed to create an SSL handle");
                        SSL_CTX_free(ctx);
                        return FAILURE;
                }
                
+               SSL_set_connect_state(sock->ssl_handle);
                SSL_set_fd(sock->ssl_handle, sock->socket);
                
                if (psock) {
                        SSL_copy_session_id(sock->ssl_handle, psock->ssl_handle);
                }
+               
        }
 
        if (activate) {
                int err;
+               X509 *peer_cert;
 
                do {
                        err = SSL_connect(sock->ssl_handle);
@@ -731,6 +736,17 @@ PHPAPI int php_stream_sock_ssl_activate_with_method(php_stream *stream, int acti
                        SSL_shutdown(sock->ssl_handle);
                        return FAILURE;
                }
+
+               /* handshake was ok; did the verification go ok too ? */
+               peer_cert = SSL_get_peer_certificate(sock->ssl_handle);
+               
+               if (FAILURE == php_openssl_apply_verification_policy(sock->ssl_handle, peer_cert, stream TSRMLS_CC)) {
+                       SSL_shutdown(sock->ssl_handle);
+                       return FAILURE;
+               }
+
+               X509_free(peer_cert);
+               
                sock->ssl_active = activate;
        } else {
                SSL_shutdown(sock->ssl_handle);
@@ -773,6 +789,22 @@ PHPAPI int php_set_sock_blocking(int socketd, int block TSRMLS_DC)
 }
 
 #if HAVE_OPENSSL_EXT
+
+static void php_ERR_error_string_n(int code, char *buf, size_t size)
+{
+       switch (code) {
+               case 0x1407E086: /* SSL2 */
+               case 0x14090086: /* SSL3 */
+                       /* There does not appear to be a symbolic constant for these two codes;
+                        * they occur when certificate verification fails. The OpenSSL provided
+                        * error message is not particularly useful, so we special case it here */
+                       strncpy(buf, "Failed to verify peer certificate. Check your `cafile' and/or `capath' context options", size);
+                       break;
+               default:
+                       ERR_error_string_n(code, buf, size);
+       }
+}
+
 static int handle_ssl_error(php_stream *stream, int nr_bytes TSRMLS_DC)
 {
        php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract;
@@ -793,6 +825,7 @@ static int handle_ssl_error(php_stream *stream, int nr_bytes TSRMLS_DC)
                        /* re-negotiation, or perhaps the SSL layer needs more
                         * packets: retry in next iteration */
                        break;
+                       
                case SSL_ERROR_SYSCALL:
                        if (ERR_peek_error() == 0) {
                                if (nr_bytes == 0) {
@@ -819,10 +852,10 @@ static int handle_ssl_error(php_stream *stream, int nr_bytes TSRMLS_DC)
                                if (ebuf) {
                                        esbuf[0] = '\n';
                                        esbuf[1] = '\0';
-                                       ERR_error_string_n(code, esbuf + 1, sizeof(esbuf) - 2);
+                                       php_ERR_error_string_n(code, esbuf + 1, sizeof(esbuf) - 2);
                                } else {
                                        esbuf[0] = '\0';
-                                       ERR_error_string_n(code, esbuf, sizeof(esbuf) - 1);
+                                       php_ERR_error_string_n(code, esbuf, sizeof(esbuf) - 1);
                                }
                                code = strlen(esbuf);
                                esbuf[code] = '\0';
@@ -840,7 +873,7 @@ static int handle_ssl_error(php_stream *stream, int nr_bytes TSRMLS_DC)
                        php_error_docref(NULL TSRMLS_CC, E_WARNING,
                                        "SSL operation failed with code %d.%s%s",
                                        err, 
-                                       ebuf ? "OpenSSL Error messages:\n" : "",
+                                       ebuf ? " OpenSSL Error messages:\n" : "",
                                        ebuf ? ebuf : "");
                                
                        retry = 0;