Add crosapi service for accessing GPM passkeys
The `PasskeyAuthenticator` crosapi service gives Lacros assertion access to GPM passkeys. Ash has access to the security domain secret needed to decrypt key material on the `WebauthnCredential` entities that represent passkeys in Sync. Bug: 1223853 Change-Id: I1a165340304e0b98207429a883c002e501d29d94 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4956453 Reviewed-by: Adam Langley <agl@chromium.org> Auto-Submit: Martin Kreichgauer <martinkr@google.com> Commit-Queue: Adam Langley <agl@chromium.org> Cr-Commit-Position: refs/heads/main@{#1226183}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
b37e609bff
commit
59b8313011
@ -2299,6 +2299,10 @@ source_set("ash") {
|
||||
"ownership/owner_settings_service_ash_factory.h",
|
||||
"ownership/ownership_histograms.cc",
|
||||
"ownership/ownership_histograms.h",
|
||||
"passkeys/passkey_authenticator_service_ash.cc",
|
||||
"passkeys/passkey_authenticator_service_ash.h",
|
||||
"passkeys/passkey_authenticator_service_factory_ash.cc",
|
||||
"passkeys/passkey_authenticator_service_factory_ash.h",
|
||||
"pcie_peripheral/ash_usb_detector.cc",
|
||||
"pcie_peripheral/ash_usb_detector.h",
|
||||
"phonehub/attestation_certificate_generator_impl.cc",
|
||||
@ -4221,6 +4225,7 @@ source_set("ash") {
|
||||
"//components/webapps/browser",
|
||||
"//components/webapps/browser:constants",
|
||||
"//components/webapps/common",
|
||||
"//components/webauthn/core/browser:browser",
|
||||
"//components/zoom",
|
||||
"//content/public/common",
|
||||
"//content/public/common:main_function_params",
|
||||
@ -5672,6 +5677,7 @@ source_set("unit_tests") {
|
||||
"os_feedback/os_feedback_screenshot_manager_unittest.cc",
|
||||
"ownership/owner_key_loader_unittest.cc",
|
||||
"ownership/owner_settings_service_ash_unittest.cc",
|
||||
"passkeys/passkey_authenticator_service_ash_unittest.cc",
|
||||
"pcie_peripheral/ash_usb_detector_unittest.cc",
|
||||
"phonehub/attestation_certificate_generator_impl_unittest.cc",
|
||||
"phonehub/browser_tabs_metadata_fetcher_impl_unittest.cc",
|
||||
@ -6411,6 +6417,8 @@ source_set("unit_tests") {
|
||||
"//components/webapps/browser",
|
||||
"//components/webapps/browser:constants",
|
||||
"//components/webapps/common",
|
||||
"//components/webauthn/core/browser",
|
||||
"//components/webauthn/core/browser:test_support",
|
||||
"//content/public/browser",
|
||||
"//content/public/common",
|
||||
"//content/test:test_support",
|
||||
|
@ -110,6 +110,8 @@
|
||||
#include "chrome/browser/ash/crosapi/web_kiosk_service_ash.h"
|
||||
#include "chrome/browser/ash/crosapi/web_page_info_ash.h"
|
||||
#include "chrome/browser/ash/input_method/editor_mediator_factory.h"
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_ash.h"
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_factory_ash.h"
|
||||
#include "chrome/browser/ash/profiles/profile_helper.h"
|
||||
#include "chrome/browser/ash/remote_apps/remote_apps_manager_factory.h"
|
||||
#include "chrome/browser/ash/sync/sync_mojo_service_ash.h"
|
||||
@ -146,6 +148,7 @@
|
||||
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/message_center.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/multi_capture_service.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/passkeys.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/screen_manager.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/select_file.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/task_manager.mojom.h"
|
||||
@ -784,6 +787,17 @@ void CrosapiAsh::BindParentAccess(
|
||||
parent_access_ash_->BindReceiver(std::move(receiver));
|
||||
}
|
||||
|
||||
void CrosapiAsh::BindPasskeyAuthenticator(
|
||||
mojo::PendingReceiver<mojom::PasskeyAuthenticator> receiver) {
|
||||
auto* passkey_authenticator =
|
||||
ash::PasskeyAuthenticatorServiceFactoryAsh::GetForProfile(
|
||||
GetAshProfile());
|
||||
if (!passkey_authenticator) {
|
||||
return;
|
||||
}
|
||||
passkey_authenticator->BindReceiver(std::move(receiver));
|
||||
}
|
||||
|
||||
void CrosapiAsh::BindPaymentAppInstance(
|
||||
mojo::PendingReceiver<chromeos::payments::mojom::PaymentAppInstance>
|
||||
receiver) {
|
||||
|
@ -100,6 +100,7 @@ class NetworkChangeAsh;
|
||||
class NetworkSettingsServiceAsh;
|
||||
class NetworkingAttributesAsh;
|
||||
class NetworkingPrivateAsh;
|
||||
class PasskeyAuthenticator;
|
||||
class ParentAccessAsh;
|
||||
class PaymentAppInstanceAsh;
|
||||
class PolicyServiceAsh;
|
||||
@ -310,6 +311,8 @@ class CrosapiAsh : public mojom::Crosapi {
|
||||
mojo::PendingReceiver<mojom::NetworkingAttributes> receiver) override;
|
||||
void BindNetworkingPrivate(
|
||||
mojo::PendingReceiver<mojom::NetworkingPrivate> receiver) override;
|
||||
void BindPasskeyAuthenticator(
|
||||
mojo::PendingReceiver<mojom::PasskeyAuthenticator> receiver) override;
|
||||
void BindParentAccess(
|
||||
mojo::PendingReceiver<mojom::ParentAccess> receiver) override;
|
||||
void BindPaymentAppInstance(
|
||||
|
@ -114,6 +114,7 @@
|
||||
#include "chromeos/crosapi/mojom/networking_attributes.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/networking_private.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/parent_access.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/passkeys.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/policy_service.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/power.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/prefs.mojom.h"
|
||||
@ -296,7 +297,7 @@ constexpr InterfaceVersionEntry MakeInterfaceVersionEntry() {
|
||||
return {T::Uuid_, T::Version_};
|
||||
}
|
||||
|
||||
static_assert(crosapi::mojom::Crosapi::Version_ == 123,
|
||||
static_assert(crosapi::mojom::Crosapi::Version_ == 124,
|
||||
"If you add a new crosapi, please add it to "
|
||||
"kInterfaceVersionEntries below.");
|
||||
|
||||
@ -382,6 +383,7 @@ constexpr InterfaceVersionEntry kInterfaceVersionEntries[] = {
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::NetworkingAttributes>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::NetworkingPrivate>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::NetworkSettingsService>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::PasskeyAuthenticator>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::PolicyService>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::Power>(),
|
||||
MakeInterfaceVersionEntry<crosapi::mojom::Prefs>(),
|
||||
|
3
chrome/browser/ash/passkeys/DEPS
Normal file
3
chrome/browser/ash/passkeys/DEPS
Normal file
@ -0,0 +1,3 @@
|
||||
include_rules = [
|
||||
"+device/fido",
|
||||
]
|
4
chrome/browser/ash/passkeys/DIR_METADATA
Normal file
4
chrome/browser/ash/passkeys/DIR_METADATA
Normal file
@ -0,0 +1,4 @@
|
||||
monorail {
|
||||
component: "Blink>WebAuthentication"
|
||||
}
|
||||
team_email: "identity-dev@chromium.org"
|
1
chrome/browser/ash/passkeys/OWNERS
Normal file
1
chrome/browser/ash/passkeys/OWNERS
Normal file
@ -0,0 +1 @@
|
||||
file://device/fido/OWNERS
|
189
chrome/browser/ash/passkeys/passkey_authenticator_service_ash.cc
Normal file
189
chrome/browser/ash/passkeys/passkey_authenticator_service_ash.cc
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_ash.h"
|
||||
|
||||
#include "base/containers/span.h"
|
||||
#include "base/notreached.h"
|
||||
#include "chromeos/crosapi/mojom/passkeys.mojom.h"
|
||||
#include "components/account_manager_core/account.h"
|
||||
#include "components/account_manager_core/account_manager_util.h"
|
||||
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
|
||||
#include "components/trusted_vault/trusted_vault_client.h"
|
||||
#include "components/webauthn/core/browser/passkey_model.h"
|
||||
#include "components/webauthn/core/browser/passkey_model_utils.h"
|
||||
#include "crypto/ec_private_key.h"
|
||||
#include "crypto/ec_signature_creator.h"
|
||||
#include "crypto/sha2.h"
|
||||
#include "device/fido/authenticator_data.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
|
||||
absl::optional<std::vector<uint8_t>> GenerateEcSignature(
|
||||
base::span<const uint8_t> pkcs8_ec_private_key,
|
||||
base::span<const uint8_t> signed_over_data) {
|
||||
auto ec_private_key =
|
||||
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(pkcs8_ec_private_key);
|
||||
if (!ec_private_key) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
auto signer = crypto::ECSignatureCreator::Create(ec_private_key.get());
|
||||
std::vector<uint8_t> signature;
|
||||
if (!signer->Sign(signed_over_data, &signature)) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PasskeyAuthenticatorServiceAsh::RequestState::RequestState() = default;
|
||||
|
||||
PasskeyAuthenticatorServiceAsh::RequestState::~RequestState() = default;
|
||||
|
||||
PasskeyAuthenticatorServiceAsh::PasskeyAuthenticatorServiceAsh(
|
||||
CoreAccountInfo account_info,
|
||||
webauthn::PasskeyModel* passkey_model,
|
||||
trusted_vault::TrustedVaultClient* trusted_vault_client)
|
||||
: primary_account_info_(std::move(account_info)),
|
||||
passkey_model_(passkey_model),
|
||||
trusted_vault_client_(trusted_vault_client) {}
|
||||
|
||||
PasskeyAuthenticatorServiceAsh::~PasskeyAuthenticatorServiceAsh() = default;
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::BindReceiver(
|
||||
mojo::PendingReceiver<crosapi::mojom::PasskeyAuthenticator>
|
||||
pending_receiver) {
|
||||
receivers_.Add(this, std::move(pending_receiver));
|
||||
}
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::Assert(
|
||||
crosapi::mojom::AccountKeyPtr account_key,
|
||||
crosapi::mojom::PasskeyAssertionRequestPtr request,
|
||||
AssertCallback callback) {
|
||||
if (!IsPrimaryAccount(account_key)) {
|
||||
std::move(callback).Run(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::kNonPrimaryAccount));
|
||||
return;
|
||||
}
|
||||
|
||||
if (request_state_) {
|
||||
std::move(callback).Run(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::kPendingRequest));
|
||||
return;
|
||||
}
|
||||
request_state_.emplace();
|
||||
request_state_->pending_assert_callback = std::move(callback);
|
||||
request_state_->assert_request = std::move(request);
|
||||
FetchTrustedVaultKeys(base::BindOnce(
|
||||
&PasskeyAuthenticatorServiceAsh::DoAssert, weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::FetchTrustedVaultKeys(
|
||||
base::OnceCallback<void()> callback) {
|
||||
trusted_vault_client_->FetchKeys(
|
||||
primary_account_info_,
|
||||
base::BindOnce(&PasskeyAuthenticatorServiceAsh::OnHaveTrustedVaultKeys,
|
||||
weak_factory_.GetWeakPtr())
|
||||
.Then(std::move(callback)));
|
||||
}
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::OnHaveTrustedVaultKeys(
|
||||
const std::vector<std::vector<uint8_t>>& keys) {
|
||||
if (keys.empty()) {
|
||||
// TODO(crbug.com/1223853): Implement security domain secret recovery UI
|
||||
// flow.
|
||||
NOTIMPLEMENTED();
|
||||
return;
|
||||
}
|
||||
request_state_->security_domain_secret = keys.back();
|
||||
}
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::DoAssert() {
|
||||
CHECK(request_state_);
|
||||
|
||||
if (!request_state_->security_domain_secret) {
|
||||
FinishAssert(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::
|
||||
kSecurityDomainSecretUnavailable));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string credential_id(
|
||||
request_state_->assert_request->credential_id.begin(),
|
||||
request_state_->assert_request->credential_id.end());
|
||||
absl::optional<sync_pb::WebauthnCredentialSpecifics> credential_specifics =
|
||||
passkey_model_->GetPasskeyByCredentialId(
|
||||
request_state_->assert_request->rp_id, credential_id);
|
||||
if (!credential_specifics) {
|
||||
FinishAssert(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::kCredentialNotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
sync_pb::WebauthnCredentialSpecifics_Encrypted credential_secrets;
|
||||
if (!webauthn::passkey_model_utils::DecryptWebauthnCredentialSpecificsData(
|
||||
*request_state_->security_domain_secret, *credential_specifics,
|
||||
&credential_secrets)) {
|
||||
FinishAssert(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::
|
||||
kSecurityDomainSecretUnavailable));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(crbug.com/1223853): Implement user verification.
|
||||
|
||||
device::AuthenticatorData authenticator_data(
|
||||
crypto::SHA256Hash(base::as_bytes(
|
||||
base::make_span(request_state_->assert_request->rp_id))),
|
||||
/*user_present=*/true,
|
||||
/*user_verified=*/true, /*backup_eligible=*/true, /*sign_counter=*/0,
|
||||
/*attested_credential_data=*/absl::nullopt, /*extensions=*/absl::nullopt);
|
||||
std::vector<uint8_t> signed_over_data =
|
||||
authenticator_data.SerializeToByteArray();
|
||||
signed_over_data.insert(
|
||||
signed_over_data.end(),
|
||||
request_state_->assert_request->client_data_hash.begin(),
|
||||
request_state_->assert_request->client_data_hash.end());
|
||||
absl::optional<std::vector<uint8_t>> assertion_signature =
|
||||
GenerateEcSignature(
|
||||
base::as_bytes(base::make_span(credential_secrets.private_key())),
|
||||
signed_over_data);
|
||||
if (!assertion_signature) {
|
||||
FinishAssert(crosapi::mojom::PasskeyAssertionResult::NewError(
|
||||
crosapi::mojom::PasskeyAssertionError::kInternalError));
|
||||
return;
|
||||
}
|
||||
|
||||
auto response = crosapi::mojom::PasskeyAssertionResponse::New();
|
||||
response->signature = std::move(*assertion_signature);
|
||||
|
||||
FinishAssert(
|
||||
crosapi::mojom::PasskeyAssertionResult::NewResponse(std::move(response)));
|
||||
}
|
||||
|
||||
void PasskeyAuthenticatorServiceAsh::FinishAssert(
|
||||
crosapi::mojom::PasskeyAssertionResultPtr result) {
|
||||
CHECK(request_state_);
|
||||
AssertCallback callback = std::move(request_state_->pending_assert_callback);
|
||||
request_state_ = absl::nullopt;
|
||||
std::move(callback).Run(std::move(result));
|
||||
}
|
||||
|
||||
bool PasskeyAuthenticatorServiceAsh::IsPrimaryAccount(
|
||||
const crosapi::mojom::AccountKeyPtr& mojo_account_key) const {
|
||||
const absl::optional<account_manager::AccountKey> account_key =
|
||||
account_manager::FromMojoAccountKey(mojo_account_key);
|
||||
if (!account_key ||
|
||||
(account_key->account_type() != account_manager::AccountType::kGaia) ||
|
||||
account_key->id().empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return account_key->id() == primary_account_info_.gaia;
|
||||
}
|
||||
|
||||
} // namespace ash
|
@ -0,0 +1,84 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_ASH_H_
|
||||
#define CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_ASH_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "base/containers/span.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "chromeos/crosapi/mojom/account_manager.mojom.h"
|
||||
#include "chromeos/crosapi/mojom/passkeys.mojom.h"
|
||||
#include "components/keyed_service/core/keyed_service.h"
|
||||
#include "components/signin/public/identity_manager/account_info.h"
|
||||
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
||||
#include "mojo/public/cpp/bindings/receiver_set.h"
|
||||
|
||||
namespace trusted_vault {
|
||||
class TrustedVaultClient;
|
||||
}
|
||||
|
||||
namespace webauthn {
|
||||
class PasskeyModel;
|
||||
}
|
||||
|
||||
namespace ash {
|
||||
|
||||
// Implements a crosapi interface for creating and asserting passkeys associated
|
||||
// with the primary profile.
|
||||
class PasskeyAuthenticatorServiceAsh
|
||||
: public crosapi::mojom::PasskeyAuthenticator,
|
||||
public KeyedService {
|
||||
public:
|
||||
// `account_info` must belong the primary profile. `passkey_model` and
|
||||
// `trusted_vault_client` must outlive this instance.
|
||||
PasskeyAuthenticatorServiceAsh(
|
||||
CoreAccountInfo account_info,
|
||||
webauthn::PasskeyModel* passkey_model,
|
||||
trusted_vault::TrustedVaultClient* trusted_vault_client);
|
||||
PasskeyAuthenticatorServiceAsh(const PasskeyAuthenticatorServiceAsh&) =
|
||||
delete;
|
||||
PasskeyAuthenticatorServiceAsh& operator=(PasskeyAuthenticatorServiceAsh&) =
|
||||
delete;
|
||||
~PasskeyAuthenticatorServiceAsh() override;
|
||||
|
||||
void BindReceiver(mojo::PendingReceiver<crosapi::mojom::PasskeyAuthenticator>
|
||||
pending_receiver);
|
||||
|
||||
void Assert(crosapi::mojom::AccountKeyPtr account_key,
|
||||
crosapi::mojom::PasskeyAssertionRequestPtr request,
|
||||
AssertCallback callback) override;
|
||||
|
||||
private:
|
||||
struct RequestState {
|
||||
RequestState();
|
||||
~RequestState();
|
||||
crosapi::mojom::PasskeyAssertionRequestPtr assert_request;
|
||||
AssertCallback pending_assert_callback;
|
||||
absl::optional<std::vector<uint8_t>> security_domain_secret;
|
||||
};
|
||||
|
||||
void FetchTrustedVaultKeys(base::OnceCallback<void()> callback);
|
||||
void OnHaveTrustedVaultKeys(const std::vector<std::vector<uint8_t>>& keys);
|
||||
|
||||
void DoAssert();
|
||||
void FinishAssert(crosapi::mojom::PasskeyAssertionResultPtr result);
|
||||
|
||||
bool IsPrimaryAccount(const crosapi::mojom::AccountKeyPtr& account_key) const;
|
||||
|
||||
const CoreAccountInfo primary_account_info_;
|
||||
const raw_ptr<webauthn::PasskeyModel> passkey_model_;
|
||||
const raw_ptr<trusted_vault::TrustedVaultClient> trusted_vault_client_;
|
||||
|
||||
absl::optional<RequestState> request_state_;
|
||||
|
||||
mojo::ReceiverSet<crosapi::mojom::PasskeyAuthenticator> receivers_;
|
||||
|
||||
base::WeakPtrFactory<PasskeyAuthenticatorServiceAsh> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace ash
|
||||
|
||||
#endif // CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_ASH_H_
|
@ -0,0 +1,156 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "base/rand_util.h"
|
||||
#include "base/test/scoped_feature_list.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "base/test/test_future.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_ash.h"
|
||||
#include "chromeos/crosapi/mojom/passkeys.mojom.h"
|
||||
#include "components/signin/public/identity_manager/account_info.h"
|
||||
#include "components/signin/public/identity_manager/identity_test_environment.h"
|
||||
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
|
||||
#include "components/trusted_vault/test/fake_trusted_vault_client.h"
|
||||
#include "components/webauthn/core/browser/passkey_model_utils.h"
|
||||
#include "components/webauthn/core/browser/test_passkey_model.h"
|
||||
#include "crypto/ec_private_key.h"
|
||||
#include "crypto/random.h"
|
||||
#include "device/fido/features.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
|
||||
using crosapi::mojom::AccountKey;
|
||||
using crosapi::mojom::AccountType;
|
||||
using crosapi::mojom::PasskeyAssertionError;
|
||||
using crosapi::mojom::PasskeyAssertionRequest;
|
||||
using crosapi::mojom::PasskeyAssertionRequestPtr;
|
||||
using crosapi::mojom::PasskeyAssertionResponse;
|
||||
using crosapi::mojom::PasskeyAssertionResponsePtr;
|
||||
using crosapi::mojom::PasskeyAssertionResultPtr;
|
||||
|
||||
constexpr std::string_view kRpId = "example.com";
|
||||
constexpr std::array<uint8_t, 3> kUserId{1, 2, 3};
|
||||
constexpr size_t kSecurityDomainSecretLength = 32;
|
||||
|
||||
sync_pb::WebauthnCredentialSpecifics NewPasskey(
|
||||
base::span<const uint8_t> domain_secret) {
|
||||
sync_pb::WebauthnCredentialSpecifics specifics;
|
||||
specifics.set_sync_id(base::RandBytesAsString(16));
|
||||
specifics.set_credential_id(base::RandBytesAsString(16));
|
||||
specifics.set_rp_id(kRpId.data());
|
||||
specifics.set_user_id({kUserId.begin(), kUserId.end()});
|
||||
specifics.set_creation_time(
|
||||
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
|
||||
|
||||
sync_pb::WebauthnCredentialSpecifics_Encrypted encrypted_pb;
|
||||
auto ec_key = crypto::ECPrivateKey::Create();
|
||||
std::vector<uint8_t> private_key_pkcs8;
|
||||
CHECK(ec_key->ExportPrivateKey(&private_key_pkcs8));
|
||||
encrypted_pb.set_private_key(
|
||||
{private_key_pkcs8.begin(), private_key_pkcs8.end()});
|
||||
CHECK(webauthn::passkey_model_utils::EncryptWebauthnCredentialSpecificsData(
|
||||
domain_secret, encrypted_pb, &specifics));
|
||||
|
||||
return specifics;
|
||||
}
|
||||
|
||||
class PasskeyAuthenticatorServiceAshTest : public testing::Test {
|
||||
protected:
|
||||
PasskeyAuthenticatorServiceAshTest() {
|
||||
account_info_ = identity_test_env_.MakePrimaryAccountAvailable(
|
||||
"example@gmail.com", signin::ConsentLevel::kSignin);
|
||||
passkey_authenticator_ = std::make_unique<PasskeyAuthenticatorServiceAsh>(
|
||||
account_info_, &passkey_model_, &trusted_vault_client_);
|
||||
passkey_authenticator_->BindReceiver(
|
||||
passkey_authenticator_remote_.BindNewPipeAndPassReceiver());
|
||||
crypto::RandBytes(base::make_span(security_domain_secret_));
|
||||
}
|
||||
|
||||
void InjectTrustedVaultKey() {
|
||||
const std::vector<std::vector<uint8_t>> keys = {
|
||||
{security_domain_secret_.begin(), security_domain_secret_.end()}};
|
||||
trusted_vault_client_.StoreKeys(account_info_.gaia, keys,
|
||||
/*last_key_version=*/1);
|
||||
}
|
||||
|
||||
sync_pb::WebauthnCredentialSpecifics InjectCredential(
|
||||
std::string_view rp_id) {
|
||||
sync_pb::WebauthnCredentialSpecifics specifics =
|
||||
NewPasskey(security_domain_secret_);
|
||||
passkey_model_.AddNewPasskeyForTesting(specifics);
|
||||
return specifics;
|
||||
}
|
||||
|
||||
base::expected<PasskeyAssertionResponsePtr, PasskeyAssertionError>
|
||||
AssertPasskey(std::string_view rp_id, std::string_view credential_id) {
|
||||
base::test::TestFuture<PasskeyAssertionResultPtr> future;
|
||||
auto request = PasskeyAssertionRequest::New();
|
||||
request->rp_id = rp_id;
|
||||
request->credential_id = {credential_id.begin(), credential_id.end()};
|
||||
passkey_authenticator_remote_->Assert(
|
||||
AccountKey::New(account_info_.gaia, AccountType::kGaia),
|
||||
std::move(request), future.GetCallback());
|
||||
// Ensure all calls to `trusted_vault_client_ are able to complete.
|
||||
passkey_authenticator_remote_.FlushForTesting();
|
||||
trusted_vault_client_.CompleteAllPendingRequests();
|
||||
PasskeyAssertionResultPtr result = future.Take();
|
||||
if (result->is_error()) {
|
||||
return base::unexpected(result->get_error());
|
||||
}
|
||||
CHECK(result->is_response());
|
||||
return std::move(result->get_response());
|
||||
}
|
||||
|
||||
base::test::SingleThreadTaskEnvironment task_environment_;
|
||||
base::test::ScopedFeatureList scoped_feature_list_{device::kChromeOsPasskeys};
|
||||
|
||||
std::unique_ptr<PasskeyAuthenticatorServiceAsh> passkey_authenticator_;
|
||||
mojo::Remote<crosapi::mojom::PasskeyAuthenticator>
|
||||
passkey_authenticator_remote_;
|
||||
|
||||
signin::IdentityTestEnvironment identity_test_env_;
|
||||
AccountInfo account_info_;
|
||||
webauthn::TestPasskeyModel passkey_model_;
|
||||
trusted_vault::FakeTrustedVaultClient trusted_vault_client_;
|
||||
|
||||
std::array<uint8_t, kSecurityDomainSecretLength> security_domain_secret_;
|
||||
};
|
||||
|
||||
// The service should return an error if a GetAssertion request is performed
|
||||
// without a security domain secret.
|
||||
// TODO(crbug.com/1223853): Implement domain secret recovery.
|
||||
TEST_F(PasskeyAuthenticatorServiceAshTest, GetAssertionWithoutDomainSecret) {
|
||||
ASSERT_TRUE(trusted_vault_client_.GetStoredKeys(account_info_.gaia).empty());
|
||||
auto result = AssertPasskey(kRpId, "unknown id");
|
||||
EXPECT_EQ(result.error(),
|
||||
PasskeyAssertionError::kSecurityDomainSecretUnavailable);
|
||||
}
|
||||
|
||||
TEST_F(PasskeyAuthenticatorServiceAshTest, GetAssertionUnknownCredential) {
|
||||
InjectTrustedVaultKey();
|
||||
sync_pb::WebauthnCredentialSpecifics credential = InjectCredential(kRpId);
|
||||
auto result = AssertPasskey(kRpId, "unknown id");
|
||||
EXPECT_EQ(result.error(), PasskeyAssertionError::kCredentialNotFound);
|
||||
}
|
||||
|
||||
TEST_F(PasskeyAuthenticatorServiceAshTest, GetAssertion) {
|
||||
InjectTrustedVaultKey();
|
||||
sync_pb::WebauthnCredentialSpecifics credential = InjectCredential(kRpId);
|
||||
auto result = AssertPasskey(kRpId, credential.credential_id());
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_FALSE(result.value()->signature.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ash
|
@ -0,0 +1,67 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_factory_ash.h"
|
||||
|
||||
#include "base/feature_list.h"
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "chrome/browser/ash/passkeys/passkey_authenticator_service_ash.h"
|
||||
#include "chrome/browser/profiles/profile.h"
|
||||
#include "chrome/browser/signin/identity_manager_factory.h"
|
||||
#include "chrome/browser/trusted_vault/trusted_vault_service_factory.h"
|
||||
#include "chrome/browser/webauthn/passkey_model_factory.h"
|
||||
#include "components/signin/public/base/consent_level.h"
|
||||
#include "components/signin/public/identity_manager/identity_manager.h"
|
||||
#include "components/sync/base/features.h"
|
||||
#include "components/trusted_vault/trusted_vault_client.h"
|
||||
#include "components/trusted_vault/trusted_vault_service.h"
|
||||
#include "components/webauthn/core/browser/passkey_model.h"
|
||||
#include "content/public/browser/storage_partition.h"
|
||||
#include "device/fido/features.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
PasskeyAuthenticatorServiceFactoryAsh*
|
||||
PasskeyAuthenticatorServiceFactoryAsh::GetInstance() {
|
||||
static base::NoDestructor<PasskeyAuthenticatorServiceFactoryAsh> instance;
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
PasskeyAuthenticatorServiceAsh*
|
||||
PasskeyAuthenticatorServiceFactoryAsh::GetForProfile(Profile* profile) {
|
||||
return static_cast<PasskeyAuthenticatorServiceAsh*>(
|
||||
GetInstance()->GetServiceForBrowserContext(profile, true));
|
||||
}
|
||||
|
||||
PasskeyAuthenticatorServiceFactoryAsh::PasskeyAuthenticatorServiceFactoryAsh()
|
||||
: ProfileKeyedServiceFactory(
|
||||
"PasskeyAuthenticatorServiceAsh",
|
||||
ProfileSelections::Builder()
|
||||
.WithRegular(ProfileSelection::kRedirectedToOriginal)
|
||||
.Build()) {
|
||||
DependsOn(IdentityManagerFactory::GetInstance());
|
||||
DependsOn(PasskeyModelFactory::GetInstance());
|
||||
DependsOn(TrustedVaultServiceFactory::GetInstance());
|
||||
}
|
||||
|
||||
PasskeyAuthenticatorServiceFactoryAsh::
|
||||
~PasskeyAuthenticatorServiceFactoryAsh() = default;
|
||||
|
||||
std::unique_ptr<KeyedService>
|
||||
PasskeyAuthenticatorServiceFactoryAsh::BuildServiceInstanceForBrowserContext(
|
||||
content::BrowserContext* context) const {
|
||||
if (!base::FeatureList::IsEnabled(device::kChromeOsPasskeys)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* profile = Profile::FromBrowserContext(context);
|
||||
return std::make_unique<PasskeyAuthenticatorServiceAsh>(
|
||||
IdentityManagerFactory::GetForProfile(profile)->GetPrimaryAccountInfo(
|
||||
signin::ConsentLevel::kSignin),
|
||||
PasskeyModelFactory::GetForProfile(profile),
|
||||
TrustedVaultServiceFactory::GetForProfile(profile)->GetTrustedVaultClient(
|
||||
trusted_vault::SecurityDomainId::kPasskeys));
|
||||
}
|
||||
|
||||
} // namespace ash
|
@ -0,0 +1,40 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_FACTORY_ASH_H_
|
||||
#define CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_FACTORY_ASH_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "base/no_destructor.h"
|
||||
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
|
||||
|
||||
namespace content {
|
||||
class BrowserContext;
|
||||
}
|
||||
|
||||
namespace ash {
|
||||
|
||||
class PasskeyAuthenticatorServiceAsh;
|
||||
|
||||
class PasskeyAuthenticatorServiceFactoryAsh
|
||||
: public ProfileKeyedServiceFactory {
|
||||
public:
|
||||
static PasskeyAuthenticatorServiceFactoryAsh* GetInstance();
|
||||
static PasskeyAuthenticatorServiceAsh* GetForProfile(Profile* profile);
|
||||
|
||||
private:
|
||||
friend class base::NoDestructor<PasskeyAuthenticatorServiceFactoryAsh>;
|
||||
|
||||
PasskeyAuthenticatorServiceFactoryAsh();
|
||||
~PasskeyAuthenticatorServiceFactoryAsh() override;
|
||||
|
||||
// BrowserContextKeyedServiceFactory:
|
||||
std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
|
||||
content::BrowserContext* context) const override;
|
||||
};
|
||||
|
||||
} // namespace ash
|
||||
|
||||
#endif // CHROME_BROWSER_ASH_PASSKEYS_PASSKEY_AUTHENTICATOR_SERVICE_FACTORY_ASH_H_
|
@ -83,6 +83,7 @@ mojom("mojom") {
|
||||
"notification.mojom",
|
||||
"nullable_primitives.mojom",
|
||||
"parent_access.mojom",
|
||||
"passkeys.mojom",
|
||||
"policy_namespace.mojom",
|
||||
"policy_service.mojom",
|
||||
"power.mojom",
|
||||
|
@ -78,6 +78,7 @@ import "chromeos/crosapi/mojom/networking_private.mojom";
|
||||
import "chromeos/crosapi/mojom/power.mojom";
|
||||
import "chromeos/crosapi/mojom/network_settings_service.mojom";
|
||||
import "chromeos/crosapi/mojom/parent_access.mojom";
|
||||
import "chromeos/crosapi/mojom/passkeys.mojom";
|
||||
import "chromeos/crosapi/mojom/prefs.mojom";
|
||||
import "chromeos/crosapi/mojom/printing_metrics.mojom";
|
||||
import "chromeos/crosapi/mojom/probe_service.mojom";
|
||||
@ -150,8 +151,8 @@ struct BrowserInfo {
|
||||
// please note the milestone when you added it, to help us reason about
|
||||
// compatibility between the client applications and older ash-chrome binaries.
|
||||
//
|
||||
// Next version: 124
|
||||
// Next method id: 125
|
||||
// Next version: 125
|
||||
// Next method id: 127
|
||||
[Stable, Uuid="8b79c34f-2bf8-4499-979a-b17cac522c1e",
|
||||
RenamedFrom="crosapi.mojom.AshChromeService"]
|
||||
interface Crosapi {
|
||||
@ -525,6 +526,11 @@ interface Crosapi {
|
||||
[MinVersion=97]
|
||||
BindParentAccess@101(pending_receiver<ParentAccess> receiver);
|
||||
|
||||
// Binds an interface for creating and asserting user passkeys.
|
||||
[MinVersion=124]
|
||||
BindPasskeyAuthenticator@126(
|
||||
pending_receiver<PasskeyAuthenticator> receiver);
|
||||
|
||||
// Binds the PaymentAppInstance to allow Playstore payment to be made for
|
||||
// web apps listing in Playstore.
|
||||
[MinVersion=113] BindPaymentAppInstance@116(
|
||||
|
73
chromeos/crosapi/mojom/passkeys.mojom
Normal file
73
chromeos/crosapi/mojom/passkeys.mojom
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
module crosapi.mojom;
|
||||
|
||||
import "chromeos/crosapi/mojom/account_manager.mojom";
|
||||
|
||||
[Stable, Extensible]
|
||||
// Represents a WebAuthn UserVerificationRequirement. For semantics, compare
|
||||
// https://www.w3.org/TR/webauthn-3/#enum-userVerificationRequirement.
|
||||
enum UserVerificationRequirement {
|
||||
kDiscouraged,
|
||||
[Default] kPreferred,
|
||||
kRequired,
|
||||
};
|
||||
|
||||
[Stable]
|
||||
// A request to generate a WebAuthn assertion for a passkey with the given RP
|
||||
// ID and credential ID.
|
||||
struct PasskeyAssertionRequest {
|
||||
string rp_id;
|
||||
array<uint8> credential_id;
|
||||
array<uint8> challenge;
|
||||
array<uint8> client_data_hash;
|
||||
UserVerificationRequirement user_verification;
|
||||
};
|
||||
|
||||
[Stable, Extensible]
|
||||
// Response status for a `PasskeyAssertionRequest`.
|
||||
enum PasskeyAssertionError {
|
||||
// Catch-all for all unexpected error conditions.
|
||||
[Default] kInternalError,
|
||||
// The request originates from a profile whose user is not the signed-in user
|
||||
// for the device.
|
||||
kNonPrimaryAccount,
|
||||
// Another request is in progress.
|
||||
kPendingRequest,
|
||||
// No passkey with the given credential ID exists.
|
||||
kCredentialNotFound,
|
||||
// Retrieving the security domain secret for decrypting passkeys failed.
|
||||
kSecurityDomainSecretUnavailable,
|
||||
};
|
||||
|
||||
[Stable]
|
||||
// The response for a successful PasskeyAssertionRequest. Contains a WebAuthn
|
||||
// signature for the chosen credential.
|
||||
struct PasskeyAssertionResponse {
|
||||
array<uint8> signature;
|
||||
};
|
||||
|
||||
[Stable]
|
||||
// The result of a PasskeyAssertionRequest.
|
||||
union PasskeyAssertionResult {
|
||||
// Set on success.
|
||||
PasskeyAssertionResponse response;
|
||||
|
||||
// Set on error.
|
||||
PasskeyAssertionError error;
|
||||
};
|
||||
|
||||
[Stable, Uuid="43bd6bb3-ca9e-4625-b05a-6353ab22847a"]
|
||||
// Defines an API for accessing passkeys (implemented in ash-chrome).
|
||||
//
|
||||
// Methods on this service will trigger OS-level UI, for example to verify the
|
||||
// user with an authentication prompt. Hence, methods may return an error if
|
||||
// another request is already in progress.
|
||||
interface PasskeyAuthenticator {
|
||||
// Generates a WebAuthn assertion signature for a given passkey. The returned
|
||||
// `assertion` is non-null iff `status` equals `kOk`.
|
||||
Assert@0(AccountKey account,
|
||||
PasskeyAssertionRequest request) => (PasskeyAssertionResult result);
|
||||
};
|
Reference in New Issue
Block a user