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

#include "net/device_bound_sessions/session_binding_utils.h"

#include <optional>
#include <string_view>

#include "base/base64url.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_expected_support.h"
#include "base/time/time.h"
#include "base/value_iterators.h"
#include "base/values.h"
#include "crypto/signature_verifier.h"
#include "net/device_bound_sessions/test_support.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace net::device_bound_sessions {

namespace {

using ::testing::Eq;
using ::testing::Pointee;

using enum crypto::AttestationStatement::Format;
using enum crypto::SignatureVerifier::SignatureAlgorithm;

constexpr auto kValidDerSignature = std::to_array<uint8_t>(
    {0x30, 0x45, 0x02, 0x20, 0x74, 0xa0, 0x6f, 0x6b, 0x2b, 0x0e, 0x82, 0x0e,
     0x03, 0x3b, 0x6e, 0x98, 0xfc, 0x89, 0x9c, 0xf3, 0x30, 0xb5, 0x56, 0xd3,
     0x29, 0x89, 0xb5, 0x82, 0x33, 0x5f, 0x9d, 0x97, 0xfb, 0x65, 0x64, 0x90,
     0x02, 0x21, 0x00, 0xbc, 0xb5, 0xee, 0x42, 0xe2, 0x5a, 0x87, 0xae, 0x21,
     0x18, 0xda, 0x7e, 0x68, 0x65, 0x30, 0xbe, 0xe5, 0x69, 0x3d, 0xc5, 0x5f,
     0xd5, 0x62, 0x45, 0x3e, 0x8d, 0x0b, 0x05, 0x1a, 0x33, 0x79, 0x8d});
constexpr auto kValidP256Spki = std::to_array<uint8_t>(
    {0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
     0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
     0x42, 0x00, 0x04, 0xc3, 0xee, 0x3a, 0x2c, 0x33, 0x80, 0x07, 0x2b, 0x9b,
     0x2a, 0x59, 0xfe, 0xd2, 0xca, 0xda, 0x65, 0x12, 0x18, 0x06, 0xe2, 0x2c,
     0x4f, 0x4f, 0x8a, 0x25, 0xe7, 0x40, 0xfc, 0x3e, 0x54, 0xd7, 0x5d, 0x86,
     0xc2, 0x00, 0x29, 0x8e, 0x6d, 0xfc, 0x16, 0x11, 0xd1, 0x85, 0xee, 0xdb,
     0xdb, 0x3c, 0x26, 0x61, 0xb0, 0xeb, 0x04, 0x41, 0xf7, 0xfd, 0x57, 0xc9,
     0x0d, 0x08, 0x11, 0x2e, 0x9a, 0xe7, 0x1c});

base::Value Base64UrlEncodedJsonToValue(std::string_view input) {
  std::string json;
  EXPECT_TRUE(base::Base64UrlDecode(
      input, base::Base64UrlDecodePolicy::DISALLOW_PADDING, &json));
  std::optional<base::Value> result =
      base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  EXPECT_TRUE(result.has_value());
  return std::move(*result);
}

}  // namespace

