Index: components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc |
diff --git a/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc b/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d03b1016cb65eb748b003efd3fbe0463ac9ee161 |
--- /dev/null |
+++ b/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc |
@@ -0,0 +1,429 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+// This file implements the tamper detection logic, where we want to detect |
+// whether there are middleboxes and whether they are tampering the response |
+// which maybe break correct communication and data transfer between Chrome |
+// and data reduction proxy. |
+// |
+// A high-level description of our tamper detection process works in two steps: |
+// 1. Data reduction proxy selects the requests we want to detect tamper; |
+// for the selected ones, data reduction proxy generates a series of |
+// fingerprints of the response, and append them to the Chrome-Proxy header; |
+// 2. At Chrome client side, once it sees such fingerprints, it uses the |
+// same method of data reduction proxy to generate the fingerprints on |
+// the response it receives, compare it to the result on the response |
+// data reduction proxy sends, i.e., the attached fingerprints in |
+// Chrome-Proxy header, to see if they are identical and report to UMA if |
+// there is any tamper detected. |
+// |
+// Right now we have 4 fingerprints (listed below). Chrome first check the |
+// fingerprint of Chrome-Proxy header. If Chrome-Proxy header has been |
+// tampered, then other fingerprints would not be checked; if not, Chrome |
+// parses the rest of the fingerprints and check whether there is tampering |
+// on each of them. |
+// |
+// 1. Chrome-Proxy header |
+// whether values of Chrome-Proxy have been tampered; |
+// 2. Via header |
+// whether there are middleboxes between Chrome and data reduction proxy; |
+// 3. Some other headers |
+// whether the values of a list of headers have been tampered; |
+// 4. Content-Length header |
+// whether the value of Content-Length is different to what data reduction |
+// proxy sends, which indicates that the response body has been tampered. |
+// |
+// Chrome reports tamper or not information for each fingerprint to UMA. In |
+// general, Chrome reports the number of tampers for each fingerprint on |
+// different carriers, as well as total number of tamper detection handled. |
+// The only special case is the 4th fingerprint, Content-Length, which we have |
+// another dimension, MIME types, Chrome reports the tamper on different MIME |
+// type independently. |
+ |
+ |
+#include <string.h> |
+#include <algorithm> |
+#include <vector> |
+ |
+#include "base/base64.h" |
+#include "base/md5.h" |
+#include "base/metrics/histogram.h" |
+#include "base/metrics/sparse_histogram.h" |
+#include "base/strings/string_number_conversions.h" |
+#include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.h" |
+ |
+#include "net/android/network_library.h" |
+#include "net/http/http_request_headers.h" |
+#include "net/http/http_util.h" |
+ |
+// Macro for UMA report. First report to either |https_histogram| or |
+// |http_histogram| depends on |scheme_is_https|, with carrier ID as bucket |
+// |mcc_mnc|. Then report to http(s)_histogram_Total, which counts the total |
+// number. |
+#define UMA_REPORT(scheme_is_https, http_histogram, https_histogram, mcc_mnc) \ |
+ do { \ |
+ if (scheme_is_https) { \ |
+ UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, mcc_mnc); \ |
+ UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \ |
+ } else { \ |
+ UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, mcc_mnc); \ |
+ UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \ |
+ }\ |
+ } while (0) |
+ |
+namespace data_reduction_proxy { |
+ |
+namespace { |
+// Two fingerprints will be added to Chrome-Proxy header. One starts with |
+// |kTamperDetectFingerprintChromeProxy|, which is the fingerprint for the |
+// Chrome-Proxy header. The other one starts with |kTamperDetectFingerprint|, |
+// which includes all other fingerprints. |
+const char kTamperDetectFingerprint[] = "fp="; |
+const char kTamperDetectFingerprintChromeProxy[] = "cp="; |
+ |
+// Fingerprint |kTamperDetectFingerprint| contains multiple |
+// fingerprints, each starts with a tag followed by "=" and its fingerprint |
+// value. Three fingerprints and their respective tags are defined below. |
+const char kTamperDetectFingerprintVia[] = "via"; |
+const char kTamperDetectFingerprintOther[] = "oh"; |
+const char kTamperDetectFingerprintContengLength[] = "cl"; |
+ |
+} // namespace |
+ |
+void DataReductionProxyTamperDetect::CheckResponseFingerprint( |
+ const net::HttpResponseHeaders* headers, |
+ const bool is_secure_scheme) |
+{ |
+ std::vector<std::string> values = DataReductionProxyTamperDetect:: |
+ GetHeaderValues(headers, "Chrome-Proxy"); |
+ |
+ // |chrome_proxy_fingerprint| holds the value of fingerprint of |
+ // Chrome-Proxy header. |
+ // |other_fingerprints| holds the value of other fingerprints. |
+ std::string chrome_proxy_fingerprint, other_fingerprints; |
+ |
+ // Check if there are fingerprints (and thus need to detect tamper). |
+ if (!ContainsTamperDetectFingerprints(&values, |
+ &chrome_proxy_fingerprint, |
+ &other_fingerprints)) |
+ return; |
+ |
+ // Found tamper detect request field. |
+ // Get carrier ID. |
+ unsigned mcc_mnc = 0; |
+ base::StringToUint(net::android::GetTelephonyNetworkOperator(), &mcc_mnc); |
+ |
+ DataReductionProxyTamperDetect tamper_detect(headers, is_secure_scheme, |
+ mcc_mnc, &values); |
+ |
+ // Check if Chrome-Proxy header has been tampered. |
+ if (tamper_detect.IsChromeProxyHeaderTampered(chrome_proxy_fingerprint)) { |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ChromeProxy", |
+ "DataReductionProxy.HTTPHeaderTampered_ChromeProxy", |
+ mcc_mnc); |
+ return; |
+ } else |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTamperDetection", |
+ "DataReductionProxy.HTTPHeaderTamperDetection", |
+ mcc_mnc); |
+ |
+ // Separate fingerprints from |other_fingerprints|. |
+ net::HttpUtil::ValuesIterator it(other_fingerprints.begin(), |
+ other_fingerprints.end(), '|'); |
+ |
+ // For each fingerprint, get its name |key| and the fingerprint value |value| |
+ // from data reduction proxy. CheckReportFingerprint will handle the tamper |
+ // detect and corresponding UMA report. |
+ size_t delimiter_pos = std::string::npos; |
+ while (it.GetNext()) { |
+ delimiter_pos = it.value().find("="); |
+ if (delimiter_pos == std::string::npos) |
+ continue; |
+ std::string key = it.value().substr(0, delimiter_pos); |
+ std::string value = it.value().substr(delimiter_pos + 1); |
+ |
+ FingerprintCode fingerprint_code = tamper_detect.GetFingerprintCode(key); |
+ switch (fingerprint_code) { |
+ case VIA: |
+ if (tamper_detect.IsViaHeaderTampered(value)) |
+ tamper_detect.ReportViaHeaderTamperedUMA(); |
+ break; |
+ case OTHERHEADERS: |
+ if (tamper_detect.AreOtherHeadersTampered(value)) |
+ tamper_detect.ReportOtherHeadersTamperedUMA(); |
+ break; |
+ case CONTENTLENGTH: |
+ if (tamper_detect.IsContentLengthHeaderTampered(value)) |
+ tamper_detect.ReportContentLengthHeaderTamperedUMA(); |
+ break; |
+ case CHROMEPROXY: |
bolian
2014/07/09 22:51:06
Replace the last two cases with
case default:
xingx
2014/07/10 03:07:37
Done.
|
+ case NONEXIST: |
+ break; |
+ } |
+ } |
+ return; |
+} |
+ |
+// Initialize fingerprint code map. |
+DataReductionProxyTamperDetect::DataReductionProxyTamperDetect( |
+ const net::HttpResponseHeaders* headers, const bool secure, |
+ const unsigned mcc_mnc_, std::vector<std::string>* values) |
+ : response_headers(headers), |
+ is_secure_scheme(secure), |
+ mcc_mnc(mcc_mnc_), |
+ clean_chrome_proxy_header_values(values) { |
+ fingperprint_tag_code_map = std::map<std::string, FingerprintCode>(); |
+ fingperprint_tag_code_map[kTamperDetectFingerprintVia] = VIA; |
+ fingperprint_tag_code_map[kTamperDetectFingerprintOther] = OTHERHEADERS; |
+ fingperprint_tag_code_map[kTamperDetectFingerprintContengLength] = |
+ CONTENTLENGTH; |
+}; |
+ |
+DataReductionProxyTamperDetect::~DataReductionProxyTamperDetect() {}; |
+ |
+// Check whether Chrome-Proxy header has been tampered. |
+// |fingerprint| is the fingerprint Chrome received from data reduction proxy, |
+// which is Base64 encoded. Decode it first. Calculate the hash value of |
+// Chrome-Proxy header. Note that |clean_chrome_proxy_header_values| holds |
+// the values of Chrome-Proxy header with its own fingerprint removed, |
+// so it's the correct values to be used to calculate fingerprint. |
+// Compare calculated fingerprint to the fingerprint from data reduction proxy |
+// (the removed value) and see there is tamper detected. |
+bool DataReductionProxyTamperDetect::IsChromeProxyHeaderTampered( |
+ const std::string& fingerprint) const { |
+ std::string received_fingerprint; |
+ if (!base::Base64Decode(fingerprint, &received_fingerprint)) |
bolian
2014/07/09 22:51:05
This should be treated as tampered, right?
xingx
2014/07/10 03:07:40
That's right, my mistake.
|
+ return false; |
+ // Calculate the MD5 hash value of Chrome-Proxy. |
+ std::string actual_fingerprint = GetMD5( |
+ ValuesToSortedString(*clean_chrome_proxy_header_values)); |
+ |
+ return received_fingerprint != actual_fingerprint; |
+} |
+ |
+// Check whether there are proxies/middleboxes between Chrome |
+// and data reduction proxy. Concretely, it checks whether there are other |
+// proxies/middleboxes' name after data reduction proxy's name in Via header. |
+bool DataReductionProxyTamperDetect::IsViaHeaderTampered( |
+ const std::string& fingerprint) const { |
+ |
+ std::vector<std::string> vias = GetHeaderValues(response_headers, "via"); |
+ |
+ // If there is no tag, then data reduction proxy's tag have been removed. |
+ if (vias.size() == 0) return true; |
+ // Check whether the last proxy/middlebox is data reduction proxy or not. |
+ |
+ bool seen_data_reduction_proxy = false; |
+ // TODO(xingx): change 2 to array size of such constant array. |
bolian
2014/07/09 22:51:06
why not do it now?
xingx
2014/07/10 03:07:38
Need to figure it out...
|
+ for (int i = 0; i < 2; ++i) { |
+ if (vias[vias.size() - 1].find(kDataReductionProxyViaValues[i]) != |
+ std::string::npos) { |
+ seen_data_reduction_proxy = true; |
+ break; |
+ } |
+ } |
+ |
+ return !seen_data_reduction_proxy; |
+} |
+ |
+// Report Via header tamper detected. |
+void DataReductionProxyTamperDetect::ReportViaHeaderTamperedUMA() const { |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_Via", |
+ "DataReductionProxy.HTTPHeaderTampered_Via", |
+ mcc_mnc); |
+} |
+ |
+// Check whether values of a predefined list of headers have been tampered. |
+// The format for |fingerprint| is: |
+// [base64fingerprint]:header_name1:header_namer2:... |
+// Firstly extract the header names in the |fingerprint|. |
+// For each header, |
+// 1) get all the values of such header; |
+// 2) sort the values alphabetically; |
+// 3) concatenate sorted values to a string and calculate MD5 hash on it. |
+// Finally, compare whether it equals to the received fingerprint. |
+bool DataReductionProxyTamperDetect::AreOtherHeadersTampered( |
+ const std::string& fingerprint) const { |
+ std::string received_fingerprint; |
+ |
+ net::HttpUtil::ValuesIterator it(fingerprint.begin(), |
+ fingerprint.end(), ':'); |
+ |
+ // Make sure there is [base64fingerprint] and it can be decoded. |
+ if (!(it.GetNext() && |
bolian
2014/07/09 22:51:06
Will the server always send "oh=" even if the head
xingx
2014/07/10 03:07:37
Yes.
Done.
|
+ base::Base64Decode(std::string(it.value()), &received_fingerprint))) |
+ return false; |
+ |
+ std::string header_values; |
+ // Enumerate the list of headers. |
+ while (it.GetNext()) { |
+ // Get values of one header. |
+ std::vector<std::string> values = |
+ GetHeaderValues(response_headers, std::string(it.value())); |
+ // Sort the values and concatenate them. |
+ header_values += ValuesToSortedString(values) + ";"; |
+ } |
+ |
+ // Calculate MD5 hash value on the concatenated string. |
+ std::string actual_fingerprint = GetMD5(header_values); |
+ |
+ return received_fingerprint != actual_fingerprint; |
+} |
+ |
+// Report other headers tamper detected. |
+void DataReductionProxyTamperDetect::ReportOtherHeadersTamperedUMA() const { |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_OtherHeaders", |
+ "DataReductionProxy.HTTPHeaderTampered_OtherHeaders", |
+ mcc_mnc); |
+} |
+ |
+ |
+// For Content-Length tamper detection... |
+// Check whether the Content-Length value is different from what |
+// data reduction proxy sees. This is an indicator that the response body |
+// have been modified. |
+// It's modified only if we can decode Content-Length numbers at both end |
+// and such two numbers are not equal. |
+bool DataReductionProxyTamperDetect::IsContentLengthHeaderTampered( |
+ const std::string& fingerprint) const { |
+ int received_content_length, actual_content_length; |
+ // If Content-Length value from data reduction proxy is not available or |
+ // it cannot be converted to integer, pass. |
+ if (base::StringToInt(fingerprint, &received_content_length)) { |
+ std::string actual_content_length_; |
+ // If there is Content-Length at Chrome client side is not available, pass. |
+ if (response_headers->GetNormalizedHeader("Content-Length", |
+ &actual_content_length_)) { |
+ // If the Content-Length value cannot be converted to integer, |
+ // i.e., not valid, pass. |
+ if (!base::StringToInt(actual_content_length_, &actual_content_length)) |
+ return false; |
+ return received_content_length != actual_content_length; |
+ } |
+ } |
+ return false; |
+} |
+ |
+// Report Content-Length tamper detected. |
+// Get MIME type of the response and report to different UMA histogram. |
+// Right now MIME types contain JavaScript, CSS, Images, and others. |
+void DataReductionProxyTamperDetect::ReportContentLengthHeaderTamperedUMA() |
+ const { |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ContentLength", |
+ "DataReductionProxy.HTTPHeaderTampered_ContentLength", |
+ mcc_mnc); |
+ |
+ // Get MIME type. |
+ std::string mime_type; |
+ response_headers->GetMimeType(&mime_type); |
+ |
+ |
+ // Report tampered JavaScript. |
+ if (mime_type.compare("text/javascript") == 0 || |
+ mime_type.compare("application/x-javascript") == 0 || |
+ mime_type.compare("application/javascript") == 0) |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ContentLength_JS", |
+ "DataReductionProxy.HTTPHeaderTampered_ContentLength_JS", |
+ mcc_mnc); |
+ // Report tampered CSSs. |
+ else if (mime_type.compare("text/css") == 0) |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ContentLength_CSS", |
+ "DataReductionProxy.HTTPHeaderTampered_ContentLength_CSS", |
+ mcc_mnc); |
+ // Report tampered images. |
+ else if (mime_type.find("image") == 0) |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Image", |
+ "DataReductionProxy.HTTPHeaderTampered_ContentLength_Image", |
+ mcc_mnc); |
+ // Report tampered other MIME types. |
+ else |
+ UMA_REPORT(is_secure_scheme, |
+ "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Other", |
+ "DataReductionProxy.HTTPHeaderTampered_ContentLength_Other", |
+ mcc_mnc); |
+} |
+ |
+DataReductionProxyTamperDetect::FingerprintCode |
+ DataReductionProxyTamperDetect::GetFingerprintCode( |
+ const std::string& tag) { |
+ std::map<std::string, DataReductionProxyTamperDetect::FingerprintCode> |
+ ::iterator it = fingperprint_tag_code_map.find(tag); |
+ |
+ return it == fingperprint_tag_code_map.end() ? NONEXIST : it->second; |
+} |
+ |
+// Enumerate the values of Chrome-Proxy header and check if there is the |
+// fingerprint of Chrome-Proxy header (kTamperDetectFingerprintChromeProxy) |
+// and other fingerprints (kTamperDetectFingerprint). If there are, save them |
+// accordingly. |
+bool DataReductionProxyTamperDetect::ContainsTamperDetectFingerprints( |
bolian
2014/07/09 22:51:06
Rename this. The name sounds like it won't change
xingx
2014/07/10 03:07:40
Done.
|
+ std::vector<std::string>* values, |
+ std::string* chrome_proxy_fingerprint, |
+ std::string* other_fingerprints) { |
+ bool contains_tamper_detect_fingerprints = false; |
+ for (size_t i = 0; i < values->size(); ++i) { |
+ if ((*values)[i].find(data_reduction_proxy:: |
bolian
2014/07/09 22:51:06
s/data_reduction_proxy:://
This is in the same nam
xingx
2014/07/10 03:07:37
Done.
|
+ kTamperDetectFingerprintChromeProxy) == 0) { |
+ contains_tamper_detect_fingerprints = true; |
+ // Save Chrome-Proxy fingerprint. |
+ *chrome_proxy_fingerprint = (*values)[i].substr(strlen( |
+ data_reduction_proxy::kTamperDetectFingerprintChromeProxy)); |
+ // Erase Chrome-Proxy's fingerprint from Chrome-Proxy header for |
+ // later fingerprint calculation. |
+ values->erase(values->begin() + i); |
+ // Adjust index |i| after erasing. |
+ --i; |
bolian
2014/07/09 22:51:06
Don't change the vector while iterating. Do it out
xingx
2014/07/10 03:07:38
Done.
|
+ } |
+ else if ((*values)[i].find(data_reduction_proxy::kTamperDetectFingerprint) |
+ == 0) { |
+ // Save other fingerprints. |
+ *other_fingerprints = (*values)[i].substr(strlen(data_reduction_proxy:: |
+ kTamperDetectFingerprint)); |
+ } |
+ } |
+ return contains_tamper_detect_fingerprints; |
+} |
+ |
+// Utility function. Sort the strings in |values| alphabetically, concatenate |
+// them into a string. |
+std::string DataReductionProxyTamperDetect::ValuesToSortedString( |
+ std::vector<std::string> &values) { |
+ std::string aggregated_values; |
+ |
+ std::sort(values.begin(), values.end()); |
+ for (size_t i = 0; i < values.size(); ++i) |
+ aggregated_values += values[i] + ","; |
+ return aggregated_values; |
+} |
+ |
+// Utility function. For a given string, calculate and return the MD5 hash |
+// value of the string. |
+std::string DataReductionProxyTamperDetect::GetMD5(const std::string &input) { |
+ base::MD5Digest digest; |
+ base::MD5Sum(input.c_str(), input.size(), &digest); |
+ return std::string((char*)digest.a, ARRAYSIZE_UNSAFE(digest.a)); |
+} |
+ |
+// Utility function. For a given |header_name|, get all its values and return |
+// the vector contains all of the values. |
+std::vector<std::string> DataReductionProxyTamperDetect::GetHeaderValues( |
+ const net::HttpResponseHeaders* headers, const std::string& header_name) { |
+ std::vector<std::string> values; |
+ std::string value; |
+ void* iter = NULL; |
+ while (headers->EnumerateHeader(&iter, header_name, &value)) { |
+ values.push_back(value); |
+ } |
+ return values; |
+} |
+ |
+} // namespace data_reduction_proxy |