Handle infinite certificate validation loops caused by OpenSSL bug #3090.

If OpenSSL is stuck in a validation loop, Squid breaks the loop and triggers a
new custom SQUID_X509_V_ERR_INFINITE_VALIDATION SSL validation error. That
error cannot be bypassed using sslproxy_cert_error because to break the loop
Squid has to tell OpenSSL that the certificate is invalid, which terminates
the SSL connection.

Validation loops exceeding SQUID_CERT_VALIDATION_ITERATION_MAX iterations
are deemed infinite. That macro is defined to be 16384, but that default can
be overwritten using CPPFLAGS.

This is a Measurement Factory project
=== modified file 'errors/templates/error-details.txt'
--- errors/templates/error-details.txt	2011-11-08 13:40:26 +0000
+++ errors/templates/error-details.txt	2013-07-25 15:23:22 +0000
@@ -1,20 +1,24 @@
+name: SQUID_X509_V_ERR_INFINITE_VALIDATION
+detail: "%ssl_error_descr: %ssl_subject"
+descr: "Cert validation infinite loop detected"
+
 name: SQUID_ERR_SSL_HANDSHAKE
 detail: "%ssl_error_descr: %ssl_lib_error"
 descr: "Handshake with SSL server failed"
 
 name: SQUID_X509_V_ERR_DOMAIN_MISMATCH
 detail: "%ssl_error_descr: %ssl_subject"
 descr: "Certificate does not match domainname"
 
 name: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT
 detail: "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name"
 descr: "Unable to get issuer certificate"
 
 name: X509_V_ERR_UNABLE_TO_GET_CRL
 detail: "%ssl_error_descr: %ssl_subject"
 descr: "Unable to get certificate CRL"
 
 name: X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE
 detail: "%ssl_error_descr: %ssl_subject"
 descr: "Unable to decrypt certificate's signature"
 

=== modified file 'src/cf.data.pre'
--- src/cf.data.pre	2013-07-15 15:47:00 +0000
+++ src/cf.data.pre	2013-07-26 08:50:47 +0000
@@ -2420,40 +2420,43 @@
 LOC: Config.ssl_client.cert_error
 TYPE: acl_access
 DOC_START
 	Use this ACL to bypass server certificate validation errors.
 
 	For example, the following lines will bypass all validation errors
 	when talking to servers for example.com. All other
 	validation errors will result in ERR_SECURE_CONNECT_FAIL error.
 
 		acl BrokenButTrustedServers dstdomain example.com
 		sslproxy_cert_error allow BrokenButTrustedServers
 		sslproxy_cert_error deny all
 
 	This clause only supports fast acl types.
 	See http://wiki.squid-cache.org/SquidFaq/SquidAcl for details.
 	Using slow acl types may result in server crashes
 
 	Without this option, all server certificate validation errors
 	terminate the transaction to protect Squid and the client.
 
+	SQUID_X509_V_ERR_INFINITE_VALIDATION error cannot be bypassed
+	but should not happen unless your OpenSSL library is buggy.
+
 	SECURITY WARNING:
 		Bypassing validation errors is dangerous because an
 		error usually implies that the server cannot be trusted
 		and the connection may be insecure.
 
 	See also: sslproxy_flags and DONT_VERIFY_PEER.
 DOC_END
 
 NAME: sslproxy_cert_sign
 IFDEF: USE_SSL
 DEFAULT: none
 POSTSCRIPTUM: signUntrusted ssl::certUntrusted
 POSTSCRIPTUM: signSelf ssl::certSelfSigned
 POSTSCRIPTUM: signTrusted all
 TYPE: sslproxy_cert_sign
 LOC: Config.ssl_client.cert_sign
 DOC_START
 
         sslproxy_cert_sign <signing algorithm> acl ...
 

=== modified file 'src/globals.h'
--- src/globals.h	2013-06-18 10:23:41 +0000
+++ src/globals.h	2013-07-25 12:51:03 +0000
@@ -120,31 +120,32 @@
 #endif
 #if _SQUID_WINDOWS_
 extern unsigned int WIN32_OS_version;	/* 0 */
 extern char *WIN32_OS_string;           /* NULL */
 extern char *WIN32_Service_name;        /* NULL */
 extern char *WIN32_Command_Line;        /* NULL */
 extern char *WIN32_Service_Command_Line; /* NULL */
 extern unsigned int WIN32_run_mode;     /* _WIN_SQUID_RUN_MODE_INTERACTIVE */
 #endif
 #if HAVE_SBRK
 extern void *sbrk_start;	/* 0 */
 #endif
 
 extern int ssl_ex_index_server;	/* -1 */
 extern int ssl_ctx_ex_index_dont_verify_domain; /* -1 */
 extern int ssl_ex_index_cert_error_check;	/* -1 */
 extern int ssl_ex_index_ssl_error_detail;      /* -1 */
 extern int ssl_ex_index_ssl_peeked_cert;      /* -1 */
 extern int ssl_ex_index_ssl_errors;   /* -1 */
 extern int ssl_ex_index_ssl_cert_chain;  /* -1 */
