]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Add server_name ACL matching server name(s) obtained from various sources such as...
authorChristos Tsantilas <chtsanti@users.sourceforge.net>
Fri, 10 Apr 2015 08:54:13 +0000 (11:54 +0300)
committerChristos Tsantilas <chtsanti@users.sourceforge.net>
Fri, 10 Apr 2015 08:54:13 +0000 (11:54 +0300)
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.

12 files changed:
src/AclRegs.cc
src/URL.h
src/acl/DomainData.h
src/acl/Makefile.am
src/acl/ServerName.cc [new file with mode: 0644]
src/acl/ServerName.h [new file with mode: 0644]
src/cf.data.pre
src/client_side.cc
src/client_side.h
src/ssl/PeerConnector.cc
src/ssl/PeerConnector.h
src/url.cc

index 0b129a521417fd9e43157304cf39ebdb7a3c43d3..9be5e3e052a34ec7b8f0328d85c799957f838a3f 100644 (file)
@@ -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<X509 *> ACLServerCertificate::X509FingerprintRegistryEntry_(new A
 
 ACL::Prototype ACLAtStep::RegistryProtoype(&ACLAtStep::RegistryEntry_, "at_step");
 ACLStrategised<Ssl::BumpStep> ACLAtStep::RegistryEntry_(new ACLAtStepData, ACLAtStepStrategy::Instance(), "at_step");
+
+ACL::Prototype ACLServerName::LiteralRegistryProtoype(&ACLServerName::LiteralRegistryEntry_, "ssl::server_name");
+ACLStrategised<char const *> 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<char const *> ACLServerName::RegexRegistryEntry_(new ACLRegexData, ACLServerNameStrategy::Instance(), "ssl::server_name_regex", ServerNameRegexFlags);
 #endif
 
 #if USE_SQUID_EUI
index 11d0dac61ede14df7a1bb8b0ec07e2260401c915..be66fe0dc1fcfd5af878b964989436c550700810 100644 (file)
--- 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);
index 3f5918e57b99cb89998bf5db70918eff1c17f3b3..a3af868d1d6d4fee58e0f8caa927c8e9a932c569 100644 (file)
@@ -19,7 +19,7 @@ class ACLDomainData : public ACLData<char const *>
 
 public:
     virtual ~ACLDomainData();
-    bool match(char const *);
+    virtual bool match(char const *);
     virtual SBufList dump() const;
     void parse();
     bool empty() const;
index 61117c66be555fbe2e3a41f345846c715e8be67f..05fb4f85824be54223fd6b0fc93831478cd90c65 100644 (file)
@@ -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 (file)
index 0000000..31a85c1
--- /dev/null
@@ -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<const char *>(a);
+    const char *d = static_cast<const char *>(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<char *>(host);
+    char const * const * result = domains->find(h, aclHostDomainCompare);
+
+    debugs(28, 3, "'" << host << "' " << (result ? "found" : "NOT found"));
+
+    return (result != NULL);
+
+}
+
+ACLData<char const *> *
+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<class MatchType>
+int
+check_cert_domain( void *check_data, ASN1_STRING *cn_data)
+{
+    char cn[1024];
+    ACLData<MatchType> * data = (ACLData<MatchType> *)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<MatchType> * &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<MatchType>))
+                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 (file)
index 0000000..9ba3fba
--- /dev/null
@@ -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<char const *> *clone() const;
+};
+
+class ACLServerNameStrategy : public ACLStrategy<char const *>
+{
+
+public:
+    virtual int match (ACLData<MatchType> * &, 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<char const *> LiteralRegistryEntry_;
+    static ACL::Prototype RegexRegistryProtoype;
+    static ACLStrategised<char const *> RegexRegistryEntry_;
+};
+
+#endif /* SQUID_ACLSERVERNAME_H */
+
index 72ee58b1f32a75da18e4a7c9de97b1ef1634a6dd..7f1c921a02cc0695fb95fa06652d28796913db42 100644 (file)
@@ -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]
index 8ea41a578e672ffb2b6d5f8050513a74a6c4cdb7..85c6a22e763819f5dbe177b50aa29d75496cd4e1 100644 (file)
@@ -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)
index 1e93498778da6e5121466f5a26d6c3dc375b1a4e..7a34f33b3b5ebda7fc6f03968f3773655c382d4e 100644 (file)
@@ -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
index 50a01c1223998dead34a6e6e5e8eddff902cbbef..5348432c7af3dd7242e0888f25ef200abba14037 100644 (file)
@@ -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<CbDialer*>(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::CertErrors *>(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);
index 740e32e1894ca67872c9bea56c92fdaa3d4d60ca..d20dbf7565547695ee40d5df56b27584ac260e6c 100644 (file)
@@ -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
index ef6c9f8994a0ee8afc6218b57995e240e0652ec8..a1efa650c1fdc26c34e3da7ab2755b4240725645 100644 (file)
@@ -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