TEST(SessionBindingUtilsTest, CreateKeyRegistrationHeaderAndPayload) {
  auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();

  std::optional<std::string> result = CreateKeyRegistrationHeaderAndPayload(
      "test_challenge", RSA_PKCS1_SHA256, spki,
      /*authorization=*/"auth");
  ASSERT_TRUE(result.has_value());

  std::vector<std::string_view> header_and_payload = base::SplitStringPiece(
      *result, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(header_and_payload.size(), 2U);
  base::Value actual_header =
      Base64UrlEncodedJsonToValue(header_and_payload[0]);
  base::Value actual_payload =
      Base64UrlEncodedJsonToValue(header_and_payload[1]);

  base::DictValue expected_header =
      base::DictValue()
          .Set("alg", "RS256")
          .Set("typ", "dbsc+jwt")
          .Set("jwk",
               base::JSONReader::Read(jwk, base::JSON_PARSE_CHROMIUM_EXTENSIONS)
                   .value());
  base::DictValue expected_payload = base::DictValue()
                                         .Set("jti", "test_challenge")
                                         .Set("authorization", "auth");

  EXPECT_EQ(actual_header, expected_header);
  EXPECT_EQ(actual_payload, expected_payload);
}

TEST(SessionBindingUtilsTest,
     CreateKeyRegistrationHeaderAndPayloadWithNullAuth) {
  auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();

  std::optional<std::string> result = CreateKeyRegistrationHeaderAndPayload(
      "test_challenge", RSA_PKCS1_SHA256, spki,
      /*authorization=*/std::nullopt);
  ASSERT_TRUE(result.has_value());

  std::vector<std::string_view> header_and_payload = base::SplitStringPiece(
      *result, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(header_and_payload.size(), 2U);
  base::Value actual_header =
      Base64UrlEncodedJsonToValue(header_and_payload[0]);
  base::Value actual_payload =
      Base64UrlEncodedJsonToValue(header_and_payload[1]);

  base::DictValue expected_header =
      base::DictValue()
          .Set("alg", "RS256")
          .Set("typ", "dbsc+jwt")
          .Set("jwk",
               base::JSONReader::Read(jwk, base::JSON_PARSE_CHROMIUM_EXTENSIONS)
                   .value());
  base::DictValue expected_payload =
      base::DictValue().Set("jti", "test_challenge");

  EXPECT_EQ(actual_header, expected_header);
  EXPECT_EQ(actual_payload, expected_payload);
}

TEST(SessionBindingUtilsTest,
     CreateKeyRegistrationHeaderAndPayloadWithNullChallenge) {
  auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();

  std::optional<std::string> result = CreateKeyRegistrationHeaderAndPayload(
      /*challenge=*/std::nullopt, RSA_PKCS1_SHA256, spki, "authorization");
  ASSERT_TRUE(result.has_value());

  std::vector<std::string_view> header_and_payload = base::SplitStringPiece(
      *result, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(header_and_payload.size(), 2U);
  base::Value actual_header =
      Base64UrlEncodedJsonToValue(header_and_payload[0]);
  base::Value actual_payload =
      Base64UrlEncodedJsonToValue(header_and_payload[1]);

  base::DictValue expected_header =
      base::DictValue()
          .Set("alg", "RS256")
          .Set("typ", "dbsc+jwt")
          .Set("jwk",
               base::JSONReader::Read(jwk, base::JSON_PARSE_CHROMIUM_EXTENSIONS)
                   .value());
  base::DictValue expected_payload =
      base::DictValue().Set("authorization", "authorization");

  EXPECT_EQ(actual_header, expected_header);
  EXPECT_EQ(actual_payload, expected_payload);
}

TEST(SessionBindingUtilsTest, CreateKeyRefreshHeaderAndPayload) {
  std::optional<std::string> result =
      CreateKeyRefreshHeaderAndPayload("test_challenge", RSA_PKCS1_SHA256);
  ASSERT_TRUE(result.has_value());

  std::vector<std::string_view> header_and_payload = base::SplitStringPiece(
      *result, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(header_and_payload.size(), 2U);
  base::Value actual_header =
      Base64UrlEncodedJsonToValue(header_and_payload[0]);
  base::Value actual_payload =
      Base64UrlEncodedJsonToValue(header_and_payload[1]);

  base::DictValue expected_header =
      base::DictValue().Set("alg", "RS256").Set("typ", "dbsc+jwt");
  base::DictValue expected_payload =
      base::DictValue().Set("jti", "test_challenge");

  EXPECT_EQ(actual_header, expected_header);
  EXPECT_EQ(actual_payload, expected_payload);
}

TEST(SessionBindingUtilsTest, AppendSignatureToHeaderAndPayload) {
  std::optional<std::string> result = AppendSignatureToHeaderAndPayload(
      "abc.efg", RSA_PKCS1_SHA256,
      /*pubkey_spki=*/{}, std::vector<uint8_t>({1, 2, 3}));
  EXPECT_EQ(result, "abc.efg.AQID");
}

TEST(SessionBindingUtilsTest,
     AppendSignatureToHeaderAndPayloadValidECDSASignature) {
  constexpr std::string_view kRawSignatureBase64UrlEncoded =
      "dKBvaysOgg4DO26Y_Imc8zC1VtMpibWCM1-dl_tlZJC8te5C4lqHriEY2n5oZTC-5Wk9xV_"
      "VYkU-jQsFGjN5jQ";

  std::optional<std::string> result = AppendSignatureToHeaderAndPayload(
      "abc.efg", ECDSA_SHA256, kValidP256Spki, kValidDerSignature);
  EXPECT_EQ(result, base::StrCat({"abc.efg.", kRawSignatureBase64UrlEncoded}));
}

TEST(SessionBindingUtilsTest,
     AppendSignatureToHeaderAndPayloadInvalidECDSASignature) {
  std::optional<std::string> result = AppendSignatureToHeaderAndPayload(
      "abc.efg", ECDSA_SHA256, kValidP256Spki,
      /*signature=*/std::vector<uint8_t>({1, 2, 3}));
  EXPECT_EQ(result, std::nullopt);
}

TEST(SessionBindingUtilsTest, AppendSignatureToHeaderAndPayloadInvalidSpki) {
  std::optional<std::string> result = AppendSignatureToHeaderAndPayload(
      "abc.efg", ECDSA_SHA256,
      /*pubkey_spki=*/std::vector<uint8_t>({1, 2, 3}), kValidDerSignature);
  EXPECT_EQ(result, std::nullopt);
}

TEST(SessionBindingUtilsTest, TestIsSecureUrl) {
  const std::vector<GURL> secure_connection_urls = {
      GURL("https://example.test/"), GURL("https://localhost:8080/"),
      GURL("http://localhost:8080/")};
  const std::vector<GURL> insecure_connection_urls = {
      GURL("http://example.test/"),
  };
  for (const auto& url : secure_connection_urls) {
    SCOPED_TRACE(base::StringPrintf("url: %s", url.spec()));
    EXPECT_TRUE(IsSecure(url));
  }
  for (const auto& url : insecure_connection_urls) {
    SCOPED_TRACE(base::StringPrintf("url: %s", url.spec()));
    EXPECT_FALSE(IsSecure(url));
  }
}

TEST(SessionBindingUtilsTest, TestMaybeIncreaseSessionUsage) {
  struct Request {
    const base::flat_map<SessionKey, SessionUsage>& device_bound_session_usage()
        const {
      return usage_map;
    }
    void set_device_bound_session_usage(const SessionKey& key,
                                        SessionUsage usage) {
      usage_map[key] = usage;
    }
    base::flat_map<SessionKey, SessionUsage> usage_map;
  };

  Request request;
  auto key1 = SessionKey{SchemefulSite(GURL("https://example.com")),
                         Session::Id("session1")};
  auto key2 = SessionKey{SchemefulSite(GURL("https://example.com")),
                         Session::Id("session2")};

  // Initialize map entry for key1.
  MaybeIncreaseSessionUsage(key1, request, SessionUsage::kSiteMatchNotInScope);
  EXPECT_EQ(request.usage_map[key1], SessionUsage::kSiteMatchNotInScope);
  // Increase to kDeferred.
  MaybeIncreaseSessionUsage(key1, request, SessionUsage::kDeferred);
  EXPECT_EQ(request.usage_map[key1], SessionUsage::kDeferred);
  // Fail to increase to kInScopeRefreshNotAllowed.
  MaybeIncreaseSessionUsage(key1, request,
                            SessionUsage::kInScopeRefreshNotAllowed);
  EXPECT_EQ(request.usage_map[key1], SessionUsage::kDeferred);

  // Initialize map entry for key2.
  MaybeIncreaseSessionUsage(key2, request,
                            SessionUsage::kInScopeRefreshNotYetNeeded);
  EXPECT_EQ(request.usage_map[key1], SessionUsage::kDeferred);
  EXPECT_EQ(request.usage_map[key2], SessionUsage::kInScopeRefreshNotYetNeeded);
  // Increase to kInScopeRefreshNotAllowed for key2.
  MaybeIncreaseSessionUsage(key2, request,
                            SessionUsage::kInScopeRefreshNotAllowed);
  EXPECT_EQ(request.usage_map[key1], SessionUsage::kDeferred);
  EXPECT_EQ(request.usage_map[key2], SessionUsage::kInScopeRefreshNotAllowed);
}

TEST(SessionBindingUtilsTest, CreateAttestationValue_Tpm) {
  base::DictValue result = CreateAttestationValue(
      {.format = kTpm, .statement = {1, 2, 3}, .signature = {4, 5, 6}});

  EXPECT_THAT(result.FindString("fmt"), Pointee(Eq("TPM")));
  // base64url of {1, 2, 3}
  EXPECT_THAT(result.FindString("stmt"), Pointee(Eq("AQID")));
  // base64url of {4, 5, 6}
  EXPECT_THAT(result.FindString("sig"), Pointee(Eq("BAUG")));
}

TEST(SessionBindingUtilsTest, CreateAttestationValue_SecureEnclave) {
  base::DictValue result = CreateAttestationValue({
      .format = kSecureEnclave,
      .statement = {1, 2, 3},
      .signature = {4, 5, 6},
  });

  EXPECT_THAT(result.FindString("fmt"), Pointee(Eq("SECURE_ENCLAVE")));
  EXPECT_THAT(result.FindString("stmt"), Pointee(Eq("AQID")));
  EXPECT_THAT(result.FindString("sig"), Pointee(Eq("BAUG")));
}

TEST(SessionBindingUtilsTest, CreateOuterRegistrationHeaderAndPayload_Success) {
  auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();

  ASSERT_OK_AND_ASSIGN(
      std::string result,
      CreateOuterRegistrationHeaderAndPayload(
          "inner_jws", RSA_PKCS1_SHA256, spki, "aud",
          {.format = kTpm, .statement = {1, 2, 3}, .signature = {4, 5, 6}}));

  ASSERT_OK_AND_ASSIGN((auto [header, payload]),
                       base::SplitStringOnce(result, '.'));
  base::Value actual_header = Base64UrlEncodedJsonToValue(header);
  base::Value actual_payload = Base64UrlEncodedJsonToValue(payload);

  base::DictValue expected_header =
      base::DictValue()
          .Set("alg", "RS256")
          .Set("typ", "dbsc+aik")
          .Set("cty", "jwt")
          .Set("jwk",
               base::JSONReader::Read(jwk, base::JSON_PARSE_CHROMIUM_EXTENSIONS)
                   .value());
  base::DictValue expected_payload = base::DictValue()
                                         .Set("aud", "aud")
                                         .Set("jti", "inner_jws")
                                         .Set("att", CreateAttestationValue({
                                                         .format = kTpm,
                                                         .statement = {1, 2, 3},
                                                         .signature = {4, 5, 6},
                                                     }));

  EXPECT_EQ(actual_header, expected_header);
  EXPECT_EQ(actual_payload, expected_payload);
}

TEST(SessionBindingUtilsTest,
     CreateOuterRegistrationHeaderAndPayload_InvalidSpki) {
  std::vector<uint8_t> invalid_spki = {1, 2, 3};
  EXPECT_EQ(
      CreateOuterRegistrationHeaderAndPayload(
          "inner_jws", RSA_PKCS1_SHA256, invalid_spki, "aud",
          {.format = kTpm, .statement = {1, 2, 3}, .signature = {4, 5, 6}}),
      std::nullopt);
}

TEST(SessionBindingUtilsTest, SecFetchSiteForReferringOrigin) {
  // Validate that W3C Sec-Fetch-Site string-literal translation correctly
  // resolves 'same-origin', 'same-site', and 'cross-site' origin relationships,
  // including strictness against unencrypted/HTTP protocol-mismatches.
  url::Origin referring_origin =
      url::Origin::Create(GURL("https://www.example.com"));

  // 1. Same-Origin: Matching cryptographic scheme, eTLD+1, and subdomain.
  EXPECT_EQ(
      SecFetchSiteForReferringOrigin(
          referring_origin, GURL("https://www.example.com/path/to/resource")),
      "same-origin");

  // 2. Same-Site: Matching cryptographic scheme and eTLD+1, but differing
  // subdomain boundaries.
  EXPECT_EQ(
      SecFetchSiteForReferringOrigin(
          referring_origin, GURL("https://subdomain.example.com/endpoint")),
      "same-site");

  // 3. Cross-Site: Disjoint registrable domain (eTLD+1).
  EXPECT_EQ(SecFetchSiteForReferringOrigin(
                referring_origin, GURL("https://www.other-origin.org/")),
            "cross-site");

  // 4. Cryptographic Protocol Mismatch: HTTPS referrer targeting an unencrypted
  // HTTP endpoint yields a cross-site classification due to SchemefulSite
  // isolation.
  EXPECT_EQ(SecFetchSiteForReferringOrigin(
                referring_origin, GURL("http://www.example.com/insecure")),
            "cross-site");
}

}  // namespace net::device_bound_sessions
