/* * Copyright (C) 1996-2023 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ /* DEBUG: section 83 SSL accelerator support */ #include "squid.h" #include "ssl/support.h" /* support.cc says this is needed */ #if USE_OPENSSL #include "comm.h" #include "fd.h" #include "fde.h" #include "globals.h" #include "ip/Address.h" #include "parser/BinaryTokenizer.h" #include "SquidTime.h" #include "ssl/bio.h" #if _SQUID_WINDOWS_ extern int socket_read_method(int, char *, int); extern int socket_write_method(int, const char *, int); #endif /* BIO callbacks */ static int squid_bio_write(BIO *h, const char *buf, int num); static int squid_bio_read(BIO *h, char *buf, int size); static int squid_bio_puts(BIO *h, const char *str); //static int squid_bio_gets(BIO *h, char *str, int size); static long squid_bio_ctrl(BIO *h, int cmd, long arg1, void *arg2); static int squid_bio_create(BIO *h); static int squid_bio_destroy(BIO *data); /* SSL callbacks */ static void squid_ssl_info(const SSL *ssl, int where, int ret); #if HAVE_LIBCRYPTO_BIO_METH_NEW static BIO_METHOD *SquidMethods = nullptr; #else /// Initialization structure for the BIO table with /// Squid-specific methods and BIO method wrappers. static BIO_METHOD SquidMethods = { BIO_TYPE_SOCKET, "squid", squid_bio_write, squid_bio_read, squid_bio_puts, NULL, // squid_bio_gets not supported squid_bio_ctrl, squid_bio_create, squid_bio_destroy, NULL // squid_callback_ctrl not supported }; #endif BIO * Ssl::Bio::Create(const int fd, Security::Io::Type type) { #if HAVE_LIBCRYPTO_BIO_METH_NEW if (!SquidMethods) { SquidMethods = BIO_meth_new(BIO_TYPE_SOCKET, "squid"); BIO_meth_set_write(SquidMethods, squid_bio_write); BIO_meth_set_read(SquidMethods, squid_bio_read); BIO_meth_set_puts(SquidMethods, squid_bio_puts); BIO_meth_set_gets(SquidMethods, NULL); BIO_meth_set_ctrl(SquidMethods, squid_bio_ctrl); BIO_meth_set_create(SquidMethods, squid_bio_create); BIO_meth_set_destroy(SquidMethods, squid_bio_destroy); } BIO_METHOD *useMethod = SquidMethods; #else BIO_METHOD *useMethod = &SquidMethods; #endif if (BIO *bio = BIO_new(useMethod)) { BIO_int_ctrl(bio, BIO_C_SET_FD, type, fd); return bio; } return NULL; } void Ssl::Bio::Link(SSL *ssl, BIO *bio) { SSL_set_bio(ssl, bio, bio); // cannot fail SSL_set_info_callback(ssl, &squid_ssl_info); // does not provide diagnostic } Ssl::Bio::Bio(const int anFd): fd_(anFd) { debugs(83, 7, "Bio constructed, this=" << this << " FD " << fd_); } Ssl::Bio::~Bio() { debugs(83, 7, "Bio destructing, this=" << this << " FD " << fd_); } int Ssl::Bio::write(const char *buf, int size, BIO *table) { errno = 0; #if _SQUID_WINDOWS_ const int result = socket_write_method(fd_, buf, size); #else const int result = default_write_method(fd_, buf, size); #endif const int xerrno = errno; debugs(83, 5, "FD " << fd_ << " wrote " << result << " <= " << size); BIO_clear_retry_flags(table); if (result < 0) { const bool ignoreError = ignoreErrno(xerrno) != 0; debugs(83, 5, "error: " << xerrno << " ignored: " << ignoreError); if (ignoreError) BIO_set_retry_write(table); } return result; } int Ssl::Bio::read(char *buf, int size, BIO *table) { errno = 0; #if _SQUID_WINDOWS_ const int result = socket_read_method(fd_, buf, size); #else const int result = default_read_method(fd_, buf, size); #endif const int xerrno = errno; debugs(83, 5, "FD " << fd_ << " read " << result << " <= " << size); BIO_clear_retry_flags(table); if (result < 0) { const bool ignoreError = ignoreErrno(xerrno) != 0; debugs(83, 5, "error: " << xerrno << " ignored: " << ignoreError); if (ignoreError) BIO_set_retry_read(table); } return result; } /// Called whenever the SSL connection state changes, an alert appears, or an /// error occurs. See SSL_set_info_callback(). void Ssl::Bio::stateChanged(const SSL *ssl, int where, int ret) { // Here we can use (where & STATE) to check the current state. // Many STATE values are possible, including: SSL_CB_CONNECT_LOOP, // SSL_CB_ACCEPT_LOOP, SSL_CB_HANDSHAKE_START, and SSL_CB_HANDSHAKE_DONE. // For example: // if (where & SSL_CB_HANDSHAKE_START) // debugs(83, 9, "Trying to establish the SSL connection"); // else if (where & SSL_CB_HANDSHAKE_DONE) // debugs(83, 9, "SSL connection established"); debugs(83, 7, "FD " << fd_ << " now: 0x" << std::hex << where << std::dec << ' ' << SSL_state_string(ssl) << " (" << SSL_state_string_long(ssl) << ")"); } Ssl::ClientBio::ClientBio(const int anFd): Bio(anFd), holdRead_(false), holdWrite_(false), helloSize(0), abortReason(nullptr) { renegotiations.configure(10*1000); } void Ssl::ClientBio::stateChanged(const SSL *ssl, int where, int ret) { Ssl::Bio::stateChanged(ssl, where, ret); // detect client-initiated renegotiations DoS (CVE-2011-1473) if (where & SSL_CB_HANDSHAKE_START) { const int reneg = renegotiations.count(1); if (abortReason) return; // already decided and informed the admin if (reneg > RenegotiationsLimit) { abortReason = "renegotiate requests flood"; debugs(83, DBG_IMPORTANT, "Terminating TLS connection [from " << fd_table[fd_].ipaddr << "] due to " << abortReason << ". This connection received " << reneg << " renegotiate requests in the last " << RenegotiationsWindow << " seconds (and " << renegotiations.remembered() << " requests total)."); } } } int Ssl::ClientBio::write(const char *buf, int size, BIO *table) { if (abortReason) { debugs(83, 3, "BIO on FD " << fd_ << " is aborted"); BIO_clear_retry_flags(table); return -1; } if (holdWrite_) { BIO_set_retry_write(table); return 0; } return Ssl::Bio::write(buf, size, table); } int Ssl::ClientBio::read(char *buf, int size, BIO *table) { if (abortReason) { debugs(83, 3, "BIO on FD " << fd_ << " is aborted"); BIO_clear_retry_flags(table); return -1; } if (holdRead_) { debugs(83, 7, "Hold flag is set, retry latter. (Hold " << size << "bytes)"); BIO_set_retry_read(table); return -1; } if (!rbuf.isEmpty()) { int bytes = (size <= (int)rbuf.length() ? size : rbuf.length()); memcpy(buf, rbuf.rawContent(), bytes); rbuf.consume(bytes); return bytes; } else return Ssl::Bio::read(buf, size, table); return -1; } Ssl::ServerBio::ServerBio(const int anFd): Bio(anFd), helloMsgSize(0), helloBuild(false), allowSplice(false), allowBump(false), holdWrite_(false), record_(false), parsedHandshake(false), parseError(false), bumpMode_(bumpNone), rbufConsumePos(0), parser_(Security::HandshakeParser::fromServer) { } void Ssl::ServerBio::stateChanged(const SSL *ssl, int where, int ret) { Ssl::Bio::stateChanged(ssl, where, ret); } void Ssl::ServerBio::setClientFeatures(Security::TlsDetails::Pointer const &details, SBuf const &aHello) { clientTlsDetails = details; clientSentHello = aHello; }; int Ssl::ServerBio::read(char *buf, int size, BIO *table) { if (parsedHandshake) // done parsing TLS Hello return readAndGive(buf, size, table); else return readAndParse(buf, size, table); } /// Read and give everything to OpenSSL. int Ssl::ServerBio::readAndGive(char *buf, const int size, BIO *table) { // If we have unused buffered bytes, give those bytes to OpenSSL now, // before reading more. TODO: Read if we have buffered less than size? if (rbufConsumePos < rbuf.length()) return giveBuffered(buf, size); if (record_) { const int result = readAndBuffer(table); if (result <= 0) return result; return giveBuffered(buf, size); } return Ssl::Bio::read(buf, size, table); } /// Read and give everything to our parser. /// When/if parsing is finished (successfully or not), start giving to OpenSSL. int Ssl::ServerBio::readAndParse(char *buf, const int size, BIO *table) { const int result = readAndBuffer(table); if (result <= 0) return result; try { if (!parser_.parseHello(rbuf)) { // need more data to finish parsing BIO_set_retry_read(table); return -1; } parsedHandshake = true; // done parsing (successfully) } catch (const std::exception &ex) { debugs(83, 2, "parsing error on FD " << fd_ << ": " << ex.what()); parsedHandshake = true; // done parsing (due to an error) parseError = true; } return giveBuffered(buf, size); } /// Reads more data into the read buffer. Returns either the number of bytes /// read or, on errors (including "try again" errors), a negative number. int Ssl::ServerBio::readAndBuffer(BIO *table) { char *space = rbuf.rawAppendStart(SQUID_TCP_SO_RCVBUF); const int result = Ssl::Bio::read(space, SQUID_TCP_SO_RCVBUF, table); if (result <= 0) return result; rbuf.rawAppendFinish(space, result); return result; } /// give previously buffered bytes to OpenSSL /// returns the number of bytes given int Ssl::ServerBio::giveBuffered(char *buf, const int size) { if (rbuf.length() <= rbufConsumePos) return -1; // buffered nothing yet const int unsent = rbuf.length() - rbufConsumePos; const int bytes = (size <= unsent ? size : unsent); memcpy(buf, rbuf.rawContent() + rbufConsumePos, bytes); rbufConsumePos += bytes; debugs(83, 7, bytes << "<=" << size << " bytes to OpenSSL"); return bytes; } // This function makes the required checks to examine if the client hello // message is compatible with the features provided by OpenSSL toolkit. // If the features are compatible and can be supported it tries to rewrite SSL // structure members, to replace the hello message created by openSSL, with the // web client SSL hello message. // This is mostly possible in the cases where the web client uses openSSL // library similar with this one used by squid. static bool adjustSSL(SSL *ssl, Security::TlsDetails::Pointer const &details, SBuf &helloMessage) { #if SQUID_USE_OPENSSL_HELLO_OVERWRITE_HACK if (!details) return false; if (!ssl->s3) { debugs(83, 5, "No SSLv3 data found!"); return false; } // If the client supports compression but our context does not support // we can not adjust. #if !defined(OPENSSL_NO_COMP) const bool requireCompression = (details->compressionSupported && ssl->ctx->comp_methods == nullptr); #else const bool requireCompression = details->compressionSupported; #endif if (requireCompression) { debugs(83, 5, "Client Hello Data supports compression, but we do not!"); return false; } #if !defined(SSL_TLSEXT_HB_ENABLED) if (details->doHeartBeats) { debugs(83, 5, "Client Hello Data supports HeartBeats but we do not support!"); return false; } #endif if (details->unsupportedExtensions) { debugs(83, 5, "Client Hello contains extensions that we do not support!"); return false; } SSL3_BUFFER *wb=&(ssl->s3->wbuf); if (wb->len < (size_t)helloMessage.length()) { debugs(83, 5, "Client Hello exceeds OpenSSL buffer: " << helloMessage.length() << " >= " << wb->len); return false; } /* Check whether all on-the-wire ciphers are supported by OpenSSL. */ const auto &wireCiphers = details->ciphers; Security::TlsDetails::Ciphers::size_type ciphersToFind = wireCiphers.size(); // RFC 5746: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV is not a true cipher suite". // It is commonly seen on the wire, including in from-OpenSSL traffic, but // SSL_get_ciphers() does not return this _pseudo_ cipher suite in my tests. // If OpenSSL supports scsvCipher, we count it (at most once) further below. #if defined(TLSEXT_TYPE_renegotiate) // the 0x00FFFF mask converts 3-byte OpenSSL cipher to our 2-byte cipher const uint16_t scsvCipher = SSL3_CK_SCSV & 0x00FFFF; #else const uint16_t scsvCipher = 0; #endif STACK_OF(SSL_CIPHER) *cipher_stack = SSL_get_ciphers(ssl); const int supportedCipherCount = sk_SSL_CIPHER_num(cipher_stack); for (int idx = 0; idx < supportedCipherCount && ciphersToFind > 0; ++idx) { const SSL_CIPHER *cipher = sk_SSL_CIPHER_value(cipher_stack, idx); const auto id = SSL_CIPHER_get_id(cipher) & 0x00FFFF; if (wireCiphers.find(id) != wireCiphers.end() && (!scsvCipher || id != scsvCipher)) --ciphersToFind; } if (ciphersToFind > 0 && scsvCipher && wireCiphers.find(scsvCipher) != wireCiphers.end()) --ciphersToFind; if (ciphersToFind > 0) { // TODO: Add slowlyReportUnsupportedCiphers() to slowly find and report each of them debugs(83, 5, "Client Hello Data has " << ciphersToFind << " ciphers that we do not support!"); return false; } debugs(83, 5, "OpenSSL SSL struct will be adjusted to mimic client hello data!"); //Adjust ssl structure data. // We need to fix the random in SSL struct: if (details->clientRandom.length() == SSL3_RANDOM_SIZE) memcpy(ssl->s3->client_random, details->clientRandom.c_str(), SSL3_RANDOM_SIZE); memcpy(wb->buf, helloMessage.rawContent(), helloMessage.length()); wb->left = helloMessage.length(); size_t mainHelloSize = helloMessage.length() - 5; const char *mainHello = helloMessage.rawContent() + 5; assert((size_t)ssl->init_buf->max > mainHelloSize); memcpy(ssl->init_buf->data, mainHello, mainHelloSize); debugs(83, 5, "Hello Data init and adjustd sizes :" << ssl->init_num << " = "<< mainHelloSize); ssl->init_num = mainHelloSize; ssl->s3->wpend_ret = mainHelloSize; ssl->s3->wpend_tot = mainHelloSize; return true; #else return false; #endif } int Ssl::ServerBio::write(const char *buf, int size, BIO *table) { if (holdWrite_) { debugs(83, 7, "postpone writing " << size << " bytes to SSL FD " << fd_); BIO_set_retry_write(table); return -1; } if (!helloBuild && (bumpMode_ == Ssl::bumpPeek || bumpMode_ == Ssl::bumpStare)) { // buf contains OpenSSL-generated ClientHello. We assume it has a // complete ClientHello and nothing else, but cannot fully verify // that quickly. We only verify that buf starts with a v3+ record // containing ClientHello. Must(size >= 2); // enough for version and content_type checks below Must(buf[1] >= 3); // record's version.major; determines buf[0] meaning Must(buf[0] == 22); // TLSPlaintext.content_type == handshake in v3+ //Hello message is the first message we write to server assert(helloMsg.isEmpty()); if (auto ssl = fd_table[fd_].ssl.get()) { if (bumpMode_ == Ssl::bumpPeek) { // we should not be here if we failed to parse the client-sent ClientHello Must(!clientSentHello.isEmpty()); if (adjustSSL(ssl, clientTlsDetails, clientSentHello)) allowBump = true; allowSplice = true; // Replace OpenSSL-generated ClientHello with client-sent one. helloMsg.append(clientSentHello); debugs(83, 7, "FD " << fd_ << ": Using client-sent ClientHello for peek mode"); } else { /*Ssl::bumpStare*/ allowBump = true; if (!clientSentHello.isEmpty() && adjustSSL(ssl, clientTlsDetails, clientSentHello)) { allowSplice = true; helloMsg.append(clientSentHello); debugs(83, 7, "FD " << fd_ << ": Using client-sent ClientHello for stare mode"); } } } // if we did not use the client-sent ClientHello, then use the OpenSSL-generated one if (helloMsg.isEmpty()) helloMsg.append(buf, size); helloBuild = true; helloMsgSize = helloMsg.length(); //allowBump = true; if (allowSplice) { // Do not write yet..... BIO_set_retry_write(table); return -1; } } if (!helloMsg.isEmpty()) { debugs(83, 7, "buffered write for FD " << fd_); int ret = Ssl::Bio::write(helloMsg.rawContent(), helloMsg.length(), table); helloMsg.consume(ret); if (!helloMsg.isEmpty()) { // We need to retry sendind data. // Say to openSSL to retry sending hello message BIO_set_retry_write(table); return -1; } // Sending hello message complete. Do not send more data for now... holdWrite_ = true; // spoof openSSL that we write what it ask us to write return size; } else return Ssl::Bio::write(buf, size, table); } void Ssl::ServerBio::flush(BIO *table) { if (!helloMsg.isEmpty()) { int ret = Ssl::Bio::write(helloMsg.rawContent(), helloMsg.length(), table); helloMsg.consume(ret); } } bool Ssl::ServerBio::resumingSession() { return parser_.resumingSession; } bool Ssl::ServerBio::encryptedCertificates() const { return parser_.details->tlsSupportedVersion && Security::Tls1p3orLater(parser_.details->tlsSupportedVersion); } /// initializes BIO table after allocation static int squid_bio_create(BIO *bi) { #if !HAVE_LIBCRYPTO_BIO_GET_INIT bi->init = 0; // set when we store Bio object and socket fd (BIO_C_SET_FD) bi->num = 0; bi->flags = 0; #else // No need to set more, openSSL initialize BIO memory to zero. #endif BIO_set_data(bi, NULL); return 1; } /// cleans BIO table before deallocation static int squid_bio_destroy(BIO *table) { delete static_cast(BIO_get_data(table)); BIO_set_data(table, NULL); return 1; } /// wrapper for Bio::write() static int squid_bio_write(BIO *table, const char *buf, int size) { Ssl::Bio *bio = static_cast(BIO_get_data(table)); assert(bio); return bio->write(buf, size, table); } /// wrapper for Bio::read() static int squid_bio_read(BIO *table, char *buf, int size) { Ssl::Bio *bio = static_cast(BIO_get_data(table)); assert(bio); return bio->read(buf, size, table); } /// implements puts() via write() static int squid_bio_puts(BIO *table, const char *str) { assert(str); return squid_bio_write(table, str, strlen(str)); } /// other BIO manipulations (those without dedicated callbacks in BIO table) static long squid_bio_ctrl(BIO *table, int cmd, long arg1, void *arg2) { debugs(83, 5, table << ' ' << cmd << '(' << arg1 << ", " << arg2 << ')'); switch (cmd) { case BIO_C_SET_FD: { assert(arg2); const int fd = *static_cast(arg2); Ssl::Bio *bio; if (arg1 == Security::Io::BIO_TO_SERVER) bio = new Ssl::ServerBio(fd); else bio = new Ssl::ClientBio(fd); assert(!BIO_get_data(table)); BIO_set_data(table, bio); BIO_set_init(table, 1); return 0; } case BIO_C_GET_FD: if (BIO_get_init(table)) { Ssl::Bio *bio = static_cast(BIO_get_data(table)); assert(bio); if (arg2) *static_cast(arg2) = bio->fd(); return bio->fd(); } return -1; case BIO_CTRL_DUP: // Should implemented if the SSL_dup openSSL API function // used anywhere in squid. return 0; case BIO_CTRL_FLUSH: if (BIO_get_init(table)) { Ssl::Bio *bio = static_cast(BIO_get_data(table)); assert(bio); bio->flush(table); return 1; } return 0; /* we may also need to implement these: case BIO_CTRL_RESET: case BIO_C_FILE_SEEK: case BIO_C_FILE_TELL: case BIO_CTRL_INFO: case BIO_CTRL_GET_CLOSE: case BIO_CTRL_SET_CLOSE: case BIO_CTRL_PENDING: case BIO_CTRL_WPENDING: */ default: return 0; } return 0; /* NOTREACHED */ } /// wrapper for Bio::stateChanged() static void squid_ssl_info(const SSL *ssl, int where, int ret) { if (BIO *table = SSL_get_rbio(ssl)) { if (Ssl::Bio *bio = static_cast(BIO_get_data(table))) bio->stateChanged(ssl, where, ret); } } void applyTlsDetailsToSSL(SSL *ssl, Security::TlsDetails::Pointer const &details, Ssl::BumpMode bumpMode) { // To increase the possibility for bumping after peek mode selection or // splicing after stare mode selection it is good to set the // SSL protocol version. // The SSL_set_ssl_method is wrong here because it will restrict the // permitted transport version to be identical to the version used in the // ClientHello message. // For example will prevent comunnicating with a tls1.0 server if the // client sent and tlsv1.2 Hello message. #if defined(TLSEXT_NAMETYPE_host_name) if (!details->serverName.isEmpty()) { SSL_set_tlsext_host_name(ssl, details->serverName.c_str()); } #endif if (!details->ciphers.empty()) { SBuf strCiphers; for (auto cipherId: details->ciphers) { unsigned char cbytes[3]; cbytes[0] = (cipherId >> 8) & 0xFF; cbytes[1] = cipherId & 0xFF; cbytes[2] = 0; if (const auto c = SSL_CIPHER_find(ssl, cbytes)) { if (!strCiphers.isEmpty()) strCiphers.append(":"); strCiphers.append(SSL_CIPHER_get_name(c)); } } if (!strCiphers.isEmpty()) SSL_set_cipher_list(ssl, strCiphers.c_str()); } #if defined(SSL_OP_NO_COMPRESSION) /* XXX: OpenSSL 0.9.8k lacks SSL_OP_NO_COMPRESSION */ if (!details->compressionSupported) SSL_set_options(ssl, SSL_OP_NO_COMPRESSION); #endif #if defined(SSL_OP_NO_TLSv1_3) // avoid "inappropriate fallback" OpenSSL error messages if (details->tlsSupportedVersion && Security::Tls1p2orEarlier(details->tlsSupportedVersion)) SSL_set_options(ssl, SSL_OP_NO_TLSv1_3); #endif #if defined(TLSEXT_STATUSTYPE_ocsp) if (details->tlsStatusRequest) SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp); #endif #if defined(TLSEXT_TYPE_application_layer_protocol_negotiation) if (!details->tlsAppLayerProtoNeg.isEmpty()) { if (bumpMode == Ssl::bumpPeek) SSL_set_alpn_protos(ssl, (const unsigned char*)details->tlsAppLayerProtoNeg.rawContent(), details->tlsAppLayerProtoNeg.length()); else { static const unsigned char supported_protos[] = {8, 'h','t','t', 'p', '/', '1', '.', '1'}; SSL_set_alpn_protos(ssl, supported_protos, sizeof(supported_protos)); } } #endif } #endif // USE_OPENSSL