chromium/device/fido/win/authenticator_unittest.cc

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

#include "device/fido/win/authenticator.h"

#include <stdint.h>

#include <memory>
#include <vector>

#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/authenticator_make_credential_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_rp_entity.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "device/fido/win/fake_webauthn_api.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace device {
namespace {

using MakeCredentialFuture =
    base::test::TestFuture<MakeCredentialStatus,
                           std::optional<AuthenticatorMakeCredentialResponse>>;

using GetAssertionFuture =
    base::test::TestFuture<GetAssertionStatus,
                           std::vector<AuthenticatorGetAssertionResponse>>;

using GetCredentialFuture =
    base::test::TestFuture<std::vector<DiscoverableCredentialMetadata>,
                           FidoRequestHandlerBase::RecognizedCredential>;

using EnumeratePlatformCredentialsFuture =
    base::test::TestFuture<std::vector<DiscoverableCredentialMetadata>>;

const std::vector<uint8_t> kCredentialId = {1, 2, 3, 4};
const std::vector<uint8_t> kCredentialId2 = {9, 0, 1, 2};
constexpr char kRpId[] = "project-altdeus.example.com";
const std::vector<uint8_t> kUserId = {5, 6, 7, 8};
constexpr char kUserName[] = "unit-aarc-noa";
constexpr char kUserDisplayName[] = "Noa";
const std::vector<uint8_t> kLargeBlob = {'b', 'l', 'o', 'b'};
const std::vector<uint8_t> kUserId2 = {1, 1, 1, 1};
constexpr char kUserName2[] = "chloe";
constexpr char kUserDisplayName2[] = "Chloe";

class WinAuthenticatorTest : public testing::Test {
 public:
  void SetUp() override {
    fake_webauthn_api_ = std::make_unique<FakeWinWebAuthnApi>();
    fake_webauthn_api_->set_supports_silent_discovery(true);
    authenticator_ = std::make_unique<WinWebAuthnApiAuthenticator>(
        /*current_window=*/nullptr, fake_webauthn_api_.get());
  }

  void SetVersion(int version) {
    fake_webauthn_api_->set_version(version);
    // `WinWebAuthnApiAuthenticator` does not expect the webauthn.dll version to
    // change during its lifetime, thus needs to be recreated for each version
    // change.
    authenticator_ = std::make_unique<WinWebAuthnApiAuthenticator>(
        /*current_window=*/nullptr, fake_webauthn_api_.get());
  }

 protected:
  std::unique_ptr<FidoAuthenticator> authenticator_;
  std::unique_ptr<FakeWinWebAuthnApi> fake_webauthn_api_;
  base::test::TaskEnvironment task_environment;
};

// Tests getting credential information for an empty allow-list request that has
// valid credentials on a Windows version that supports silent discovery.
TEST_F(WinAuthenticatorTest,
       GetCredentialInformationForRequest_HasCredentials) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp, user);

  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  DiscoverableCredentialMetadata expected = DiscoverableCredentialMetadata(
      AuthenticatorType::kWinNative, kRpId, kCredentialId, user);
  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{expected});
  EXPECT_EQ(
      std::get<1>(future.Get()),
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential);
  EXPECT_FALSE(fake_webauthn_api_->last_get_credentials_options()
                   ->bBrowserInPrivateMode);
}

// Tests a request with the off the record flag on passes the
// bBrowserInPrivateMode option to the Windows API.
TEST_F(WinAuthenticatorTest, GetCredentialInformationForRequest_Incognito) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);

  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  CtapGetAssertionOptions options;
  options.is_off_the_record_context = true;
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), std::move(options), future.GetCallback());
  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(fake_webauthn_api_->last_get_credentials_options()
                  ->bBrowserInPrivateMode);
}

// Tests getting credential information for an empty allow-list request that
// does not have valid credentials on a Windows version that supports silent
// discovery.
TEST_F(WinAuthenticatorTest, GetCredentialInformationForRequest_NoCredentials) {
  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(
      std::get<1>(future.Get()),
      FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential);
}

