0

[webauthn] Use software keys when TPM unavailable

Use the Microsoft Software Key Storage Provider to back enclave
unexportable keys on Windows when the machine doesn't have a TPM. This
allows those machines to use Google Password Manager passkeys.

This feature is behind the disabled-by-default
WebAuthenticationMicrosoftSoftwareUnexportableKeyProvider flag.

Bug: 398125798
Change-Id: Ie38e810dd5655072be824b32a8b4ba606dbd1897
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6297628
Reviewed-by: David Benjamin <davidben@chromium.org>
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1425207}
This commit is contained in:
Nina Satragno
2025-02-26 09:21:48 -08:00
committed by Chromium LUCI CQ
parent 43316e2717
commit 7db4fffc81
7 changed files with 182 additions and 64 deletions

@ -11,6 +11,7 @@
#include "build/chromeos_buildflags.h"
#include "crypto/unexportable_key.h"
#include "crypto/user_verifying_key.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/features.h"
#if BUILDFLAG(IS_MAC)
@ -49,7 +50,17 @@ GetWebAuthnUnexportableKeyProvider() {
config.keychain_access_group =
EnclaveManager::kEnclaveKeysKeychainAccessGroup;
#endif // BUILDFLAG(IS_MAC)
return crypto::GetUnexportableKeyProvider(std::move(config));
std::unique_ptr<crypto::UnexportableKeyProvider> provider =
crypto::GetUnexportableKeyProvider(std::move(config));
if ((!provider || provider->SelectAlgorithm(
device::enclave::kSigningAlgorithms) == std::nullopt) &&
base::FeatureList::IsEnabled(
device::kWebAuthnMicrosoftSoftwareUnexportableKeyProvider)) {
// On Windows, if there is no TPM support, use the Microsoft Software Key
// Storage Provider instead.
provider = crypto::GetMicrosoftSoftwareUnexportableKeyProvider();
}
return provider;
}
std::unique_ptr<crypto::UserVerifyingKeyProvider>

