Arquivos
hhvm/hphp/runtime/base/file/ssl_socket.cpp
T
smith 98466ea3fd Litstr must die, episode I.
Converted lots of callsites to StaticString, removed a few
dead overloaded litstr functions.
2013-04-17 10:12:48 -07:00

700 linhas
20 KiB
C++

/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010- Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| 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. |
+----------------------------------------------------------------------+
*/
#include <runtime/base/file/ssl_socket.h>
#include <runtime/base/complex_types.h>
#include <util/util.h>
#include <poll.h>
namespace HPHP {
///////////////////////////////////////////////////////////////////////////////
StaticString SSLSocket::s_class_name("SSLSocket");
StaticString Certificate::s_class_name("OpenSSL X.509");
///////////////////////////////////////////////////////////////////////////////
Mutex SSLSocket::s_mutex;
int SSLSocket::s_ex_data_index = -1;
int SSLSocket::GetSSLExDataIndex() {
if (s_ex_data_index >= 0) {
return s_ex_data_index;
}
Lock lock(s_mutex);
if (s_ex_data_index < 0) {
s_ex_data_index = SSL_get_ex_new_index(0, (void*)"PHP stream index",
nullptr, nullptr, nullptr);
assert(s_ex_data_index >= 0);
}
return s_ex_data_index;
}
static const StaticString s_allow_self_signed("allow_self_signed");
static const StaticString s_verify_depth("verify_depth");
static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
int ret = preverify_ok;
/* determine the status for the current cert */
X509_STORE_CTX_get_current_cert(ctx);
int err = X509_STORE_CTX_get_error(ctx);
int depth = X509_STORE_CTX_get_error_depth(ctx);
/* conjure the stream & context to use */
SSL *ssl = (SSL*)X509_STORE_CTX_get_ex_data
(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
SSLSocket *stream =
(SSLSocket*)SSL_get_ex_data(ssl, SSLSocket::GetSSLExDataIndex());
/* if allow_self_signed is set, make sure that verification succeeds */
if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT &&
stream->getContext()[s_allow_self_signed].toBoolean()) {
ret = 1;
}
/* check the depth */
Variant vdepth = stream->getContext()[s_verify_depth];
if (vdepth.toBoolean() && depth > vdepth.toInt64()) {
ret = 0;
X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_CHAIN_TOO_LONG);
}
return ret;
}
static const StaticString s_passphrase("passphrase");
static int passwd_callback(char *buf, int num, int verify, void *data) {
/* TODO: could expand this to make a callback into PHP user-space */
SSLSocket *stream = (SSLSocket *)data;
String passphrase = stream->getContext()[s_passphrase];
if (!passphrase.empty() && passphrase.size() < num - 1) {
memcpy(buf, passphrase.data(), passphrase.size() + 1);
return passphrase.size();
}
return 0;
}
static const StaticString s_verify_peer("verify_peer");
static const StaticString s_cafile("cafile");
static const StaticString s_capath("capath");
static const StaticString s_ciphers("ciphers");
static const StaticString s_local_cert("local_cert");
SSL *SSLSocket::createSSL(SSL_CTX *ctx) {
ERR_clear_error();
/* look at options in the stream and set appropriate verification flags */
if (m_context[s_verify_peer].toBoolean()) {
/* turn on verification callback */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
/* CA stuff */
String cafile = m_context[s_cafile];
String capath = m_context[s_capath];
if (!cafile.empty() || !capath.empty()) {
if (!SSL_CTX_load_verify_locations(ctx, cafile.data(), capath.data())) {
raise_warning("Unable to set verify locations `%s' `%s'",
cafile.data(), capath.data());
return nullptr;
}
}
int64_t depth = m_context[s_verify_depth].toInt64();
if (depth) {
SSL_CTX_set_verify_depth(ctx, depth);
}
} else {
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
}
/* callback for the passphrase (for localcert) */
if (!m_context[s_passphrase].toString().empty()) {
SSL_CTX_set_default_passwd_cb_userdata(ctx, this);
SSL_CTX_set_default_passwd_cb(ctx, passwd_callback);
}
String cipherlist = m_context[s_ciphers].toString();
if (cipherlist.empty()) {
cipherlist = "DEFAULT";
}
SSL_CTX_set_cipher_list(ctx, cipherlist.data());
String certfile = m_context[s_local_cert].toString();
if (!certfile.empty()) {
String resolved_path_buff = File::TranslatePath(certfile);
if (!resolved_path_buff.empty()) {
/* a certificate to use for authentication */
if (SSL_CTX_use_certificate_chain_file(ctx, resolved_path_buff.data())
!= 1) {
raise_warning("Unable to set local cert chain file `%s'; Check "
"that your cafile/capath settings include details of "
"your certificate and its issuer", certfile.data());
return nullptr;
}
if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff.data(),
SSL_FILETYPE_PEM) != 1) {
raise_warning("Unable to set private key file `%s'",
resolved_path_buff.data());
return nullptr;
}
SSL *tmpssl = SSL_new(ctx);
X509 *cert = SSL_get_certificate(tmpssl);
if (cert) {
EVP_PKEY *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)) {
raise_warning("Private key does not match certificate!");
}
}
}
SSL *ssl = SSL_new(ctx);
if (ssl) {
SSL_set_ex_data(ssl, GetSSLExDataIndex(), this); /* map SSL => stream */
}
return ssl;
}
///////////////////////////////////////////////////////////////////////////////
// constructors and destructor
SSLSocket::SSLSocket()
: m_handle(nullptr), m_ssl_active(false), m_method((CryptoMethod)-1),
m_client(false), m_connect_timeout(0), m_enable_on_connect(false),
m_state_set(false), m_is_blocked(true) {
}
SSLSocket::SSLSocket(int sockfd, int type, const char *address /* = NULL */,
int port /* = 0 */)
: Socket(sockfd, type, address, port),
m_handle(nullptr), m_ssl_active(false), m_method((CryptoMethod)-1),
m_client(false), m_connect_timeout(0), m_enable_on_connect(false),
m_state_set(false), m_is_blocked(true) {
}
SSLSocket::~SSLSocket() {
m_context.reset(); // for sweeping
closeImpl();
}
bool SSLSocket::onConnect() {
return setupCrypto() && enableCrypto();
}
bool SSLSocket::onAccept() {
if (m_fd >= 0 && m_enable_on_connect) {
switch (m_method) {
case ClientSSLv23: m_method = ServerSSLv23; break;
case ClientSSLv2: m_method = ServerSSLv2; break;
case ClientSSLv3: m_method = ServerSSLv3; break;
case ClientTLS: m_method = ServerTLS; break;
default:
assert(false);
}
if (setupCrypto() && enableCrypto()) {
return true;
}
raise_warning("Failed to enable crypto");
close();
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
bool SSLSocket::handleError(int64_t nr_bytes, bool is_init) {
char esbuf[512];
string ebuf;
unsigned long ecode;
bool retry = true;
int err = SSL_get_error(m_handle, nr_bytes);
switch (err) {
case SSL_ERROR_ZERO_RETURN:
/* SSL terminated (but socket may still be active) */
retry = false;
break;
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
/* re-negotiation, or perhaps the SSL layer needs more
* packets: retry in next iteration */
errno = EAGAIN;
retry = (is_init || m_is_blocked);
break;
case SSL_ERROR_SYSCALL:
if (ERR_peek_error() == 0) {
if (nr_bytes == 0) {
if (ERR_get_error()) {
raise_warning("SSL: fatal protocol error");
}
SSL_set_shutdown(m_handle, SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN);
m_eof = true;
retry = false;
} else {
string estr = Util::safe_strerror(errno);
raise_warning("SSL: %s", estr.c_str());
retry = false;
}
break;
}
/* fall through */
default:
/* some other error */
ecode = ERR_get_error();
switch (ERR_GET_REASON(ecode)) {
case SSL_R_NO_SHARED_CIPHER:
raise_warning("SSL_R_NO_SHARED_CIPHER: no suitable shared cipher "
"could be used. This could be because the server is "
"missing an SSL certificate (local_cert context "
"option)");
retry = false;
break;
default:
do {
// NULL is automatically added
ERR_error_string_n(ecode, esbuf, sizeof(esbuf));
if (!ebuf.empty()) {
ebuf += '\n';
}
ebuf += esbuf;
} while ((ecode = ERR_get_error()) != 0);
raise_warning("SSL operation failed with code %d. %s%s",
err, !ebuf.empty() ? "OpenSSL Error messages:\n" : "",
!ebuf.empty() ? ebuf.c_str() : "");
}
retry = false;
errno = 0;
}
return retry;
}
///////////////////////////////////////////////////////////////////////////////
SSLSocket *SSLSocket::Create(const char *&name, int port, double timeout) {
CryptoMethod method;
if (strncmp(name, "ssl://", 6) == 0) {
name += 6;
method = ClientSSLv23;
} else if (strncmp(name, "sslv2://", 8) == 0) {
name += 8;
method = ClientSSLv2;
} else if (strncmp(name, "sslv3://", 8) == 0) {
name += 8;
method = ClientSSLv3;
} else if (strncmp(name, "tls://", 6) == 0) {
name += 6;
method = ClientTLS;
} else {
return nullptr;
}
int domain = AF_INET;
int type = SOCK_STREAM;
SSLSocket *sock = new SSLSocket(socket(domain, type, 0), domain, name, port);
sock->m_method = method;
sock->m_connect_timeout = timeout;
sock->m_enable_on_connect = true;
return sock;
}
bool SSLSocket::close() {
return closeImpl();
}
bool SSLSocket::closeImpl() {
if (m_ssl_active) {
SSL_shutdown(m_handle);
m_ssl_active = false;
}
if (m_handle) {
SSL_free(m_handle);
m_handle = nullptr;
}
return Socket::closeImpl();
}
int64_t SSLSocket::readImpl(char *buffer, int64_t length) {
int64_t nr_bytes = 0;
if (m_ssl_active) {
bool retry = true;
do {
if (m_is_blocked) {
Socket::waitForData();
if (m_timedOut) {
break;
}
// could get here and we only have parts of an SSL packet
}
nr_bytes = SSL_read(m_handle, buffer, length);
if (nr_bytes > 0) break; /* we got the data */
retry = handleError(nr_bytes, false);
m_eof = (!retry && errno != EAGAIN && !SSL_pending(m_handle));
} while (retry);
} else {
nr_bytes = Socket::readImpl(buffer, length);
}
return nr_bytes < 0 ? 0 : nr_bytes;
}
int64_t SSLSocket::writeImpl(const char *buffer, int64_t length) {
int didwrite;
if (m_ssl_active) {
bool retry = true;
do {
didwrite = SSL_write(m_handle, buffer, length);
if (didwrite > 0) break;
retry = handleError(didwrite, false);
} while (retry);
} else {
didwrite = Socket::writeImpl(buffer, length);
}
return didwrite < 0 ? 0 : didwrite;
}
bool SSLSocket::setupCrypto(SSLSocket *session /* = NULL */) {
if (m_handle) {
raise_warning("SSL/TLS already set-up for this stream");
return false;
}
/* need to do slightly different things, based on client/server method,
* so lets remember which method was selected */
#if OPENSSL_VERSION_NUMBER < 0x00909000L
SSL_METHOD *smethod;
#else
const SSL_METHOD *smethod;
#endif
switch (m_method) {
case ClientSSLv23: m_client = true; smethod = SSLv23_client_method(); break;
case ClientSSLv3: m_client = true; smethod = SSLv3_client_method(); break;
case ClientTLS: m_client = true; smethod = TLSv1_client_method(); break;
case ServerSSLv23: m_client = false; smethod = SSLv23_server_method(); break;
case ServerSSLv3: m_client = false; smethod = SSLv3_server_method(); break;
/* SSLv2 protocol might be disabled in the OpenSSL library */
#ifndef OPENSSL_NO_SSL2
case ClientSSLv2: m_client = true; smethod = SSLv2_client_method(); break;
case ServerSSLv2: m_client = false; smethod = SSLv2_server_method(); break;
#else
case ClientSSLv2:
case ServerSSLv2:
raise_warning("OpenSSL library does not support SSL2 protocol");
return false;
break;
#endif
case ServerTLS: m_client = false; smethod = TLSv1_server_method(); break;
default:
return false;
}
SSL_CTX *ctx = SSL_CTX_new(smethod);
if (ctx == nullptr) {
raise_warning("failed to create an SSL context");
return false;
}
SSL_CTX_set_options(ctx, SSL_OP_ALL);
m_handle = createSSL(ctx);
if (m_handle == nullptr) {
raise_warning("failed to create an SSL handle");
SSL_CTX_free(ctx);
return false;
}
if (!SSL_set_fd(m_handle, m_fd)) {
handleError(0, true);
}
if (session) {
SSL_copy_session_id(m_handle, session->m_handle);
}
return true;
}
static const StaticString s_CN_match("CN_match");
bool SSLSocket::applyVerificationPolicy(X509 *peer) {
/* verification is turned off */
if (!m_context[s_verify_peer].toBoolean()) {
return true;
}
if (peer == nullptr) {
raise_warning("Could not get peer certificate");
return false;
}
int err = SSL_get_verify_result(m_handle);
switch (err) {
case X509_V_OK:
/* fine */
break;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
if (m_context[s_allow_self_signed].toBoolean()) {
/* allowed */
break;
}
/* not allowed, so fall through */
default:
raise_warning("Could not verify peer: code:%d %s", err,
X509_verify_cert_error_string(err));
return false;
}
/* if the cert passed the usual checks, apply our own local policies now */
/* Does the common name match ? (used primarily for https://) */
String cnmatch = m_context[s_CN_match].toString();
if (!cnmatch.empty()) {
X509_NAME *name = X509_get_subject_name(peer);
char buf[1024];
int name_len = X509_NAME_get_text_by_NID(name, NID_commonName, buf,
sizeof(buf));
if (name_len < 0) {
raise_warning("Unable to locate peer certificate CN");
return false;
} else if (name_len != (int)strlen(buf)) {
raise_warning("Peer certificate CN=`%.*s' is malformed", name_len, buf);
return false;
}
bool match = (strcmp(cnmatch, buf) == 0);
if (!match && strlen(buf) > 3 && buf[0] == '*' && buf[1] == '.') {
/* Try wildcard */
if (strchr(buf+2, '.')) {
const char *tmp = strstr(cnmatch, buf+1);
match = tmp && strcmp(tmp, buf+2) && tmp == strchr(cnmatch, '.');
}
}
if (!match) {
/* didn't match */
raise_warning("Peer certificate CN=`%.*s' did not match expected CN=`%s'",
name_len, buf, cnmatch.data());
return false;
}
}
return true;
}
static const StaticString s_capture_peer_cert("capture_peer_cert");
static const StaticString s_peer_certificate("peer_certificate");
static const StaticString s_capture_peer_cert_chain("capture_peer_cert_chain");
static const StaticString s_peer_certificate_chain("peer_certificate_chain");
bool SSLSocket::enableCrypto(bool activate /* = true */) {
if (activate && !m_ssl_active) {
double timeout = m_connect_timeout;
bool blocked = m_is_blocked;
if (!m_state_set) {
if (m_client) {
SSL_set_connect_state(m_handle);
} else {
SSL_set_accept_state(m_handle);
}
m_state_set = true;
}
if (m_client && setBlocking(false)) {
m_is_blocked = false;
}
int n;
bool retry = true;
do {
if (m_client) {
struct timeval tvs, tve;
struct timezone tz;
gettimeofday(&tvs, &tz);
n = SSL_connect(m_handle);
gettimeofday(&tve, &tz);
timeout -= (tve.tv_sec + (double) tve.tv_usec / 1000000) -
(tvs.tv_sec + (double) tvs.tv_usec / 1000000);
if (timeout < 0) {
raise_warning("SSL: connection timeout");
return -1;
}
} else {
n = SSL_accept(m_handle);
}
if (n <= 0) {
retry = handleError(n, true);
} else {
break;
}
} while (retry);
if (m_client && m_is_blocked != blocked && setBlocking(blocked)) {
m_is_blocked = blocked;
}
if (n == 1) {
X509 *peer_cert = SSL_get_peer_certificate(m_handle);
if (!applyVerificationPolicy(peer_cert)) {
SSL_shutdown(m_handle);
} else {
m_ssl_active = true;
/* allow the script to capture the peer cert
* and/or the certificate chain */
if (m_context[s_capture_peer_cert].toBoolean()) {
Object cert(new Certificate(peer_cert));
m_context.set(s_peer_certificate, cert);
peer_cert = nullptr;
}
if (m_context[s_capture_peer_cert_chain].toBoolean()) {
Array arr;
STACK_OF(X509) *chain = SSL_get_peer_cert_chain(m_handle);
if (chain) {
for (int i = 0; i < sk_X509_num(chain); i++) {
X509 *mycert = X509_dup(sk_X509_value(chain, i));
arr.append(Object(new Certificate(mycert)));
}
}
m_context.set(s_peer_certificate_chain, arr);
}
}
if (peer_cert) {
X509_free(peer_cert);
}
} else {
n = errno == EAGAIN ? 0 : -1;
}
return n >= 0;
} else if (!activate && m_ssl_active) {
/* deactivate - common for server/client */
SSL_shutdown(m_handle);
m_ssl_active = false;
}
return true;
}
bool SSLSocket::checkLiveness() {
if (m_fd == -1) {
return false;
}
pollfd p;
p.fd = m_fd;
p.events = POLLIN | POLLERR | POLLHUP | POLLPRI;
p.revents = 0;
if (poll(&p, 1, 0) > 0 && p.revents > 0) {
char buf;
if (m_ssl_active) {
while (true) {
int n = SSL_peek(m_handle, &buf, sizeof(buf));
if (n <= 0) {
int err = SSL_get_error(m_handle, n);
if (err == SSL_ERROR_SYSCALL) {
return errno == EAGAIN;
}
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
/* re-negotiate */
continue;
}
/* any other problem is a fatal error */
return false;
}
/* either peek succeeded or there was an error; we
* have set the alive flag appropriately */
break;
}
} else if (0 == recv(m_fd, &buf, sizeof(buf), MSG_PEEK) &&
errno != EAGAIN) {
return false;
}
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Certificate
BIO *Certificate::ReadData(CVarRef var, bool *file /* = NULL */) {
if (var.isString() || var.isObject()) {
String svar = var.toString();
if (svar.substr(0, 7) == "file://") {
if (file) *file = true;
BIO *ret = BIO_new_file((char*)svar.substr(7).data(), "r");
if (ret == nullptr) {
raise_warning("error opening the file, %s", svar.data());
}
return ret;
}
if (file) *file = false;
return BIO_new_mem_buf((char*)svar.data(), svar.size());
}
return nullptr;
}
Object Certificate::Get(CVarRef var) {
if (var.isResource()) {
return var.toObject();
}
if (var.isString() || var.isObject()) {
bool file;
BIO *in = ReadData(var, &file);
if (in == nullptr) return Object();
X509 *cert;
/*
if (file) {
cert = PEM_read_bio_X509(in, NULL, NULL, NULL);
} else {
cert = (X509 *)PEM_ASN1_read_bio
((char *(*)())d2i_X509, PEM_STRING_X509, in, NULL, NULL, NULL);
}
*/
cert = PEM_read_bio_X509(in, nullptr, nullptr, nullptr);
BIO_free(in);
if (cert) {
return Object(new Certificate(cert));
}
}
return Object();
}
///////////////////////////////////////////////////////////////////////////////
}