[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}
diff --git a/chrome/browser/webauthn/unexportable_key_utils.cc b/chrome/browser/webauthn/unexportable_key_utils.cc index cd748db..989e7aa 100644 --- a/chrome/browser/webauthn/unexportable_key_utils.cc +++ b/chrome/browser/webauthn/unexportable_key_utils.cc
@@ -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 @@ 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>
diff --git a/crypto/unexportable_key.cc b/crypto/unexportable_key.cc index a199d5e9..fcdf781 100644 --- a/crypto/unexportable_key.cc +++ b/crypto/unexportable_key.cc
@@ -24,6 +24,8 @@ #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 @@ #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)
diff --git a/crypto/unexportable_key.h b/crypto/unexportable_key.h index 468df428c..b19648ff 100644 --- a/crypto/unexportable_key.h +++ b/crypto/unexportable_key.h
@@ -252,6 +252,17 @@ 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.
diff --git a/crypto/unexportable_key_unittest.cc b/crypto/unexportable_key_unittest.cc index 07b1d65..eaca35c 100644 --- a/crypto/unexportable_key_unittest.cc +++ b/crypto/unexportable_key_unittest.cc
@@ -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 @@ 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 @@ 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 @@ .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;
diff --git a/crypto/unexportable_key_win.cc b/crypto/unexportable_key_win.cc index 7338c89..3e3e423 100644 --- a/crypto/unexportable_key_win.cc +++ b/crypto/unexportable_key_win.cc
@@ -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 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 @@ 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 @@ // 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 @@ 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 @@ 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 @@ ScopedNCryptProvider provider; ScopedNCryptKey key; - if (!LoadWrappedTPMKey(wrapped, provider, key)) { + if (!LoadWrappedKey(wrapped, provider, provider_type_, key)) { return nullptr; } @@ -548,13 +608,20 @@ // 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 @@ 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 @@ // 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 @@ 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 @@ 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>
diff --git a/device/fido/features.cc b/device/fido/features.cc index 4aa198e..fc3c9df 100644 --- a/device/fido/features.cc +++ b/device/fido/features.cc
@@ -167,9 +167,14 @@ "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
diff --git a/device/fido/features.h b/device/fido/features.h index a2ccdbd..bd3ebcf 100644 --- a/device/fido/features.h +++ b/device/fido/features.h
@@ -140,6 +140,11 @@ 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_