@ -24,6 +24,8 @@ bool UnexportableSigningKey::IsHardwareBacked() const {
#if BUILDFLAG(IS_WIN)
std::unique_ptr<UnexportableKeyProvider> GetUnexportableKeyProviderWin();
std::unique_ptr<UnexportableKeyProvider>
GetMicrosoftSoftwareUnexportableKeyProviderWin();
std::unique_ptr<VirtualUnexportableKeyProvider>
GetVirtualUnexportableKeyProviderWin();
#elif BUILDFLAG(IS_MAC)
@ -50,6 +52,15 @@ std::unique_ptr<UnexportableKeyProvider> GetUnexportableKeyProvider(
#endif
}
std::unique_ptr<UnexportableKeyProvider>
GetMicrosoftSoftwareUnexportableKeyProvider() {
#if BUILDFLAG(IS_WIN)
return GetMicrosoftSoftwareUnexportableKeyProviderWin();
#else
return nullptr;
#endif
}
std::unique_ptr<VirtualUnexportableKeyProvider>
GetVirtualUnexportableKeyProvider_DO_NOT_USE_METRICS_ONLY() {
#if BUILDFLAG(IS_WIN)

@ -252,6 +252,17 @@ class CRYPTO_EXPORT VirtualUnexportableKeyProvider {
CRYPTO_EXPORT std::unique_ptr<UnexportableKeyProvider>
GetUnexportableKeyProvider(UnexportableKeyProvider::Config config);
// GetMicrosoftSoftwareUnexportableKeyProvider returns an
// |UnexportableKeyProvider| that is backed by the Microsoft Software Key
// Storage Provider. Keys stored in this fashion are available to both the
// software that created them, and any software running locally with
// administrative privileges.
// Microsoft Software keys are less secure than TPM backed keys, so
// |GetUnexportableKeyProvider| should be preferred, but they are more widely
// available.
CRYPTO_EXPORT std::unique_ptr<UnexportableKeyProvider>
GetMicrosoftSoftwareUnexportableKeyProvider();
// GetVirtualUnexportableKeyProvider_DO_NOT_USE_METRICS_ONLY returns a
// |VirtualUnexportableKeyProvider| for the current platform, or nullptr if
// there isn't one. This should currently only be used for metrics gathering.

@ -20,6 +20,18 @@
namespace {
enum class Provider {
kTPM,
kMock,
kMicrosoftSoftware,
};
const Provider kAllProviders[] = {
Provider::kTPM,
Provider::kMock,
Provider::kMicrosoftSoftware,
};
const crypto::SignatureVerifier::SignatureAlgorithm kAllAlgorithms[] = {
crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256,
crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256,
@ -29,10 +41,21 @@ const crypto::SignatureVerifier::SignatureAlgorithm kAllAlgorithms[] = {
constexpr char kTestKeychainAccessGroup[] = "test-keychain-access-group";
#endif // BUILDFLAG(IS_MAC)
std::string ToString(Provider provider) {
switch (provider) {
case Provider::kTPM:
return "TPM";
case Provider::kMock:
return "Mock";
case Provider::kMicrosoftSoftware:
return "Microsoft Software";
}
}
class UnexportableKeySigningTest
: public testing::TestWithParam<
std::tuple<crypto::SignatureVerifier::SignatureAlgorithm,
bool,
Provider,
bool>> {
public:
void SetUp() override {
@ -58,30 +81,30 @@ class UnexportableKeySigningTest
INSTANTIATE_TEST_SUITE_P(All,
UnexportableKeySigningTest,
testing::Combine(testing::ValuesIn(kAllAlgorithms),
testing::Bool(),
testing::ValuesIn(kAllProviders),
testing::Bool()));
TEST_P(UnexportableKeySigningTest, RoundTrip) {
const crypto::SignatureVerifier::SignatureAlgorithm algo =
std::get<0>(GetParam());
const bool mock_enabled = std::get<1>(GetParam());
const Provider provider_type = std::get<1>(GetParam());
switch (algo) {
case crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256:
LOG(INFO) << "ECDSA P-256, mock=" << mock_enabled;
LOG(INFO) << "ECDSA P-256, provider=" << ToString(provider_type);
break;
case crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256:
LOG(INFO) << "RSA, mock=" << mock_enabled;
LOG(INFO) << "RSA, provider=" << ToString(provider_type);
break;
default:
ASSERT_TRUE(false);
}
SCOPED_TRACE(static_cast<int>(algo));
SCOPED_TRACE(mock_enabled);
SCOPED_TRACE(ToString(provider_type));
std::optional<crypto::ScopedMockUnexportableKeyProvider> mock;
if (mock_enabled) {
if (provider_type == Provider::kMock) {
mock.emplace();
}
@ -92,8 +115,12 @@ TEST_P(UnexportableKeySigningTest, RoundTrip) {
.keychain_access_group = kTestKeychainAccessGroup
#endif // BUILDLFAG(IS_MAC)
};
std::unique_ptr<crypto::UnexportableKeyProvider> provider =
crypto::GetUnexportableKeyProvider(std::move(config));
std::unique_ptr<crypto::UnexportableKeyProvider> provider;
if (provider_type == Provider::kMicrosoftSoftware) {
provider = crypto::GetMicrosoftSoftwareUnexportableKeyProvider();
} else {
provider = crypto::GetUnexportableKeyProvider(std::move(config));
}
if (!provider) {
LOG(INFO) << "Skipping test because of lack of hardware support.";
return;

@ -14,6 +14,7 @@
#include <vector>
#include "base/base64.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
@ -53,6 +54,23 @@ const char kMetricVirtualOpenKeyError[] = "Crypto.TpmError.VirtualOpenKey";
const char kMetricVirtualOpenStorageError[] =
"Crypto.TpmError.VirtualOpenStorage";
enum class ProviderType {
// Keys will be backed by a TPM. Requires TPM support.
kTPM,
// Keys will be backed by software. Widely available.
kSoftware
};
LPCWSTR GetWindowsIdentifierForProvider(ProviderType type) {
switch (type) {
case ProviderType::kTPM:
return MS_PLATFORM_CRYPTO_PROVIDER;
case ProviderType::kSoftware:
return MS_KEY_STORAGE_PROVIDER;
}
}
std::u16string KeyIdToWindowsLabel(base::span<const uint8_t> key_id) {
return u"unexportable-key-" + base::UTF8ToUTF16(base::Base64Encode(key_id));
}
@ -336,6 +354,46 @@ base::expected<std::vector<uint8_t>, SECURITY_STATUS> SignRSA(
return sig;
}
bool LoadWrappedKey(base::span<const uint8_t> wrapped,
ScopedNCryptProvider& provider,
ProviderType provider_type,
ScopedNCryptKey& key) {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
if (FAILED(NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
GetWindowsIdentifierForProvider(provider_type),
/*flags=*/0))) {
return false;
}
SECURITY_STATUS import_status = -1;
if (base::FeatureList::IsEnabled(features::kLabelWindowsUnexportableKeys)) {
// Current versions of Chrome label keys with a random identifier. Attempt
// to obtain a handle from the identifier.
std::u16string key_label = KeyIdToWindowsLabel(wrapped);
import_status =
NCryptOpenKey(provider.get(), ScopedNCryptKey::Receiver(key).get(),
base::as_wcstr(key_label),
/*dwLegacyKeySpec=*/0, /*dwFlags=*/0);
}
if (FAILED(import_status)) {
// Previous versions of Chrome used an undocumented Windows feature to
// export a wrapped key. Attempt to obtain a handle from the wrapped key to
// continue to support old keys.
import_status = NCryptImportKey(
provider.get(), /*hImportKey=*/NULL, BCRYPT_OPAQUE_KEY_BLOB,
/*pParameterList=*/nullptr, ScopedNCryptKey::Receiver(key).get(),
const_cast<PBYTE>(wrapped.data()), wrapped.size(),
/*dwFlags=*/NCRYPT_SILENT_FLAG);
}
if (FAILED(import_status)) {
LogTPMOperationError(TPMOperation::kWrappedKeyCreation, import_status,
std::nullopt);
return false;
}
return true;
}
// ECDSAKey wraps a TPM-stored P-256 ECDSA key.
class ECDSAKey : public UnexportableSigningKey {
public:
@ -420,6 +478,8 @@ class RSAKey : public UnexportableSigningKey {
// Provider to expose TPM-backed keys on Windows.
class UnexportableKeyProviderWin : public UnexportableKeyProvider {
public:
explicit UnexportableKeyProviderWin(ProviderType provider_type)
: provider_type_(provider_type) {}
~UnexportableKeyProviderWin() override = default;
std::optional<SignatureVerifier::SignatureAlgorithm> SelectAlgorithm(
@ -430,7 +490,7 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
if (FAILED(NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_PLATFORM_CRYPTO_PROVIDER, /*flags=*/0))) {
GetWindowsIdentifierForProvider(provider_type_), /*flags=*/0))) {
return std::nullopt;
}
}
@ -449,7 +509,7 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
if (FAILED(NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_PLATFORM_CRYPTO_PROVIDER, /*flags=*/0))) {
GetWindowsIdentifierForProvider(provider_type_), /*flags=*/0))) {
return nullptr;
}
}
@ -536,7 +596,7 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
ScopedNCryptProvider provider;
ScopedNCryptKey key;
if (!LoadWrappedTPMKey(wrapped, provider, key)) {
if (!LoadWrappedKey(wrapped, provider, provider_type_, key)) {
return nullptr;
}
@ -548,13 +608,20 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
// The documentation suggests that |NCRYPT_ALGORITHM_PROPERTY| should return
// the original algorithm, i.e. |BCRYPT_ECDSA_P256_ALGORITHM| for ECDSA. But
// it actually returns just "ECDSA" for that case.
static const wchar_t kECDSA[] = L"ECDSA";
static const wchar_t kRSA[] = BCRYPT_RSA_ALGORITHM;
// it actually returns just "ECDSA" for keys backed by the TPM.
// Note that these intentionally include the NUL terminator, since they're
// comparing against a c-style string that happens to be represented as an
// std::vector.
static constexpr wchar_t kECDSA[] = L"ECDSA";
static const base::span<const uint8_t> kECDSA_TPM =
base::as_byte_span(kECDSA);
static const base::span<const uint8_t> kECDSA_Software =
base::as_byte_span(BCRYPT_ECDSA_P256_ALGORITHM);
static const base::span<const uint8_t> kRSA =
base::as_byte_span(BCRYPT_RSA_ALGORITHM);
std::optional<std::vector<uint8_t>> spki;
if (algo_bytes->size() == sizeof(kECDSA) &&
memcmp(algo_bytes->data(), kECDSA, sizeof(kECDSA)) == 0) {
if (algo_bytes == kECDSA_Software || algo_bytes == kECDSA_TPM) {
spki = GetP256ECDSASPKI(key.get());
if (!spki) {
return nullptr;
@ -562,8 +629,7 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
return std::make_unique<ECDSAKey>(
std::move(key), std::vector<uint8_t>(wrapped.begin(), wrapped.end()),
std::move(spki.value()));
} else if (algo_bytes->size() == sizeof(kRSA) &&
memcmp(algo_bytes->data(), kRSA, sizeof(kRSA)) == 0) {
} else if (algo_bytes == kRSA) {
spki = GetRSASPKI(key.get());
if (!spki) {
return nullptr;
@ -580,6 +646,9 @@ class UnexportableKeyProviderWin : public UnexportableKeyProvider {
// Unexportable keys are stateless on Windows.
return true;
}
private:
ProviderType provider_type_;
};
// ECDSASoftwareKey wraps a Credential Guard stored P-256 ECDSA key.
@ -801,22 +870,25 @@ class VirtualUnexportableKeyProviderWin
const std::optional<std::vector<uint8_t>> algo_bytes =
GetKeyProperty(key.get(), NCRYPT_ALGORITHM_PROPERTY);
// This is the expected behavior, but note it is different from
// TPM backed keys.
static const wchar_t kECDSA[] = BCRYPT_ECDSA_P256_ALGORITHM;
static const wchar_t kRSA[] = BCRYPT_RSA_ALGORITHM;
// This is the expected behavior, but note it is different from TPM backed
// keys.
// Note that these intentionally include the NUL terminator, since they're
// comparing against a c-style string that happens to be represented as an
// std::vector.
static const base::span<const uint8_t> kECDSA_Software =
base::as_byte_span(BCRYPT_ECDSA_P256_ALGORITHM);
static const base::span<const uint8_t> kRSA =
base::as_byte_span(BCRYPT_RSA_ALGORITHM);
std::optional<std::vector<uint8_t>> spki;
if (algo_bytes->size() == sizeof(kECDSA) &&
memcmp(algo_bytes->data(), kECDSA, sizeof(kECDSA)) == 0) {
if (algo_bytes == kECDSA_Software) {
spki = GetP256ECDSASPKI(key.get());
if (!spki) {
return nullptr;
}
return std::make_unique<ECDSASoftwareKey>(std::move(key), name,
std::move(spki.value()));
} else if (algo_bytes->size() == sizeof(kRSA) &&
memcmp(algo_bytes->data(), kRSA, sizeof(kRSA)) == 0) {
} else if (algo_bytes == kRSA) {
spki = GetRSASPKI(key.get());
if (!spki) {
return nullptr;
@ -834,44 +906,20 @@ class VirtualUnexportableKeyProviderWin
bool LoadWrappedTPMKey(base::span<const uint8_t> wrapped,
ScopedNCryptProvider& provider,
ScopedNCryptKey& key) {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
if (FAILED(NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_PLATFORM_CRYPTO_PROVIDER,
/*flags=*/0))) {
return false;
}
SECURITY_STATUS import_status = -1;
if (base::FeatureList::IsEnabled(features::kLabelWindowsUnexportableKeys)) {
// Current versions of Chrome label keys with a random identifier. Attempt
// to obtain a handle from the identifier.
std::u16string key_label = KeyIdToWindowsLabel(wrapped);
import_status =
NCryptOpenKey(provider.get(), ScopedNCryptKey::Receiver(key).get(),
base::as_wcstr(key_label),
/*dwLegacyKeySpec=*/0, /*dwFlags=*/0);
}
if (FAILED(import_status)) {
// Previous versions of Chrome used an undocumented Windows feature to
// export a wrapped key. Attempt to obtain a handle from the wrapped key to
// continue to support old keys.
import_status = NCryptImportKey(
provider.get(), /*hImportKey=*/NULL, BCRYPT_OPAQUE_KEY_BLOB,
/*pParameterList=*/nullptr, ScopedNCryptKey::Receiver(key).get(),
const_cast<PBYTE>(wrapped.data()), wrapped.size(),
/*dwFlags=*/NCRYPT_SILENT_FLAG);
}
if (FAILED(import_status)) {
LogTPMOperationError(TPMOperation::kWrappedKeyCreation, import_status,
std::nullopt);
return false;
}
return true;
return LoadWrappedKey(wrapped, provider, ProviderType::kTPM, key);
}
std::unique_ptr<UnexportableKeyProvider> GetUnexportableKeyProviderWin() {
return std::make_unique<UnexportableKeyProviderWin>();
return std::make_unique<UnexportableKeyProviderWin>(ProviderType::kTPM);
}
std::unique_ptr<UnexportableKeyProvider>
GetMicrosoftSoftwareUnexportableKeyProviderWin() {
if (!base::FeatureList::IsEnabled(features::kLabelWindowsUnexportableKeys)) {
// The software provider requires kLabelWindowsUnexportableKeys to work.
return nullptr;
}
return std::make_unique<UnexportableKeyProviderWin>(ProviderType::kSoftware);
}
std::unique_ptr<VirtualUnexportableKeyProvider>

@ -167,9 +167,14 @@ BASE_FEATURE(kSyncSecurityDomainBeforePINRenewal,
"kWebAuthenticationSyncSecurityDomainBeforePINRenewal",
base::FEATURE_ENABLED_BY_DEFAULT);
// Net yet enabled by default.
// Not yet enabled by default.
BASE_FEATURE(kWebAuthnRemoteDesktopAllowedOriginsPolicy,
"WebAuthenticationRemoteDesktopAllowedOriginsPolicy",
base::FEATURE_DISABLED_BY_DEFAULT);
// Not yet enabled by default.
BASE_FEATURE(kWebAuthnMicrosoftSoftwareUnexportableKeyProvider,
"WebAuthenticationMicrosoftSoftwareUnexportableKeyProvider",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace device

@ -140,6 +140,11 @@ BASE_DECLARE_FEATURE(kSyncSecurityDomainBeforePINRenewal);
COMPONENT_EXPORT(DEVICE_FIDO)
BASE_DECLARE_FEATURE(kWebAuthnRemoteDesktopAllowedOriginsPolicy);
// Enables using the Microsoft Software Key Storage Provider to store
// unexportable keys when a TPM is not available.
COMPONENT_EXPORT(DEVICE_FIDO)
BASE_DECLARE_FEATURE(kWebAuthnMicrosoftSoftwareUnexportableKeyProvider);
} // namespace device
#endif // DEVICE_FIDO_FEATURES_H_