+extern int ssl_ex_index_ssl_validation_counter;  /* -1 */
 
 extern const char *external_acl_message;      /* NULL */
 extern int opt_send_signal;	/* -1 */
 extern int opt_no_daemon; /* 0 */
 extern int opt_parse_cfg_only; /* 0 */
 
 /// current Squid process number (e.g., 4).
 /// Zero for SMP-unaware code and in no-SMP mode.
 extern int KidIdentifier; /* 0 */
 
 #endif /* SQUID_GLOBALS_H */

=== modified file 'src/ssl/ErrorDetail.cc'
--- src/ssl/ErrorDetail.cc	2013-02-12 11:34:35 +0000
+++ src/ssl/ErrorDetail.cc	2013-07-25 15:20:28 +0000
@@ -2,40 +2,42 @@
 #include "errorpage.h"
 #include "ssl/ErrorDetail.h"
 #if HAVE_MAP
 #include <map>
 #endif
 #if HAVE_CLIMITS
 #include <climits>
 #endif
 
 struct SslErrorEntry {
     Ssl::ssl_error_t value;
     const char *name;
 };
 
 static const char *SslErrorDetailDefaultStr = "SSL handshake error (%err_name)";
 //Use std::map to optimize search
 typedef std::map<Ssl::ssl_error_t, const SslErrorEntry *> SslErrors;
 SslErrors TheSslErrors;
 
 static SslErrorEntry TheSslErrorArray[] = {
+    {SQUID_X509_V_ERR_INFINITE_VALIDATION,
+     "SQUID_X509_V_ERR_INFINITE_VALIDATION"},
     {SQUID_X509_V_ERR_CERT_CHANGE,
         "SQUID_X509_V_ERR_CERT_CHANGE"},
     {SQUID_ERR_SSL_HANDSHAKE,
      "SQUID_ERR_SSL_HANDSHAKE"},
     {SQUID_X509_V_ERR_DOMAIN_MISMATCH,
      "SQUID_X509_V_ERR_DOMAIN_MISMATCH"},
     {X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT,
      "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT"},
     {X509_V_ERR_UNABLE_TO_GET_CRL,
      "X509_V_ERR_UNABLE_TO_GET_CRL"},
     {X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE,
      "X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE"},
     {X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE,
      "X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE"},
     {X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY,
      "X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY"},
     {X509_V_ERR_CERT_SIGNATURE_FAILURE,
      "X509_V_ERR_CERT_SIGNATURE_FAILURE"},
     {X509_V_ERR_CRL_SIGNATURE_FAILURE,
      "X509_V_ERR_CRL_SIGNATURE_FAILURE"},

=== modified file 'src/ssl/support.cc'
--- src/ssl/support.cc	2013-06-30 15:54:22 +0000
+++ src/ssl/support.cc	2013-07-26 09:01:35 +0000
@@ -222,40 +222,57 @@
 
 /// \ingroup ServerProtocolSSLInternal
 static int
 ssl_verify_cb(int ok, X509_STORE_CTX * ctx)
 {
     // preserve original ctx->error before SSL_ calls can overwrite it
     Ssl::ssl_error_t error_no = ok ? SSL_ERROR_NONE : ctx->error;
 
     char buffer[256] = "";
     SSL *ssl = (SSL *)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
     SSL_CTX *sslctx = SSL_get_SSL_CTX(ssl);
     const char *server = (const char *)SSL_get_ex_data(ssl, ssl_ex_index_server);
     void *dont_verify_domain = SSL_CTX_get_ex_data(sslctx, ssl_ctx_ex_index_dont_verify_domain);
     ACLChecklist *check = (ACLChecklist*)SSL_get_ex_data(ssl, ssl_ex_index_cert_error_check);
     X509 *peeked_cert = (X509 *)SSL_get_ex_data(ssl, ssl_ex_index_ssl_peeked_cert);
     X509 *peer_cert = ctx->cert;
 
     X509_NAME_oneline(X509_get_subject_name(peer_cert), buffer,
                       sizeof(buffer));
 
+    // detect infinite loops
+    int *validationCounter = static_cast<int *>(SSL_get_ex_data(ssl, ssl_ex_index_ssl_validation_counter));
+    if (!validationCounter) {
+        validationCounter = new int(1);
+        SSL_set_ex_data(ssl, ssl_ex_index_ssl_validation_counter, validationCounter);
+    } else {
+        // overflows allowed if SQUID_CERT_VALIDATION_ITERATION_MAX >= MAX_INT
+        (*validationCounter)++;
+    }
+
+    if ((*validationCounter) >= SQUID_CERT_VALIDATION_ITERATION_MAX) {
+        ok = 0; // or the validation loop will never stop
+        error_no = SQUID_X509_V_ERR_INFINITE_VALIDATION;
+        debugs(83, 2, "SQUID_X509_V_ERR_INFINITE_VALIDATION: " <<
+               *validationCounter << " iterations while checking " << buffer);
+    }
+
     if (ok) {
         debugs(83, 5, "SSL Certificate signature OK: " << buffer);
 
         // Check for domain mismatch only if the current certificate is the peer certificate.
         if (server && peer_cert == X509_STORE_CTX_get_current_cert(ctx)) {
             if (!Ssl::checkX509ServerValidity(peer_cert, server)) {
                 debugs(83, 2, "SQUID_X509_V_ERR_DOMAIN_MISMATCH: Certificate " << buffer << " does not match domainname " << server);
                 ok = 0;
                 error_no = SQUID_X509_V_ERR_DOMAIN_MISMATCH;
             }
         }
     }
 
     if (ok && peeked_cert) {
         // Check whether the already peeked certificate matches the new one.
         if (X509_cmp(peer_cert, peeked_cert) != 0) {
             debugs(83, 2, "SQUID_X509_V_ERR_CERT_CHANGE: Certificate " << buffer << " does not match peeked certificate");
             ok = 0;
             error_no =  SQUID_X509_V_ERR_CERT_CHANGE;
         }
@@ -265,64 +282,68 @@
         X509 *broken_cert =  X509_STORE_CTX_get_current_cert(ctx);
         if (!broken_cert)
             broken_cert = peer_cert;
 
         Ssl::CertErrors *errs = static_cast<Ssl::CertErrors *>(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors));
         if (!errs) {
             errs = new Ssl::CertErrors(Ssl::CertError(error_no, broken_cert));
             if (!SSL_set_ex_data(ssl, ssl_ex_index_ssl_errors,  (void *)errs)) {
                 debugs(83, 2, "Failed to set ssl error_no in ssl_verify_cb: Certificate " << buffer);
                 delete errs;
                 errs = NULL;
             }
         } else // remember another error number
             errs->push_back_unique(Ssl::CertError(error_no, broken_cert));
 
         if (const char *err_descr = Ssl::GetErrorDescr(error_no))
             debugs(83, 5, err_descr << ": " << buffer);
         else
             debugs(83, DBG_IMPORTANT, "SSL unknown certificate error " << error_no << " in " << buffer);
 
-        if (check) {
-            ACLFilledChecklist *filledCheck = Filled(check);
-            assert(!filledCheck->sslErrors);
-            filledCheck->sslErrors = new Ssl::CertErrors(Ssl::CertError(error_no, broken_cert));
-            filledCheck->serverCert.resetAndLock(peer_cert);
-            if (check->fastCheck() == ACCESS_ALLOWED) {
-                debugs(83, 3, "bypassing SSL error " << error_no << " in " << buffer);
-                ok = 1;
-            } else {
-                debugs(83, 5, "confirming SSL error " << error_no);
+        // Check if the certificate error can be bypassed.
+        // Infinity validation loop errors can not bypassed.
+        if (error_no != SQUID_X509_V_ERR_INFINITE_VALIDATION) {
+            if (check) {
+                ACLFilledChecklist *filledCheck = Filled(check);
+                assert(!filledCheck->sslErrors);
+                filledCheck->sslErrors = new Ssl::CertErrors(Ssl::CertError(error_no, broken_cert));
+                filledCheck->serverCert.resetAndLock(peer_cert);
+                if (check->fastCheck() == ACCESS_ALLOWED) {
+                    debugs(83, 3, "bypassing SSL error " << error_no << " in " << buffer);
+                    ok = 1;
+                } else {
+                    debugs(83, 5, "confirming SSL error " << error_no);
+                }
+                delete filledCheck->sslErrors;
+                filledCheck->sslErrors = NULL;
+                filledCheck->serverCert.reset(NULL);
             }
-            delete filledCheck->sslErrors;
-            filledCheck->sslErrors = NULL;
-            filledCheck->serverCert.reset(NULL);
-        }
-        // If the certificate validator is used then we need to allow all errors and
-        // pass them to certficate validator for more processing
-        else if (Ssl::TheConfig.ssl_crt_validator) {
-            ok = 1;
-            // Check if we have stored certificates chain. Store if not.
-            if (!SSL_get_ex_data(ssl, ssl_ex_index_ssl_cert_chain)) {
-                STACK_OF(X509) *certStack = X509_STORE_CTX_get1_chain(ctx);
-                if (certStack && !SSL_set_ex_data(ssl, ssl_ex_index_ssl_cert_chain, certStack))
-                    sk_X509_pop_free(certStack, X509_free);
+            // If the certificate validator is used then we need to allow all errors and
+            // pass them to certficate validator for more processing
+            else if (Ssl::TheConfig.ssl_crt_validator) {
+                ok = 1;
+                // Check if we have stored certificates chain. Store if not.
+                if (!SSL_get_ex_data(ssl, ssl_ex_index_ssl_cert_chain)) {
+                    STACK_OF(X509) *certStack = X509_STORE_CTX_get1_chain(ctx);
+                    if (certStack && !SSL_set_ex_data(ssl, ssl_ex_index_ssl_cert_chain, certStack))
+                        sk_X509_pop_free(certStack, X509_free);
+                }
             }
         }
     }
 
     if (!dont_verify_domain && server) {}
 
     if (!ok && !SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail) ) {
 
         // Find the broken certificate. It may be intermediate.
         X509 *broken_cert = peer_cert; // reasonable default if search fails
         // Our SQUID_X509_V_ERR_DOMAIN_MISMATCH implies peer_cert is at fault.
         if (error_no != SQUID_X509_V_ERR_DOMAIN_MISMATCH) {
             if (X509 *last_used_cert = X509_STORE_CTX_get_current_cert(ctx))
                 broken_cert = last_used_cert;
         }
 
         Ssl::ErrorDetail *errDetail =
             new Ssl::ErrorDetail(error_no, peer_cert, broken_cert);
 
         if (!SSL_set_ex_data(ssl, ssl_ex_index_ssl_error_detail,  errDetail)) {
@@ -634,40 +655,49 @@
     delete static_cast<ACLChecklist *>(ptr); // may be NULL
 }
 
 // "free" function for SSL_get_ex_new_index("ssl_error_detail")
 static void
 ssl_free_ErrorDetail(void *, void *ptr, CRYPTO_EX_DATA *,
                      int, long, void *)
 {
     Ssl::ErrorDetail  *errDetail = static_cast <Ssl::ErrorDetail *>(ptr);
     delete errDetail;
 }
 
 static void
 ssl_free_SslErrors(void *, void *ptr, CRYPTO_EX_DATA *,
                    int, long, void *)
 {
     Ssl::CertErrors *errs = static_cast <Ssl::CertErrors*>(ptr);
     delete errs;
 }
 
+// "free" function for SSL_get_ex_new_index("ssl_ex_index_ssl_validation_counter")
+static void
+ssl_free_int(void *, void *ptr, CRYPTO_EX_DATA *,
+                 int, long, void *)
+{
+    int *counter = static_cast <int *>(ptr);
+    delete counter;
+}
+
 /// \ingroup ServerProtocolSSLInternal
 /// Callback handler function to release STACK_OF(X509) "ex" data stored
 /// in an SSL object.
 static void
 ssl_free_CertChain(void *, void *ptr, CRYPTO_EX_DATA *,
                    int, long, void *)
 {
     STACK_OF(X509) *certsChain = static_cast <STACK_OF(X509) *>(ptr);
     sk_X509_pop_free(certsChain,X509_free);
 }
 
 // "free" function for X509 certificates
 static void
 ssl_free_X509(void *, void *ptr, CRYPTO_EX_DATA *,
               int, long, void *)
 {
     X509  *cert = static_cast <X509 *>(ptr);
     X509_free(cert);
 }
 
@@ -696,40 +726,41 @@
                        ERR_error_string(ssl_error, NULL));
             }
         }
 
 #else
         if (Config.SSL.ssl_engine) {
             fatalf("Your OpenSSL has no SSL engine support\n");
         }
 
 #endif
 
     }
 
     ssl_ex_index_server = SSL_get_ex_new_index(0, (void *) "server", NULL, NULL, NULL);
     ssl_ctx_ex_index_dont_verify_domain = SSL_CTX_get_ex_new_index(0, (void *) "dont_verify_domain", NULL, NULL, NULL);
     ssl_ex_index_cert_error_check = SSL_get_ex_new_index(0, (void *) "cert_error_check", NULL, &ssl_dupAclChecklist, &ssl_freeAclChecklist);
     ssl_ex_index_ssl_error_detail = SSL_get_ex_new_index(0, (void *) "ssl_error_detail", NULL, NULL, &ssl_free_ErrorDetail);
     ssl_ex_index_ssl_peeked_cert  = SSL_get_ex_new_index(0, (void *) "ssl_peeked_cert", NULL, NULL, &ssl_free_X509);
     ssl_ex_index_ssl_errors =  SSL_get_ex_new_index(0, (void *) "ssl_errors", NULL, NULL, &ssl_free_SslErrors);
     ssl_ex_index_ssl_cert_chain = SSL_get_ex_new_index(0, (void *) "ssl_cert_chain", NULL, NULL, &ssl_free_CertChain);
