// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/scoped_blocking_call.h"
#include "build/build_config.h"
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/ssl/cert_verifier_platform_browser_test.h"
#include "chrome/browser/ssl/ssl_browsertest_util.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/platform_browser_test.h"
#include "components/security_interstitials/core/controller_client.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/base/net_errors.h"
#include "net/cert/cert_status_flags.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/cert/x509_certificate.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/test_data_directory.h"
#include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom.h"

namespace AuthState = ssl_test_util::AuthState;
namespace CertError = ssl_test_util::CertError;

namespace {

class CRLSetBrowserTest : public PlatformBrowserTest {
 public:
  CRLSetBrowserTest() : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
  ~CRLSetBrowserTest() override = default;

  void SetUpOnMainThread() override {
    PlatformBrowserTest::SetUpOnMainThread();

    host_resolver()->AddRule("*", "127.0.0.1");

    https_server_.ServeFilesFromSourceDirectory("chrome/test/data/");
  }

 protected:
  content::WebContents* GetActiveWebContents() {
    return chrome_test_utils::GetActiveWebContents(this);
  }

  net::EmbeddedTestServer https_server_;
};

const char kHstsTestHostName[] = "hsts-example.test";

}  // namespace

IN_PROC_BROWSER_TEST_F(CRLSetBrowserTest, TestCRLSetRevoked) {
  ASSERT_TRUE(https_server_.Start());

  std::string crl_set_bytes;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ReadFileToString(
        net::GetTestCertsDirectory().AppendASCII("crlset_by_leaf_spki.raw"),
        &crl_set_bytes);
  }
  base::RunLoop run_loop;
  content::GetCertVerifierServiceFactory()->UpdateCRLSet(
      base::as_byte_span(crl_set_bytes), run_loop.QuitClosure());
  run_loop.Run();

  bool interstitial_expected =
      ssl_test_util::CertVerifierSupportsCRLSetBlocking();

  // If an interstitial is expected, NavigateToURL should fail, because the
  // interstitial will be the committed URL, rather than the navigated URL.
  EXPECT_EQ(!interstitial_expected,
            content::NavigateToURL(GetActiveWebContents(),
                                   https_server_.GetURL("/ssl/google.html")));

  ASSERT_EQ(interstitial_expected,
            chrome_browser_interstitials::IsShowingInterstitial(
                GetActiveWebContents()));
  ASSERT_TRUE(
      WaitForRenderFrameReady(GetActiveWebContents()->GetPrimaryMainFrame()));

  if (interstitial_expected) {
    ASSERT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(
        GetActiveWebContents()));

    ssl_test_util::CheckAuthenticationBrokenState(
        GetActiveWebContents(), net::CERT_STATUS_REVOKED,
        AuthState::SHOWING_INTERSTITIAL);
  } else {
    ssl_test_util::CheckAuthenticatedState(GetActiveWebContents(),
                                           AuthState::NONE);
  }
}

// Test that CRLSets configured to block MITM certificates cause the
// known interception interstitial.
IN_PROC_BROWSER_TEST_F(CRLSetBrowserTest, TestCRLSetBlockedInterception) {
  ASSERT_TRUE(https_server_.Start());

  // Load a CRLSet that marks the root as a blocked MITM.
  std::string crl_set_bytes;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ReadFileToString(net::GetTestCertsDirectory().AppendASCII(
                               "crlset_blocked_interception_by_root.raw"),
                           &crl_set_bytes);
  }
  base::RunLoop run_loop;
  content::GetCertVerifierServiceFactory()->UpdateCRLSet(
      base::as_byte_span(crl_set_bytes), run_loop.QuitClosure());
  run_loop.Run();

  bool interstitial_expected =
      ssl_test_util::CertVerifierSupportsCRLSetBlocking();

  // If an interstitial is expected, NavigateToURL should fail, because the
  // interstitial will be the committed URL, rather than the navigated URL.
  EXPECT_EQ(!interstitial_expected,
            content::NavigateToURL(GetActiveWebContents(),
                                   https_server_.GetURL("/ssl/google.html")));

  ASSERT_EQ(interstitial_expected,
            chrome_browser_interstitials::IsShowingInterstitial(
                GetActiveWebContents()));
  ASSERT_TRUE(
      WaitForRenderFrameReady(GetActiveWebContents()->GetPrimaryMainFrame()));

  if (interstitial_expected) {
    ASSERT_TRUE(
        chrome_browser_interstitials::IsShowingBlockedInterceptionInterstitial(
            GetActiveWebContents()));

    ssl_test_util::CheckAuthenticationBrokenState(
        GetActiveWebContents(),
        net::CERT_STATUS_KNOWN_INTERCEPTION_BLOCKED | net::CERT_STATUS_REVOKED,
        AuthState::SHOWING_INTERSTITIAL);
  } else {
    ssl_test_util::CheckAuthenticatedState(GetActiveWebContents(),
                                           AuthState::NONE);
  }
}

