From: Christos Tsantilas Date: Fri, 10 Apr 2015 08:54:13 +0000 (+0300) Subject: Add server_name ACL matching server name(s) obtained from various sources such as... X-Git-Tag: merge-candidate-3-v1~186 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=69f690801009f074741b1c5d8be6339d908b95d8;p=thirdparty%2Fsquid.git Add server_name ACL matching server name(s) obtained from various sources such as CONNECT request URI, client SNI, and SSL server certificate CN. During each SslBump step, Squid improves its understanding of a "true server name", with a bias towards server-provided (and Squid-validated) information. The server-provided server names are retrieved from the server certificate CN and Subject Alternate Names. The new server_name ACL matches any of alternate names and CN. If the CN or an alternate name is a wildcard, then the new ACL matches any domain that matches the domain with the wildcard. Other than supporting many sources of server name information (including sources that may supply Squid with multiple server name variants and wildcards), the new ACL is similar to dstdomain. Also added a server_name_regex ACL. This is a Measurement Factory project. --- diff --git a/src/AclRegs.cc b/src/AclRegs.cc index 0b129a5214..9be5e3e052 100644 --- a/src/AclRegs.cc +++ b/src/AclRegs.cc @@ -74,6 +74,7 @@ #if USE_OPENSSL #include "acl/Certificate.h" #include "acl/CertificateData.h" +#include "acl/ServerName.h" #include "acl/SslError.h" #include "acl/SslErrorData.h" #endif @@ -177,6 +178,12 @@ ACLStrategised ACLServerCertificate::X509FingerprintRegistryEntry_(new A ACL::Prototype ACLAtStep::RegistryProtoype(&ACLAtStep::RegistryEntry_, "at_step"); ACLStrategised ACLAtStep::RegistryEntry_(new ACLAtStepData, ACLAtStepStrategy::Instance(), "at_step"); + +ACL::Prototype ACLServerName::LiteralRegistryProtoype(&ACLServerName::LiteralRegistryEntry_, "ssl::server_name"); +ACLStrategised ACLServerName::LiteralRegistryEntry_(new ACLServerNameData, ACLServerNameStrategy::Instance(), "ssl::server_name"); +ACL::Prototype ACLServerName::RegexRegistryProtoype(&ACLServerName::RegexRegistryEntry_, "ssl::server_name_regex"); +ACLFlag ServerNameRegexFlags[] = {ACL_F_REGEX_CASE, ACL_F_END}; +ACLStrategised ACLServerName::RegexRegistryEntry_(new ACLRegexData, ACLServerNameStrategy::Instance(), "ssl::server_name_regex", ServerNameRegexFlags); #endif #if USE_SQUID_EUI diff --git a/src/URL.h b/src/URL.h index 11d0dac61e..be66fe0dc1 100644 --- a/src/URL.h +++ b/src/URL.h @@ -77,7 +77,38 @@ bool urlIsRelative(const char *); char *urlMakeAbsolute(const HttpRequest *, const char *); char *urlRInternal(const char *host, unsigned short port, const char *dir, const char *name); char *urlInternal(const char *dir, const char *name); -int matchDomainName(const char *host, const char *domain); + +/** + * matchDomainName() compares a hostname (usually extracted from traffic) + * with a domainname (usually from an ACL) according to the following rules: + * + * HOST | DOMAIN | MATCH? + * -------------|-------------|------ + * foo.com | foo.com | YES + * .foo.com | foo.com | YES + * x.foo.com | foo.com | NO + * foo.com | .foo.com | YES + * .foo.com | .foo.com | YES + * x.foo.com | .foo.com | YES + * + * We strip leading dots on hosts (but not domains!) so that + * ".foo.com" is always the same as "foo.com". + * + * if honorWildcards is true then the matchDomainName() also accepts + * optional wildcards on hostname: + * + * HOST | DOMAIN | MATCH? + * -------------|--------------|------- + * *.foo.com | x.foo.com | YES + * *.foo.com | .x.foo.com | YES + * *.foo.com | .foo.com | YES + * *.foo.com | foo.com | NO + * + * \retval 0 means the host matches the domain + * \retval 1 means the host is greater than the domain + * \retval -1 means the host is less than the domain + */ +int matchDomainName(const char *host, const char *domain, bool honorWildcards = false); int urlCheckRequest(const HttpRequest *); int urlDefaultPort(AnyP::ProtocolType p); char *urlHostname(const char *url); diff --git a/src/acl/DomainData.h b/src/acl/DomainData.h index 3f5918e57b..a3af868d1d 100644 --- a/src/acl/DomainData.h +++ b/src/acl/DomainData.h @@ -19,7 +19,7 @@ class ACLDomainData : public ACLData public: virtual ~ACLDomainData(); - bool match(char const *); + virtual bool match(char const *); virtual SBufList dump() const; void parse(); bool empty() const; diff --git a/src/acl/Makefile.am b/src/acl/Makefile.am index 61117c66be..05fb4f8582 100644 --- a/src/acl/Makefile.am +++ b/src/acl/Makefile.am @@ -111,6 +111,8 @@ libacls_la_SOURCES = \ RequestHeaderStrategy.h \ RequestMimeType.cc \ RequestMimeType.h \ + ServerName.cc \ + ServerName.h \ SourceAsn.h \ SourceDomain.cc \ SourceDomain.h \ diff --git a/src/acl/ServerName.cc b/src/acl/ServerName.cc new file mode 100644 index 0000000000..31a85c1c14 --- /dev/null +++ b/src/acl/ServerName.cc @@ -0,0 +1,117 @@ +/* + * Copyright (C) 1996-2015 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 28 Access Control */ + +#include "squid.h" +#include "acl/Checklist.h" +#include "acl/ServerName.h" +#include "acl/DomainData.h" +#include "acl/RegexData.h" +#include "client_side.h" +#include "fde.h" +#include "HttpRequest.h" +#include "ipcache.h" +#include "SquidString.h" +#include "ssl/bio.h" +#include "ssl/ServerBump.h" +#include "ssl/support.h" +#include "URL.h" + +// Compare function for tree search algorithms +static int +aclHostDomainCompare( char *const &a, char * const &b) +{ + const char *h = static_cast(a); + const char *d = static_cast(b); + debugs(28, 7, "Match:" << h << " <> " << d); + return matchDomainName(h, d, true); +} + +bool +ACLServerNameData::match(const char *host) +{ + if (host == NULL) + return 0; + + debugs(28, 3, "checking '" << host << "'"); + + char *h = const_cast(host); + char const * const * result = domains->find(h, aclHostDomainCompare); + + debugs(28, 3, "'" << host << "' " << (result ? "found" : "NOT found")); + + return (result != NULL); + +} + +ACLData * +ACLServerNameData::clone() const +{ + /* Splay trees don't clone yet. */ + assert (!domains); + return new ACLServerNameData; +} + +/// A helper function to be used with Ssl::matchX509CommonNames(). +/// \retval 0 when the name (cn or an alternate name) matches acl data +/// \retval 1 when the name does not match +template +int +check_cert_domain( void *check_data, ASN1_STRING *cn_data) +{ + char cn[1024]; + ACLData * data = (ACLData *)check_data; + + if (cn_data->length > (int)sizeof(cn) - 1) + return 1; // ignore data that does not fit our buffer + + memcpy(cn, cn_data->data, cn_data->length); + cn[cn_data->length] = '\0'; + debugs(28, 4, "Verifying certificate name/subjectAltName " << cn); + if (data->match(cn)) + return 0; + return 1; +} + +int +ACLServerNameStrategy::match (ACLData * &data, ACLFilledChecklist *checklist, ACLFlags &flags) +{ + assert(checklist != NULL && checklist->request != NULL); + + if (checklist->conn() && checklist->conn()->serverBump()) { + if (X509 *peer_cert = checklist->conn()->serverBump()->serverCert.get()) { + if (Ssl::matchX509CommonNames(peer_cert, (void *)data, check_cert_domain)) + return 1; + } + } + + const char *serverName = NULL; + if (checklist->conn() && !checklist->conn()->sslCommonName().isEmpty()) { + SBuf scn = checklist->conn()->sslCommonName(); + serverName = scn.c_str(); + } + + if (serverName == NULL) + serverName = checklist->request->GetHost(); + + if (serverName && data->match(serverName)) { + return 1; + } + + return data->match("none"); +} + +ACLServerNameStrategy * +ACLServerNameStrategy::Instance() +{ + return &Instance_; +} + +ACLServerNameStrategy ACLServerNameStrategy::Instance_; + diff --git a/src/acl/ServerName.h b/src/acl/ServerName.h new file mode 100644 index 0000000000..9ba3fba73b --- /dev/null +++ b/src/acl/ServerName.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 1996-2015 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. + */ + +#ifndef SQUID_ACLSERVERNAME_H +#define SQUID_ACLSERVERNAME_H + +#include "acl/Acl.h" +#include "acl/Checklist.h" +#include "acl/Data.h" +#include "acl/Strategised.h" +#include "acl/DomainData.h" + +class ACLServerNameData : public ACLDomainData { + MEMPROXY_CLASS(ACLServerNameData); +public: + ACLServerNameData() : ACLDomainData(){} + virtual bool match(const char *); + virtual ACLData *clone() const; +}; + +class ACLServerNameStrategy : public ACLStrategy +{ + +public: + virtual int match (ACLData * &, ACLFilledChecklist *, ACLFlags &); + static ACLServerNameStrategy *Instance(); + virtual bool requiresRequest() const {return true;} + + /** + * Not implemented to prevent copies of the instance. + \par + * Not private to prevent brain dead g+++ warnings about + * private constructors with no friends + */ + ACLServerNameStrategy(ACLServerNameStrategy const &); + +private: + static ACLServerNameStrategy Instance_; + ACLServerNameStrategy() {} + + ACLServerNameStrategy&operator=(ACLServerNameStrategy const &); +}; + +class ACLServerName +{ + +private: + static ACL::Prototype LiteralRegistryProtoype; + static ACLStrategised LiteralRegistryEntry_; + static ACL::Prototype RegexRegistryProtoype; + static ACLStrategised RegexRegistryEntry_; +}; + +#endif /* SQUID_ACLSERVERNAME_H */ + diff --git a/src/cf.data.pre b/src/cf.data.pre index 72ee58b1f3..7f1c921a02 100644 --- a/src/cf.data.pre +++ b/src/cf.data.pre @@ -1168,6 +1168,18 @@ IF USE_OPENSSL # SslBump1: After getting TCP-level and HTTP CONNECT info. # SslBump2: After getting SSL Client Hello info. # SslBump3: After getting SSL Server Hello info. + + acl aclname ssl::server_name .foo.com ... + # matches server name obtained from various sources [fast] + # + # The server name is obtained during Ssl-Bump steps from such sources + # as CONNECT request URI, client SNI, and SSL server certificate CN. + # During each Ssl-Bump step, Squid may improve its understanding of a + # "true server name". Unlike dstdomain, this ACL does not perform + # DNS lookups. + + acl aclname ssl::server_name_regex [-i] \.foo\.com ... + # regex matches server name obtained from various sources [fast] ENDIF acl aclname any-of acl1 acl2 ... # match any one of the acls [fast or slow] diff --git a/src/client_side.cc b/src/client_side.cc index 8ea41a578e..85c6a22e76 100644 --- a/src/client_side.cc +++ b/src/client_side.cc @@ -3700,6 +3700,14 @@ clientNegotiateSSL(int fd, void *data) " has no certificate."); } +#if defined(TLSEXT_NAMETYPE_host_name) + if (!conn->serverBump()) { + // when in bumpClientFirst mode, get the server name from SNI + if (const char *server = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name)) + conn->resetSslCommonName(server); + } +#endif + conn->readSomeData(); } @@ -3868,7 +3876,7 @@ ConnStateData::sslCrtdHandleReply(const Helper::Reply &reply) void ConnStateData::buildSslCertGenerationParams(Ssl::CertificateProperties &certProperties) { - certProperties.commonName = sslCommonName.size() > 0 ? sslCommonName.termedBuf() : sslConnectHostOrIp.termedBuf(); + certProperties.commonName = sslCommonName_.isEmpty() ? sslConnectHostOrIp.termedBuf() : sslCommonName_.c_str(); // fake certificate adaptation requires bump-server-first mode if (!sslServerBump) { @@ -4089,7 +4097,7 @@ ConnStateData::switchToHttps(HttpRequest *request, Ssl::BumpMode bumpServerMode) assert(!switchedToHttps_); sslConnectHostOrIp = request->GetHost(); - sslCommonName = request->GetHost(); + resetSslCommonName(request->GetHost()); // We are going to read new request flags.readMore = true; @@ -4166,8 +4174,10 @@ clientPeekAndSpliceSSL(int fd, void *data) if (bio->gotHello()) { if (conn->serverBump()) { Ssl::Bio::sslFeatures const &features = bio->getFeatures(); - if (!features.serverName.isEmpty()) + if (!features.serverName.isEmpty()) { conn->serverBump()->clientSni = features.serverName; + conn->resetSslCommonName(features.serverName.c_str()); + } } debugs(83, 5, "I got hello. Start forwarding the request!!! "); @@ -4322,30 +4332,11 @@ ConnStateData::httpsPeeked(Comm::ConnectionPointer serverConnection) Must(sslServerBump != NULL); if (Comm::IsConnOpen(serverConnection)) { - SSL *ssl = fd_table[serverConnection->fd].ssl; - assert(ssl); - Ssl::X509_Pointer serverCert(SSL_get_peer_certificate(ssl)); - assert(serverCert.get() != NULL); - sslCommonName = Ssl::CommonHostName(serverCert.get()); - debugs(33, 5, HERE << "HTTPS server CN: " << sslCommonName << - " bumped: " << *serverConnection); - pinConnection(serverConnection, NULL, NULL, false); debugs(33, 5, HERE << "bumped HTTPS server: " << sslConnectHostOrIp); } else { debugs(33, 5, HERE << "Error while bumping: " << sslConnectHostOrIp); - Ip::Address intendedDest; - intendedDest = sslConnectHostOrIp.termedBuf(); - const bool isConnectRequest = !port->flags.isIntercepted(); - - // Squid serves its own error page and closes, so we want - // a CN that causes no additional browser errors. Possible - // only when bumping CONNECT with a user-typed address. - if (intendedDest.isAnyAddr() || isConnectRequest) - sslCommonName = sslConnectHostOrIp; - else if (sslServerBump->serverCert.get()) - sslCommonName = Ssl::CommonHostName(sslServerBump->serverCert.get()); // copy error detail from bump-server-first request to CONNECT request if (currentobject != NULL && currentobject->http != NULL && currentobject->http->request) diff --git a/src/client_side.h b/src/client_side.h index 1e93498778..7a34f33b3b 100644 --- a/src/client_side.h +++ b/src/client_side.h @@ -380,6 +380,8 @@ public: else assert(sslServerBump == srvBump); } + const SBuf &sslCommonName() const {return sslCommonName_;} + void resetSslCommonName(const char *name) {sslCommonName_ = name;} /// Fill the certAdaptParams with the required data for certificate adaptation /// and create the key for storing/retrieve the certificate to/from the cache void buildSslCertGenerationParams(Ssl::CertificateProperties &certProperties); @@ -471,7 +473,7 @@ private: bool switchedToHttps_; /// The SSL server host name appears in CONNECT request or the server ip address for the intercepted requests String sslConnectHostOrIp; ///< The SSL server host name as passed in the CONNECT request - String sslCommonName; ///< CN name for SSL certificate generation + SBuf sslCommonName_; ///< CN name for SSL certificate generation String sslBumpCertKey; ///< Key to use to store/retrieve generated certificate /// HTTPS server cert. fetching state for bump-ssl-server-first diff --git a/src/ssl/PeerConnector.cc b/src/ssl/PeerConnector.cc index 50a01c1223..5348432c7a 100644 --- a/src/ssl/PeerConnector.cc +++ b/src/ssl/PeerConnector.cc @@ -45,7 +45,8 @@ Ssl::PeerConnector::PeerConnector( callback(aCallback), negotiationTimeout(timeout), startTime(squid_curtime), - splice(false) + splice(false), + serverCertificateHandled(false) { // if this throws, the caller's cb dialer is not our CbDialer Must(dynamic_cast(callback->getDialer())); @@ -259,17 +260,42 @@ Ssl::PeerConnector::negotiateSsl() 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; - if (request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - serverBump->serverCert.reset(SSL_get_peer_certificate(ssl)); + 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); @@ -323,16 +349,15 @@ Ssl::PeerConnector::cbCheckForPeekAndSpliceDone(allow_t answer, void *data) void Ssl::PeerConnector::checkForPeekAndSplice() { - SSL *ssl = fd_table[serverConn->fd].ssl; // Mark Step3 of bumping if (request->clientConnectionManager.valid()) { if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { serverBump->step = Ssl::bumpStep3; - if (!serverBump->serverCert.get()) - serverBump->serverCert.reset(SSL_get_peer_certificate(ssl)); } } + handleServerCertificate(); + ACLFilledChecklist *acl_checklist = new ACLFilledChecklist( ::Config.accessList.ssl_bump, request.getRaw(), NULL); diff --git a/src/ssl/PeerConnector.h b/src/ssl/PeerConnector.h index 740e32e189..d20dbf7565 100644 --- a/src/ssl/PeerConnector.h +++ b/src/ssl/PeerConnector.h @@ -144,6 +144,10 @@ 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 &); @@ -161,6 +165,7 @@ private: 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 serverCertificateHandled; ///< whether handleServerCertificate() succeeded }; } // namespace Ssl diff --git a/src/url.cc b/src/url.cc index ef6c9f8994..a1efa650c1 100644 --- a/src/url.cc +++ b/src/url.cc @@ -683,30 +683,8 @@ urlMakeAbsolute(const HttpRequest * req, const char *relUrl) return (urlbuf); } -/* - * matchDomainName() compares a hostname with a domainname according - * to the following rules: - * - * HOST DOMAIN MATCH? - * ------------- ------------- ------ - * foo.com foo.com YES - * .foo.com foo.com YES - * x.foo.com foo.com NO - * foo.com .foo.com YES - * .foo.com .foo.com YES - * x.foo.com .foo.com YES - * - * We strip leading dots on hosts (but not domains!) so that - * ".foo.com" is is always the same as "foo.com". - * - * Return values: - * 0 means the host matches the domain - * 1 means the host is greater than the domain - * -1 means the host is less than the domain - */ - int -matchDomainName(const char *h, const char *d) +matchDomainName(const char *h, const char *d, bool honorWildcards) { int dl; int hl; @@ -763,6 +741,13 @@ matchDomainName(const char *h, const char *d) /* * We found different characters in the same position (from the end). */ + + // If the h has a form of "*.foo.com" and d has a form of "x.foo.com" + // then the h[hl] points to '*', h[hl+1] to '.' and d[dl] to 'x' + // The following checks are safe, the "h[hl + 1]" in the worst case is '\0'. + if (honorWildcards && h[hl] == '*' && h[hl + 1] == '.') + return 0; + /* * If one of those character is '.' then its special. In order * for splay tree sorting to work properly, "x-foo.com" must