From: Christos Tsantilas Date: Tue, 5 May 2015 09:09:27 +0000 (+0300) Subject: Secure ICAP X-Git-Tag: merge-candidate-3-v1~139 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1b091aec81e98bd1ca1d23139b7360cd6d6ee083;p=thirdparty%2Fsquid.git Secure ICAP This patch adds support for ICAP services that require SSL/TLS transport connections. The same options used for the cache_peer directive are used for the icap_service directive, with similar certificate validation logic. To mark an ICAP service as "secure", use an "icaps://" service URI scheme when listing your service via an icap_service directive. The industry is using a "Secure ICAP" term, and Squid follows that convention, but "icaps" seems more appropriate for a _scheme_ name. Squid uses port 11344 for Secure ICAP by default, following another popular proxy convention. The old 1344 default for plain ICAP ports has not changed. Technical Details ================== This patch: - Splits Ssl::PeerConnector class into Ssl::PeerConnector parent and two kids: Ssl::BlindPeerConnector, a basic SSL connector for cache_peers, and Ssl::PeekingPeerConnector, a peek-and-splice SSL connector for HTTP servers. - Adds a third Ssl::IcapPeerConnector kid to connect to Secure ICAP servers. - Fixes ErrorState class to avoid crashes on nil ErrorState::request member. (Ssl::IcapPeerConnector may generate an ErrorState with a nil request). - Modifies the ACL peername to use the Secure ICAP server name as value while connecting to an ICAP server. This is useful to make SSL certificate policies based on ICAP server name. However, this change is undocumented until we decide whether a dedicated ACL would be better. This is a Measurement Factory project. --- diff --git a/src/FwdState.cc b/src/FwdState.cc index 8a23111de1..95c64fbfb8 100644 --- a/src/FwdState.cc +++ b/src/FwdState.cc @@ -695,8 +695,8 @@ FwdState::connectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, in FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this)); // Use positive timeout when less than one second is left. const time_t sslNegotiationTimeout = max(static_cast(1), timeLeft()); - Ssl::PeerConnector *connector = - new Ssl::PeerConnector(requestPointer, serverConnection(), clientConn, callback, sslNegotiationTimeout); + Ssl::PeekingPeerConnector *connector = + new Ssl::PeekingPeerConnector(requestPointer, serverConnection(), clientConn, callback, sslNegotiationTimeout); AsyncJob::Start(connector); // will call our callback return; } @@ -1251,7 +1251,7 @@ getOutgoingAddress(HttpRequest * request, Comm::ConnectionPointer conn) } ACLFilledChecklist ch(NULL, request, NULL); - ch.dst_peer = conn->getPeer(); + ch.dst_peer_name = conn->getPeer() ? conn->getPeer()->name : NULL; ch.dst_addr = conn->remote; // TODO use the connection details in ACL. diff --git a/src/PeerPoolMgr.cc b/src/PeerPoolMgr.cc index 52d9bb236e..49cf370aa7 100644 --- a/src/PeerPoolMgr.cc +++ b/src/PeerPoolMgr.cc @@ -129,8 +129,8 @@ PeerPoolMgr::handleOpenedConnection(const CommConnectCbParams ¶ms) const int timeUsed = squid_curtime - params.conn->startTime(); // Use positive timeout when less than one second is left for conn. const int timeLeft = max(1, (peerTimeout - timeUsed)); - Ssl::PeerConnector *connector = - new Ssl::PeerConnector(request, params.conn, NULL, securer, timeLeft); + Ssl::BlindPeerConnector *connector = + new Ssl::BlindPeerConnector(request, params.conn, securer, timeLeft); AsyncJob::Start(connector); // will call our callback return; } diff --git a/src/acl/FilledChecklist.cc b/src/acl/FilledChecklist.cc index 0db8f54f9a..b66f609904 100644 --- a/src/acl/FilledChecklist.cc +++ b/src/acl/FilledChecklist.cc @@ -23,7 +23,6 @@ CBDATA_CLASS_INIT(ACLFilledChecklist); ACLFilledChecklist::ACLFilledChecklist() : - dst_peer(NULL), dst_rdns(NULL), request (NULL), reply (NULL), @@ -136,7 +135,6 @@ ACLFilledChecklist::markSourceDomainChecked() * checkCallback() will delete the list (i.e., self). */ ACLFilledChecklist::ACLFilledChecklist(const acl_access *A, HttpRequest *http_request, const char *ident): - dst_peer(NULL), dst_rdns(NULL), request(NULL), reply(NULL), diff --git a/src/acl/FilledChecklist.h b/src/acl/FilledChecklist.h index 09c06f511a..641272513e 100644 --- a/src/acl/FilledChecklist.h +++ b/src/acl/FilledChecklist.h @@ -67,7 +67,7 @@ public: Ip::Address src_addr; Ip::Address dst_addr; Ip::Address my_addr; - CachePeer *dst_peer; + SBuf dst_peer_name; char *dst_rdns; HttpRequest *request; diff --git a/src/acl/PeerName.cc b/src/acl/PeerName.cc index 816e66d050..247b9c4e7b 100644 --- a/src/acl/PeerName.cc +++ b/src/acl/PeerName.cc @@ -16,8 +16,8 @@ int ACLPeerNameStrategy::match (ACLData * &data, ACLFilledChecklist *checklist, ACLFlags &) { - if (checklist->dst_peer != NULL && checklist->dst_peer->name != NULL) - return data->match(checklist->dst_peer->name); + if (!checklist->dst_peer_name.isEmpty()) + return data->match(checklist->dst_peer_name.c_str()); return 0; } diff --git a/src/adaptation/ServiceConfig.cc b/src/adaptation/ServiceConfig.cc index 8cbce85e42..3b4338dc4d 100644 --- a/src/adaptation/ServiceConfig.cc +++ b/src/adaptation/ServiceConfig.cc @@ -127,6 +127,18 @@ Adaptation::ServiceConfig::parse() else if (strcmp(name, "on-overload") == 0) { grokked = grokOnOverload(onOverload, value); onOverloadSet = true; + } else if (strncmp(name, "ssl", 3) == 0 || strncmp(name, "tls-", 4) == 0) { +#if !USE_OPENSSL + debugs(3, DBG_PARSE_NOTE(DBG_IMPORTANT), "WARNING: adaptation option '" << token << "' requires --with-openssl. ICAP service option ignored."); +#else + // name prefix is "ssl" or "tls-" + std::string tmp = name + (name[0] == 's' ? 3 : 4); + tmp += "="; + tmp += value; + secure.parse(tmp.c_str()); + secure.encryptTransport = true; + grokked = true; +#endif } else grokked = grokExtension(name, value); @@ -214,6 +226,10 @@ Adaptation::ServiceConfig::grokUri(const char *value) } host.limitInit(s, len); +#if USE_OPENSSL + if (secure.sslDomain.isEmpty()) + secure.sslDomain.assign(host.rawBuf(), host.size()); +#endif s = e; port = -1; diff --git a/src/adaptation/ServiceConfig.h b/src/adaptation/ServiceConfig.h index 71df6c3287..b1e51df4be 100644 --- a/src/adaptation/ServiceConfig.h +++ b/src/adaptation/ServiceConfig.h @@ -11,6 +11,7 @@ #include "adaptation/Elements.h" #include "base/RefCount.h" +#include "security/PeerOptions.h" #include "SquidString.h" namespace Adaptation @@ -47,6 +48,9 @@ public: bool routing; ///< whether this service may determine the next service(s) bool ipv6; ///< whether this service uses IPv6 transport (default IPv4) + // security settings for adaptation service + Security::PeerOptions secure; + protected: Method parseMethod(const char *buf) const; VectPoint parseVectPoint(const char *buf) const; diff --git a/src/adaptation/icap/ServiceRep.cc b/src/adaptation/icap/ServiceRep.cc index 0c8053e9da..221fe4d768 100644 --- a/src/adaptation/icap/ServiceRep.cc +++ b/src/adaptation/icap/ServiceRep.cc @@ -26,6 +26,9 @@ #include "SquidConfig.h" #include "SquidTime.h" +#define DEFAULT_ICAP_PORT 1344 +#define DEFAULT_ICAPS_PORT 11344 + CBDATA_NAMESPACED_CLASS_INIT(Adaptation::Icap, ServiceRep); Adaptation::Icap::ServiceRep::ServiceRep(const ServiceConfigPointer &svcCfg): @@ -59,15 +62,28 @@ Adaptation::Icap::ServiceRep::finalize() // use /etc/services or default port if needed const bool have_port = cfg().port >= 0; if (!have_port) { - struct servent *serv = getservbyname("icap", "tcp"); + struct servent *serv; + if (cfg().protocol.caseCmp("icaps") == 0) + serv = getservbyname("icaps", "tcp"); + else + serv = getservbyname("icap", "tcp"); if (serv) { writeableCfg().port = htons(serv->s_port); } else { - writeableCfg().port = 1344; + writeableCfg().port = cfg().protocol.caseCmp("icaps") == 0 ? DEFAULT_ICAPS_PORT : DEFAULT_ICAP_PORT; } } + if (cfg().protocol.caseCmp("icaps") == 0) + writeableCfg().secure.encryptTransport = true; + + if (cfg().secure.encryptTransport) { + debugs(3, DBG_IMPORTANT, "Initializing service " << cfg().resource << " SSL context"); + sslContext = writeableCfg().secure.createContext(true); + } + + theSessionFailures.configure(TheConfig.oldest_service_failure > 0 ? TheConfig.oldest_service_failure : -1); } diff --git a/src/adaptation/icap/ServiceRep.h b/src/adaptation/icap/ServiceRep.h index 13102de0d3..a03b9ad8d0 100644 --- a/src/adaptation/icap/ServiceRep.h +++ b/src/adaptation/icap/ServiceRep.h @@ -110,6 +110,11 @@ public: // treat these as private, they are for callbacks only // receive either an ICAP OPTIONS response header or an abort message virtual void noteAdaptationAnswer(const Answer &answer); + Security::ContextPointer sslContext; +#if USE_OPENSSL + SSL_SESSION *sslSession; +#endif + private: // stores Prepare() callback info diff --git a/src/adaptation/icap/Xaction.cc b/src/adaptation/icap/Xaction.cc index 62afdccc7a..215fbaba0f 100644 --- a/src/adaptation/icap/Xaction.cc +++ b/src/adaptation/icap/Xaction.cc @@ -9,6 +9,7 @@ /* DEBUG: section 93 ICAP (RFC 3507) Client */ #include "squid.h" +#include "acl/FilledChecklist.h" #include "adaptation/icap/Config.h" #include "adaptation/icap/Launcher.h" #include "adaptation/icap/Xaction.h" @@ -21,6 +22,7 @@ #include "CommCalls.h" #include "err_detail_type.h" #include "fde.h" +#include "globals.h" #include "FwdState.h" #include "HttpMsg.h" #include "HttpReply.h" @@ -31,6 +33,45 @@ #include "SquidConfig.h" #include "SquidTime.h" +#if USE_OPENSSL +/// Gives Ssl::PeerConnector access to Answer in the PeerPoolMgr callback dialer. +class MyIcapAnswerDialer: public UnaryMemFunT, + public Ssl::PeerConnector::CbDialer +{ +public: + MyIcapAnswerDialer(const JobPointer &aJob, Method aMethod): + UnaryMemFunT(aJob, aMethod, Security::EncryptorAnswer()) {} + + /* Ssl::PeerConnector::CbDialer API */ + virtual Security::EncryptorAnswer &answer() { return arg1; } +}; + +namespace Ssl +{ +/// A simple PeerConnector for Secure ICAP services. No SslBump capabilities. +class IcapPeerConnector: public PeerConnector { + CBDATA_CLASS(IcapPeerConnector); +public: + IcapPeerConnector( + Adaptation::Icap::ServiceRep::Pointer &service, + const Comm::ConnectionPointer &aServerConn, + AsyncCall::Pointer &aCallback, const time_t timeout = 0): + AsyncJob("Ssl::IcapPeerConnector"), + PeerConnector(aServerConn, aCallback, timeout), icapService(service) {} + + /* PeerConnector API */ + virtual SSL *initializeSsl(); + virtual void noteNegotiationDone(ErrorState *error); + virtual SSL_CTX *getSslContext() {return icapService->sslContext; } + +private: + Adaptation::Icap::ServiceRep::Pointer icapService; +}; +} // namespace Ssl + +CBDATA_NAMESPACED_CLASS_INIT(Ssl, IcapPeerConnector); +#endif + Adaptation::Icap::Xaction::Xaction(const char *aTypeName, Adaptation::Icap::ServiceRep::Pointer &aService): AsyncJob(aTypeName), Adaptation::Initiate(aTypeName), @@ -252,6 +293,23 @@ void Adaptation::Icap::Xaction::noteCommConnected(const CommConnectCbParams &io) CloseDialer(this,&Adaptation::Icap::Xaction::noteCommClosed)); comm_add_close_handler(io.conn->fd, closer); +#if USE_OPENSSL + // If it is a reused connection and the SSL object is build + // we should not negotiate new SSL session + SSL *ssl = fd_table[io.conn->fd].ssl; + if (!ssl && service().cfg().secure.encryptTransport) { + CbcPointer me(this); + securer = asyncCall(93, 4, "Adaptation::Icap::Xaction::handleSecuredPeer", + MyIcapAnswerDialer(me, &Adaptation::Icap::Xaction::handleSecuredPeer)); + + Ssl::PeerConnector::HttpRequestPointer tmpReq(NULL); + Ssl::IcapPeerConnector *sslConnector = + new Ssl::IcapPeerConnector(theService, io.conn, securer, TheConfig.connect_timeout(service().cfg().bypass)); + AsyncJob::Start(sslConnector); // will call our callback + return; + } +#endif + // ?? fd_table[io.conn->fd].noteUse(icapPconnPool); service().noteConnectionUse(connection); @@ -323,6 +381,10 @@ void Adaptation::Icap::Xaction::handleCommTimedout() // unexpected connection close while talking to the ICAP service void Adaptation::Icap::Xaction::noteCommClosed(const CommCloseCbParams &) { + if (securer != NULL) { + securer->cancel("Connection closed before SSL negotiation finished"); + securer = NULL; + } closer = NULL; handleCommClosed(); } @@ -351,7 +413,7 @@ void Adaptation::Icap::Xaction::callEnd() bool Adaptation::Icap::Xaction::doneAll() const { - return !connector && !reader && !writer && Adaptation::Initiate::doneAll(); + return !connector && !securer && !reader && !writer && Adaptation::Initiate::doneAll(); } void Adaptation::Icap::Xaction::updateTimeout() @@ -637,3 +699,73 @@ bool Adaptation::Icap::Xaction::fillVirginHttpHeader(MemBuf &) const return false; } +#if USE_OPENSSL +SSL * +Ssl::IcapPeerConnector::initializeSsl() +{ + SSL *ssl = Ssl::PeerConnector::initializeSsl(); + if (!ssl) + return NULL; + + assert(!icapService->cfg().secure.sslDomain.isEmpty()); + SBuf *host = new SBuf(icapService->cfg().secure.sslDomain); + SSL_set_ex_data(ssl, ssl_ex_index_server, host); + + ACLFilledChecklist *check = (ACLFilledChecklist *)SSL_get_ex_data(ssl, ssl_ex_index_cert_error_check); + if (check) + check->dst_peer_name = *host; + + if (icapService->sslSession) + SSL_set_session(ssl, icapService->sslSession); + + return ssl; +} + +void +Ssl::IcapPeerConnector::noteNegotiationDone(ErrorState *error) +{ + if (error) + return; + + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + assert(ssl); + if (!SSL_session_reused(ssl)) { + if (icapService->sslSession) + SSL_SESSION_free(icapService->sslSession); + icapService->sslSession = SSL_get1_session(ssl); + } +} + +void +Adaptation::Icap::Xaction::handleSecuredPeer(Security::EncryptorAnswer &answer) +{ + Must(securer != NULL); + securer = NULL; + + if (closer != NULL) { + if (answer.conn != NULL) + comm_remove_close_handler(answer.conn->fd, closer); + else + closer->cancel("securing completed"); + closer = NULL; + } + + if (answer.error.get()) { + if (answer.conn != NULL) + answer.conn->close(); + debugs(93, 2, typeName << + " SSL negotiation to " << service().cfg().uri << " failed"); + service().noteConnectionFailed("failure"); + detailError(ERR_DETAIL_ICAP_XACT_SSL_START); + throw TexcHere("cannot connect to the SSL ICAP service"); + } + + debugs(93, 5, "SSL negotiation to " << service().cfg().uri << " complete"); + + service().noteConnectionUse(answer.conn); + + handleCommConnected(); +} +#endif + diff --git a/src/adaptation/icap/Xaction.h b/src/adaptation/icap/Xaction.h index 2f5945741b..2f2134e23c 100644 --- a/src/adaptation/icap/Xaction.h +++ b/src/adaptation/icap/Xaction.h @@ -17,6 +17,7 @@ #include "HttpReply.h" #include "ipcache.h" #include "SBuf.h" +#include "ssl/PeerConnector.h" class MemBuf; @@ -72,6 +73,7 @@ protected: virtual void handleCommTimedout(); virtual void handleCommClosed(); + void handleSecuredPeer(Security::EncryptorAnswer &answer); /// record error detail if possible virtual void detailError(int) {} @@ -153,6 +155,7 @@ protected: private: Comm::ConnOpener *cs; + AsyncCall::Pointer securer; ///< whether we are securing a connection }; } // namespace Icap diff --git a/src/cf.data.pre b/src/cf.data.pre index 642ca00cc4..a044926c9f 100644 --- a/src/cf.data.pre +++ b/src/cf.data.pre @@ -8370,6 +8370,12 @@ DOC_START uri: icap://servername:port/servicepath ICAP server and service location. + icaps://servername:port/servicepath + The "icap:" URI scheme is used for traditional ICAP server and + service location (default port is 1344, connections are not + encrypted). The "icaps:" URI scheme is for Secure ICAP + services that use SSL/TLS-encrypted ICAP connections (by + default, on port 11344). ICAP does not allow a single service to handle both REQMOD and RESPMOD transactions. Squid does not enforce that requirement. You can specify @@ -8435,12 +8441,81 @@ DOC_START Use the given number as the Max-Connections limit, regardless of the Max-Connections value given by the service, if any. + ==== SSL / ICAPS / TLS OPTIONS ==== + + These options are used for Secure ICAP (icaps://....) services only. + + sslcert=/path/to/ssl/certificate + A client SSL certificate to use when connecting to + this icap server. + + sslkey=/path/to/ssl/key + The private SSL key corresponding to sslcert above. + If 'sslkey' is not specified 'sslcert' is assumed to + reference a combined file containing both the + certificate and the key. + + sslversion=1|3|4|5|6 + The SSL version to use when connecting to this icap + server + 1 = automatic (default) + 3 = SSL v3 only + 4 = TLS v1.0 only + 5 = TLS v1.1 only + 6 = TLS v1.2 only + + sslcipher=... The list of valid SSL ciphers to use when connecting + to this icap server. + + ssloptions=... Specify various SSL implementation options: + + NO_SSLv3 Disallow the use of SSLv3 + NO_TLSv1 Disallow the use of TLSv1.0 + NO_TLSv1_1 Disallow the use of TLSv1.1 + NO_TLSv1_2 Disallow the use of TLSv1.2 + SINGLE_DH_USE + Always create a new key when using + temporary/ephemeral DH key exchanges + ALL Enable various bug workarounds + suggested as "harmless" by OpenSSL + Be warned that this reduces SSL/TLS + strength to some attacks. + + See the OpenSSL SSL_CTX_set_options documentation for a + more complete list. + + sslcafile=... A file containing additional CA certificates to use + when verifying the icap server certificate. + + sslcapath=... A directory containing additional CA certificates to + use when verifying the icap server certificate. + + sslcrlfile=... A certificate revocation list file to use when + verifying the icap server certificate. + + sslflags=... Specify various flags modifying the SSL implementation: + + DONT_VERIFY_PEER + Accept certificates even if they fail to + verify. + NO_DEFAULT_CA + Don't use the default CA list built in + to OpenSSL. + DONT_VERIFY_DOMAIN + Don't verify the icap server certificate + matches the server name + + ssldomain= The icap server name as advertised in it's certificate. + Used for verifying the correctness of the received icap + server certificate. If not specified the icap server + hostname extracted from ICAP URI will be used. + Older icap_service format without optional named parameters is deprecated but supported for backward compatibility. Example: icap_service svcBlocker reqmod_precache icap://icap1.mydomain.net:1344/reqmod bypass=0 -icap_service svcLogger reqmod_precache icap://icap2.mydomain.net:1344/respmod routing=on +icap_service svcLogger reqmod_precache icaps://icap2.mydomain.net:11344/reqmod routing=on DOC_END NAME: icap_class diff --git a/src/err_detail_type.h b/src/err_detail_type.h index a430c7af60..5aee6e0c3e 100644 --- a/src/err_detail_type.h +++ b/src/err_detail_type.h @@ -23,6 +23,7 @@ typedef enum { ERR_DETAIL_RESPMOD_BLOCK_EARLY, // RESPMOD denied client access to HTTP response, before any part of the response was sent ERR_DETAIL_RESPMOD_BLOCK_LATE, // RESPMOD denied client access to HTTP response, after [a part of] the response was sent ERR_DETAIL_ICAP_XACT_START, // transaction start failure + ERR_DETAIL_ICAP_XACT_SSL_START, // transaction start failure ERR_DETAIL_ICAP_XACT_BODY_CONSUMER_ABORT, // transaction body consumer gone ERR_DETAIL_ICAP_INIT_GONE, // initiator gone ERR_DETAIL_ICAP_XACT_CLOSE, // ICAP connection closed unexpectedly diff --git a/src/errorpage.cc b/src/errorpage.cc index 06ad38d84f..0e8b2e3daf 100644 --- a/src/errorpage.cc +++ b/src/errorpage.cc @@ -955,8 +955,11 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion case 'R': if (building_deny_info_url) { - p = (request->urlpath.size() != 0 ? request->urlpath.termedBuf() : "/"); - no_urlescape = 1; + if (request != NULL) { + p = (request->urlpath.size() != 0 ? request->urlpath.termedBuf() : "/"); + no_urlescape = 1; + } else + p = "[no request]"; break; } if (NULL != request) { @@ -1147,7 +1150,7 @@ ErrorState::BuildHttpReply() status = httpStatus; else { // Use 307 for HTTP/1.1 non-GET/HEAD requests. - if (request->method != Http::METHOD_GET && request->method != Http::METHOD_HEAD && request->http_ver >= Http::ProtocolVersion(1,1)) + if (request != NULL && request->method != Http::METHOD_GET && request->method != Http::METHOD_HEAD && request->http_ver >= Http::ProtocolVersion(1,1)) status = Http::scTemporaryRedirect; } diff --git a/src/ssl/PeerConnector.cc b/src/ssl/PeerConnector.cc index 9ea1525356..336076ed3c 100644 --- a/src/ssl/PeerConnector.cc +++ b/src/ssl/PeerConnector.cc @@ -31,23 +31,19 @@ #include "ssl/support.h" CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeerConnector); +CBDATA_NAMESPACED_CLASS_INIT(Ssl, BlindPeerConnector); +CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeekingPeerConnector); Ssl::PeerConnector::PeerConnector( - HttpRequestPointer &aRequest, const Comm::ConnectionPointer &aServerConn, - const Comm::ConnectionPointer &aClientConn, AsyncCall::Pointer &aCallback, const time_t timeout): AsyncJob("Ssl::PeerConnector"), - request(aRequest), serverConn(aServerConn), - clientConn(aClientConn), callback(aCallback), negotiationTimeout(timeout), startTime(squid_curtime), - splice(false), - resumingSession(false), - serverCertificateHandled(false) + useCertValidator_(false) { // if this throws, the caller's cb dialer is not our CbDialer Must(dynamic_cast(callback->getDialer())); @@ -55,6 +51,7 @@ Ssl::PeerConnector::PeerConnector( Ssl::PeerConnector::~PeerConnector() { + cbdataReferenceDone(certErrors); debugs(83, 5, "Peer connector " << this << " gone"); } @@ -69,10 +66,8 @@ Ssl::PeerConnector::start() { AsyncJob::start(); - if (prepareSocket()) { - initializeSsl(); + if (prepareSocket() && (initializeSsl() != NULL)) negotiateSsl(); - } } void @@ -105,100 +100,23 @@ Ssl::PeerConnector::prepareSocket() return true; } -void +SSL * Ssl::PeerConnector::initializeSsl() { - SSL_CTX *sslContext = NULL; - const CachePeer *peer = serverConnection()->getPeer(); - const int fd = serverConnection()->fd; - - if (peer) { - assert(peer->secure.encryptTransport); - sslContext = peer->sslContext; - } else { - // XXX: locate a per-server context in Security:: instead - sslContext = ::Config.ssl_client.sslContext; - } - + SSL_CTX *sslContext = getSslContext(); assert(sslContext); + const int fd = serverConnection()->fd; + SSL *ssl = Ssl::CreateClient(sslContext, fd, "server https start"); if (!ssl) { ErrorState *anErr = new ErrorState(ERR_SOCKET_FAILURE, Http::scInternalServerError, request.getRaw()); anErr->xerrno = errno; debugs(83, DBG_IMPORTANT, "Error allocating SSL handle: " << ERR_error_string(ERR_get_error(), NULL)); - bail(anErr); - return; - } - - if (peer) { - // NP: domain may be a raw-IP but it is now always set - assert(!peer->secure.sslDomain.isEmpty()); - - // const loss is okay here, ssl_ex_index_server is only read and not assigned a destructor - SBuf *host = new SBuf(peer->secure.sslDomain); - SSL_set_ex_data(ssl, ssl_ex_index_server, host); - - if (peer->sslSession) - SSL_set_session(ssl, peer->sslSession); - } else if (ConnStateData *csd = request->clientConnectionManager.valid()) { - // client connection is required in the case we need to splice - // or terminate client and server connections - assert(clientConn != NULL); - SBuf *hostName = NULL; - Ssl::ClientBio *cltBio = NULL; - - //Enable Status_request tls extension, required to bump some clients - SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp); - - // In server-first bumping mode, clientSsl is NULL. - if (SSL *clientSsl = fd_table[clientConn->fd].ssl) { - BIO *b = SSL_get_rbio(clientSsl); - cltBio = static_cast(b->ptr); - const Ssl::Bio::sslFeatures &features = cltBio->getFeatures(); - if (!features.serverName.isEmpty()) - hostName = new SBuf(features.serverName); - } - - if (!hostName) { - // While we are peeking at the certificate, we may not know the server - // name that the client will request (after interception or CONNECT) - // unless it was the CONNECT request with a user-typed address. - const bool isConnectRequest = !csd->port->flags.isIntercepted(); - if (!request->flags.sslPeek || isConnectRequest) - hostName = new SBuf(request->GetHost()); - } - - if (hostName) - SSL_set_ex_data(ssl, ssl_ex_index_server, (void*)hostName); - Must(!csd->serverBump() || csd->serverBump()->step <= Ssl::bumpStep2); - if (csd->sslBumpMode == Ssl::bumpPeek || csd->sslBumpMode == Ssl::bumpStare) { - assert(cltBio); - const Ssl::Bio::sslFeatures &features = cltBio->getFeatures(); - if (features.sslVersion != -1) { - features.applyToSSL(ssl, csd->sslBumpMode); - // Should we allow it for all protocols? - if (features.sslVersion >= 3) { - BIO *b = SSL_get_rbio(ssl); - Ssl::ServerBio *srvBio = static_cast(b->ptr); - // Inherite client features, like SSL version, SNI and other - srvBio->setClientFeatures(features); - srvBio->recordInput(true); - srvBio->mode(csd->sslBumpMode); - } - } - } else { - // Set client SSL options - SSL_set_options(ssl, ::Security::ProxyOutgoingConfig.parsedOptions); - - // Use SNI TLS extension only when we connect directly - // to the origin server and we know the server host name. - const char *sniServer = hostName ? hostName->c_str() : - (!request->GetHostIsNumeric() ? request->GetHost() : NULL); - if (sniServer) - Ssl::setClientSNI(ssl, sniServer); - } + noteNegotiationDone(anErr); + bail(anErr); + return NULL; } // If CertValidation Helper used do not lookup checklist for errors, @@ -212,15 +130,7 @@ Ssl::PeerConnector::initializeSsl() SSL_set_ex_data(ssl, ssl_ex_index_cert_error_check, check); } } - - // store peeked cert to check SQUID_X509_V_ERR_CERT_CHANGE - X509 *peeked_cert; - if (request->clientConnectionManager.valid() && - request->clientConnectionManager->serverBump() && - (peeked_cert = request->clientConnectionManager->serverBump()->serverCert.get())) { - CRYPTO_add(&(peeked_cert->references),1,CRYPTO_LOCK_X509); - SSL_set_ex_data(ssl, ssl_ex_index_ssl_peeked_cert, peeked_cert); - } + return ssl; } void @@ -251,67 +161,19 @@ Ssl::PeerConnector::negotiateSsl() return; // we might be gone by now } - if (serverConnection()->getPeer() && !SSL_session_reused(ssl)) { - if (serverConnection()->getPeer()->sslSession) - SSL_SESSION_free(serverConnection()->getPeer()->sslSession); - - serverConnection()->getPeer()->sslSession = SSL_get1_session(ssl); - } - if (!sslFinalized()) return; callBack(); } -void -Ssl::PeerConnector::handleServerCertificate() -{ - if (serverCertificateHandled) - return; - - if (ConnStateData *csd = request->clientConnectionManager.valid()) { - const int fd = serverConnection()->fd; - SSL *ssl = fd_table[fd].ssl; - Ssl::X509_Pointer serverCert(SSL_get_peer_certificate(ssl)); - if (!serverCert.get()) - return; - - serverCertificateHandled = true; - - csd->resetSslCommonName(Ssl::CommonHostName(serverCert.get())); - debugs(83, 5, "HTTPS server CN: " << csd->sslCommonName() << - " bumped: " << *serverConnection()); - - // remember the server certificate for later use - if (Ssl::ServerBump *serverBump = csd->serverBump()) { - serverBump->serverCert.reset(serverCert.release()); - } - } -} - bool Ssl::PeerConnector::sslFinalized() { - const int fd = serverConnection()->fd; - SSL *ssl = fd_table[fd].ssl; - - // In the case the session is resuming, the certificates does not exist and - // we did not do any cert validation - if (resumingSession) - return true; - - handleServerCertificate(); - - if (ConnStateData *csd = request->clientConnectionManager.valid()) { - if (Ssl::ServerBump *serverBump = csd->serverBump()) { - // remember validation errors, if any - if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) - serverBump->sslErrors = cbdataReference(errs); - } - } + if (Ssl::TheConfig.ssl_crt_validator && useCertValidator_) { + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; - if (Ssl::TheConfig.ssl_crt_validator) { Ssl::CertValidationRequest validationRequest; // WARNING: Currently we do not use any locking for any of the // members of the Ssl::CertValidationRequest class. In this code the @@ -335,28 +197,29 @@ Ssl::PeerConnector::sslFinalized() "validate that certificate."); // fall through to do blocking in-process generation. ErrorState *anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw()); + + noteNegotiationDone(anErr); bail(anErr); - if (serverConnection()->getPeer()) { - peerConnectFailed(serverConnection()->getPeer()); - } serverConn->close(); return true; } } + + noteNegotiationDone(NULL); return true; } void switchToTunnel(HttpRequest *request, Comm::ConnectionPointer & clientConn, Comm::ConnectionPointer &srvConn); void -Ssl::PeerConnector::cbCheckForPeekAndSpliceDone(allow_t answer, void *data) +Ssl::PeekingPeerConnector::cbCheckForPeekAndSpliceDone(allow_t answer, void *data) { - Ssl::PeerConnector *peerConnect = (Ssl::PeerConnector *) data; + Ssl::PeekingPeerConnector *peerConnect = (Ssl::PeekingPeerConnector *) data; peerConnect->checkForPeekAndSpliceDone((Ssl::BumpMode)answer.kind); } void -Ssl::PeerConnector::checkForPeekAndSplice() +Ssl::PeekingPeerConnector::checkForPeekAndSplice() { // Mark Step3 of bumping if (request->clientConnectionManager.valid()) { @@ -370,11 +233,11 @@ Ssl::PeerConnector::checkForPeekAndSplice() ACLFilledChecklist *acl_checklist = new ACLFilledChecklist( ::Config.accessList.ssl_bump, request.getRaw(), NULL); - acl_checklist->nonBlockingCheck(Ssl::PeerConnector::cbCheckForPeekAndSpliceDone, this); + acl_checklist->nonBlockingCheck(Ssl::PeekingPeerConnector::cbCheckForPeekAndSpliceDone, this); } void -Ssl::PeerConnector::checkForPeekAndSpliceDone(Ssl::BumpMode const action) +Ssl::PeekingPeerConnector::checkForPeekAndSpliceDone(Ssl::BumpMode const action) { SSL *ssl = fd_table[serverConn->fd].ssl; BIO *b = SSL_get_rbio(ssl); @@ -404,14 +267,15 @@ Ssl::PeerConnector::checkForPeekAndSpliceDone(Ssl::BumpMode const action) //Allow write, proceed with the connection srvBio->holdWrite(false); srvBio->recordInput(false); - Comm::SetSelect(serverConn->fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0); debugs(83,5, "Retry the fwdNegotiateSSL on FD " << serverConn->fd); + Ssl::PeerConnector::noteWantWrite(); } else { splice = true; // Ssl Negotiation stops here. Last SSL checks for valid certificates // and if done, switch to tunnel mode - if (sslFinalized()) - switchToTunnel(request.getRaw(), clientConn, serverConn); + if (sslFinalized()) { + debugs(83,5, "Abort NegotiateSSL on FD " << serverConn->fd << " and splice the connection"); + } } } @@ -440,40 +304,28 @@ Ssl::PeerConnector::sslCrtvdHandleReply(Ssl::CertValidationResponse const &valid validatorFailed = true; if (!errDetails && !validatorFailed) { - if (splice) - switchToTunnel(request.getRaw(), clientConn, serverConn); - else - callBack(); + noteNegotiationDone(NULL); + callBack(); return; } + if (errs) { + if (certErrors) + cbdataReferenceDone(certErrors); + certErrors = cbdataReference(errs); + } + ErrorState *anErr = NULL; if (validatorFailed) { anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw()); } else { - - // Check the list error with - if (errDetails && request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - // remember validation errors, if any - if (errs) { - if (serverBump->sslErrors) - cbdataReferenceDone(serverBump->sslErrors); - serverBump->sslErrors = cbdataReference(errs); - } - } - } - anErr = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scServiceUnavailable, request.getRaw()); anErr->detail = errDetails; /*anErr->xerrno= Should preserved*/ } + noteNegotiationDone(anErr); bail(anErr); - if (serverConnection()->getPeer()) { - peerConnectFailed(serverConnection()->getPeer()); - } serverConn->close(); return; } @@ -549,86 +401,70 @@ Ssl::PeerConnector::handleNegotiateError(const int ret) unsigned long ssl_lib_error = SSL_ERROR_NONE; SSL *ssl = fd_table[fd].ssl; int ssl_error = SSL_get_error(ssl, ret); - BIO *b = SSL_get_rbio(ssl); - Ssl::ServerBio *srvBio = static_cast(b->ptr); - -#ifdef EPROTO - int sysErrNo = EPROTO; -#else - int sysErrNo = EACCES; -#endif switch (ssl_error) { - case SSL_ERROR_WANT_READ: - setReadTimeout(); - Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, this, 0); + noteWantRead(); return; case SSL_ERROR_WANT_WRITE: - if ((srvBio->bumpMode() == Ssl::bumpPeek || srvBio->bumpMode() == Ssl::bumpStare) && srvBio->holdWrite()) { - debugs(81, DBG_IMPORTANT, "hold write on SSL connection on FD " << fd); - checkForPeekAndSplice(); - return; - } - Comm::SetSelect(fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0); + noteWantWrite(); return; case SSL_ERROR_SSL: case SSL_ERROR_SYSCALL: ssl_lib_error = ERR_get_error(); + // proceed to the general error handling code + break; + default: + // no special error handling for all other errors + break; + } + noteSslNegotiationError(ret, ssl_error, ssl_lib_error); +} - // In Peek mode, the ClientHello message sent to the server. If the - // server resuming a previous (spliced) SSL session with the client, - // then probably we are here because local SSL object does not know - // anything about the session being resumed. - // - if (srvBio->bumpMode() == Ssl::bumpPeek && (resumingSession = srvBio->resumingSession())) { - // we currently splice all resumed sessions unconditionally - if (const bool spliceResumed = true) { - checkForPeekAndSpliceDone(Ssl::bumpSplice); - return; - } // else fall through to find a matching ssl_bump action (with limited info) - } - - // If we are in peek-and-splice mode and still we did not write to - // server yet, try to see if we should splice. - // In this case the connection can be saved. - // If the checklist decision is do not splice a new error will - // occure in the next SSL_connect call, and we will fail again. - // Abort on certificate validation errors to avoid splicing and - // thus hiding them. - // Abort if no certificate found probably because of malformed or - // unsupported server Hello message (TODO: make configurable). -#if 1 - if (!SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail) && - (srvBio->bumpMode() == Ssl::bumpPeek || srvBio->bumpMode() == Ssl::bumpStare) && srvBio->holdWrite()) { - Ssl::X509_Pointer serverCert(SSL_get_peer_certificate(ssl)); - if (serverCert.get()) { - debugs(81, 3, "Error (" << ERR_error_string(ssl_lib_error, NULL) << ") but, hold write on SSL connection on FD " << fd); - checkForPeekAndSplice(); - return; - } - } -#endif +void +Ssl::PeerConnector::noteWantRead() +{ + setReadTimeout(); + const int fd = serverConnection()->fd; + Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, this, 0); +} - // store/report errno when ssl_error is SSL_ERROR_SYSCALL, ssl_lib_error is 0, and ret is -1 - if (ssl_error == SSL_ERROR_SYSCALL && ret == -1 && ssl_lib_error == 0) - sysErrNo = errno; +void +Ssl::PeerConnector::noteWantWrite() +{ + const int fd = serverConnection()->fd; + Comm::SetSelect(fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0); + return; +} - debugs(83, DBG_IMPORTANT, "Error negotiating SSL on FD " << fd << - ": " << ERR_error_string(ssl_lib_error, NULL) << " (" << - ssl_error << "/" << ret << "/" << errno << ")"); +void +Ssl::PeerConnector::noteSslNegotiationError(const int ret, const int ssl_error, const int ssl_lib_error) +{ +#ifdef EPROTO + int sysErrNo = EPROTO; +#else + int sysErrNo = EACCES; +#endif - break; // proceed to the general error handling code + // store/report errno when ssl_error is SSL_ERROR_SYSCALL, ssl_lib_error is 0, and ret is -1 + if (ssl_error == SSL_ERROR_SYSCALL && ret == -1 && ssl_lib_error == 0) + sysErrNo = errno; - default: - break; // no special error handling for all other errors - } + const int fd = serverConnection()->fd; + debugs(83, DBG_IMPORTANT, "Error negotiating SSL on FD " << fd << + ": " << ERR_error_string(ssl_lib_error, NULL) << " (" << + ssl_error << "/" << ret << "/" << errno << ")"); - ErrorState *const anErr = ErrorState::NewForwarding(ERR_SECURE_CONNECT_FAIL, request.getRaw()); + ErrorState *anErr = NULL; + if (request != NULL) + anErr = ErrorState::NewForwarding(ERR_SECURE_CONNECT_FAIL, request.getRaw()); + else + anErr = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scServiceUnavailable, NULL); anErr->xerrno = sysErrNo; + SSL *ssl = fd_table[fd].ssl; Ssl::ErrorDetail *errFromFailure = (Ssl::ErrorDetail *)SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail); if (errFromFailure != NULL) { // The errFromFailure is attached to the ssl object @@ -645,30 +481,12 @@ Ssl::PeerConnector::handleNegotiateError(const int ret) if (ssl_lib_error != SSL_ERROR_NONE) anErr->detail->setLibError(ssl_lib_error); - if (request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - serverBump->serverCert.resetAndLock(anErr->detail->peerCert()); - - // remember validation errors, if any - if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) - serverBump->sslErrors = cbdataReference(errs); - } - - // For intercepted connections, set the host name to the server - // certificate CN. Otherwise, we just hope that CONNECT is using - // a user-entered address (a host name or a user-entered IP). - const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted(); - if (request->flags.sslPeek && !isConnectRequest) { - if (X509 *srvX509 = anErr->detail->peerCert()) { - if (const char *name = Ssl::CommonHostName(srvX509)) { - request->SetHost(name); - debugs(83, 3, HERE << "reset request host: " << name); - } - } - } - } + assert(certErrors == NULL); + // remember validation errors, if any + if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) + certErrors = cbdataReference(errs); + noteNegotiationDone(anErr); bail(anErr); } @@ -676,15 +494,6 @@ void Ssl::PeerConnector::bail(ErrorState *error) { Must(error); // or the recepient will not know there was a problem - - // XXX: forward.cc calls peerConnectSucceeded() after an OK TCP connect but - // we call peerConnectFailed() if SSL failed afterwards. Is that OK? - // It is not clear whether we should call peerConnectSucceeded/Failed() - // based on TCP results, SSL results, or both. And the code is probably not - // consistent in this aspect across tunnelling and forwarding modules. - if (CachePeer *p = serverConnection()->getPeer()) - peerConnectFailed(p); - Must(callback != NULL); CbDialer *dialer = dynamic_cast(callback->getDialer()); Must(dialer); @@ -745,3 +554,281 @@ Ssl::PeerConnector::status() const return buf.content(); } +SSL_CTX * +Ssl::BlindPeerConnector::getSslContext() +{ + if (const CachePeer *peer = serverConnection()->getPeer()) { + assert(peer->secure.encryptTransport); + SSL_CTX *sslContext = peer->sslContext; + return sslContext; + } + return NULL; +} + +SSL * +Ssl::BlindPeerConnector::initializeSsl() +{ + SSL *ssl = Ssl::PeerConnector::initializeSsl(); + if (!ssl) + return NULL; + + const CachePeer *peer = serverConnection()->getPeer(); + assert(peer); + + // NP: domain may be a raw-IP but it is now always set + assert(!peer->secure.sslDomain.isEmpty()); + + // const loss is okay here, ssl_ex_index_server is only read and not assigned a destructor + SBuf *host = new SBuf(peer->secure.sslDomain); + SSL_set_ex_data(ssl, ssl_ex_index_server, host); + + if (peer->sslSession) + SSL_set_session(ssl, peer->sslSession); + + return ssl; +} + +void +Ssl::BlindPeerConnector::noteNegotiationDone(ErrorState *error) +{ + if (error) { + // XXX: forward.cc calls peerConnectSucceeded() after an OK TCP connect but + // we call peerConnectFailed() if SSL failed afterwards. Is that OK? + // It is not clear whether we should call peerConnectSucceeded/Failed() + // based on TCP results, SSL results, or both. And the code is probably not + // consistent in this aspect across tunnelling and forwarding modules. + if (CachePeer *p = serverConnection()->getPeer()) + peerConnectFailed(p); + return; + } + + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + if (serverConnection()->getPeer() && !SSL_session_reused(ssl)) { + if (serverConnection()->getPeer()->sslSession) + SSL_SESSION_free(serverConnection()->getPeer()->sslSession); + + serverConnection()->getPeer()->sslSession = SSL_get1_session(ssl); + } +} + +SSL_CTX * +Ssl::PeekingPeerConnector::getSslContext() +{ + // XXX: locate a per-server context in Security:: instead + return ::Config.ssl_client.sslContext; +} + +SSL * +Ssl::PeekingPeerConnector::initializeSsl() +{ + SSL *ssl = Ssl::PeerConnector::initializeSsl(); + if (!ssl) + return NULL; + + if (ConnStateData *csd = request->clientConnectionManager.valid()) { + + // client connection is required in the case we need to splice + // or terminate client and server connections + assert(clientConn != NULL); + SBuf *hostName = NULL; + Ssl::ClientBio *cltBio = NULL; + + //Enable Status_request tls extension, required to bump some clients + SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp); + + // In server-first bumping mode, clientSsl is NULL. + if (SSL *clientSsl = fd_table[clientConn->fd].ssl) { + BIO *b = SSL_get_rbio(clientSsl); + cltBio = static_cast(b->ptr); + const Ssl::Bio::sslFeatures &features = cltBio->getFeatures(); + if (!features.serverName.isEmpty()) + hostName = new SBuf(features.serverName); + } + + if (!hostName) { + // While we are peeking at the certificate, we may not know the server + // name that the client will request (after interception or CONNECT) + // unless it was the CONNECT request with a user-typed address. + const bool isConnectRequest = !csd->port->flags.isIntercepted(); + if (!request->flags.sslPeek || isConnectRequest) + hostName = new SBuf(request->GetHost()); + } + + if (hostName) + SSL_set_ex_data(ssl, ssl_ex_index_server, (void*)hostName); + + Must(!csd->serverBump() || csd->serverBump()->step <= Ssl::bumpStep2); + if (csd->sslBumpMode == Ssl::bumpPeek || csd->sslBumpMode == Ssl::bumpStare) { + assert(cltBio); + const Ssl::Bio::sslFeatures &features = cltBio->getFeatures(); + if (features.sslVersion != -1) { + features.applyToSSL(ssl, csd->sslBumpMode); + // Should we allow it for all protocols? + if (features.sslVersion >= 3) { + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + // Inherite client features, like SSL version, SNI and other + srvBio->setClientFeatures(features); + srvBio->recordInput(true); + srvBio->mode(csd->sslBumpMode); + } + } + } else { + // Set client SSL options + SSL_set_options(ssl, ::Security::ProxyOutgoingConfig.parsedOptions); + + // Use SNI TLS extension only when we connect directly + // to the origin server and we know the server host name. + const char *sniServer = hostName ? hostName->c_str() : + (!request->GetHostIsNumeric() ? request->GetHost() : NULL); + if (sniServer) + Ssl::setClientSNI(ssl, sniServer); + } + + // store peeked cert to check SQUID_X509_V_ERR_CERT_CHANGE + X509 *peeked_cert; + if (csd->serverBump() && + (peeked_cert = csd->serverBump()->serverCert.get())) { + CRYPTO_add(&(peeked_cert->references),1,CRYPTO_LOCK_X509); + SSL_set_ex_data(ssl, ssl_ex_index_ssl_peeked_cert, peeked_cert); + } + } + + return ssl; +} + +void +Ssl::PeekingPeerConnector::noteNegotiationDone(ErrorState *error) +{ + SSL *ssl = fd_table[serverConnection()->fd].ssl; + + // Check the list error with + if (!request->clientConnectionManager.valid() || ! ssl) + return; + + // remember the server certificate from the ErrorDetail object + if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { + // remember validation errors, if any + if (certErrors) { + if (serverBump->sslErrors) + cbdataReferenceDone(serverBump->sslErrors); + serverBump->sslErrors = cbdataReference(certErrors); + } + + if (!serverBump->serverCert.get()) { + // remember the server certificate from the ErrorDetail object + if (error && error->detail && error->detail->peerCert()) + serverBump->serverCert.resetAndLock(error->detail->peerCert()); + else { + handleServerCertificate(); + } + } + + if (error) { + // For intercepted connections, set the host name to the server + // certificate CN. Otherwise, we just hope that CONNECT is using + // a user-entered address (a host name or a user-entered IP). + const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted(); + if (request->flags.sslPeek && !isConnectRequest) { + if (X509 *srvX509 = serverBump->serverCert.get()) { + if (const char *name = Ssl::CommonHostName(srvX509)) { + request->SetHost(name); + debugs(83, 3, "reset request host: " << name); + } + } + } + } + } + + if (!error && splice) + switchToTunnel(request.getRaw(), clientConn, serverConn); +} + +void +Ssl::PeekingPeerConnector::noteWantWrite() +{ + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + + if ((srvBio->bumpMode() == Ssl::bumpPeek || srvBio->bumpMode() == Ssl::bumpStare) && srvBio->holdWrite()) { + debugs(81, DBG_IMPORTANT, "hold write on SSL connection on FD " << fd); + checkForPeekAndSplice(); + return; + } + + Ssl::PeerConnector::noteWantWrite(); +} + +void +Ssl::PeekingPeerConnector::noteSslNegotiationError(const int result, const int ssl_error, const int ssl_lib_error) +{ + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + BIO *b = SSL_get_rbio(ssl); + Ssl::ServerBio *srvBio = static_cast(b->ptr); + + // In Peek mode, the ClientHello message sent to the server. If the + // server resuming a previous (spliced) SSL session with the client, + // then probably we are here because local SSL object does not know + // anything about the session being resumed. + // + if (srvBio->bumpMode() == Ssl::bumpPeek && (resumingSession = srvBio->resumingSession())) { + // we currently splice all resumed sessions unconditionally + if (const bool spliceResumed = true) { + bypassCertValidator(); + checkForPeekAndSpliceDone(Ssl::bumpSplice); + return; + } // else fall through to find a matching ssl_bump action (with limited info) + } + + // If we are in peek-and-splice mode and still we did not write to + // server yet, try to see if we should splice. + // In this case the connection can be saved. + // If the checklist decision is do not splice a new error will + // occur in the next SSL_connect call, and we will fail again. + // Abort on certificate validation errors to avoid splicing and + // thus hiding them. + // Abort if no certificate found probably because of malformed or + // unsupported server Hello message (TODO: make configurable). + if (!SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail) && + (srvBio->bumpMode() == Ssl::bumpPeek || srvBio->bumpMode() == Ssl::bumpStare) && srvBio->holdWrite()) { + Ssl::X509_Pointer serverCert(SSL_get_peer_certificate(ssl)); + if (serverCert.get()) { + debugs(81, 3, "Error (" << ERR_error_string(ssl_lib_error, NULL) << ") but, hold write on SSL connection on FD " << fd); + checkForPeekAndSplice(); + return; + } + } + + // else call parent noteNegotiationError to produce an error page + Ssl::PeerConnector::noteSslNegotiationError(result, ssl_error, ssl_lib_error); +} + +void +Ssl::PeekingPeerConnector::handleServerCertificate() +{ + if (serverCertificateHandled) + return; + + if (ConnStateData *csd = request->clientConnectionManager.valid()) { + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + Ssl::X509_Pointer serverCert(SSL_get_peer_certificate(ssl)); + if (!serverCert.get()) + return; + + serverCertificateHandled = true; + + csd->resetSslCommonName(Ssl::CommonHostName(serverCert.get())); + debugs(83, 5, "HTTPS server CN: " << csd->sslCommonName() << + " bumped: " << *serverConnection()); + + // remember the server certificate for later use + if (Ssl::ServerBump *serverBump = csd->serverBump()) { + serverBump->serverCert.reset(serverCert.release()); + } + } +} diff --git a/src/ssl/PeerConnector.h b/src/ssl/PeerConnector.h index 75b76855eb..1dee04c6ca 100644 --- a/src/ssl/PeerConnector.h +++ b/src/ssl/PeerConnector.h @@ -27,10 +27,9 @@ class CertValidationResponse; /** \par - * Connects Squid client-side to an SSL peer (cache_peer ... ssl). - * Handles peer certificate validation. - * Used by TunnelStateData, FwdState, and PeerPoolMgr to start talking to an - * SSL peer. + * Connects Squid to SSL/TLS-capable peers or services. + * Contains common code and interfaces of various specialized PeerConnectors, + * including peer certificate validation code. \par * The caller receives a call back with Security::EncryptorAnswer. If answer.error * is not nil, then there was an error and the SSL connection to the SSL peer @@ -54,7 +53,7 @@ class CertValidationResponse; \par * This job never closes the connection, even on errors. If a 3rd-party * closes the connection, this job simply quits without informing the caller. -*/ + */ class PeerConnector: virtual public AsyncJob { CBDATA_CLASS(PeerConnector); @@ -72,9 +71,7 @@ public: typedef RefCount HttpRequestPointer; public: - PeerConnector(HttpRequestPointer &aRequest, - const Comm::ConnectionPointer &aServerConn, - const Comm::ConnectionPointer &aClientConn, + PeerConnector(const Comm::ConnectionPointer &aServerConn, AsyncCall::Pointer &aCallback, const time_t timeout = 0); virtual ~PeerConnector(); @@ -100,7 +97,7 @@ protected: /// silent server void setReadTimeout(); - void initializeSsl(); ///< Initializes SSL state + virtual SSL *initializeSsl(); ///< Initializes SSL state /// Performs a single secure connection negotiation step. /// It is called multiple times untill the negotiation finish or aborted. @@ -111,29 +108,53 @@ protected: /// Otherwise, returns true, regardless of negotiation success/failure. bool sslFinalized(); - /// Initiates the ssl_bump acl check in step3 SSL bump step to decide - /// about bumping, splicing or terminating the connection. - void checkForPeekAndSplice(); - - /// Callback function for ssl_bump acl check in step3 SSL bump step. - /// Handles the final bumping decision. - void checkForPeekAndSpliceDone(Ssl::BumpMode const); - /// Called when the SSL negotiation step aborted because data needs to /// be transferred to/from SSL server or on error. In the first case /// setups the appropriate Comm::SetSelect handler. In second case /// fill an error and report to the PeerConnector caller. void handleNegotiateError(const int result); -private: - PeerConnector(const PeerConnector &); // not implemented - PeerConnector &operator =(const PeerConnector &); // not implemented + /// Called when the openSSL SSL_connect fnction request more data from + /// the remote SSL server. Sets the read timeout and sets the + /// Squid COMM_SELECT_READ handler. + void noteWantRead(); + + /// Called when the openSSL SSL_connect function needs to write data to + /// the remote SSL server. Sets the Squid COMM_SELECT_WRITE handler. + virtual void noteWantWrite(); + + /// Called when the SSL_connect function aborts with an SSL negotiation error + /// \param result the SSL_connect return code + /// \param ssl_error the error code returned from the SSL_get_error function + /// \param ssl_lib_error the error returned from the ERR_Get_Error function + virtual void noteSslNegotiationError(const int result, const int ssl_error, const int ssl_lib_error); + + /// Called when the SSL negotiation to the server completed and the certificates + /// validated using the cert validator. + /// \param error if not NULL the SSL negotiation was aborted with an error + virtual void noteNegotiationDone(ErrorState *error) {} + + /// Must implemented by the kid classes to return the SSL_CTX object to use + /// for building the SSL objects. + virtual SSL_CTX *getSslContext() = 0; /// mimics FwdState to minimize changes to FwdState::initiate/negotiateSsl Comm::ConnectionPointer const &serverConnection() const { return serverConn; } void bail(ErrorState *error); ///< Return an error to the PeerConnector caller + /// If called the certificates validator will not used + void bypassCertValidator() {useCertValidator_ = false;} + + HttpRequestPointer request; ///< peer connection trigger or cause + Comm::ConnectionPointer serverConn; ///< TCP connection to the peer + /// Certificate errors found from SSL validation procedure or from cert + /// validator + Ssl::CertErrors *certErrors; +private: + PeerConnector(const PeerConnector &); // not implemented + PeerConnector &operator =(const PeerConnector &); // not implemented + /// Callback the caller class, and pass the ready to communicate secure /// connection or an error if PeerConnector failed. void callBack(); @@ -144,27 +165,79 @@ private: /// Check SSL errors returned from cert validator against sslproxy_cert_error access list Ssl::CertErrors *sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &, Ssl::ErrorDetail *&); - /// Updates associated client connection manager members - /// if the server certificate was received from the server. - void handleServerCertificate(); - /// Callback function called when squid receive message from cert validator helper static void sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &); /// A wrapper function for negotiateSsl for use with Comm::SetSelect static void NegotiateSsl(int fd, void *data); + AsyncCall::Pointer callback; ///< we call this with the results + AsyncCall::Pointer closeHandler; ///< we call this when the connection closed + time_t negotiationTimeout; ///< the SSL connection timeout to use + time_t startTime; ///< when the peer connector negotiation started + bool useCertValidator_; ///< whether the certificate validator should bypassed +}; + +/// A simple PeerConnector for SSL/TLS cache_peers. No SslBump capabilities. +class BlindPeerConnector: public PeerConnector { + CBDATA_CLASS(BlindPeerConnector); +public: + BlindPeerConnector(HttpRequestPointer &aRequest, + const Comm::ConnectionPointer &aServerConn, + AsyncCall::Pointer &aCallback, const time_t timeout = 0): AsyncJob("Ssl::BlindPeerConnector"), + PeerConnector(aServerConn, aCallback, timeout) { request = aRequest; } + + /* PeerConnector API */ + + /// Calls parent initializeSSL, configure the created SSL object to try reuse SSL session + /// and sets the hostname to use for certificates validation + virtual SSL *initializeSsl(); + + /// Return the configured SSL_CTX object + virtual SSL_CTX *getSslContext(); + + /// On error calls peerConnectFailed function, on success store the used SSL session + /// for later use + virtual void noteNegotiationDone(ErrorState *error); +}; + +/// A PeerConnector for HTTP origin servers. Capable of SslBumping. +class PeekingPeerConnector: public PeerConnector { + CBDATA_CLASS(PeekingPeerConnector); +public: + PeekingPeerConnector(HttpRequestPointer &aRequest, + const Comm::ConnectionPointer &aServerConn, + const Comm::ConnectionPointer &aClientConn, + AsyncCall::Pointer &aCallback, const time_t timeout = 0): AsyncJob("Ssl::PeekingPeerConnector"), + PeerConnector(aServerConn, aCallback, timeout), clientConn(aClientConn), splice(false), resumingSession(false), serverCertificateHandled(false) { request = aRequest; } + + /* PeerConnector API */ + virtual SSL *initializeSsl(); + virtual SSL_CTX *getSslContext(); + virtual void noteWantWrite(); + virtual void noteSslNegotiationError(const int result, const int ssl_error, const int ssl_lib_error); + virtual void noteNegotiationDone(ErrorState *error); + + /// Updates associated client connection manager members + /// if the server certificate was received from the server. + void handleServerCertificate(); + + /// Initiates the ssl_bump acl check in step3 SSL bump step to decide + /// about bumping, splicing or terminating the connection. + void checkForPeekAndSplice(); + + /// Callback function for ssl_bump acl check in step3 SSL bump step. + /// Handles the final bumping decision. + void checkForPeekAndSpliceDone(Ssl::BumpMode const); /// A wrapper function for checkForPeekAndSpliceDone for use with acl static void cbCheckForPeekAndSpliceDone(allow_t answer, void *data); - HttpRequestPointer request; ///< peer connection trigger or cause - Comm::ConnectionPointer serverConn; ///< TCP connection to the peer +private: Comm::ConnectionPointer clientConn; ///< TCP connection to the client AsyncCall::Pointer callback; ///< we call this with the results AsyncCall::Pointer closeHandler; ///< we call this when the connection closed - time_t negotiationTimeout; ///< the ssl connection timeout to use time_t startTime; ///< when the peer connector negotiation started - bool splice; ///< Whether we are going to splice or not + bool splice; ///< whether we are going to splice or not bool resumingSession; ///< whether it is an SSL resuming session connection bool serverCertificateHandled; ///< whether handleServerCertificate() succeeded }; diff --git a/src/tunnel.cc b/src/tunnel.cc index 5b279f06ac..f16fe6245f 100644 --- a/src/tunnel.cc +++ b/src/tunnel.cc @@ -1022,8 +1022,8 @@ TunnelStateData::connectToPeer() AsyncCall::Pointer callback = asyncCall(5,4, "TunnelStateData::ConnectedToPeer", MyAnswerDialer(&TunnelStateData::connectedToPeer, this)); - Ssl::PeerConnector *connector = - new Ssl::PeerConnector(request, server.conn, client.conn, callback); + Ssl::BlindPeerConnector *connector = + new Ssl::BlindPeerConnector(request, server.conn, callback); AsyncJob::Start(connector); // will call our callback return; }