// Test that CRLSets configured to identify known MITM certificates do not
// cause an interstitial unless the MITM certificate is blocked.
IN_PROC_BROWSER_TEST_F(CRLSetBrowserTest, TestCRLSetKnownInterception) {
  ASSERT_TRUE(https_server_.Start());

  // Load a CRLSet that marks the root as a known MITM.
  std::string crl_set_bytes;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ReadFileToString(net::GetTestCertsDirectory().AppendASCII(
                               "crlset_known_interception_by_root.raw"),
                           &crl_set_bytes);
  }
  base::RunLoop run_loop;
  content::GetCertVerifierServiceFactory()->UpdateCRLSet(
      base::as_byte_span(crl_set_bytes), run_loop.QuitClosure());
  run_loop.Run();

  // Navigate to the page. It should not cause an interstitial, but should
  // allow for the display of additional information that interception is
  // happening.
  ASSERT_TRUE(content::NavigateToURL(GetActiveWebContents(),
                                     https_server_.GetURL("/ssl/google.html")));
  ssl_test_util::CheckAuthenticatedState(GetActiveWebContents(),
                                         AuthState::NONE);

  content::NavigationEntry* entry =
      GetActiveWebContents()->GetController().GetVisibleEntry();
  ASSERT_TRUE(entry);
  EXPECT_TRUE(entry->GetSSL().cert_status &
              net::CERT_STATUS_KNOWN_INTERCEPTION_DETECTED);
}

// While TestCRLSetBlockedInterception and TestCRLSetKnownInterception use
// a real CertVerifier in order to test that a real CRLSet is delivered and
// processed, testing HSTS requires with a MockCertVerifier so that the
// cert will match the intended hostname, and thus only fail because it's a
// blocked MITM certificate. This requires using a CertVerifierBrowserTest,
// which is not suitable for the previous tests because it does not test
// CRLSets.
class CRLSetInterceptionBrowserTest : public CertVerifierPlatformBrowserTest {
 public:
  CRLSetInterceptionBrowserTest()
      : CertVerifierPlatformBrowserTest(),
        https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}

  void SetUpOnMainThread() override {
    CertVerifierPlatformBrowserTest::SetUpOnMainThread();
    ssl_test_util::SetHSTSForHostName(
        GetActiveWebContents()->GetBrowserContext(), kHstsTestHostName);

    host_resolver()->AddRule("*", "127.0.0.1");
    https_server_.ServeFilesFromSourceDirectory("chrome/test/data/");
  }

 protected:
  content::WebContents* GetActiveWebContents() {
    return chrome_test_utils::GetActiveWebContents(this);
  }

  net::EmbeddedTestServer https_server_;
};

IN_PROC_BROWSER_TEST_F(CRLSetInterceptionBrowserTest,
                       KnownInterceptionWorksOnHSTS) {
  ASSERT_TRUE(https_server_.Start());

  net::CertVerifyResult verify_result;
  verify_result.verified_cert = https_server_.GetCertificate();
  verify_result.cert_status =
      net::CERT_STATUS_REVOKED | net::CERT_STATUS_KNOWN_INTERCEPTION_BLOCKED;

  // Simulate verification returning that the certificate is a blocked
  // interception certificate.
  mock_cert_verifier()->AddResultForCertAndHost(
      https_server_.GetCertificate(), kHstsTestHostName, verify_result,
      net::ERR_CERT_KNOWN_INTERCEPTION_BLOCKED);

  GURL::Replacements replacements;
  replacements.SetHostStr(kHstsTestHostName);
  GURL url =
      https_server_.GetURL("/ssl/google.html").ReplaceComponents(replacements);

  // Expect navigation to fail, because this should always result in an
  // interstitial.
  ASSERT_FALSE(content::NavigateToURL(GetActiveWebContents(), url));

  ASSERT_TRUE(chrome_browser_interstitials::IsShowingInterstitial(
      GetActiveWebContents()));
  ASSERT_TRUE(
      WaitForRenderFrameReady(GetActiveWebContents()->GetPrimaryMainFrame()));

  ssl_test_util::CheckSecurityState(
      GetActiveWebContents(),
      net::CERT_STATUS_KNOWN_INTERCEPTION_BLOCKED | net::CERT_STATUS_REVOKED,
      security_state::DANGEROUS, AuthState::SHOWING_INTERSTITIAL);

  ASSERT_TRUE(
      chrome_browser_interstitials::IsShowingBlockedInterceptionInterstitial(
          GetActiveWebContents()));

  // Expect there to be a proceed link, even for HSTS.
  EXPECT_TRUE(chrome_browser_interstitials::InterstitialHasProceedLink(
      GetActiveWebContents()->GetPrimaryMainFrame()));
}