// Tests the authenticator handling of an unexpected error from the Windows API.
TEST_F(WinAuthenticatorTest, GetCredentialInformationForRequest_UnknownError) {
  fake_webauthn_api_->set_hresult(ERROR_NOT_SUPPORTED);
  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(std::get<1>(future.Get()),
            FidoRequestHandlerBase::RecognizedCredential::kUnknown);
}

// Tests the authenticator handling of attempting to get credential information
// for a version of the Windows API that does not support silent discovery.
TEST_F(WinAuthenticatorTest, GetCredentialInformationForRequest_Unsupported) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp, user);
  fake_webauthn_api_->set_supports_silent_discovery(false);

  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  DiscoverableCredentialMetadata expected = DiscoverableCredentialMetadata(
      AuthenticatorType::kWinNative, kRpId, kCredentialId, user);
  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(std::get<1>(future.Get()),
            FidoRequestHandlerBase::RecognizedCredential::kUnknown);
}

// Tests that for non empty allow-list requests with a matching discoverable
// credential, the authenticator returns a credential list with only matching
// credentials.
TEST_F(WinAuthenticatorTest,
       GetCredentialInformationForRequest_NonEmptyAllowList_Found) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user1(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp, user1);

  PublicKeyCredentialUserEntity user2(kUserId2, kUserName2, kUserDisplayName2);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId2, rp,
                                                   std::move(user2));

  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  request.allow_list.emplace_back(CredentialType::kPublicKey, kCredentialId);
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  DiscoverableCredentialMetadata expected = DiscoverableCredentialMetadata(
      AuthenticatorType::kWinNative, kRpId, kCredentialId, user1);
  EXPECT_THAT(std::get<0>(future.Get()), testing::ElementsAre(expected));
  EXPECT_EQ(
      std::get<1>(future.Get()),
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential);
}

// Tests that for non empty allow-list requests without a matching discoverable
// credential, the authenticator returns an empty credential list and reports no
// credential availability.
TEST_F(WinAuthenticatorTest,
       GetCredentialInformationForRequest_NonEmptyAllowList_NotMatching) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp,
                                                   std::move(user));

  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
  request.allow_list.emplace_back(CredentialType::kPublicKey, kCredentialId2);
  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(
      std::get<1>(future.Get()),
      FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential);
}

// Tests that for non empty allow-list requests without an internal transport
// credential, the authenticator returns an empty credential list and reports no
// credential availability, even if silent discovery is not supported.
TEST_F(WinAuthenticatorTest,
       GetCredentialInformationForRequest_NonEmptyAllowList_NoInternal) {
  fake_webauthn_api_->set_supports_silent_discovery(false);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");

  PublicKeyCredentialDescriptor credential(CredentialType::kPublicKey,
                                           kCredentialId2);
  credential.transports = {FidoTransportProtocol::kUsbHumanInterfaceDevice,
                           FidoTransportProtocol::kHybrid};
  request.allow_list.emplace_back(std::move(credential));

  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(
      std::get<1>(future.Get()),
      FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential);
}

// Tests that for non empty allow-list requests with an internal transport
// credential, the authenticator returns an empty credential list reports
// unknown credential availability when silent discovery is not supported.
TEST_F(WinAuthenticatorTest,
       GetCredentialInformationForRequest_NonEmptyAllowList_Internal) {
  fake_webauthn_api_->set_supports_silent_discovery(false);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");

  PublicKeyCredentialDescriptor credential(CredentialType::kPublicKey,
                                           kCredentialId2);
  credential.transports = {FidoTransportProtocol::kInternal,
                           FidoTransportProtocol::kHybrid};
  request.allow_list.emplace_back(std::move(credential));

  GetCredentialFuture future;
  authenticator_->GetPlatformCredentialInfoForRequest(
      std::move(request), CtapGetAssertionOptions(), future.GetCallback());
  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(std::get<0>(future.Get()),
            std::vector<DiscoverableCredentialMetadata>{});
  EXPECT_EQ(std::get<1>(future.Get()),
            FidoRequestHandlerBase::RecognizedCredential::kUnknown);
}

