[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_