Add UMA metrics for detecting and reporting update certificate changes.

LibcurlHttpFetcher checks if the update server certificate has changed
since last update, and stores an eventual report in prefs. UpdateCheckScheduler
submits to UMA reports from the previous update.

BUG=chromium-os:19842
TEST=Included unittest for the new class and tested locally on an Alex device,
since we need to verify against the actual server certificates.

Change-Id: I5bee5d648982cd7618db09b67d5bff377eaa1fc1
Reviewed-on: http://gerrit.chromium.org/gerrit/7565
Reviewed-by: Bruno Pontes Soares Rocha <bpontes@chromium.org>
Tested-by: Bruno Pontes Soares Rocha <bpontes@chromium.org>
Reviewed-by: Andrew de los Reyes <adlr@chromium.org>
Reviewed-by: Darin Petkov <petkov@chromium.org>
diff --git a/certificate_checker.cc b/certificate_checker.cc
new file mode 100644
index 0000000..579038e
--- /dev/null
+++ b/certificate_checker.cc
@@ -0,0 +1,185 @@
+// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "update_engine/certificate_checker.h"
+
+#include <string>
+
+#include <base/string_number_conversions.h>
+#include <base/string_util.h>
+#include <base/logging.h>
+#include <curl/curl.h>
+#include <metrics/metrics_library.h>
+#include <openssl/evp.h>
+#include <openssl/ssl.h>
+
+#include "update_engine/prefs_interface.h"
+#include "update_engine/utils.h"
+
+using std::string;
+
+namespace chromeos_update_engine {
+
+namespace {
+// This should be in the same order of CertificateChecker::ServerToCheck, with
+// the exception of kNone.
+static const char* kReportToSendKey[2] =
+    {kPrefsCertificateReportToSendUpdate,
+     kPrefsCertificateReportToSendDownload};
+}  // namespace {}
+
+bool OpenSSLWrapper::GetCertificateDigest(X509_STORE_CTX* x509_ctx,
+                                          int* out_depth,
+                                          unsigned int* out_digest_length,
+                                          unsigned char* out_digest) const {
+  TEST_AND_RETURN_FALSE(out_digest);
+  X509* certificate = X509_STORE_CTX_get_current_cert(x509_ctx);
+  TEST_AND_RETURN_FALSE(certificate);
+  int depth = X509_STORE_CTX_get_error_depth(x509_ctx);
+  if (out_depth)
+    *out_depth = depth;
+
+  unsigned int len;
+  const EVP_MD* digest_function = EVP_sha256();
+  bool success = X509_digest(certificate, digest_function, out_digest, &len);
+
+  if (success && out_digest_length)
+    *out_digest_length = len;
+  return success;
+}
+
+// static
+MetricsLibraryInterface* CertificateChecker::metrics_lib_ = NULL;
+
+// static
+PrefsInterface* CertificateChecker::prefs_ = NULL;
+
+// static
+OpenSSLWrapper* CertificateChecker::openssl_wrapper_ = NULL;
+
+// static
+CURLcode CertificateChecker::ProcessSSLContext(CURL* curl_handle,
+                                               SSL_CTX* ssl_ctx,
+                                               void* ptr) {
+  // From here we set the SSL_CTX to another callback, from the openssl library,
+  // which will be called after each server certificate is validated. However,
+  // since openssl does not allow us to pass our own data pointer to the
+  // callback, the certificate check will have to be done statically. Since we
+  // need to know which update server we are using in order to check the
+  // certificate, we hardcode Chrome OS's two known update servers here, and
+  // define a different static callback for each. Since this code should only
+  // run in official builds, this should not be a problem. However, if an update
+  // server different from the ones listed here is used, the check will not
+  // take place.
+  ServerToCheck* server_to_check = reinterpret_cast<ServerToCheck*>(ptr);
+
+  // We check which server to check and set the appropriate static callback.
+  if (*server_to_check == kUpdate)
+    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, VerifySSLCallbackUpdateCheck);
+  if (*server_to_check == kDownload)
+    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, VerifySSLCallbackDownload);
+
+  return CURLE_OK;
+}
+
+// static
+int CertificateChecker::VerifySSLCallbackUpdateCheck(int preverify_ok,
+                                                     X509_STORE_CTX* x509_ctx) {
+  return CertificateChecker::CheckCertificateChange(
+      kUpdate, preverify_ok, x509_ctx) ? 1 : 0;
+}
+
+// static
+int CertificateChecker::VerifySSLCallbackDownload(int preverify_ok,
+                                                  X509_STORE_CTX* x509_ctx) {
+  return CertificateChecker::CheckCertificateChange(
+      kDownload, preverify_ok, x509_ctx) ? 1 : 0;
+}
+
+// static
+bool CertificateChecker::CheckCertificateChange(
+    ServerToCheck server_to_check, int preverify_ok,
+    X509_STORE_CTX* x509_ctx) {
+  static const char kUMAActionCertChanged[] =
+      "Updater.ServerCertificateChanged";
+  static const char kUMAActionCertFailed[] = "Updater.ServerCertificateFailed";
+  TEST_AND_RETURN_FALSE(server_to_check != kNone);
+
+  // If pre-verification failed, we are not interested in the current
+  // certificate. We store a report to UMA and just propagate the fail result.
+  if (!preverify_ok) {
+    LOG_IF(WARNING, !prefs_->SetString(kReportToSendKey[server_to_check],
+                                       kUMAActionCertFailed))
+        << "Failed to store UMA report on a failure to validate "
+        << "certificate from update server.";
+    return false;
+  }
+
+  int depth;
+  unsigned int digest_length;
+  unsigned char digest[EVP_MAX_MD_SIZE];
+
+  if (!openssl_wrapper_->GetCertificateDigest(x509_ctx,
+                                              &depth,
+                                              &digest_length,
+                                              digest)) {
+    LOG(WARNING) << "Failed to generate digest of X509 certificate "
+                 << "from update server.";
+    return true;
+  }
+
+  // We convert the raw bytes of the digest to an hex string, for storage in
+  // prefs.
+  string digest_string = base::HexEncode(digest, digest_length);
+
+  string storage_key = StringPrintf("%s-%d-%d",
+                                    kPrefsUpdateServerCertificate,
+                                    server_to_check,
+                                    depth);
+  string stored_digest;
+  // If there's no stored certificate, we just store the current one and return.
+  if (!prefs_->GetString(storage_key, &stored_digest)) {
+    LOG_IF(WARNING, !prefs_->SetString(storage_key, digest_string))
+        << "Failed to store server certificate on storage key " << storage_key;
+    return true;
+  }
+
+  // Certificate changed, we store a report to UMA and store the most recent
+  // certificate.
+  if (stored_digest != digest_string) {
+    LOG_IF(WARNING, !prefs_->SetString(kReportToSendKey[server_to_check],
+                                       kUMAActionCertChanged))
+        << "Failed to store UMA report on a change on the "
+        << "certificate from update server.";
+    LOG_IF(WARNING, !prefs_->SetString(storage_key, digest_string))
+        << "Failed to store server certificate on storage key " << storage_key;
+  }
+
+  // Since we don't perform actual SSL verification, we return success.
+  return true;
+}
+
+// static
+void CertificateChecker::FlushReport() {
+  // This check shouldn't be needed, but it is useful for testing.
+  TEST_AND_RETURN(metrics_lib_ && prefs_);
+
+  // We flush reports for both servers.
+  for (size_t i = 0; i < arraysize(kReportToSendKey); i++) {
+    string report_to_send;
+    if (prefs_->GetString(kReportToSendKey[i], &report_to_send) &&
+        !report_to_send.empty()) {
+      // There is a report to be sent. We send it and erase it.
+      LOG_IF(WARNING, !metrics_lib_->SendUserActionToUMA(report_to_send))
+          << "Failed to send server certificate report to UMA: "
+          << report_to_send;
+      // Since prefs doesn't provide deletion, we just set it as an empty
+      // string.
+      LOG_IF(WARNING, !prefs_->SetString(kReportToSendKey[i], ""))
+          << "Failed to erase server certificate report to be sent to UMA";
+    }
+  }
+}
+
+}  // namespace chromeos_update_engine