From: Daniel Lowrey Date: Tue, 4 Mar 2014 01:57:33 +0000 (-0700) Subject: Refactor + reorganize openssl files X-Git-Tag: php-5.6.0beta1~3^2~141 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=27849c998a77a093449dec4b051dfc266d5123ec;p=php Refactor + reorganize openssl files - All streams-related code now lives in xp_ssl.c. Previously stream code was split across both openssl.c and xp_ssl.c - Folded superfluous php_openssl_structs.h into xp_ssl.c - Server-specific options now set on SSL_CTX instead of SSL - Deprecate SNI_server_name ctx option - Miscellaneous refactoring --- diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index b480be954f..97740d361e 100755 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -29,7 +29,6 @@ #include "php.h" #include "php_ini.h" #include "php_openssl.h" -#include "php_openssl_structs.h" /* PHP Includes */ #include "ext/standard/file.h" @@ -53,15 +52,6 @@ #include #include -/* Windows platform includes */ -#ifdef PHP_WIN32 -# include -/* These are from Wincrypt.h, they conflict with OpenSSL */ -# undef X509_NAME -# undef X509_CERT_PAIR -# undef X509_EXTENSIONS -#endif - /* Common */ #include @@ -92,15 +82,6 @@ #define HAVE_EVP_PKEY_EC 1 #endif -#define PHP_OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH 9 -#define PHP_OPENSSL_DEFAULT_STREAM_CIPHERS "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:" \ - "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:" \ - "DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:" \ - "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:" \ - "ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:" \ - "DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:" \ - "AES256-GCM-SHA384:AES128:AES256:HIGH:!SSLv2:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!RC4:!ADH" - /* FIXME: Use the openssl constants instead of * enum. It is now impossible to match real values * against php constants. Also sorry to break the @@ -598,7 +579,11 @@ php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl) { return (php_stream*)SSL_get_ex_data(ssl, ssl_stream_data_index); } -/* }}} */ + +int php_openssl_get_ssl_stream_data_index() +{ + return ssl_stream_data_index; +} /* openssl -> PHP "bridging" */ /* true global; readonly after module startup */ @@ -633,13 +618,11 @@ struct php_x509_request { /* {{{ */ static X509 * php_openssl_x509_from_zval(zval ** val, int makeresource, long * resourceval TSRMLS_DC); static EVP_PKEY * php_openssl_evp_from_zval(zval ** val, int public_key, char * passphrase, int makeresource, long * resourceval TSRMLS_DC); static int php_openssl_is_private_key(EVP_PKEY* pkey TSRMLS_DC); -static X509_STORE * setup_verify(zval * calist TSRMLS_DC); +static X509_STORE * setup_verify(zval * calist TSRMLS_DC); static STACK_OF(X509) * load_all_certs_from_file(char *certfile); static X509_REQ * php_openssl_csr_from_zval(zval ** val, int makeresource, long * resourceval TSRMLS_DC); static EVP_PKEY * php_openssl_generate_private_key(struct php_x509_request * req TSRMLS_DC); -#define PHP_X509_NAME_ENTRY_TO_UTF8(ne, i, out) ASN1_STRING_to_UTF8(&out, X509_NAME_ENTRY_get_data(X509_NAME_get_entry(ne, i))) - static void add_assoc_name_entry(zval * val, char * key, X509_NAME * name, int shortname TSRMLS_DC) /* {{{ */ { zval **data; @@ -1183,8 +1166,7 @@ PHP_MINIT_FUNCTION(openssl) REGISTER_LONG_CONSTANT("OPENSSL_PKCS1_OAEP_PADDING", RSA_PKCS1_OAEP_PADDING, CONST_CS|CONST_PERSISTENT); /* Informational stream wrapper constants */ - REGISTER_STRING_CONSTANT("OPENSSL_DEFAULT_STREAM_CIPHERS", PHP_OPENSSL_DEFAULT_STREAM_CIPHERS, CONST_CS|CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH", PHP_OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH, CONST_CS|CONST_PERSISTENT); + REGISTER_STRING_CONSTANT("OPENSSL_DEFAULT_STREAM_CIPHERS", OPENSSL_DEFAULT_STREAM_CIPHERS, CONST_CS|CONST_PERSISTENT); /* Ciphers */ #ifndef OPENSSL_NO_RC2 @@ -1806,7 +1788,7 @@ static int php_x509_fingerprint_cmp(X509 *peer, const char *method, const char * return result; } -static zend_bool php_x509_fingerprint_match(X509 *peer, zval *val TSRMLS_DC) +zend_bool php_x509_fingerprint_match(X509 *peer, zval *val TSRMLS_DC) { if (Z_TYPE_P(val) == IS_STRING) { const char *method = NULL; @@ -5009,672 +4991,7 @@ PHP_FUNCTION(openssl_open) } /* }}} */ -/* SSL verification functions */ - -#define GET_VER_OPT(name) (stream->context && 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; - int err, depth, ret; - zval **val; - - ret = preverify_ok; - - /* determine the status for the current cert */ - 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; - -} -/* }}} */ - -static zend_bool matches_wildcard_name(const char *subjectname, const char *certname) -{ - char *wildcard; - int prefix_len, suffix_len, subject_len; - - if (strcasecmp(subjectname, certname) == 0) { - return 1; - } - - if (!(wildcard = strchr(certname, '*'))) { - return 0; - } - - // 1) prefix, if not empty, must match subject - prefix_len = wildcard - certname; - if (prefix_len && strncasecmp(subjectname, certname, prefix_len) != 0) { - return 0; - } - - suffix_len = strlen(wildcard + 1); - subject_len = strlen(subjectname); - if (suffix_len <= subject_len) { - /* 2) suffix must match - * 3) no . between prefix and suffix - **/ - return strcasecmp(wildcard + 1, subjectname + subject_len - suffix_len) == 0 && - memchr(subjectname + prefix_len, '.', subject_len - suffix_len - prefix_len) == NULL; - } - - return 0; -} - -static zend_bool matches_san_list(X509 *peer, const char *subject_name TSRMLS_DC) -{ - int i, san_name_len; - zend_bool is_match = 0; - unsigned char *cert_name; - - GENERAL_NAMES *alt_names = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0); - int alt_name_count = sk_GENERAL_NAME_num(alt_names); - - for (i = 0; i < alt_name_count; i++) { - GENERAL_NAME *san = sk_GENERAL_NAME_value(alt_names, i); - if (san->type != GEN_DNS) { - /* we only care about DNS names */ - continue; - } - - san_name_len = ASN1_STRING_length(san->d.dNSName); - ASN1_STRING_to_UTF8(&cert_name, san->d.dNSName); - - /* prevent null byte poisoning */ - if (san_name_len != strlen((const char*)cert_name)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer SAN entry is malformed"); - } else { - is_match = strcasecmp(subject_name, (const char*)cert_name) == 0; - } - - OPENSSL_free(cert_name); - - if (is_match) { - break; - } - } - - return is_match; -} - -static zend_bool matches_common_name(X509 *peer, const char *subject_name TSRMLS_DC) -{ - char buf[1024]; - X509_NAME *cert_name; - zend_bool is_match = 0; - int cert_name_len; - - cert_name = X509_get_subject_name(peer); - cert_name_len = X509_NAME_get_text_by_NID(cert_name, NID_commonName, buf, sizeof(buf)); - - if (cert_name_len == -1) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to locate peer certificate CN"); - } else if (cert_name_len != strlen(buf)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer certificate CN=`%.*s' is malformed", cert_name_len, buf); - } else if (matches_wildcard_name(subject_name, buf)) { - is_match = 1; - } else { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer certificate CN=`%.*s' did not match expected CN=`%s'", cert_name_len, buf, subject_name); - } - - return is_match; -} - -int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC) /* {{{ */ -{ - zval **val = NULL; - char *peer_name = NULL; - int err; - zend_bool must_verify_peer; - zend_bool must_verify_peer_name; - zend_bool must_verify_fingerprint; - zend_bool has_cnmatch_ctx_opt; - php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; - - must_verify_peer = GET_VER_OPT("verify_peer") - ? zend_is_true(*val) - : sslsock->is_client; - - has_cnmatch_ctx_opt = GET_VER_OPT("CN_match"); - must_verify_peer_name = (has_cnmatch_ctx_opt || GET_VER_OPT("verify_peer_name")) - ? zend_is_true(*val) - : sslsock->is_client; - - must_verify_fingerprint = (GET_VER_OPT("peer_fingerprint") && zend_is_true(*val)); - - if ((must_verify_peer || must_verify_peer_name || must_verify_fingerprint) && peer == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not get peer certificate"); - return FAILURE; - } - - /* Verify the peer against using CA file/path settings */ - if (must_verify_peer) { - 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 a peer_fingerprint match is required this trumps peer and peer_name verification */ - if (must_verify_fingerprint) { - if (Z_TYPE_PP(val) == IS_STRING || Z_TYPE_PP(val) == IS_ARRAY) { - if (!php_x509_fingerprint_match(peer, *val TSRMLS_CC)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Peer fingerprint doesn't match" - ); - return FAILURE; - } - } else { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Expected peer fingerprint must be a string or an array" - ); - } - } - - /* verify the host name presented in the peer certificate */ - if (must_verify_peer_name) { - GET_VER_OPT_STRING("peer_name", peer_name); - - if (has_cnmatch_ctx_opt) { - GET_VER_OPT_STRING("CN_match", peer_name); - php_error(E_DEPRECATED, - "the 'CN_match' SSL context option is deprecated in favor of 'peer_name'" - ); - } - /* If no peer name was specified we use the autodetected url name in client environments */ - if (peer_name == NULL && sslsock->is_client) { - peer_name = sslsock->url_name; - } - - if (peer_name) { - if (matches_san_list(peer, peer_name TSRMLS_CC)) { - return SUCCESS; - } else if (matches_common_name(peer, peer_name TSRMLS_CC)) { - return SUCCESS; - } else { - return FAILURE; - } - } else { - 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; -} -/* }}} */ - -#if defined(PHP_WIN32) && OPENSSL_VERSION_NUMBER >= 0x00907000L -#define RETURN_CERT_VERIFY_FAILURE(code) X509_STORE_CTX_set_error(x509_store_ctx, code); return 0; -static int win_cert_verify_callback(X509_STORE_CTX *x509_store_ctx, void *arg) /* {{{ */ -{ - PCCERT_CONTEXT cert_ctx = NULL; - PCCERT_CHAIN_CONTEXT cert_chain_ctx = NULL; - - php_stream *stream; - php_openssl_netstream_data_t *sslsock; - zval **val; - zend_bool is_self_signed = 0; - - TSRMLS_FETCH(); - - stream = (php_stream*)arg; - sslsock = (php_openssl_netstream_data_t*)stream->abstract; - - { /* First convert the x509 struct back to a DER encoded buffer and let Windows decode it into a form it can work with */ - unsigned char *der_buf = NULL; - int der_len; - - der_len = i2d_X509(x509_store_ctx->cert, &der_buf); - if (der_len < 0) { - unsigned long err_code, e; - char err_buf[512]; - - while ((e = ERR_get_error()) != 0) { - err_code = e; - } - - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error encoding X509 certificate: %d: %s", err_code, ERR_error_string(err_code, err_buf)); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - cert_ctx = CertCreateCertificateContext(X509_ASN_ENCODING, der_buf, der_len); - OPENSSL_free(der_buf); - if (cert_ctx == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error creating certificate context: %s", php_win_err()); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - } - - { /* Next fetch the relevant cert chain from the store */ - CERT_ENHKEY_USAGE enhkey_usage = {0}; - CERT_USAGE_MATCH cert_usage = {0}; - CERT_CHAIN_PARA chain_params = {sizeof(CERT_CHAIN_PARA)}; - LPSTR usages[] = {szOID_PKIX_KP_SERVER_AUTH, szOID_SERVER_GATED_CRYPTO, szOID_SGC_NETSCAPE}; - DWORD chain_flags = 0; - unsigned long verify_depth = PHP_OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH; - unsigned int i; - - enhkey_usage.cUsageIdentifier = 3; - enhkey_usage.rgpszUsageIdentifier = usages; - cert_usage.dwType = USAGE_MATCH_TYPE_OR; - cert_usage.Usage = enhkey_usage; - chain_params.RequestedUsage = cert_usage; - chain_flags = CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT; - - if (!CertGetCertificateChain(NULL, cert_ctx, NULL, NULL, &chain_params, chain_flags, NULL, &cert_chain_ctx)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error getting certificate chain: %s", php_win_err()); - CertFreeCertificateContext(cert_ctx); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - /* check if the cert is self-signed */ - if (cert_chain_ctx->cChain > 0 && cert_chain_ctx->rgpChain[0]->cElement > 0 - && (cert_chain_ctx->rgpChain[0]->rgpElement[0]->TrustStatus.dwInfoStatus & CERT_TRUST_IS_SELF_SIGNED) != 0) { - is_self_signed = 1; - } - - /* check the depth */ - if (GET_VER_OPT("verify_depth")) { - convert_to_long_ex(val); - verify_depth = (unsigned long)Z_LVAL_PP(val); - } - - for (i = 0; i < cert_chain_ctx->cChain; i++) { - if (cert_chain_ctx->rgpChain[i]->cElement > verify_depth) { - CertFreeCertificateChain(cert_chain_ctx); - CertFreeCertificateContext(cert_ctx); - RETURN_CERT_VERIFY_FAILURE(X509_V_ERR_CERT_CHAIN_TOO_LONG); - } - } - } - - { /* Then verify it against a policy */ - SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_policy_params = {sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA)}; - CERT_CHAIN_POLICY_PARA chain_policy_params = {sizeof(CERT_CHAIN_POLICY_PARA)}; - CERT_CHAIN_POLICY_STATUS chain_policy_status = {sizeof(CERT_CHAIN_POLICY_STATUS)}; - LPWSTR server_name = NULL; - BOOL verify_result; - - { /* This looks ridiculous and it is - but we validate the name ourselves using the peer_name - ctx option, so just use the CN from the cert here */ - - X509_NAME *cert_name; - unsigned char *cert_name_utf8; - int index, cert_name_utf8_len; - DWORD num_wchars; - - cert_name = X509_get_subject_name(x509_store_ctx->cert); - index = X509_NAME_get_index_by_NID(cert_name, NID_commonName, -1); - if (index < 0) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to locate certificate CN"); - CertFreeCertificateChain(cert_chain_ctx); - CertFreeCertificateContext(cert_ctx); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - cert_name_utf8_len = PHP_X509_NAME_ENTRY_TO_UTF8(cert_name, index, cert_name_utf8); - - num_wchars = MultiByteToWideChar(CP_UTF8, 0, (char*)cert_name_utf8, -1, NULL, 0); - if (num_wchars == 0) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to convert %s to wide character string", cert_name_utf8); - OPENSSL_free(cert_name_utf8); - CertFreeCertificateChain(cert_chain_ctx); - CertFreeCertificateContext(cert_ctx); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - server_name = emalloc((num_wchars * sizeof(WCHAR)) + sizeof(WCHAR)); - - num_wchars = MultiByteToWideChar(CP_UTF8, 0, (char*)cert_name_utf8, -1, server_name, num_wchars); - if (num_wchars == 0) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to convert %s to wide character string", cert_name_utf8); - efree(server_name); - OPENSSL_free(cert_name_utf8); - CertFreeCertificateChain(cert_chain_ctx); - CertFreeCertificateContext(cert_ctx); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - OPENSSL_free(cert_name_utf8); - } - - ssl_policy_params.dwAuthType = (sslsock->is_client) ? AUTHTYPE_SERVER : AUTHTYPE_CLIENT; - ssl_policy_params.pwszServerName = server_name; - chain_policy_params.pvExtraPolicyPara = &ssl_policy_params; - - verify_result = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, cert_chain_ctx, &chain_policy_params, &chain_policy_status); - - efree(server_name); - CertFreeCertificateChain(cert_chain_ctx); - CertFreeCertificateContext(cert_ctx); - - if (!verify_result) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error verifying certificate chain policy: %s", php_win_err()); - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - - if (chain_policy_status.dwError != 0) { - /* The chain does not match the policy */ - if (is_self_signed && chain_policy_status.dwError == CERT_E_UNTRUSTEDROOT - && GET_VER_OPT("allow_self_signed") && zval_is_true(*val)) { - /* allow self-signed certs */ - X509_STORE_CTX_set_error(x509_store_ctx, X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT); - } else { - RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); - } - } - } - - return 1; -} -/* }}} */ -#endif - -static long load_stream_cafile(X509_STORE *cert_store, const char *cafile TSRMLS_DC) /* {{{ */ -{ - php_stream *stream; - X509 *cert; - BIO *buffer; - int buffer_active = 0; - char *line; - size_t line_len; - long certs_added = 0; - - stream = php_stream_open_wrapper(cafile, "rb", 0, NULL); - - if (stream == NULL) { - php_error(E_WARNING, "failed loading cafile stream: `%s'", cafile); - return 0; - } else if (stream->wrapper->is_url) { - php_stream_close(stream); - php_error(E_WARNING, "remote cafile streams are disabled for security purposes"); - return 0; - } - - cert_start: { - line = php_stream_get_line(stream, NULL, 0, &line_len); - if (line == NULL) { - goto stream_complete; - } else if (!strcmp(line, "-----BEGIN CERTIFICATE-----\n") || - !strcmp(line, "-----BEGIN CERTIFICATE-----\r\n") - ) { - buffer = BIO_new(BIO_s_mem()); - buffer_active = 1; - goto cert_line; - } else { - efree(line); - goto cert_start; - } - } - - cert_line: { - BIO_puts(buffer, line); - efree(line); - line = php_stream_get_line(stream, NULL, 0, &line_len); - if (line == NULL) { - goto stream_complete; - } else if (!strcmp(line, "-----END CERTIFICATE-----") || - !strcmp(line, "-----END CERTIFICATE-----\n") || - !strcmp(line, "-----END CERTIFICATE-----\r\n") - ) { - goto add_cert; - } else { - goto cert_line; - } - } - - add_cert: { - BIO_puts(buffer, line); - efree(line); - cert = PEM_read_bio_X509(buffer, NULL, 0, NULL); - BIO_free(buffer); - buffer_active = 0; - if (cert && X509_STORE_add_cert(cert_store, cert)) { - ++certs_added; - } - goto cert_start; - } - - stream_complete: { - php_stream_close(stream); - if (buffer_active == 1) { - BIO_free(buffer); - } - } - - if (certs_added == 0) { - php_error(E_WARNING, "no valid certs found cafile stream: `%s'", cafile); - } - - return certs_added; -} -/* }}} */ - -static void enable_peer_verify_callback(SSL_CTX *ctx, php_stream *stream) /* {{{ */ -{ - zval **val = NULL; - - /* turn on verification callback */ - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback); - - 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_depth(ctx, PHP_OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH); - } -} -/* }}} */ - -static int enable_peer_verification(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ -{ - zval **val = NULL; - char *cafile = NULL; - char *capath = NULL; - - GET_VER_OPT_STRING("cafile", cafile); - GET_VER_OPT_STRING("capath", capath); - - if (!cafile) { - cafile = zend_ini_string("openssl.cafile", sizeof("openssl.cafile"), 0); - cafile = strlen(cafile) ? cafile : NULL; - } - - if (!capath) { - capath = zend_ini_string("openssl.capath", sizeof("openssl.capath"), 0); - capath = strlen(capath) ? capath : NULL; - } - - if (cafile || capath) { - if (!SSL_CTX_load_verify_locations(ctx, cafile, capath)) { - if (cafile && !load_stream_cafile(SSL_CTX_get_cert_store(ctx), cafile TSRMLS_CC)) { - return 0; - } - } - - enable_peer_verify_callback(ctx, stream); - } else { -#if defined(PHP_WIN32) && OPENSSL_VERSION_NUMBER >= 0x00907000L - SSL_CTX_set_cert_verify_callback(ctx, win_cert_verify_callback, (void *)stream); - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); -#else - php_openssl_netstream_data_t *sslsock; - sslsock = (php_openssl_netstream_data_t*)stream->abstract; - - if (sslsock->is_client && !SSL_CTX_set_default_verify_paths(ctx)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Unable to set default verify locations and no CA settings specified"); - return 0; - } - - enable_peer_verify_callback(ctx, stream); -#endif - } - - return 1; -} -/* }}} */ - -static int disable_peer_verification(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ -{ - SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); - - return 1; -} -/* }}} */ - -SSL *php_SSL_new_from_context(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ -{ - zval **val = NULL; - char *certfile = NULL; - char *cipherlist = NULL; - int ok = 1; - SSL *ssl; - - ERR_clear_error(); - - /* look at context options in the stream and set appropriate verification flags */ - if (GET_VER_OPT("verify_peer") && !zval_is_true(*val)) { - ok = disable_peer_verification(ctx, stream TSRMLS_CC); - } else { - ok = enable_peer_verification(ctx, stream TSRMLS_CC); - } - - if (!ok) { - return 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("ciphers", cipherlist); - if (!cipherlist) { - cipherlist = PHP_OPENSSL_DEFAULT_STREAM_CIPHERS; - } - if (SSL_CTX_set_cipher_list(ctx, cipherlist) != 1) { - return NULL; - } - - GET_VER_OPT_STRING("local_cert", certfile); - if (certfile) { - char resolved_path_buff[MAXPATHLEN]; - const char * private_key = NULL; - - if (VCWD_REALPATH(certfile, resolved_path_buff)) { - /* a certificate to use for authentication */ - if (SSL_CTX_use_certificate_chain_file(ctx, resolved_path_buff) != 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; - } - GET_VER_OPT_STRING("local_pk", private_key); - - if (private_key) { - char resolved_path_buff_pk[MAXPATHLEN]; - if (VCWD_REALPATH(private_key, resolved_path_buff_pk)) { - if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff_pk, SSL_FILETYPE_PEM) != 1) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set private key file `%s'", resolved_path_buff_pk); - return NULL; - } - } - } else { - if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff, SSL_FILETYPE_PEM) != 1) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set private key file `%s'", resolved_path_buff); - return NULL; - } - } - -#if OPENSSL_VERSION_NUMBER < 0x10001001L - do { - /* Unnecessary as of OpenSSLv1.0.1 (will segfault if used with >= 10001001 ) */ - X509 *cert = NULL; - EVP_PKEY *key = NULL; - SSL *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); - } while (0); -#endif - if (!SSL_CTX_check_private_key(ctx)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "Private key does not match certificate!"); - } - } - } - - ssl = SSL_new(ctx); - - if (ssl) { - /* map SSL => stream */ - SSL_set_ex_data(ssl, ssl_stream_data_index, stream); - } - - return ssl; -} -/* }}} */ static void openssl_add_method_or_alias(const OBJ_NAME *name, void *arg) /* {{{ */ { diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index a823d30bd8..968919eb64 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -30,8 +30,16 @@ extern zend_module_entry openssl_module_entry; #define OPENSSL_ZERO_PADDING 2 /* Used for client-initiated handshake renegotiation DoS protection*/ -#define DEFAULT_RENEG_LIMIT 2 -#define DEFAULT_RENEG_WINDOW 300 +#define OPENSSL_DEFAULT_RENEG_LIMIT 2 +#define OPENSSL_DEFAULT_RENEG_WINDOW 300 +#define OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH 9 +#define OPENSSL_DEFAULT_STREAM_CIPHERS "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:" \ + "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:" \ + "DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:" \ + "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:" \ + "ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:" \ + "DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:AES128:AES256:HIGH:!SSLv2:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!RC4:!ADH" php_stream_transport_factory_func php_openssl_ssl_socket_factory; diff --git a/ext/openssl/php_openssl_structs.h b/ext/openssl/php_openssl_structs.h deleted file mode 100644 index 562c756cd3..0000000000 --- a/ext/openssl/php_openssl_structs.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | PHP Version 5 | - +----------------------------------------------------------------------+ - | Copyright (c) 1997-2013 The PHP Group | - +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | http://www.php.net/license/3_01.txt | - | If you did not receive a copy of the PHP license and are unable to | - | obtain it through the world-wide-web, please send a note to | - | license@php.net so we can mail you a copy immediately. | - +----------------------------------------------------------------------+ - | Author: Wez Furlong | - | Daniel Lowrey | - +----------------------------------------------------------------------+ -*/ - -/* $Id$ */ - -#include "php_network.h" -#include - -typedef struct _php_openssl_handshake_bucket_t { - long prev_handshake; - long limit; - long window; - float tokens; - unsigned should_close; -} php_openssl_handshake_bucket_t; - -/* This implementation is very closely tied to the that of the native - * sockets implemented in the core. - * Don't try this technique in other extensions! - * */ - -typedef struct _php_openssl_netstream_data_t { - php_netstream_data_t s; - SSL *ssl_handle; - SSL_CTX *ctx; - struct timeval connect_timeout; - int enable_on_connect; - int is_client; - int ssl_active; - php_stream_xport_crypt_method_t method; - php_openssl_handshake_bucket_t *reneg; - char *url_name; - unsigned state_set:1; - unsigned _spare:31; -} php_openssl_netstream_data_t; diff --git a/ext/openssl/tests/bug65538_002.phpt b/ext/openssl/tests/bug65538_002.phpt index 760b720e73..dfc6f94ff7 100644 --- a/ext/openssl/tests/bug65538_002.phpt +++ b/ext/openssl/tests/bug65538_002.phpt @@ -12,8 +12,6 @@ file_get_contents('https://github.com', false, $clientCtx); --EXPECTF-- Warning: remote cafile streams are disabled for security purposes in %s on line %d -Warning: file_get_contents(): failed to create an SSL handle in %s on line %d - Warning: file_get_contents(): Failed to enable crypto in %s on line %d Warning: file_get_contents(%s): failed to open stream: operation failed in %s on line %d diff --git a/ext/openssl/tests/capture_peer_cert_001.phpt b/ext/openssl/tests/capture_peer_cert_001.phpt index 2cec06a127..0396cace43 100644 --- a/ext/openssl/tests/capture_peer_cert_001.phpt +++ b/ext/openssl/tests/capture_peer_cert_001.phpt @@ -24,7 +24,7 @@ $clientCode = <<<'CODE' $clientFlags = STREAM_CLIENT_CONNECT; $clientCtx = stream_context_create(['ssl' => [ 'capture_peer_cert' => true, - 'cafile' => __DIR__ . '/bug54992-ca.pem' + 'cafile' => __DIR__ . '/bug54992-ca.pem' ]]); phpt_wait(); diff --git a/ext/openssl/tests/sni_001.phpt b/ext/openssl/tests/sni_001.phpt index e6c05f7ec2..e7dbf3f19e 100644 --- a/ext/openssl/tests/sni_001.phpt +++ b/ext/openssl/tests/sni_001.phpt @@ -89,7 +89,7 @@ do_http_test('https://bob.sni.velox.ch./', context('bob.sni.velox.ch')); 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, 'ssl', 'peer_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); @@ -110,7 +110,7 @@ do_ssl_test('ssl://mallory.sni.velox.ch:443', context('mallory.sni.velox.ch')); echo "-- raw SSL stream with user supplied sni --\n"; $context = context('bob.sni.velox.ch'); -stream_context_set_option($context, 'ssl', 'SNI_server_name', 'bob.sni.velox.ch'); +stream_context_set_option($context, 'ssl', 'peer_name', 'bob.sni.velox.ch'); do_ssl_test('ssl://mallory.sni.velox.ch:443', $context); @@ -134,7 +134,7 @@ 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'); +stream_context_set_option($context, 'ssl', 'peer_name', 'bob.sni.velox.ch'); do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context); @@ -148,7 +148,7 @@ 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'); +stream_context_set_option($context, 'ssl', 'peer_name', str_repeat('a.', 500) . '.sni.velox.ch'); do_enable_crypto_test('tcp://mallory.sni.velox.ch:443', $context); diff --git a/ext/openssl/tests/tlsv1.0_wrapper.phpt b/ext/openssl/tests/tlsv1.0_wrapper.phpt index 7479259426..c0477dfe32 100644 --- a/ext/openssl/tests/tlsv1.0_wrapper.phpt +++ b/ext/openssl/tests/tlsv1.0_wrapper.phpt @@ -16,7 +16,7 @@ $serverCode = <<<'CODE' phpt_notify(); for ($i=0; $i < 3; $i++) { - @stream_socket_accept($server, 1); + @stream_socket_accept($server, 3); } CODE; @@ -29,13 +29,13 @@ $clientCode = <<<'CODE' phpt_wait(); - $client = stream_socket_client("tlsv1.0://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = stream_socket_client("tlsv1.0://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); CODE; diff --git a/ext/openssl/tests/tlsv1.1_wrapper.phpt b/ext/openssl/tests/tlsv1.1_wrapper.phpt index 3e067a14b7..a5dba299ec 100644 --- a/ext/openssl/tests/tlsv1.1_wrapper.phpt +++ b/ext/openssl/tests/tlsv1.1_wrapper.phpt @@ -17,7 +17,7 @@ $serverCode = <<<'CODE' phpt_notify(); for ($i=0; $i < 3; $i++) { - @stream_socket_accept($server, 1); + @stream_socket_accept($server, 3); } CODE; @@ -30,13 +30,13 @@ $clientCode = <<<'CODE' phpt_wait(); - $client = stream_socket_client("tlsv1.1://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = stream_socket_client("tlsv1.1://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); CODE; diff --git a/ext/openssl/tests/tlsv1.2_wrapper.phpt b/ext/openssl/tests/tlsv1.2_wrapper.phpt index ca967d18b4..42980cf661 100644 --- a/ext/openssl/tests/tlsv1.2_wrapper.phpt +++ b/ext/openssl/tests/tlsv1.2_wrapper.phpt @@ -17,7 +17,7 @@ $serverCode = <<<'CODE' phpt_notify(); for ($i=0; $i < 3; $i++) { - @stream_socket_accept($server, 1); + @stream_socket_accept($server, 3); } CODE; @@ -30,13 +30,13 @@ $clientCode = <<<'CODE' phpt_wait(); - $client = stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = stream_socket_client("tlsv1.2://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("sslv3://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); - $client = @stream_socket_client("tlsv1.1://127.0.0.1:64321", $errno, $errstr, 1, $flags, $ctx); + $client = @stream_socket_client("tlsv1.1://127.0.0.1:64321", $errno, $errstr, 3, $flags, $ctx); var_dump($client); CODE; diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index d63519e949..6d7f8e94a7 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -12,7 +12,9 @@ | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ - | Author: Wez Furlong | + | Authors: Wez Furlong | + | Daniel Lowrey | + | Chris Wright | +----------------------------------------------------------------------+ */ @@ -24,13 +26,20 @@ #include "streams/php_streams_int.h" #include "ext/standard/php_smart_str.h" #include "php_openssl.h" -#include "php_openssl_structs.h" +#include "php_network.h" #include #include +#include #include #ifdef PHP_WIN32 +#include "win32/winutil.h" #include "win32/time.h" +#include +/* These are from Wincrypt.h, they conflict with OpenSSL */ +#undef X509_NAME +#undef X509_CERT_PAIR +#undef X509_EXTENSIONS #endif #ifdef NETWARE @@ -38,7 +47,11 @@ #endif #if !defined(OPENSSL_NO_ECDH) && OPENSSL_VERSION_NUMBER >= 0x0090800fL -#define HAVE_ECDH +#define HAVE_ECDH 1 +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x00908070L && !defined(OPENSSL_NO_TLSEXT) +#define HAVE_SNI 1 #endif /* Flags for determining allowed stream crypto methods */ @@ -49,16 +62,54 @@ #define STREAM_CRYPTO_METHOD_TLSv1_1 (1<<4) #define STREAM_CRYPTO_METHOD_TLSv1_2 (1<<5) -int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC); -SSL *php_SSL_new_from_context(SSL_CTX *ctx, php_stream *stream TSRMLS_DC); +/* Simplify ssl context option retrieval */ +#define GET_VER_OPT(name) (stream->context && 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); } +#define GET_VER_OPT_LONG(name, num) if (GET_VER_OPT(name)) { convert_to_long_ex(val); num = Z_LVAL_PP(val); } + +/* Used for peer verification in windows */ +#define PHP_X509_NAME_ENTRY_TO_UTF8(ne, i, out) ASN1_STRING_to_UTF8(&out, X509_NAME_ENTRY_get_data(X509_NAME_get_entry(ne, i))) + extern php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl); -int php_openssl_get_x509_list_id(void); +extern zend_bool php_x509_fingerprint_match(X509 *peer, zval *val TSRMLS_DC); +extern int php_openssl_get_ssl_stream_data_index(); +extern int php_openssl_get_x509_list_id(void); php_stream_ops php_openssl_socket_ops; +/* Provides leaky bucket handhsake renegotiation rate-limiting */ +typedef struct _php_openssl_handshake_bucket_t { + long prev_handshake; + long limit; + long window; + float tokens; + unsigned should_close; +} php_openssl_handshake_bucket_t; + +/* This implementation is very closely tied to the that of the native + * sockets implemented in the core. + * Don't try this technique in other extensions! + * */ +typedef struct _php_openssl_netstream_data_t { + php_netstream_data_t s; + SSL *ssl_handle; + SSL_CTX *ctx; + struct timeval connect_timeout; + int enable_on_connect; + int is_client; + int ssl_active; + php_stream_xport_crypt_method_t method; + php_openssl_handshake_bucket_t *reneg; + php_openssl_sni_cert_t *sni_certs; + unsigned sni_cert_count; + char *url_name; + unsigned state_set:1; + unsigned _spare:31; +} php_openssl_netstream_data_t; + /* it doesn't matter that we do some hash traversal here, since it is done only * in an error condition arising from a network connection problem */ -static int is_http_stream_talking_to_iis(php_stream *stream TSRMLS_DC) +static int is_http_stream_talking_to_iis(php_stream *stream TSRMLS_DC) /* {{{ */ { if (stream->wrapperdata && stream->wrapper && strcasecmp(stream->wrapper->wops->label, "HTTP") == 0) { /* the wrapperdata is an array zval containing the headers */ @@ -81,8 +132,9 @@ static int is_http_stream_talking_to_iis(php_stream *stream TSRMLS_DC) } return 0; } +/* }}} */ -static int handle_ssl_error(php_stream *stream, int nr_bytes, zend_bool is_init TSRMLS_DC) +static int handle_ssl_error(php_stream *stream, int nr_bytes, zend_bool is_init TSRMLS_DC) /* {{{ */ { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; int err = SSL_get_error(sslsock->ssl_handle, nr_bytes); @@ -164,268 +216,616 @@ static int handle_ssl_error(php_stream *stream, int nr_bytes, zend_bool is_init } return retry; } +/* }}} */ + +static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) /* {{{ */ +{ + php_stream *stream; + SSL *ssl; + int err, depth, ret; + zval **val; + unsigned long allowed_depth = OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH; + + ret = preverify_ok; + + /* determine the status for the current cert */ + 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, php_openssl_get_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") && + zend_is_true(*val) + ) { + ret = 1; + } + + /* check the depth */ + GET_VER_OPT_LONG("verify_depth", allowed_depth); + if ((unsigned long)depth > allowed_depth) { + ret = 0; + X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_CHAIN_TOO_LONG); + } + return ret; +} +/* }}} */ -static size_t php_openssl_sockop_write(php_stream *stream, const char *buf, size_t count TSRMLS_DC) +static zend_bool matches_wildcard_name(const char *subjectname, const char *certname) /* {{{ */ { - php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; - int didwrite; - - if (sslsock->ssl_active) { - int retry = 1; + char *wildcard = NULL; + int prefix_len, suffix_len, subject_len; - do { - didwrite = SSL_write(sslsock->ssl_handle, buf, count); + if (strcasecmp(subjectname, certname) == 0) { + return 1; + } - if (didwrite <= 0) { - retry = handle_ssl_error(stream, didwrite, 0 TSRMLS_CC); - } else { - break; - } - } while(retry); + if (!(wildcard = strchr(certname, '*'))) { + return 0; + } - if (didwrite > 0) { - php_stream_notify_progress_increment(stream->context, didwrite, 0); - } - } else { - didwrite = php_stream_socket_ops.write(stream, buf, count TSRMLS_CC); + // 1) prefix, if not empty, must match subject + prefix_len = wildcard - certname; + if (prefix_len && strncasecmp(subjectname, certname, prefix_len) != 0) { + return 0; } - if (didwrite < 0) { - didwrite = 0; + suffix_len = strlen(wildcard + 1); + subject_len = strlen(subjectname); + if (suffix_len <= subject_len) { + /* 2) suffix must match + * 3) no . between prefix and suffix + **/ + return strcasecmp(wildcard + 1, subjectname + subject_len - suffix_len) == 0 && + memchr(subjectname + prefix_len, '.', subject_len - suffix_len - prefix_len) == NULL; } - - return didwrite; + + return 0; } +/* }}} */ -static size_t php_openssl_sockop_read(php_stream *stream, char *buf, size_t count TSRMLS_DC) +static zend_bool matches_san_list(X509 *peer, const char *subject_name TSRMLS_DC) /* {{{ */ { - php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; - int nr_bytes = 0; + int i, san_name_len; + zend_bool is_match = 0; + unsigned char *cert_name = NULL; + + GENERAL_NAMES *alt_names = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0); + int alt_name_count = sk_GENERAL_NAME_num(alt_names); + + for (i = 0; i < alt_name_count; i++) { + GENERAL_NAME *san = sk_GENERAL_NAME_value(alt_names, i); + if (san->type != GEN_DNS) { + /* we only care about DNS names */ + continue; + } - if (sslsock->ssl_active) { - int retry = 1; + san_name_len = ASN1_STRING_length(san->d.dNSName); + ASN1_STRING_to_UTF8(&cert_name, san->d.dNSName); - do { - nr_bytes = SSL_read(sslsock->ssl_handle, buf, count); + /* prevent null byte poisoning */ + if (san_name_len != strlen((const char*)cert_name)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer SAN entry is malformed"); + } else { + is_match = strcasecmp(subject_name, (const char*)cert_name) == 0; + } - if (sslsock->reneg && sslsock->reneg->should_close) { - /* renegotiation rate limiting triggered */ - php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR TSRMLS_CC); - nr_bytes = 0; - stream->eof = 1; - break; - } else if (nr_bytes <= 0) { - retry = handle_ssl_error(stream, nr_bytes, 0 TSRMLS_CC); - stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle)); - - } else { - /* we got the data */ - break; - } - } while (retry); + OPENSSL_free(cert_name); - if (nr_bytes > 0) { - php_stream_notify_progress_increment(stream->context, nr_bytes, 0); + if (is_match) { + break; } } - else - { - nr_bytes = php_stream_socket_ops.read(stream, buf, count TSRMLS_CC); - } - if (nr_bytes < 0) { - nr_bytes = 0; + return is_match; +} +/* }}} */ + +static zend_bool matches_common_name(X509 *peer, const char *subject_name TSRMLS_DC) /* {{{ */ +{ + char buf[1024]; + X509_NAME *cert_name; + zend_bool is_match = 0; + int cert_name_len; + + cert_name = X509_get_subject_name(peer); + cert_name_len = X509_NAME_get_text_by_NID(cert_name, NID_commonName, buf, sizeof(buf)); + + if (cert_name_len == -1) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to locate peer certificate CN"); + } else if (cert_name_len != strlen(buf)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer certificate CN=`%.*s' is malformed", cert_name_len, buf); + } else if (matches_wildcard_name(subject_name, buf)) { + is_match = 1; + } else { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer certificate CN=`%.*s' did not match expected CN=`%s'", cert_name_len, buf, subject_name); } - return nr_bytes; + return is_match; } +/* }}} */ -static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_DC) +static int apply_peer_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC) /* {{{ */ { + zval **val = NULL; + char *peer_name = NULL; + int err, + must_verify_peer, + must_verify_peer_name, + must_verify_fingerprint, + has_cnmatch_ctx_opt; + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; -#ifdef PHP_WIN32 - int n; -#endif - if (close_handle) { - if (sslsock->ssl_active) { - SSL_shutdown(sslsock->ssl_handle); - sslsock->ssl_active = 0; - } - if (sslsock->ssl_handle) { - SSL_free(sslsock->ssl_handle); - sslsock->ssl_handle = NULL; - } - if (sslsock->ctx) { - SSL_CTX_free(sslsock->ctx); - sslsock->ctx = NULL; - } -#ifdef PHP_WIN32 - if (sslsock->s.socket == -1) - sslsock->s.socket = SOCK_ERR; -#endif - if (sslsock->s.socket != SOCK_ERR) { -#ifdef PHP_WIN32 - /* prevent more data from coming in */ - shutdown(sslsock->s.socket, SHUT_RD); + must_verify_peer = GET_VER_OPT("verify_peer") + ? zend_is_true(*val) + : sslsock->is_client; - /* try to make sure that the OS sends all data before we close the connection. - * Essentially, we are waiting for the socket to become writeable, which means - * that all pending data has been sent. - * We use a small timeout which should encourage the OS to send the data, - * but at the same time avoid hanging indefinitely. - * */ - do { - n = php_pollfd_for_ms(sslsock->s.socket, POLLOUT, 500); - } while (n == -1 && php_socket_errno() == EINTR); -#endif - closesocket(sslsock->s.socket); - sslsock->s.socket = SOCK_ERR; - } + has_cnmatch_ctx_opt = GET_VER_OPT("CN_match"); + must_verify_peer_name = (has_cnmatch_ctx_opt || GET_VER_OPT("verify_peer_name")) + ? zend_is_true(*val) + : sslsock->is_client; + + must_verify_fingerprint = (GET_VER_OPT("peer_fingerprint") && zend_is_true(*val)); + + if ((must_verify_peer || must_verify_peer_name || must_verify_fingerprint) && peer == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not get peer certificate"); + return FAILURE; } - if (sslsock->url_name) { - pefree(sslsock->url_name, php_stream_is_persistent(stream)); + /* Verify the peer against using CA file/path settings */ + if (must_verify_peer) { + 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") && zend_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 (sslsock->reneg) { - pefree(sslsock->reneg, php_stream_is_persistent(stream)); + /* If a peer_fingerprint match is required this trumps peer and peer_name verification */ + if (must_verify_fingerprint) { + if (Z_TYPE_PP(val) == IS_STRING || Z_TYPE_PP(val) == IS_ARRAY) { + if (!php_x509_fingerprint_match(peer, *val TSRMLS_CC)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "Peer fingerprint doesn't match" + ); + return FAILURE; + } + } else { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "Expected peer fingerprint must be a string or an array" + ); + } } - pefree(sslsock, php_stream_is_persistent(stream)); + /* verify the host name presented in the peer certificate */ + if (must_verify_peer_name) { + GET_VER_OPT_STRING("peer_name", peer_name); - return 0; -} + if (has_cnmatch_ctx_opt) { + GET_VER_OPT_STRING("CN_match", peer_name); + php_error(E_DEPRECATED, + "the 'CN_match' SSL context option is deprecated in favor of 'peer_name'" + ); + } + /* If no peer name was specified we use the autodetected url name in client environments */ + if (peer_name == NULL && sslsock->is_client) { + peer_name = sslsock->url_name; + } -static int php_openssl_sockop_flush(php_stream *stream TSRMLS_DC) -{ - return php_stream_socket_ops.flush(stream TSRMLS_CC); + if (peer_name) { + if (matches_san_list(peer, peer_name TSRMLS_CC)) { + return SUCCESS; + } else if (matches_common_name(peer, peer_name TSRMLS_CC)) { + return SUCCESS; + } else { + return FAILURE; + } + } else { + return FAILURE; + } + } + + return SUCCESS; } +/* }}} */ -static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb TSRMLS_DC) +static int passwd_callback(char *buf, int num, int verify, void *data) /* {{{ */ { - return php_stream_socket_ops.stat(stream, ssb TSRMLS_CC); + 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; } +/* }}} */ -static inline void limit_handshake_reneg(const SSL *ssl) /* {{{ */ +#if defined(PHP_WIN32) && OPENSSL_VERSION_NUMBER >= 0x00907000L +#define RETURN_CERT_VERIFY_FAILURE(code) X509_STORE_CTX_set_error(x509_store_ctx, code); return 0; +static int win_cert_verify_callback(X509_STORE_CTX *x509_store_ctx, void *arg) /* {{{ */ { + PCCERT_CONTEXT cert_ctx = NULL; + PCCERT_CHAIN_CONTEXT cert_chain_ctx = NULL; + php_stream *stream; php_openssl_netstream_data_t *sslsock; - struct timeval now; - long elapsed_time; + zval **val; + zend_bool is_self_signed = 0; - stream = php_openssl_get_stream_from_ssl_handle(ssl); + TSRMLS_FETCH(); + + stream = (php_stream*)arg; sslsock = (php_openssl_netstream_data_t*)stream->abstract; - gettimeofday(&now, NULL); - /* The initial handshake is never rate-limited */ - if (sslsock->reneg->prev_handshake == 0) { - sslsock->reneg->prev_handshake = now.tv_sec; - return; - } + { /* First convert the x509 struct back to a DER encoded buffer and let Windows decode it into a form it can work with */ + unsigned char *der_buf = NULL; + int der_len; - elapsed_time = (now.tv_sec - sslsock->reneg->prev_handshake); - sslsock->reneg->prev_handshake = now.tv_sec; - sslsock->reneg->tokens -= (elapsed_time * (sslsock->reneg->limit / sslsock->reneg->window)); + der_len = i2d_X509(x509_store_ctx->cert, &der_buf); + if (der_len < 0) { + unsigned long err_code, e; + char err_buf[512]; - if (sslsock->reneg->tokens < 0) { - sslsock->reneg->tokens = 0; + while ((e = ERR_get_error()) != 0) { + err_code = e; + } + + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error encoding X509 certificate: %d: %s", err_code, ERR_error_string(err_code, err_buf)); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); + } + + cert_ctx = CertCreateCertificateContext(X509_ASN_ENCODING, der_buf, der_len); + OPENSSL_free(der_buf); + + if (cert_ctx == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error creating certificate context: %s", php_win_err()); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); + } } - ++sslsock->reneg->tokens; - /* The token level exceeds our allowed limit */ - if (sslsock->reneg->tokens > sslsock->reneg->limit) { - zval **val; + { /* Next fetch the relevant cert chain from the store */ + CERT_ENHKEY_USAGE enhkey_usage = {0}; + CERT_USAGE_MATCH cert_usage = {0}; + CERT_CHAIN_PARA chain_params = {sizeof(CERT_CHAIN_PARA)}; + LPSTR usages[] = {szOID_PKIX_KP_SERVER_AUTH, szOID_SERVER_GATED_CRYPTO, szOID_SGC_NETSCAPE}; + DWORD chain_flags = 0; + unsigned long allowed_depth = OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH; + unsigned int i; + + enhkey_usage.cUsageIdentifier = 3; + enhkey_usage.rgpszUsageIdentifier = usages; + cert_usage.dwType = USAGE_MATCH_TYPE_OR; + cert_usage.Usage = enhkey_usage; + chain_params.RequestedUsage = cert_usage; + chain_flags = CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT; + + if (!CertGetCertificateChain(NULL, cert_ctx, NULL, NULL, &chain_params, chain_flags, NULL, &cert_chain_ctx)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error getting certificate chain: %s", php_win_err()); + CertFreeCertificateContext(cert_ctx); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); + } - TSRMLS_FETCH(); + /* check if the cert is self-signed */ + if (cert_chain_ctx->cChain > 0 && cert_chain_ctx->rgpChain[0]->cElement > 0 + && (cert_chain_ctx->rgpChain[0]->rgpElement[0]->TrustStatus.dwInfoStatus & CERT_TRUST_IS_SELF_SIGNED) != 0) { + is_self_signed = 1; + } - sslsock->reneg->should_close = 1; + /* check the depth */ + GET_VER_OPT_LONG("verify_depth", allowed_depth); - if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, - "ssl", "reneg_limit_callback", &val) - ) { - zval *param, **params[1], *retval; + for (i = 0; i < cert_chain_ctx->cChain; i++) { + if (cert_chain_ctx->rgpChain[i]->cElement > allowed_depth) { + CertFreeCertificateChain(cert_chain_ctx); + CertFreeCertificateContext(cert_ctx); + RETURN_CERT_VERIFY_FAILURE(X509_V_ERR_CERT_CHAIN_TOO_LONG); + } + } + } - MAKE_STD_ZVAL(param); - php_stream_to_zval(stream, param); - params[0] = ¶m; + { /* Then verify it against a policy */ + SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_policy_params = {sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA)}; + CERT_CHAIN_POLICY_PARA chain_policy_params = {sizeof(CERT_CHAIN_POLICY_PARA)}; + CERT_CHAIN_POLICY_STATUS chain_policy_status = {sizeof(CERT_CHAIN_POLICY_STATUS)}; + LPWSTR server_name = NULL; + BOOL verify_result; + + { /* This looks ridiculous and it is - but we validate the name ourselves using the peer_name + ctx option, so just use the CN from the cert here */ + + X509_NAME *cert_name; + unsigned char *cert_name_utf8; + int index, cert_name_utf8_len; + DWORD num_wchars; + + cert_name = X509_get_subject_name(x509_store_ctx->cert); + index = X509_NAME_get_index_by_NID(cert_name, NID_commonName, -1); + if (index < 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to locate certificate CN"); + CertFreeCertificateChain(cert_chain_ctx); + CertFreeCertificateContext(cert_ctx); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); + } - /* Closing the stream inside this callback would segfault! */ - stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; - if (FAILURE == call_user_function_ex(EG(function_table), NULL, *val, &retval, 1, params, 0, NULL TSRMLS_CC)) { - php_error(E_WARNING, "SSL: failed invoking reneg limit notification callback"); + cert_name_utf8_len = PHP_X509_NAME_ENTRY_TO_UTF8(cert_name, index, cert_name_utf8); + + num_wchars = MultiByteToWideChar(CP_UTF8, 0, (char*)cert_name_utf8, -1, NULL, 0); + if (num_wchars == 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to convert %s to wide character string", cert_name_utf8); + OPENSSL_free(cert_name_utf8); + CertFreeCertificateChain(cert_chain_ctx); + CertFreeCertificateContext(cert_ctx); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); } - stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; - /* If the reneg_limit_callback returned true don't auto-close */ - if (retval != NULL && Z_TYPE_P(retval) == IS_BOOL && Z_BVAL_P(retval) == 1) { - sslsock->reneg->should_close = 0; + server_name = emalloc((num_wchars * sizeof(WCHAR)) + sizeof(WCHAR)); + + num_wchars = MultiByteToWideChar(CP_UTF8, 0, (char*)cert_name_utf8, -1, server_name, num_wchars); + if (num_wchars == 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to convert %s to wide character string", cert_name_utf8); + efree(server_name); + OPENSSL_free(cert_name_utf8); + CertFreeCertificateChain(cert_chain_ctx); + CertFreeCertificateContext(cert_ctx); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); } - FREE_ZVAL(param); - if (retval != NULL) { - zval_ptr_dtor(&retval); + OPENSSL_free(cert_name_utf8); + } + + ssl_policy_params.dwAuthType = (sslsock->is_client) ? AUTHTYPE_SERVER : AUTHTYPE_CLIENT; + ssl_policy_params.pwszServerName = server_name; + chain_policy_params.pvExtraPolicyPara = &ssl_policy_params; + + verify_result = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, cert_chain_ctx, &chain_policy_params, &chain_policy_status); + + efree(server_name); + CertFreeCertificateChain(cert_chain_ctx); + CertFreeCertificateContext(cert_ctx); + + if (!verify_result) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error verifying certificate chain policy: %s", php_win_err()); + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); + } + + if (chain_policy_status.dwError != 0) { + /* The chain does not match the policy */ + if (is_self_signed && chain_policy_status.dwError == CERT_E_UNTRUSTEDROOT + && GET_VER_OPT("allow_self_signed") && zend_is_true(*val)) { + /* allow self-signed certs */ + X509_STORE_CTX_set_error(x509_store_ctx, X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT); + } else { + RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED); } - } else { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "SSL: client-initiated handshake rate limit exceeded by peer"); } } -} -/* }}} */ -static void php_openssl_info_callback(const SSL *ssl, int where, int ret) /* {{{ */ -{ - /* Rate-limit client-initiated handshake renegotiation to prevent DoS */ - if (where & SSL_CB_HANDSHAKE_START) { - limit_handshake_reneg(ssl); - } + return 1; } /* }}} */ +#endif -static inline void init_handshake_limiting(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +static long load_stream_cafile(X509_STORE *cert_store, const char *cafile TSRMLS_DC) /* {{{ */ { - zval **val; - long limit = DEFAULT_RENEG_LIMIT; - long window = DEFAULT_RENEG_WINDOW; + php_stream *stream; + X509 *cert; + BIO *buffer; + int buffer_active = 0; + char *line = NULL; + size_t line_len; + long certs_added = 0; - if (stream->context && - SUCCESS == php_stream_context_get_option(stream->context, - "ssl", "reneg_limit", &val) - ) { - convert_to_long(*val); - limit = Z_LVAL_PP(val); - } + stream = php_stream_open_wrapper(cafile, "rb", 0, NULL); - /* No renegotiation rate-limiting */ - if (limit < 0) { - return; + if (stream == NULL) { + php_error(E_WARNING, "failed loading cafile stream: `%s'", cafile); + return 0; + } else if (stream->wrapper->is_url) { + php_stream_close(stream); + php_error(E_WARNING, "remote cafile streams are disabled for security purposes"); + return 0; } - if (stream->context && - SUCCESS == php_stream_context_get_option(stream->context, - "ssl", "reneg_window", &val) - ) { - convert_to_long(*val); - window = Z_LVAL_PP(val); + cert_start: { + line = php_stream_get_line(stream, NULL, 0, &line_len); + if (line == NULL) { + goto stream_complete; + } else if (!strcmp(line, "-----BEGIN CERTIFICATE-----\n") || + !strcmp(line, "-----BEGIN CERTIFICATE-----\r\n") + ) { + buffer = BIO_new(BIO_s_mem()); + buffer_active = 1; + goto cert_line; + } else { + efree(line); + goto cert_start; + } } - sslsock->reneg = (void*)pemalloc(sizeof(php_openssl_handshake_bucket_t), - php_stream_is_persistent(stream) - ); + cert_line: { + BIO_puts(buffer, line); + efree(line); + line = php_stream_get_line(stream, NULL, 0, &line_len); + if (line == NULL) { + goto stream_complete; + } else if (!strcmp(line, "-----END CERTIFICATE-----") || + !strcmp(line, "-----END CERTIFICATE-----\n") || + !strcmp(line, "-----END CERTIFICATE-----\r\n") + ) { + goto add_cert; + } else { + goto cert_line; + } + } - sslsock->reneg->limit = limit; - sslsock->reneg->window = window; - sslsock->reneg->prev_handshake = 0; - sslsock->reneg->tokens = 0; - sslsock->reneg->should_close = 0; + add_cert: { + BIO_puts(buffer, line); + efree(line); + cert = PEM_read_bio_X509(buffer, NULL, 0, NULL); + BIO_free(buffer); + buffer_active = 0; + if (cert && X509_STORE_add_cert(cert_store, cert)) { + ++certs_added; + } + goto cert_start; + } + + stream_complete: { + php_stream_close(stream); + if (buffer_active == 1) { + BIO_free(buffer); + } + } + + if (certs_added == 0) { + php_error(E_WARNING, "no valid certs found cafile stream: `%s'", cafile); + } + + return certs_added; +} +/* }}} */ + +static int enable_peer_verification(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ +{ + zval **val = NULL; + char *cafile = NULL; + char *capath = NULL; + + GET_VER_OPT_STRING("cafile", cafile); + GET_VER_OPT_STRING("capath", capath); + + if (!cafile) { + cafile = zend_ini_string("openssl.cafile", sizeof("openssl.cafile"), 0); + cafile = strlen(cafile) ? cafile : NULL; + } + + if (!capath) { + capath = zend_ini_string("openssl.capath", sizeof("openssl.capath"), 0); + capath = strlen(capath) ? capath : NULL; + } + + if (cafile || capath) { + if (!SSL_CTX_load_verify_locations(ctx, cafile, capath)) { + if (cafile && !load_stream_cafile(SSL_CTX_get_cert_store(ctx), cafile TSRMLS_CC)) { + return FAILURE; + } + } + } else { +#if defined(PHP_WIN32) && OPENSSL_VERSION_NUMBER >= 0x00907000L + SSL_CTX_set_cert_verify_callback(ctx, win_cert_verify_callback, (void *)stream); + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); +#else + php_openssl_netstream_data_t *sslsock; + sslsock = (php_openssl_netstream_data_t*)stream->abstract; + + if (sslsock->is_client && !SSL_CTX_set_default_verify_paths(ctx)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "Unable to set default verify locations and no CA settings specified"); + return FAILURE; + } +#endif + } + + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback); + + return SUCCESS; +} +/* }}} */ - SSL_CTX_set_info_callback(sslsock->ctx, php_openssl_info_callback); +static void disable_peer_verification(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ +{ + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); } /* }}} */ -static const SSL_METHOD *php_select_crypto_method(long method_value, int is_client TSRMLS_DC) +static int set_local_cert(SSL_CTX *ctx, php_stream *stream TSRMLS_DC) /* {{{ */ +{ + zval **val = NULL; + char *certfile = NULL; + + GET_VER_OPT_STRING("local_cert", certfile); + + if (certfile) { + char resolved_path_buff[MAXPATHLEN]; + const char * private_key = NULL; + + if (VCWD_REALPATH(certfile, resolved_path_buff)) { + /* a certificate to use for authentication */ + if (SSL_CTX_use_certificate_chain_file(ctx, resolved_path_buff) != 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 FAILURE; + } + GET_VER_OPT_STRING("local_pk", private_key); + + if (private_key) { + char resolved_path_buff_pk[MAXPATHLEN]; + if (VCWD_REALPATH(private_key, resolved_path_buff_pk)) { + if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff_pk, SSL_FILETYPE_PEM) != 1) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set private key file `%s'", resolved_path_buff_pk); + return FAILURE; + } + } + } else { + if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff, SSL_FILETYPE_PEM) != 1) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to set private key file `%s'", resolved_path_buff); + return FAILURE; + } + } + +#if OPENSSL_VERSION_NUMBER < 0x10001001L + do { + /* Unnecessary as of OpenSSLv1.0.1 (will segfault if used with >= 10001001 ) */ + X509 *cert = NULL; + EVP_PKEY *key = NULL; + SSL *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); + } while (0); +#endif + if (!SSL_CTX_check_private_key(ctx)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Private key does not match certificate!"); + } + } + } + + return SUCCESS; +} +/* }}} */ + +static const SSL_METHOD *php_select_crypto_method(long method_value, int is_client TSRMLS_DC) /* {{{ */ { if (method_value == STREAM_CRYPTO_METHOD_SSLv2) { #ifndef OPENSSL_NO_SSL2 @@ -461,8 +861,9 @@ static const SSL_METHOD *php_select_crypto_method(long method_value, int is_clie return NULL; } } +/* }}} */ -static long php_get_crypto_method_ctx_flags(long method_flags TSRMLS_DC) +static long php_get_crypto_method_ctx_flags(long method_flags TSRMLS_DC) /* {{{ */ { long ssl_ctx_options = SSL_OP_ALL; @@ -493,191 +894,135 @@ static long php_get_crypto_method_ctx_flags(long method_flags TSRMLS_DC) return ssl_ctx_options; } +/* }}} */ -static inline int php_openssl_setup_crypto(php_stream *stream, - php_openssl_netstream_data_t *sslsock, - php_stream_xport_crypto_param *cparam - TSRMLS_DC) +static void limit_handshake_reneg(const SSL *ssl) /* {{{ */ { - const SSL_METHOD *method; - long ssl_ctx_options; - long method_flags; - zval **val; -#ifdef SSL_MODE_RELEASE_BUFFERS - long mode; -#endif + php_stream *stream; + php_openssl_netstream_data_t *sslsock; + struct timeval now; + long elapsed_time; - if (sslsock->ssl_handle) { - if (sslsock->s.is_blocked) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL/TLS already set-up for this stream"); - return -1; - } else { - return 0; - } + stream = php_openssl_get_stream_from_ssl_handle(ssl); + sslsock = (php_openssl_netstream_data_t*)stream->abstract; + gettimeofday(&now, NULL); + + /* The initial handshake is never rate-limited */ + if (sslsock->reneg->prev_handshake == 0) { + sslsock->reneg->prev_handshake = now.tv_sec; + return; } - /* need to do slightly different things, based on client/server method, - * so lets remember which method was selected */ - sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT; - method_flags = ((cparam->inputs.method >> 1) << 1); + elapsed_time = (now.tv_sec - sslsock->reneg->prev_handshake); + sslsock->reneg->prev_handshake = now.tv_sec; + sslsock->reneg->tokens -= (elapsed_time * (sslsock->reneg->limit / sslsock->reneg->window)); - /* Should we use a specific crypto method or is generic SSLv23 okay? */ - if ((method_flags & (method_flags-1)) == 0) { - ssl_ctx_options = SSL_OP_ALL; - method = php_select_crypto_method(method_flags, sslsock->is_client TSRMLS_CC); - if (method == NULL) { - return -1; - } - } else { - method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); - ssl_ctx_options = php_get_crypto_method_ctx_flags(method_flags TSRMLS_CC); - if (ssl_ctx_options == -1) { - return -1; - } + if (sslsock->reneg->tokens < 0) { + sslsock->reneg->tokens = 0; } + ++sslsock->reneg->tokens; -#if OPENSSL_VERSION_NUMBER >= 0x10001001L - sslsock->ctx = SSL_CTX_new(method); -#else - sslsock->ctx = SSL_CTX_new((SSL_METHOD*)method); -#endif - if (sslsock->ctx == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to create an SSL context"); - return -1; - } + /* The token level exceeds our allowed limit */ + if (sslsock->reneg->tokens > sslsock->reneg->limit) { + zval **val; -#if OPENSSL_VERSION_NUMBER >= 0x0090605fL - ssl_ctx_options &= ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; -#endif - SSL_CTX_set_options(sslsock->ctx, ssl_ctx_options); + TSRMLS_FETCH(); -#if OPENSSL_VERSION_NUMBER >= 0x0090806fL - { - if (stream->context && SUCCESS == php_stream_context_get_option( - stream->context, "ssl", "no_ticket", &val) && - zend_is_true(*val) - ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_NO_TICKET); - } - } -#endif + sslsock->reneg->should_close = 1; -#if OPENSSL_VERSION_NUMBER >= 0x10000000L - { - if (stream->context && (FAILURE == php_stream_context_get_option( - stream->context, "ssl", "disable_compression", &val) || - zend_is_true(*val)) + if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_limit_callback", &val) ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_NO_COMPRESSION); - } - } -#endif + zval *param, **params[1], *retval; - if (!sslsock->is_client && stream->context && SUCCESS == php_stream_context_get_option( - stream->context, "ssl", "honor_cipher_order", &val) && - zend_is_true(*val) - ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); - } + MAKE_STD_ZVAL(param); + php_stream_to_zval(stream, param); + params[0] = ¶m; - if (!sslsock->is_client && stream->context && SUCCESS == php_stream_context_get_option( - stream->context, "ssl", "single_dh_use", &val) && - zend_is_true(*val) - ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_SINGLE_DH_USE); - } + /* Closing the stream inside this callback would segfault! */ + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; + if (FAILURE == call_user_function_ex(EG(function_table), NULL, *val, &retval, 1, params, 0, NULL TSRMLS_CC)) { + php_error(E_WARNING, "SSL: failed invoking reneg limit notification callback"); + } + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; - if (!sslsock->is_client && stream->context && SUCCESS == php_stream_context_get_option( - stream->context, "ssl", "single_ecdh_use", &val) && - zend_is_true(*val) - ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_SINGLE_ECDH_USE); - } + /* If the reneg_limit_callback returned true don't auto-close */ + if (retval != NULL && Z_TYPE_P(retval) == IS_BOOL && Z_BVAL_P(retval) == 1) { + sslsock->reneg->should_close = 0; + } - sslsock->ssl_handle = php_SSL_new_from_context(sslsock->ctx, stream TSRMLS_CC); - if (sslsock->ssl_handle == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to create an SSL handle"); - SSL_CTX_free(sslsock->ctx); - sslsock->ctx = NULL; - return -1; + FREE_ZVAL(param); + if (retval != NULL) { + zval_ptr_dtor(&retval); + } + } else { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "SSL: client-initiated handshake rate limit exceeded by peer"); + } } +} +/* }}} */ - if (!sslsock->is_client && stream->context && SUCCESS == php_stream_context_get_option( - stream->context, "ssl", "honor_cipher_order", &val) && - zend_is_true(*val) - ) { - SSL_CTX_set_options(sslsock->ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); +static void info_callback(const SSL *ssl, int where, int ret) /* {{{ */ +{ + /* Rate-limit client-initiated handshake renegotiation to prevent DoS */ + if (where & SSL_CB_HANDSHAKE_START) { + limit_handshake_reneg(ssl); } +} +/* }}} */ -#ifdef SSL_MODE_RELEASE_BUFFERS - mode = SSL_get_mode(sslsock->ssl_handle); - SSL_set_mode(sslsock->ssl_handle, mode | SSL_MODE_RELEASE_BUFFERS); -#endif +static void init_server_reneg_limit(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +{ + zval **val; + long limit = OPENSSL_DEFAULT_RENEG_LIMIT; + long window = OPENSSL_DEFAULT_RENEG_WINDOW; - if (!sslsock->is_client) { - init_handshake_limiting(stream, sslsock); + if (stream->context && + SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_limit", &val) + ) { + convert_to_long(*val); + limit = Z_LVAL_PP(val); } - if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { - handle_ssl_error(stream, 0, 1 TSRMLS_CC); + /* No renegotiation rate-limiting */ + if (limit < 0) { + return; } - if (cparam->inputs.session) { - if (cparam->inputs.session->ops != &php_openssl_socket_ops) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "supplied session stream must be an SSL enabled stream"); - } else if (((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "supplied SSL session stream is not initialized"); - } else { - SSL_copy_session_id(sslsock->ssl_handle, ((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle); - } + if (stream->context && + SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_window", &val) + ) { + convert_to_long(*val); + window = Z_LVAL_PP(val); } - return 0; -} -static zval *php_capture_ssl_session_meta(SSL *ssl_handle) -{ - zval *meta_arr; - char *proto_str; - long proto = SSL_version(ssl_handle); - const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_handle); - - switch (proto) { -#if OPENSSL_VERSION_NUMBER >= 0x10001001L - case TLS1_2_VERSION: proto_str = "TLSv1.2"; break; - case TLS1_1_VERSION: proto_str = "TLSv1.1"; break; -#endif - case TLS1_VERSION: proto_str = "TLSv1"; break; - case SSL3_VERSION: proto_str = "SSLv3"; break; - case SSL2_VERSION: proto_str = "SSLv2"; break; - default: proto_str = "UNKNOWN"; - } + sslsock->reneg = (void*)pemalloc(sizeof(php_openssl_handshake_bucket_t), + php_stream_is_persistent(stream) + ); - MAKE_STD_ZVAL(meta_arr); - array_init(meta_arr); - add_assoc_string(meta_arr, "protocol", proto_str, 1); - add_assoc_string(meta_arr, "cipher_name", (char *) SSL_CIPHER_get_name(cipher), 1); - add_assoc_long(meta_arr, "cipher_bits", SSL_CIPHER_get_bits(cipher, NULL)); - add_assoc_string(meta_arr, "cipher_version", SSL_CIPHER_get_version(cipher), 1); + sslsock->reneg->limit = limit; + sslsock->reneg->window = window; + sslsock->reneg->prev_handshake = 0; + sslsock->reneg->tokens = 0; + sslsock->reneg->should_close = 0; - return meta_arr; + SSL_set_info_callback(sslsock->ssl_handle, info_callback); } +/* }}} */ -static int php_set_server_rsa_key(php_stream *stream, - php_openssl_netstream_data_t *sslsock - TSRMLS_DC) +static int set_server_rsa_key(php_stream *stream, SSL_CTX *ctx TSRMLS_DC) /* {{{ */ { zval ** val; int rsa_key_size; RSA* rsa; - int retval = 1; if (php_stream_context_get_option(stream->context, "ssl", "rsa_key_size", &val) == SUCCESS) { rsa_key_size = (int) Z_LVAL_PP(val); if ((rsa_key_size != 1) && (rsa_key_size & (rsa_key_size - 1))) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "RSA key size requires a power of 2: %d", - rsa_key_size); - + php_error_docref(NULL TSRMLS_CC, E_WARNING, "RSA key size requires a power of 2: %d", rsa_key_size); rsa_key_size = 2048; } } else { @@ -686,21 +1031,19 @@ static int php_set_server_rsa_key(php_stream *stream, rsa = RSA_generate_key(rsa_key_size, RSA_F4, NULL, NULL); - if (!SSL_set_tmp_rsa(sslsock->ssl_handle, rsa)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Failed setting RSA key"); - retval = 0; + if (!SSL_CTX_set_tmp_rsa(ctx, rsa)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed setting RSA key"); + RSA_free(rsa); + return FAILURE; } RSA_free(rsa); - return retval; + return SUCCESS; } +/* }}} */ - -static int php_set_server_dh_param(php_openssl_netstream_data_t *sslsock, - char *dh_path - TSRMLS_DC) +static int set_server_dh_param(SSL_CTX *ctx, char *dh_path TSRMLS_DC) /* {{{ */ { DH *dh; BIO* bio; @@ -708,124 +1051,322 @@ static int php_set_server_dh_param(php_openssl_netstream_data_t *sslsock, bio = BIO_new_file(dh_path, "r"); if (bio == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Invalid dh_param file: %s", - dh_path); - - return 0; + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid dh_param file: %s", dh_path); + return FAILURE; } dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL); BIO_free(bio); - if (dh == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Failed reading DH params from file: %s", - dh_path); + if (dh == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed reading DH params from file: %s", dh_path); + return FAILURE; + } + + if (SSL_CTX_set_tmp_dh(ctx, dh) < 0) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "DH param assignment failed"); + DH_free(dh); + return FAILURE; + } + + DH_free(dh); + + return SUCCESS; +} +/* }}} */ + +#ifdef HAVE_ECDH +static int set_server_ecdh_curve(php_stream *stream, SSL_CTX *ctx TSRMLS_DC) /* {{{ */ +{ + zval **val; + int curve_nid; + char *curve_str; + EC_KEY *ecdh; + + if (php_stream_context_get_option(stream->context, "ssl", "ecdh_curve", &val) == SUCCESS) { + convert_to_string_ex(val); + curve_str = Z_STRVAL_PP(val); + curve_nid = OBJ_sn2nid(curve_str); + if (curve_nid == NID_undef) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid ECDH curve: %s", curve_str); + return FAILURE; + } + } else { + curve_nid = NID_X9_62_prime256v1; + } + + ecdh = EC_KEY_new_by_curve_name(curve_nid); + if (ecdh == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "Failed generating ECDH curve"); + + return FAILURE; + } + + SSL_CTX_set_tmp_ecdh(ctx, ecdh); + EC_KEY_free(ecdh); + + return SUCCESS; +} +/* }}} */ +#endif + +static int set_server_specific_opts(php_stream *stream, SSL_CTX *ctx TSRMLS_DC) /* {{{ */ +{ + zval **val; + long ssl_ctx_options = SSL_CTX_get_options(ctx); + +#ifdef HAVE_ECDH + if (FAILURE == set_server_ecdh_curve(stream, ctx TSRMLS_CC)) { + return FAILURE; + } +#else + if (SUCCESS == php_stream_context_get_option(stream->context, "ssl", "ecdh_curve", &val)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "ECDH curve support not compiled into the OpenSSL lib against which PHP is linked"); + + return FAILURE; + } +#endif + + if (php_stream_context_get_option(stream->context, "ssl", "dh_param", &val) == SUCCESS) { + convert_to_string_ex(val); + if (FAILURE == set_server_dh_param(ctx, Z_STRVAL_PP(val) TSRMLS_CC)) { + return FAILURE; + } + } + + if (FAILURE == set_server_rsa_key(stream, ctx TSRMLS_CC)) { + return FAILURE; + } + + if (SUCCESS == php_stream_context_get_option( + stream->context, "ssl", "honor_cipher_order", &val) && + zend_is_true(*val) + ) { + ssl_ctx_options |= SSL_OP_CIPHER_SERVER_PREFERENCE; + } + + if (SUCCESS == php_stream_context_get_option( + stream->context, "ssl", "single_dh_use", &val) && + zend_is_true(*val) + ) { + ssl_ctx_options |= SSL_OP_SINGLE_DH_USE; + } + + if (SUCCESS == php_stream_context_get_option( + stream->context, "ssl", "single_ecdh_use", &val) && + zend_is_true(*val) + ) { + ssl_ctx_options |= SSL_OP_SINGLE_ECDH_USE; + } + + SSL_CTX_set_options(ctx, ssl_ctx_options); + + return SUCCESS; +} +/* }}} */ + +#ifdef HAVE_SNI +static void enable_client_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +{ + zval **val; + char *sni_server_name; + + /* If SNI is explicitly disabled we're finished here */ + if (GET_VER_OPT("SNI_enabled") && !zend_is_true(*val)) { + return; + } + + sni_server_name = sslsock->url_name; + + GET_VER_OPT_STRING("peer_name", sni_server_name); + + if (GET_VER_OPT("SNI_server_name")) { + GET_VER_OPT_STRING("SNI_server_name", sni_server_name); + php_error(E_DEPRECATED, "SNI_server_name is deprecated in favor of peer_name"); + } + + if (sni_server_name) { + SSL_set_tlsext_host_name(sslsock->ssl_handle, sni_server_name); + } +} +/* }}} */ +#endif + +int php_openssl_setup_crypto(php_stream *stream, + php_openssl_netstream_data_t *sslsock, + php_stream_xport_crypto_param *cparam + TSRMLS_DC) /* {{{ */ +{ + const SSL_METHOD *method; + long ssl_ctx_options; + long method_flags; + char *cipherlist = NULL; + zval **val; + + if (sslsock->ssl_handle) { + if (sslsock->s.is_blocked) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL/TLS already set-up for this stream"); + return FAILURE; + } else { + return SUCCESS; + } + } + + ERR_clear_error(); + + /* We need to do slightly different things based on client/server method + * so lets remember which method was selected */ + sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT; + method_flags = ((cparam->inputs.method >> 1) << 1); + + /* Should we use a specific crypto method or is generic SSLv23 okay? */ + if ((method_flags & (method_flags-1)) == 0) { + ssl_ctx_options = SSL_OP_ALL; + method = php_select_crypto_method(method_flags, sslsock->is_client TSRMLS_CC); + if (method == NULL) { + return FAILURE; + } + } else { + method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); + ssl_ctx_options = php_get_crypto_method_ctx_flags(method_flags TSRMLS_CC); + if (ssl_ctx_options == -1) { + return FAILURE; + } + } + +#if OPENSSL_VERSION_NUMBER >= 0x10001001L + sslsock->ctx = SSL_CTX_new(method); +#else + /* Avoid const warning with old versions */ + sslsock->ctx = SSL_CTX_new((SSL_METHOD*)method); +#endif - return 0; + if (sslsock->ctx == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL context creation failure"); + return FAILURE; } - if (SSL_set_tmp_dh(sslsock->ssl_handle, dh) < 0) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "DH param assignment failed"); - DH_free(dh); - return 0; +#if OPENSSL_VERSION_NUMBER >= 0x0090806fL + if (GET_VER_OPT("no_ticket") && zend_is_true(*val)) { + ssl_ctx_options |= SSL_OP_NO_TICKET; } +#endif - DH_free(dh); - - return 1; -} +#if OPENSSL_VERSION_NUMBER >= 0x0090605fL + ssl_ctx_options &= ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; +#endif -static int php_enable_server_crypto_opts(php_stream *stream, - php_openssl_netstream_data_t *sslsock - TSRMLS_DC) -{ - zval **val; -#ifdef HAVE_ECDH - int curve_nid; - EC_KEY *ecdh; +#if OPENSSL_VERSION_NUMBER >= 0x10000000L + if (!GET_VER_OPT("disable_compression") || zend_is_true(*val)) { + ssl_ctx_options |= SSL_OP_NO_COMPRESSION; + } #endif - if (php_stream_context_get_option(stream->context, "ssl", "dh_param", &val) == SUCCESS) { - convert_to_string_ex(val); - if (!php_set_server_dh_param(sslsock, Z_STRVAL_PP(val) TSRMLS_CC)) { - return 0; - } + if (GET_VER_OPT("verify_peer") && !zend_is_true(*val)) { + disable_peer_verification(sslsock->ctx, stream TSRMLS_CC); + } else if (FAILURE == enable_peer_verification(sslsock->ctx, stream TSRMLS_CC)) { + return FAILURE; } -#ifdef HAVE_ECDH + /* callback for the passphrase (for localcert) */ + if (GET_VER_OPT("passphrase")) { + SSL_CTX_set_default_passwd_cb_userdata(sslsock->ctx, stream); + SSL_CTX_set_default_passwd_cb(sslsock->ctx, passwd_callback); + } - if (php_stream_context_get_option(stream->context, "ssl", "ecdh_curve", &val) == SUCCESS) { - char *curve_str; - convert_to_string_ex(val); - curve_str = Z_STRVAL_PP(val); - curve_nid = OBJ_sn2nid(curve_str); - if (curve_nid == NID_undef) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Invalid ECDH curve: %s", - curve_str); + GET_VER_OPT_STRING("ciphers", cipherlist); + if (!cipherlist) { + cipherlist = OPENSSL_DEFAULT_STREAM_CIPHERS; + } + if (SSL_CTX_set_cipher_list(sslsock->ctx, cipherlist) != 1) { + return FAILURE; + } - return 0; - } - } else { - curve_nid = NID_X9_62_prime256v1; + if (FAILURE == set_local_cert(sslsock->ctx, stream TSRMLS_CC)) { + return FAILURE; } - ecdh = EC_KEY_new_by_curve_name(curve_nid); - if (ecdh == NULL) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "Failed generating ECDH curve"); + SSL_CTX_set_options(sslsock->ctx, ssl_ctx_options); - return 0; + if (sslsock->is_client == 0 && + stream->context && + FAILURE == set_server_specific_opts(stream, sslsock->ctx TSRMLS_CC) + ) { + return FAILURE; } - SSL_set_tmp_ecdh(sslsock->ssl_handle, ecdh); - EC_KEY_free(ecdh); + sslsock->ssl_handle = SSL_new(sslsock->ctx); + if (sslsock->ssl_handle == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL handle creation failure"); + SSL_CTX_free(sslsock->ctx); + sslsock->ctx = NULL; + return FAILURE; + } else { + SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream); + } -#else + if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { + handle_ssl_error(stream, 0, 1 TSRMLS_CC); + } - if (php_stream_context_get_option(stream->context, "ssl", "ecdh_curve", &val) == SUCCESS) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, - "ECDH curve support not compiled into the OpenSSL lib against which PHP is linked"); + /* Enable server-side handshake renegotiation rate-limiting */ + if (sslsock->is_client == 0) { + init_server_reneg_limit(stream, sslsock); } +#ifdef SSL_MODE_RELEASE_BUFFERS + do { + long mode = SSL_get_mode(sslsock->ssl_handle); + SSL_set_mode(sslsock->ssl_handle, mode | SSL_MODE_RELEASE_BUFFERS); + } while (0); #endif - if (!php_set_server_rsa_key(stream, sslsock TSRMLS_CC)) { - return 0; + if (cparam->inputs.session) { + if (cparam->inputs.session->ops != &php_openssl_socket_ops) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "supplied session stream must be an SSL enabled stream"); + } else if (((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle == NULL) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "supplied SSL session stream is not initialized"); + } else { + SSL_copy_session_id(sslsock->ssl_handle, ((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle); + } } - return 1; + return SUCCESS; } +/* }}} */ -#if OPENSSL_VERSION_NUMBER >= 0x00908070L && !defined(OPENSSL_NO_TLSEXT) -static inline void enable_client_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +static zval *capture_session_meta(SSL *ssl_handle) /* {{{ */ { - zval **val; + zval *meta_arr; + char *proto_str; + long proto = SSL_version(ssl_handle); + const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_handle); - if (stream->context && - (php_stream_context_get_option(stream->context, "ssl", "SNI_enabled", &val) == FAILURE - || zend_is_true(*val)) - ) { - if (php_stream_context_get_option(stream->context, "ssl", "SNI_server_name", &val) == SUCCESS) { - convert_to_string_ex(val); - SSL_set_tlsext_host_name(sslsock->ssl_handle, Z_STRVAL_PP(val)); - } else if (sslsock->url_name) { - SSL_set_tlsext_host_name(sslsock->ssl_handle, sslsock->url_name); - } - } else if (!stream->context && sslsock->url_name) { - SSL_set_tlsext_host_name(sslsock->ssl_handle, sslsock->url_name); + switch (proto) { +#if OPENSSL_VERSION_NUMBER >= 0x10001001L + case TLS1_2_VERSION: proto_str = "TLSv1.2"; break; + case TLS1_1_VERSION: proto_str = "TLSv1.1"; break; +#endif + case TLS1_VERSION: proto_str = "TLSv1"; break; + case SSL3_VERSION: proto_str = "SSLv3"; break; + case SSL2_VERSION: proto_str = "SSLv2"; break; + default: proto_str = "UNKNOWN"; } + + MAKE_STD_ZVAL(meta_arr); + array_init(meta_arr); + add_assoc_string(meta_arr, "protocol", proto_str, 1); + add_assoc_string(meta_arr, "cipher_name", (char *) SSL_CIPHER_get_name(cipher), 1); + add_assoc_long(meta_arr, "cipher_bits", SSL_CIPHER_get_bits(cipher, NULL)); + add_assoc_string(meta_arr, "cipher_version", SSL_CIPHER_get_version(cipher), 1); + + return meta_arr; } /* }}} */ -#endif -static int capture_peer_certs(php_stream *stream, - php_openssl_netstream_data_t *sslsock, - X509 *peer_cert - TSRMLS_DC) +static int capture_peer_certs(php_stream *stream, php_openssl_netstream_data_t *sslsock, X509 *peer_cert TSRMLS_DC) /* {{{ */ { zval **val, *zcert; int cert_captured = 0; @@ -873,8 +1414,9 @@ static int capture_peer_certs(php_stream *stream, return cert_captured; } +/* }}} */ -static inline int php_openssl_enable_crypto(php_stream *stream, +static int php_openssl_enable_crypto(php_stream *stream, php_openssl_netstream_data_t *sslsock, php_stream_xport_crypto_param *cparam TSRMLS_DC) @@ -890,18 +1432,12 @@ static inline int php_openssl_enable_crypto(php_stream *stream, int blocked = sslsock->s.is_blocked, has_timeout = 0; -#if OPENSSL_VERSION_NUMBER >= 0x00908070L && !defined(OPENSSL_NO_TLSEXT) -{ +#ifdef HAVE_SNI if (sslsock->is_client) { enable_client_sni(stream, sslsock); } -} #endif - if (!sslsock->is_client && !php_enable_server_crypto_opts(stream, sslsock TSRMLS_CC)) { - return -1; - } - if (!sslsock->state_set) { if (sslsock->is_client) { SSL_set_connect_state(sslsock->ssl_handle); @@ -910,7 +1446,7 @@ static inline int php_openssl_enable_crypto(php_stream *stream, } sslsock->state_set = 1; } - + if (SUCCESS == php_set_sock_blocking(sslsock->s.socket, 0 TSRMLS_CC)) { sslsock->s.is_blocked = 0; } @@ -984,7 +1520,7 @@ static inline int php_openssl_enable_crypto(php_stream *stream, cert_captured = capture_peer_certs(stream, sslsock, peer_cert TSRMLS_CC); } - if (FAILURE == php_openssl_apply_verification_policy(sslsock->ssl_handle, peer_cert, stream TSRMLS_CC)) { + if (FAILURE == apply_peer_verification_policy(sslsock->ssl_handle, peer_cert, stream TSRMLS_CC)) { SSL_shutdown(sslsock->ssl_handle); n = -1; } else { @@ -997,7 +1533,7 @@ static inline int php_openssl_enable_crypto(php_stream *stream, "ssl", "capture_session_meta", &val) && zend_is_true(*val) ) { - zval *meta_arr = php_capture_ssl_session_meta(sslsock->ssl_handle); + zval *meta_arr = capture_session_meta(sslsock->ssl_handle); php_stream_context_set_option(stream->context, "ssl", "session_meta", meta_arr); zval_dtor(meta_arr); efree(meta_arr); @@ -1029,6 +1565,154 @@ static inline int php_openssl_enable_crypto(php_stream *stream, return -1; } +static size_t php_openssl_sockop_write(php_stream *stream, const char *buf, size_t count TSRMLS_DC) /* {{{ */ +{ + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; + int didwrite; + + if (sslsock->ssl_active) { + int retry = 1; + + do { + didwrite = SSL_write(sslsock->ssl_handle, buf, count); + + if (didwrite <= 0) { + retry = handle_ssl_error(stream, didwrite, 0 TSRMLS_CC); + } else { + break; + } + } while(retry); + + if (didwrite > 0) { + php_stream_notify_progress_increment(stream->context, didwrite, 0); + } + } else { + didwrite = php_stream_socket_ops.write(stream, buf, count TSRMLS_CC); + } + + if (didwrite < 0) { + didwrite = 0; + } + + return didwrite; +} +/* }}} */ + +static size_t php_openssl_sockop_read(php_stream *stream, char *buf, size_t count TSRMLS_DC) /* {{{ */ +{ + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; + int nr_bytes = 0; + + if (sslsock->ssl_active) { + int retry = 1; + + do { + nr_bytes = SSL_read(sslsock->ssl_handle, buf, count); + + if (sslsock->reneg && sslsock->reneg->should_close) { + /* renegotiation rate limiting triggered */ + php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR TSRMLS_CC); + nr_bytes = 0; + stream->eof = 1; + break; + } else if (nr_bytes <= 0) { + retry = handle_ssl_error(stream, nr_bytes, 0 TSRMLS_CC); + stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle)); + + } else { + /* we got the data */ + break; + } + } while (retry); + + if (nr_bytes > 0) { + php_stream_notify_progress_increment(stream->context, nr_bytes, 0); + } + } + else + { + nr_bytes = php_stream_socket_ops.read(stream, buf, count TSRMLS_CC); + } + + if (nr_bytes < 0) { + nr_bytes = 0; + } + + return nr_bytes; +} +/* }}} */ + +static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_DC) /* {{{ */ +{ + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; +#ifdef PHP_WIN32 + int n; +#endif + unsigned i; + + if (close_handle) { + if (sslsock->ssl_active) { + SSL_shutdown(sslsock->ssl_handle); + sslsock->ssl_active = 0; + } + if (sslsock->ssl_handle) { + SSL_free(sslsock->ssl_handle); + sslsock->ssl_handle = NULL; + } + if (sslsock->ctx) { + SSL_CTX_free(sslsock->ctx); + sslsock->ctx = NULL; + } +#ifdef PHP_WIN32 + if (sslsock->s.socket == -1) + sslsock->s.socket = SOCK_ERR; +#endif + if (sslsock->s.socket != SOCK_ERR) { +#ifdef PHP_WIN32 + /* prevent more data from coming in */ + shutdown(sslsock->s.socket, SHUT_RD); + + /* try to make sure that the OS sends all data before we close the connection. + * Essentially, we are waiting for the socket to become writeable, which means + * that all pending data has been sent. + * We use a small timeout which should encourage the OS to send the data, + * but at the same time avoid hanging indefinitely. + * */ + do { + n = php_pollfd_for_ms(sslsock->s.socket, POLLOUT, 500); + } while (n == -1 && php_socket_errno() == EINTR); +#endif + closesocket(sslsock->s.socket); + sslsock->s.socket = SOCK_ERR; + } + } + + if (sslsock->url_name) { + pefree(sslsock->url_name, php_stream_is_persistent(stream)); + } + + if (sslsock->reneg) { + pefree(sslsock->reneg, php_stream_is_persistent(stream)); + } + + pefree(sslsock, php_stream_is_persistent(stream)); + + return 0; +} +/* }}} */ + +static int php_openssl_sockop_flush(php_stream *stream TSRMLS_DC) /* {{{ */ +{ + return php_stream_socket_ops.flush(stream TSRMLS_CC); +} +/* }}} */ + +static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb TSRMLS_DC) /* {{{ */ +{ + return php_stream_socket_ops.stat(stream, ssb TSRMLS_CC); +} +/* }}} */ + static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_netstream_data_t *sock, php_stream_xport_param *xparam STREAMS_DC TSRMLS_DC) { @@ -1092,6 +1776,7 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ return xparam->outputs.client == NULL ? -1 : 0; } + static int php_openssl_sockop_set_option(php_stream *stream, int option, int value, void *ptrparam TSRMLS_DC) { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract;