chromium/net/device_bound_sessions/registration_fetcher_param_unittest.cc

// 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/registration_fetcher_param.h"

#include <optional>

#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "crypto/signature_verifier.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net::device_bound_sessions {

namespace {

constexpr char kRegistrationHeader[] = "Sec-Session-Registration";
using crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256;
using crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256;
using ::testing::UnorderedElementsAre;

scoped_refptr<net::HttpResponseHeaders> CreateHeaders(
    std::optional<std::string> path,
    std::optional<std::string> algs,
    std::optional<std::string> challenge,
    scoped_refptr<net::HttpResponseHeaders> headers = nullptr) {
  const std::string algs_string = (algs && !algs->empty()) ? *algs : "()";
  const std::string path_string =
      path ? base::StrCat({";path=\"", *path, "\""}) : "";
  const std::string challenge_string =
      challenge ? base::StrCat({";challenge=\"", *challenge, "\""}) : "";
  const std::string full_string =
      base::StrCat({algs_string, path_string, challenge_string});

  if (!headers) {
    headers = HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  }
  headers->AddHeader(kRegistrationHeader, full_string);

  return headers;
}

TEST(RegistrationFetcherParamTest, BasicValid) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("startsession", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, ExtraUnrecognizedAlgorithm) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("startsession", "(ES256 bf512)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, NoHeader) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_TRUE(params.empty());
}

TEST(RegistrationFetcherParamTest, ChallengeFirst) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->SetHeader(
      kRegistrationHeader,
      "(RS256 ES256);challenge=\"challenge1\";path=\"first\"");

  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/first"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "challenge1");
}

TEST(RegistrationFetcherParamTest, NoSpaces) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->SetHeader(
      kRegistrationHeader,
      "(RS256 ES256);path=\"startsession\";challenge=\"challenge1\"");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "challenge1");
}

TEST(RegistrationFetcherParamTest, TwoRegistrations) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/first", "(ES256 RS256)", "c1");
  CreateHeaders("/second", "(ES256)", "challenge2", response_headers);
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 2U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(), GURL("https://www.example.com/first"));
  EXPECT_THAT(p1.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "c1");

  auto p2 = std::move(params[1]);
  EXPECT_EQ(p2.registration_endpoint(), GURL("https://www.example.com/second"));
  EXPECT_THAT(p2.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p2.challenge(), "challenge2");
}

TEST(RegistrationFetcherParamTest, ValidInvalid) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/first", "(ES256 RS256)", "c1");
  CreateHeaders("/second", "(es256)", "challenge2", response_headers);
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(), GURL("https://www.example.com/first"));
  EXPECT_THAT(p1.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, AddedInvalidNonsenseCharacters) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->AddHeader(kRegistrationHeader,
                              "(RS256);path=\"new\";challenge=\"test\";;=;");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_TRUE(params.empty());
}

TEST(RegistrationFetcherParamTest, AddedValidNonsenseCharacters) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->AddHeader(
      kRegistrationHeader,
      "(RS256);path=\"new\";challenge=\"test\";nonsense=\";';'\",OTHER");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(), GURL("https://www.example.com/new"));
  EXPECT_THAT(p1.supported_algos(), UnorderedElementsAre(RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "test");
}

TEST(RegistrationFetcherParamTest, AlgAsString) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->AddHeader(kRegistrationHeader,
                              "(\"RS256\");path=\"new\";challenge=\"test\"");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_TRUE(params.empty());
}

TEST(RegistrationFetcherParamTest, PathAsToken) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->AddHeader(kRegistrationHeader,
                              "(RS256);path=new;challenge=\"test\"");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_TRUE(params.empty());
}

TEST(RegistrationFetcherParamTest, ChallengeAsByteSequence) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->AddHeader(kRegistrationHeader,
                              "(RS256);path=\"new\";challenge=:Y29kZWQ=:");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_TRUE(params.empty());
}

TEST(RegistrationFetcherParamTest, ValidInvalidValid) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/first", "(ES256 RS256)", "c1");
  CreateHeaders("/second", "(es256)", "challenge2", response_headers);
  CreateHeaders("/third", "(ES256)", "challenge3", response_headers);

  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 2U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(), GURL("https://www.example.com/first"));
  EXPECT_THAT(p1.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "c1");

  auto p2 = std::move(params[1]);
  EXPECT_EQ(p2.registration_endpoint(), GURL("https://www.example.com/third"));
  EXPECT_THAT(p2.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p2.challenge(), "challenge3");
}

TEST(RegistrationFetcherParamTest, ThreeRegistrations) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/startsession", "(ES256 RS256)", "c1");
  CreateHeaders("/new", "(ES256)", "coded", response_headers);
  CreateHeaders("/third", "(ES256)", "another", response_headers);

  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 3U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(p1.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "c1");

  auto p2 = std::move(params[1]);
  EXPECT_EQ(p2.registration_endpoint(), GURL("https://www.example.com/new"));
  EXPECT_THAT(p2.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p2.challenge(), "coded");

  auto p3 = std::move(params[2]);
  EXPECT_EQ(p3.registration_endpoint(), GURL("https://www.example.com/third"));
  EXPECT_THAT(p3.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p3.challenge(), "another");
}

