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 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 #endif #if HAVE_CLIMITS #include #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 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(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_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(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 (ptr); delete errDetail; } static void ssl_free_SslErrors(void *, void *ptr, CRYPTO_EX_DATA *, int, long, void *) { Ssl::CertErrors *errs = static_cast (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 (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 (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 (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 #endif #if HAVE_OPENSSL_X509V3_H #include #endif #if HAVE_OPENSSL_ERR_H #include #endif #if HAVE_OPENSSL_ENGINE_H #include #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 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);