+    ssl_ex_index_ssl_validation_counter = SSL_get_ex_new_index(0, (void *) "ssl_validation_counter", NULL, NULL, &ssl_free_int);
 }
 
 /// \ingroup ServerProtocolSSLInternal
 static int
 ssl_load_crl(SSL_CTX *sslContext, const char *CRLfile)
 {
     X509_STORE *st = SSL_CTX_get_cert_store(sslContext);
     X509_CRL *crl;
     BIO *in = BIO_new_file(CRLfile, "r");
     int count = 0;
 
     if (!in) {
         debugs(83, 2, "WARNING: Failed to open CRL file '" << CRLfile << "'");
         return 0;
     }
 
     while ((crl = PEM_read_bio_X509_CRL(in,NULL,NULL,NULL))) {
         if (!X509_STORE_add_crl(st, crl))
             debugs(83, 2, "WARNING: Failed to add CRL from file '" << CRLfile << "'");
         else

=== modified file 'src/ssl/support.h'
--- src/ssl/support.h	2013-06-07 00:18:11 +0000
+++ src/ssl/support.h	2013-07-26 09:02:26 +0000
@@ -38,47 +38,55 @@
 
 #if HAVE_OPENSSL_SSL_H
 #include <openssl/ssl.h>
 #endif
 #if HAVE_OPENSSL_X509V3_H
 #include <openssl/x509v3.h>
 #endif
 #if HAVE_OPENSSL_ERR_H
 #include <openssl/err.h>
 #endif
 #if HAVE_OPENSSL_ENGINE_H
 #include <openssl/engine.h>
 #endif
 
 /**
  \defgroup ServerProtocolSSLAPI Server-Side SSL API
  \ingroup ServerProtocol
  */
 
 // Custom SSL errors; assumes all official errors are positive
+#define SQUID_X509_V_ERR_INFINITE_VALIDATION -4
 #define SQUID_X509_V_ERR_CERT_CHANGE -3
 #define SQUID_ERR_SSL_HANDSHAKE -2
 #define SQUID_X509_V_ERR_DOMAIN_MISMATCH -1
 // All SSL errors range: from smallest (negative) custom to largest SSL error
 #define SQUID_SSL_ERROR_MIN SQUID_X509_V_ERR_CERT_CHANGE
 #define SQUID_SSL_ERROR_MAX INT_MAX
 
+// Maximum certificate validation callbacks. OpenSSL versions exceeding this
+// limit are deemed stuck in an infinite validation loop (OpenSSL bug #3090)
+// and will trigger the SQUID_X509_V_ERR_INFINITE_VALIDATION error.
+#ifndef SQUID_CERT_VALIDATION_ITERATION_MAX
+#define SQUID_CERT_VALIDATION_ITERATION_MAX 16384
+#endif
+
 namespace AnyP
 {
 class PortCfg;
 };
 
 namespace Ssl
 {
 /// Squid defined error code (<0),  an error code returned by SSL X509 api, or SSL_ERROR_NONE
 typedef int ssl_error_t;
 
 typedef CbDataList<Ssl::ssl_error_t> Errors;
 
 /// An SSL certificate-related error.
 /// Pairs an error code with the certificate experiencing the error.
 class CertError
 {
 public:
     ssl_error_t code; ///< certificate error code
     X509_Pointer cert; ///< certificate with the above error code
     CertError(ssl_error_t anErr, X509 *aCert);