TEST(RegistrationFetcherParamTest, ThreeRegistrationsList) {
  GURL registration_request = GURL("https://www.example.com/registration");
  // Testing customized header
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/startsession", "(ES256 RS256)", "c1");
  response_headers->AddHeader(kRegistrationHeader,
                              "(ES256);path=\"new\";challenge=\"coded\", "
                              "(ES256);path=\"third\";challenge=\"another\"");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 3U);
  auto p1 = std::move(params[0]);
  EXPECT_EQ(p1.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(p1.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(p1.challenge(), "c1");

  auto p2 = std::move(params[1]);
  EXPECT_EQ(p2.registration_endpoint(), GURL("https://www.example.com/new"));
  EXPECT_THAT(p2.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p2.challenge(), "coded");

  auto p3 = std::move(params[2]);
  EXPECT_EQ(p3.registration_endpoint(), GURL("https://www.example.com/third"));
  EXPECT_THAT(p3.supported_algos(), UnorderedElementsAre(ECDSA_SHA256));
  EXPECT_EQ(p3.challenge(), "another");
}

TEST(RegistrationFetcherParamTest, StartWithSlash) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/startsession", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, EscapeOnce) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("/%2561", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(), GURL("https://www.example.com/%61"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, InvalidUrl) {
  GURL registration_request = GURL("https://[/");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("new", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 0U);
}

TEST(RegistrationFetcherParamTest, HasUrlEncoded) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("test%2Fstart", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/test/start"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, FullUrl) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers = CreateHeaders(
      "https://accounts.example.com/startsession", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://accounts.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, SwapAlgo) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("startsession", "(ES256 RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  EXPECT_THAT(param.supported_algos(),
              UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, OneAlgo) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      CreateHeaders("startsession", "(RS256)", "c1");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/startsession"));
  ASSERT_THAT(param.supported_algos(), UnorderedElementsAre(RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, InvalidParamIgnored) {
  GURL registration_request = GURL("https://www.example.com/registration");
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  response_headers->SetHeader(
      kRegistrationHeader,
      "(RS256);path=\"first\";challenge=\"c1\";another=true");
  std::vector<RegistrationFetcherParam> params =
      RegistrationFetcherParam::CreateIfValid(registration_request,
                                              response_headers.get());
  ASSERT_EQ(params.size(), 1U);
  auto param = std::move(params[0]);
  EXPECT_EQ(param.registration_endpoint(),
            GURL("https://www.example.com/first"));
  ASSERT_THAT(param.supported_algos(), UnorderedElementsAre(RSA_PKCS1_SHA256));
  EXPECT_EQ(param.challenge(), "c1");
}

TEST(RegistrationFetcherParamTest, InvalidInputs) {
  struct Input {
    std::string request_url;
    std::optional<std::string> path;
    std::optional<std::string> algos;
    std::optional<std::string> challenge;
  };

  const Input kInvalidInputs[] = {
      // All invalid
      {"https://www.example.com/reg", "", "()", ""},
      // All missing
      {"https://www.example.com/reg", std::nullopt, std::nullopt, std::nullopt},
      // All valid different Url
      {"https://www.example.com/registration",
       "https://accounts.different.url/startsession", "(RS256)", "c1"},
      // Empty request Url
      {"", "start", "(RS256)", "c1"},
      // Empty algo
      {"https://www.example.com/reg", "start", "()", "c1"},
      // Missing algo
      {"https://www.example.com/reg", "start", std::nullopt, "c1"},
      // Missing registration
      {"https://www.example.com/reg", std::nullopt, "(ES256 RS256)", "c1"},
      // Missing challenge
      {"https://www.example.com/reg", "start", "(ES256 RS256)", std::nullopt},
      // Empty challenge
      {"https://www.example.com/reg", "start", "(ES256 RS256)", ""},
      // Challenge invalid utf8
      {"https://www.example.com/reg", "start", "(ES256 RS256)", "ab\xC0\x80"}};

  for (const auto& input : kInvalidInputs) {
    GURL registration_request = GURL(input.request_url);
    scoped_refptr<net::HttpResponseHeaders> response_headers =
        CreateHeaders(input.path, input.algos, input.challenge);
    SCOPED_TRACE(registration_request.spec() + "; " +
                 response_headers->raw_headers());
    std::vector<RegistrationFetcherParam> params =
        RegistrationFetcherParam::CreateIfValid(registration_request,
                                                response_headers.get());
    EXPECT_TRUE(params.empty());
  }
}

}  // namespace

}  // namespace net::device_bound_sessions