From: Wez Furlong Date: Sat, 26 Apr 2003 21:34:49 +0000 (+0000) Subject: Fix the lack of SSL certificate verification support for ssl:// sockets and X-Git-Tag: php-4.3.2RC2~16 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=71a63bc126011a6d80c26755923956aadfcadc1f;p=php Fix the lack of SSL certificate verification support for ssl:// sockets and 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: 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(). --- diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index cabe01fa20..974ff7c361 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -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 diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index 89da3167e1..bf26c2bcb1 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -65,6 +65,10 @@ PHP_FUNCTION(openssl_csr_export); PHP_FUNCTION(openssl_csr_export_to_file); PHP_FUNCTION(openssl_csr_sign); +#include +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 diff --git a/ext/standard/file.c b/ext/standard/file.c index 2a930c5774..c82424a7d6 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -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); diff --git a/ext/standard/file.h b/ext/standard/file.h index 1458cf5edd..7b1c8a77c0 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -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 */ diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index c73ff481b5..f6e47a0059 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -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) { diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 8a0bcf81d1..7b2d9add22 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -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); diff --git a/main/network.c b/main/network.c index 5084fbd14c..ec83f9e101 100644 --- a/main/network.c +++ b/main/network.c @@ -51,6 +51,7 @@ #ifdef HAVE_OPENSSL_EXT #include +#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;