TEST_F(WinAuthenticatorTest, EnumeratePlatformCredentials_NotSupported) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp, user);
  fake_webauthn_api_->set_supports_silent_discovery(false);

  base::test::TestFuture<std::vector<DiscoverableCredentialMetadata>> future;
  WinWebAuthnApiAuthenticator::EnumeratePlatformCredentials(
      fake_webauthn_api_.get(), future.GetCallback());

  while (!future.IsReady()) {
    base::RunLoop().RunUntilIdle();
  }

  EXPECT_TRUE(future.Get().empty());
}

TEST_F(WinAuthenticatorTest, EnumeratePlatformCredentials_Supported) {
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, rp, user);
  fake_webauthn_api_->set_supports_silent_discovery(true);

  base::test::TestFuture<std::vector<DiscoverableCredentialMetadata>> future;
  WinWebAuthnApiAuthenticator::EnumeratePlatformCredentials(
      fake_webauthn_api_.get(), future.GetCallback());

  while (!future.IsReady()) {
    base::RunLoop().RunUntilIdle();
  }

  std::vector<DiscoverableCredentialMetadata> creds = future.Take();
  ASSERT_EQ(creds.size(), 1u);
  const DiscoverableCredentialMetadata& cred = creds[0];
  EXPECT_EQ(cred.source, AuthenticatorType::kWinNative);
  EXPECT_EQ(cred.rp_id, kRpId);
  EXPECT_EQ(cred.cred_id, kCredentialId);
  EXPECT_EQ(cred.user.name, kUserName);
  EXPECT_EQ(cred.user.display_name, kUserDisplayName);
}

TEST_F(WinAuthenticatorTest, IsConditionalMediationAvailable) {
  for (bool silent_discovery : {false, true}) {
    SCOPED_TRACE(silent_discovery);
    fake_webauthn_api_->set_supports_silent_discovery(silent_discovery);
    base::test::TestFuture<bool> future;
    base::RunLoop run_loop;
    WinWebAuthnApiAuthenticator::IsConditionalMediationAvailable(
        fake_webauthn_api_.get(),
        base::BindLambdaForTesting([&](bool is_available) {
          EXPECT_EQ(is_available, silent_discovery);
          run_loop.Quit();
        }));
    run_loop.Run();
  }
}

TEST_F(WinAuthenticatorTest, MakeCredentialLargeBlob) {
  enum Availability : bool {
    kNotAvailable = false,
    kAvailable = true,
  };

  enum Result : bool {
    kDoesNotHaveLargeBlob = false,
    kHasLargeBlob = true,
  };

  struct LargeBlobTestCase {
    LargeBlobSupport requirement;
    Availability availability;
    Result result;
  };

  std::array<LargeBlobTestCase, 5> test_cases = {{
      {LargeBlobSupport::kNotRequested, kAvailable, kDoesNotHaveLargeBlob},
      {LargeBlobSupport::kNotRequested, kNotAvailable, kDoesNotHaveLargeBlob},
      {LargeBlobSupport::kPreferred, kAvailable, kHasLargeBlob},
      {LargeBlobSupport::kPreferred, kNotAvailable, kDoesNotHaveLargeBlob},
      {LargeBlobSupport::kRequired, kAvailable, kHasLargeBlob},
      // Calling the Windows API with large blob = required is not allowed if
      // it's not supported by the API version.
  }};

  for (const LargeBlobTestCase& test_case : test_cases) {
    SCOPED_TRACE(testing::Message()
                 << "requirement=" << static_cast<bool>(test_case.requirement)
                 << ", availability="
                 << static_cast<bool>(test_case.availability));

    SetVersion(test_case.availability ? WEBAUTHN_API_VERSION_3
                                      : WEBAUTHN_API_VERSION_2);
    EXPECT_EQ(authenticator_->Options().large_blob_type.has_value(),
              test_case.availability);
    PublicKeyCredentialRpEntity rp("adrestian-empire.com");
    PublicKeyCredentialUserEntity user(std::vector<uint8_t>{1, 2, 3, 4},
                                       "[email protected]", "Edelgard");
    CtapMakeCredentialRequest request(
        test_data::kClientDataJson, rp, user,
        PublicKeyCredentialParams({{CredentialType::kPublicKey, -257}}));
    MakeCredentialOptions options;
    options.large_blob_support = test_case.requirement;
    MakeCredentialFuture future;
    authenticator_->MakeCredential(std::move(request), options,
                                   future.GetCallback());
    EXPECT_TRUE(future.Wait());
    ASSERT_EQ(std::get<0>(future.Get()), MakeCredentialStatus::kSuccess);
    EXPECT_EQ(std::get<1>(future.Get())->large_blob_type.has_value(),
              test_case.result);
  }
}

