Skip to content

Commit

Permalink
device/fido: support largeBlobs as an extension.
Browse files Browse the repository at this point in the history
Since largeBlobs are large (relative to the size of a security key),
they are generally not included in the request itself but rather stored
in an authenticator-global space that can be accessed in a streaming
fashion.

However, hybrid authenticators do not wish to simulate an
authenticator-global storage space like this, and don't have a problem
with larger requests. This change adds support for transacting
largeBlobs directly in requests via a new "largeBlob" extension that
such authenticators can support.

See https://github.com/fido-alliance/fido-2-specs/pull/1374.

Bug: 1414925
Change-Id: I91606e1dfa0cdec6803c4cc71afdb37bf0131b11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4223051
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: Nina Satragno <nsatragno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1104880}
  • Loading branch information
Adam Langley authored and Chromium LUCI CQ committed Feb 14, 2023
1 parent 8dadbaf commit a254f0b
Show file tree
Hide file tree
Showing 34 changed files with 632 additions and 158 deletions.
2 changes: 1 addition & 1 deletion content/browser/webauth/authenticator_common_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1818,7 +1818,7 @@ AuthenticatorCommonImpl::CreateMakeCredentialResponse(
case RequestExtension::kLargeBlobEnable:
response->echo_large_blob = true;
response->supports_large_blob =
response_data.has_associated_large_blob_key;
response_data.large_blob_type.has_value();
break;
case RequestExtension::kCredBlob:
response->echo_cred_blob = true;
Expand Down
162 changes: 145 additions & 17 deletions content/browser/webauth/authenticator_impl_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7468,47 +7468,72 @@ static const char* BlobSupportDescription(device::LargeBlobSupport support) {
}

TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialLargeBlob) {
const auto BlobRequired = device::LargeBlobSupport::kRequired;
const auto BlobPreferred = device::LargeBlobSupport::kPreferred;
const auto BlobNotRequested = device::LargeBlobSupport::kNotRequested;
constexpr auto BlobRequired = device::LargeBlobSupport::kRequired;
constexpr auto BlobPreferred = device::LargeBlobSupport::kPreferred;
constexpr auto BlobNotRequested = device::LargeBlobSupport::kNotRequested;
constexpr auto nullopt = absl::nullopt;

constexpr struct {
bool large_blob_support;
bool large_blob_extension;
absl::optional<bool> large_blob_support;
bool rk_required;
device::LargeBlobSupport large_blob_enable;
bool request_success;
bool did_create_large_blob;
} kLargeBlobTestCases[] = {
// clang-format off
// support, rk, enabled, success, did create
{ true, true, BlobRequired, true, true},
{ true, true, BlobPreferred, true, true},
{ true, true, BlobNotRequested, true, false},
{ true, false, BlobRequired, false, false},
{ true, false, BlobPreferred, true, false},
{ true, true, BlobNotRequested, true, false},
{ false, true, BlobRequired, false, false},
{ false, true, BlobPreferred, true, false},
{ true, true, BlobNotRequested, true, false},
// ext, support, rk, enabled, success, did create
{ false, true, true, BlobRequired, true, true},
{ false, true, true, BlobPreferred, true, true},
{ false, true, true, BlobNotRequested, true, false},
{ false, true, false, BlobRequired, false, false},
{ false, true, false, BlobPreferred, true, false},
{ false, true, true, BlobNotRequested, true, false},
{ false, false, true, BlobRequired, false, false},
{ false, false, true, BlobPreferred, true, false},
{ false, true, true, BlobNotRequested, true, false},

{ true, true, true, BlobRequired, true, true},
{ true, true, true, BlobPreferred, true, true},
{ true, true, true, BlobNotRequested, true, false},
{ true, true, false, BlobRequired, false, false},
{ true, true, false, BlobPreferred, true, false},
{ true, true, true, BlobNotRequested, true, false},
{ true, nullopt, true, BlobRequired, false, false},
{ true, nullopt, true, BlobPreferred, true, false},
{ true, true, true, BlobNotRequested, true, false},
{ true, false, true, BlobPreferred, true, false},
{ true, false, true, BlobRequired, false, false},
// clang-format on
};
for (auto& test : kLargeBlobTestCases) {
SCOPED_TRACE(::testing::Message() << "support=" << test.large_blob_support);
if (test.large_blob_support) {
SCOPED_TRACE(::testing::Message()
<< "support=" << *test.large_blob_support);
} else {
SCOPED_TRACE(::testing::Message() << "support={}");
}
SCOPED_TRACE(::testing::Message() << "rk_required=" << test.rk_required);
SCOPED_TRACE(::testing::Message()
<< "enabled="
<< BlobSupportDescription(test.large_blob_enable));
SCOPED_TRACE(::testing::Message() << "success=" << test.request_success);
SCOPED_TRACE(::testing::Message()
<< "did create=" << test.did_create_large_blob);
SCOPED_TRACE(::testing::Message()
<< "large_blob_extension=" << test.large_blob_extension);

device::VirtualCtap2Device::Config config;
config.pin_support = true;
config.pin_uv_auth_token_support = true;
config.resident_key_support = true;
config.ctap2_versions = {std::begin(device::kCtap2Versions2_1),
std::end(device::kCtap2Versions2_1)};
config.large_blob_support = test.large_blob_support;
if (test.large_blob_extension) {
config.large_blob_extension_support = test.large_blob_support;
} else {
config.large_blob_support = *test.large_blob_support;
}
virtual_device_factory_->SetCtap2Config(config);

PublicKeyCredentialCreationOptionsPtr options = make_credential_options(
Expand All @@ -7526,7 +7551,7 @@ TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialLargeBlob) {
virtual_device_factory_->mutable_state()
->registrations.begin()
->second;
EXPECT_EQ(test.did_create_large_blob,
EXPECT_EQ(test.did_create_large_blob && !test.large_blob_extension,
registration.large_blob_key.has_value());
EXPECT_EQ(test.large_blob_enable != BlobNotRequested,
result.response->echo_large_blob);
Expand Down Expand Up @@ -7678,6 +7703,109 @@ TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionLargeBlobWrite) {
}
}

TEST_F(ResidentKeyAuthenticatorImplTest,
GetAssertionLargeBlobExtensionNoSupport) {
device::VirtualCtap2Device::Config config;
config.pin_support = true;
config.pin_uv_auth_token_support = true;
config.resident_key_support = true;
config.ctap2_versions = {std::begin(device::kCtap2Versions2_1),
std::end(device::kCtap2Versions2_1)};
virtual_device_factory_->SetCtap2Config(config);

const std::vector<uint8_t> cred_id = {4, 3, 2, 1};
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey(
cred_id, kTestRelyingPartyId,
/*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt));

// Try to read a large blob that doesn't exist and couldn't exist because the
// authenticator doesn't support large blobs.
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, cred_id)};
options->large_blob_read = true;
GetAssertionResult result = AuthenticatorGetAssertion(std::move(options));
ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status);
EXPECT_TRUE(result.response->echo_large_blob);
EXPECT_FALSE(result.response->echo_large_blob_written);
ASSERT_FALSE(result.response->large_blob);
}

TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionLargeBlobExtension) {
device::VirtualCtap2Device::Config config;
config.pin_support = true;
config.pin_uv_auth_token_support = true;
config.resident_key_support = true;
config.large_blob_extension_support = true;
config.ctap2_versions = {std::begin(device::kCtap2Versions2_1),
std::end(device::kCtap2Versions2_1)};
virtual_device_factory_->SetCtap2Config(config);

const std::vector<uint8_t> large_blob = {'b', 'l', 'o', 'b'};
const std::vector<uint8_t> cred_id = {4, 3, 2, 1};
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey(
cred_id, kTestRelyingPartyId,
/*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt));

{
// Try to read a large blob that doesn't exist.
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, cred_id)};
options->large_blob_read = true;
GetAssertionResult result = AuthenticatorGetAssertion(std::move(options));
ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status);
EXPECT_TRUE(result.response->echo_large_blob);
EXPECT_FALSE(result.response->echo_large_blob_written);
ASSERT_FALSE(result.response->large_blob);
}

{
// Write a large blob.
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, cred_id)};
options->large_blob_write = large_blob;
GetAssertionResult result = AuthenticatorGetAssertion(std::move(options));
ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status);
EXPECT_TRUE(result.response->echo_large_blob);
EXPECT_TRUE(result.response->echo_large_blob_written);
EXPECT_FALSE(result.response->large_blob);
}

{
// Read it back.
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, cred_id)};
options->large_blob_read = true;
GetAssertionResult result = AuthenticatorGetAssertion(std::move(options));
ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status);
EXPECT_TRUE(result.response->echo_large_blob);
EXPECT_FALSE(result.response->echo_large_blob_written);
ASSERT_TRUE(result.response->large_blob);
EXPECT_EQ(large_blob, *result.response->large_blob);
}

// Corrupt the large blob data and attempt to read it back. The invalid
// large blob should be ignored.
virtual_device_factory_->mutable_state()
->registrations.begin()
->second.large_blob->compressed_data = {1, 2, 3, 4};

{
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, cred_id)};
options->large_blob_read = true;
GetAssertionResult result = AuthenticatorGetAssertion(std::move(options));
ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status);
EXPECT_TRUE(result.response->echo_large_blob);
EXPECT_FALSE(result.response->echo_large_blob_written);
ASSERT_FALSE(result.response->large_blob);
}
}

static const char* ProtectionPolicyDescription(
blink::mojom::ProtectionPolicy p) {
switch (p) {
Expand Down
6 changes: 6 additions & 0 deletions device/fido/authenticator_get_assertion_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "base/component_export.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/fido_constants.h"
#include "device/fido/large_blob.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
Expand Down Expand Up @@ -75,6 +76,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorGetAssertionResponse {
// request.
bool large_blob_written = false;

// Contains the compressed largeBlob data when the extension form is used.
// This will be decompressed during processing and used to populate
// `large_blob`.
absl::optional<LargeBlob> large_blob_extension;

// The transport used to generate this response. This is unknown when using
// the Windows WebAuthn API.
absl::optional<FidoTransportProtocol> transport_used;
Expand Down
8 changes: 7 additions & 1 deletion device/fido/authenticator_make_credential_response.cc
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ std::vector<uint8_t> AsCTAPStyleCBORBytes(
if (response.enterprise_attestation_returned) {
map.emplace(4, true);
}
if (response.has_associated_large_blob_key) {
if (response.large_blob_type == LargeBlobSupportType::kKey) {
// Chrome ignores the value of the large blob key on make credential
// requests.
map.emplace(5, cbor::Value(std::array<uint8_t, kLargeBlobKeyLength>()));
Expand All @@ -136,6 +136,12 @@ std::vector<uint8_t> AsCTAPStyleCBORBytes(
prf.emplace(kExtensionPRFEnabled, true);
unsigned_extension_outputs.emplace(kExtensionPRF, std::move(prf));
}
if (response.large_blob_type == LargeBlobSupportType::kExtension) {
cbor::Value::MapValue large_blob_ext;
large_blob_ext.emplace(kExtensionLargeBlobSupported, true);
unsigned_extension_outputs.emplace(kExtensionLargeBlob,
std::move(large_blob_ext));
}
if (!unsigned_extension_outputs.empty()) {
map.emplace(6, std::move(unsigned_extension_outputs));
}
Expand Down
7 changes: 4 additions & 3 deletions device/fido/authenticator_make_credential_response.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorMakeCredentialResponse {
absl::optional<FidoTransportProtocol> transport_used;

// Whether the credential that was created has an associated large blob key or
// not. This can only be true if the credential is created with the
// largeBlobKey extension on a capable authenticator.
bool has_associated_large_blob_key = false;
// supports the largeBlob extension. This can only be true if the credential
// is created with the largeBlob or largeBlobKey extension on a capable
// authenticator.
absl::optional<LargeBlobSupportType> large_blob_type;

// Whether a PRF is configured for this credential. This only reflects the
// output of the `prf` extension. Any output from the `hmac-secret` extension
Expand Down
2 changes: 1 addition & 1 deletion device/fido/authenticator_supported_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ cbor::Value AsCBOR(const AuthenticatorSupportedOptions& options) {
option_map.emplace(kEnterpriseAttestationKey, true);
}

if (options.supports_large_blobs) {
if (options.large_blob_type == LargeBlobSupportType::kKey) {
option_map.emplace(kLargeBlobsKey, true);
}

Expand Down
6 changes: 3 additions & 3 deletions device/fido/authenticator_supported_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSupportedOptions {
// uninteresting to Chromium because we do not support the administrative
// operation to configure it. Thus this member reduces to a boolean.)
bool enterprise_attestation = false;
// Indicates whether the authenticator supports the authenticatorLargeBlobs
// command.
bool supports_large_blobs = false;
// Whether the authenticator supports large blobs, and, if so, the method of
// that support.
absl::optional<LargeBlobSupportType> large_blob_type;
// Indicates whether user verification must be used for make credential, final
// (i.e. not pre-flight) get assertion requests, and writing large blobs. An
// |always_uv| value of true will make uv=0 get assertion requests return
Expand Down
7 changes: 4 additions & 3 deletions device/fido/credential_management_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ void CredentialManagementHandler::OnHavePIN(std::string pin) {
state_ = State::kGettingPINToken;
std::vector<pin::Permissions> permissions = {
pin::Permissions::kCredentialManagement};
if (authenticator_->Options().supports_large_blobs) {
if (authenticator_->Options().large_blob_type == LargeBlobSupportType::kKey) {
permissions.push_back(pin::Permissions::kLargeBlobWrite);
}
authenticator_->GetPINToken(
Expand Down Expand Up @@ -167,7 +167,7 @@ void CredentialManagementHandler::OnHavePINToken(
}

pin_token_ = response;
if (authenticator_->Options().supports_large_blobs) {
if (authenticator_->Options().large_blob_type == LargeBlobSupportType::kKey) {
authenticator_->GarbageCollectLargeBlob(
*pin_token_,
base::BindOnce(&CredentialManagementHandler::OnInitFinished,
Expand Down Expand Up @@ -218,7 +218,8 @@ void CredentialManagementHandler::OnDeleteCredentials(
}

if (remaining_credential_ids.empty()) {
if (authenticator_->Options().supports_large_blobs) {
if (authenticator_->Options().large_blob_type ==
LargeBlobSupportType::kKey) {
authenticator_->GarbageCollectLargeBlob(*pin_token_, std::move(callback));
return;
}
Expand Down
53 changes: 53 additions & 0 deletions device/fido/ctap_get_assertion_request.cc
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,40 @@ absl::optional<CtapGetAssertionRequest> CtapGetAssertionRequest::Parse(
}
}
std::sort(request.prf_inputs.begin(), request.prf_inputs.end());
} else if (extension_id == kExtensionLargeBlob) {
if (!extension.second.is_map()) {
return absl::nullopt;
}
const cbor::Value::MapValue& large_blob_ext = extension.second.GetMap();
const auto read_it =
large_blob_ext.find(cbor::Value(kExtensionLargeBlobRead));
const bool has_read = read_it != large_blob_ext.end();

const auto write_it =
large_blob_ext.find(cbor::Value(kExtensionLargeBlobWrite));
const bool has_write = write_it != large_blob_ext.end();

const auto original_size_it =
large_blob_ext.find(cbor::Value(kExtensionLargeBlobOriginalSize));
const bool has_original_size = original_size_it != large_blob_ext.end();

if ((has_read && !read_it->second.is_bool()) ||
(has_write && !write_it->second.is_bytestring()) ||
(has_original_size && !original_size_it->second.is_unsigned())) {
return absl::nullopt;
}

if (has_read && !has_write && !has_original_size) {
request.large_blob_extension_read = read_it->second.GetBool();
} else if (!has_read && has_write && has_original_size) {
request.large_blob_extension_write.emplace(
write_it->second.GetBytestring(),
base::checked_cast<size_t>(
original_size_it->second.GetUnsigned()));
} else {
// No other combinations of keys are acceptable.
return absl::nullopt;
}
}
}
}
Expand Down Expand Up @@ -366,6 +400,25 @@ AsCTAPRequestValuePair(const CtapGetAssertionRequest& request) {
extensions.emplace(kExtensionLargeBlobKey, cbor::Value(true));
}

if (request.large_blob_extension_read) {
DCHECK(!request.large_blob_key);
cbor::Value::MapValue large_blob_ext;
large_blob_ext.emplace(kExtensionLargeBlobRead, true);
extensions.emplace(kExtensionLargeBlob, std::move(large_blob_ext));
}

if (request.large_blob_extension_write) {
DCHECK(!request.large_blob_key);
const LargeBlob& large_blob = *request.large_blob_extension_write;
cbor::Value::MapValue large_blob_ext;
large_blob_ext.emplace(kExtensionLargeBlobWrite,
large_blob.compressed_data);
large_blob_ext.emplace(
kExtensionLargeBlobOriginalSize,
base::checked_cast<int64_t>(large_blob.original_size));
extensions.emplace(kExtensionLargeBlob, std::move(large_blob_ext));
}

if (request.hmac_secret) {
const auto& hmac_secret = *request.hmac_secret;
cbor::Value::MapValue hmac_extension;
Expand Down

0 comments on commit a254f0b

Please sign in to comment.