// Tests that making a credential with attachment=undefined forces the
// attachment to cross-platform if large blob is required.
// This is because largeBlob=required is ignored by the Windows platform
// authenticator at the time of writing (Feb 2023).
TEST_F(WinAuthenticatorTest, MakeCredentialLargeBlobAttachmentUndefined) {
  SetVersion(WEBAUTHN_API_VERSION_3);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  CtapMakeCredentialRequest request(
      test_data::kClientDataJson, rp, user,
      PublicKeyCredentialParams({{CredentialType::kPublicKey, -257}}));
  request.authenticator_attachment = AuthenticatorAttachment::kAny;
  fake_webauthn_api_->set_preferred_attachment(
      WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM);
  MakeCredentialOptions options;
  options.large_blob_support = LargeBlobSupport::kRequired;
  MakeCredentialFuture future;
  authenticator_->MakeCredential(std::move(request), options,
                                 future.GetCallback());
  EXPECT_TRUE(future.Wait());
  ASSERT_EQ(std::get<0>(future.Get()), MakeCredentialStatus::kSuccess);
  EXPECT_TRUE(std::get<1>(future.Get())->large_blob_type.has_value());
  EXPECT_NE(std::get<1>(future.Get())->transport_used,
            FidoTransportProtocol::kInternal);
}

TEST_F(WinAuthenticatorTest, GetAssertionLargeBlobNotSupported) {
  SetVersion(WEBAUTHN_API_VERSION_2);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, std::move(rp),
                                                   std::move(user));
  {
    // Read large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_read = true;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob.has_value());
  }
  {
    // Write large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_write = kLargeBlob;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob_written);
  }
}

TEST_F(WinAuthenticatorTest, GetAssertionLargeBlobError) {
  SetVersion(WEBAUTHN_API_VERSION_3);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, std::move(rp),
                                                   std::move(user));
  fake_webauthn_api_->set_large_blob_result(
      WEBAUTHN_CRED_LARGE_BLOB_STATUS_NOT_SUPPORTED);
  {
    // Read large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_read = true;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob.has_value());
  }
  {
    // Write large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_write = kLargeBlob;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob_written);
  }
}

TEST_F(WinAuthenticatorTest, GetAssertionLargeBlobSuccess) {
  SetVersion(WEBAUTHN_API_VERSION_3);
  PublicKeyCredentialRpEntity rp(kRpId);
  PublicKeyCredentialUserEntity user(kUserId, kUserName, kUserDisplayName);
  fake_webauthn_api_->InjectDiscoverableCredential(kCredentialId, std::move(rp),
                                                   std::move(user));
  {
    // Read large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_read = true;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob.has_value());
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob_written);
  }
  {
    // Write large blob.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_write = kLargeBlob;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob.has_value());
    EXPECT_TRUE(std::get<1>(future.Get()).at(0).large_blob_written);
  }
  {
    // Read the large blob that was just written.
    CtapGetAssertionRequest request(kRpId, /*client_data_json=*/"");
    CtapGetAssertionOptions options;
    options.large_blob_read = true;
    GetAssertionFuture future;
    authenticator_->GetAssertion(std::move(request), std::move(options),
                                 future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess);
    EXPECT_TRUE(std::get<1>(future.Get()).at(0).large_blob.has_value());
    EXPECT_EQ(*std::get<1>(future.Get()).at(0).large_blob, kLargeBlob);
    EXPECT_FALSE(std::get<1>(future.Get()).at(0).large_blob_written);
  }
}

}  // namespace
}  // namespace device