Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Crypto
DescriptionInappropriate implementation in Crypto
ComponentCrypto
Bug ClassLogic Error
Tracker520516655
Fix commit49d90ea1fbcb (chromium/src) +44/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
FindUnexportableKeys
crypto/apple/unexportable_key_apple.mm
modified
ScopedMockKeychain
crypto/apple/unexportable_key_apple_unittest.mm
modified
TEST
crypto/apple/unexportable_key_apple_unittest.mm
modified

Files Changed

  • crypto/apple/unexportable_key_apple.mm
  • crypto/apple/unexportable_key_apple_unittest.mm
From 49d90ea1fbcbe0eb8d9cdcb9de2fb0179dc9ef1b Mon Sep 17 00:00:00 2001
From: Jan Wilken Dörrie <jdoerrie@chromium.org>
Date: Thu, 25 Jun 2026 02:04:00 -0700
Subject: [PATCH] [crypto] Restrict keychain queries on macOS

Ensure that keychain queries for unexportable keys are scoped to the
data protection keychain.

Bug: 520516655
Change-Id: I67fe92097b757d97549235ad5c4f2a7c6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7966018
Reviewed-by: Elly <ellyjones@chromium.org>
Commit-Queue: Jan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1652247}
---

diff --git a/crypto/apple/unexportable_key_apple.mm b/crypto/apple/unexportable_key_apple.mm
index f7abf443..f8f9cd9 100644
--- a/crypto/apple/unexportable_key_apple.mm
+++ b/crypto/apple/unexportable_key_apple.mm
@@ -150,6 +150,7 @@
 FindUnexportableKeys(FindUnexportableKeysOptions options) {
   auto [access_group, application_tag_prefix, wrapped_key, lacontext] = options;
   NSMutableDictionary* query = [NSMutableDictionary dictionaryWithDictionary:@{
+    CFToNSPtrCast(kSecUseDataProtectionKeychain) : @YES,
     CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
     CFToNSPtrCast(kSecAttrKeyType) :
         CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
diff --git a/crypto/apple/unexportable_key_apple_unittest.mm b/crypto/apple/unexportable_key_apple_unittest.mm
index e26809c6..769b86a 100644
--- a/crypto/apple/unexportable_key_apple_unittest.mm
+++ b/crypto/apple/unexportable_key_apple_unittest.mm
@@ -9,6 +9,8 @@
 #include <cstdint>
 #include <vector>
 
+#include "base/apple/foundation_util.h"
+#include "base/check_deref.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/time/time.h"
 #include "base/time/time_override.h"
@@ -673,6 +675,47 @@
       key_specific->GetWrappedKey()));
 }
 
+class ScopedMockKeychain : public crypto::apple::FakeKeychainV2 {
+ public:
+  explicit ScopedMockKeychain(const std::string& keychain_access_group)
+      : FakeKeychainV2(keychain_access_group) {
+    SetInstanceOverride(this);
+  }
+
+  ~ScopedMockKeychain() override { ClearInstanceOverride(); }
+
+  MOCK_METHOD(OSStatus,
+              ItemCopyMatching,
+              (CFDictionaryRef query, CFTypeRef* result),
+              (override));
+};
+
+// Setting `kSecUseDataProtectionKeychain` to `@YES` is required to explicitly
+// restrict the keychain search to the modern Data Protection keychain,
+// preventing fallback to legacy keychains where access group filtering is
+// broken. This test ensures the flag is always present in the query.
+TEST(UnexportableKeyMacFiltersTest, QueriesDataProtectionKeychain) {
+  ScopedMockKeychain mock_keychain(kTestKeychainAccessGroup);
+
+  EXPECT_CALL(mock_keychain, ItemCopyMatching)
+      .WillOnce([&mock_keychain](CFDictionaryRef query, CFTypeRef* result) {
+        auto use_data_protection =
+            base::apple::GetValueFromDictionary<CFBooleanRef>(
+                query, kSecUseDataProtectionKeychain);
+        EXPECT_TRUE(CFBooleanGetValue(use_data_protection));
+        return mock_keychain.FakeKeychainV2::ItemCopyMatching(query, result);
+      });
+
+  std::unique_ptr<UnexportableKeyProvider> provider =
+      GetUnexportableKeyProvider({
+          .keychain_access_group = kTestKeychainAccessGroup,
+          .application_tag = kTestApplicationTag,
+      });
+
+  // Trigger a query.
+  CHECK_DEREF(provider).AsStatefulUnexportableKeyProvider()->GetAllKeysSlowly();
+}
+
 }  // namespace
 
 }  // namespace crypto::apple
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/crypto/apple/unexportable_key_apple_unittest.mm b/crypto/apple/unexportable_key_apple_unittest.mm
index e26809c6..769b86a 100644
--- a/crypto/apple/unexportable_key_apple_unittest.mm
+++ b/crypto/apple/unexportable_key_apple_unittest.mm
@@ -9,6 +9,8 @@
 #include <cstdint>
 #include <vector>
 
+#include "base/apple/foundation_util.h"
+#include "base/check_deref.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/time/time.h"
 #include "base/time/time_override.h"
@@ -673,6 +675,47 @@
       key_specific->GetWrappedKey()));
 }
 
+class ScopedMockKeychain : public crypto::apple::FakeKeychainV2 {
+ public:
+  explicit ScopedMockKeychain(const std::string& keychain_access_group)
+      : FakeKeychainV2(keychain_access_group) {
+    SetInstanceOverride(this);
+  }
+
+  ~ScopedMockKeychain() override { ClearInstanceOverride(); }
+
+  MOCK_METHOD(OSStatus,
+              ItemCopyMatching,
+              (CFDictionaryRef query, CFTypeRef* result),
+              (override));
+};
+
+// Setting `kSecUseDataProtectionKeychain` to `@YES` is required to explicitly
+// restrict the keychain search to the modern Data Protection keychain,
+// preventing fallback to legacy keychains where access group filtering is
+// broken. This test ensures the flag is always present in the query.
+TEST(UnexportableKeyMacFiltersTest, QueriesDataProtectionKeychain) {
+  ScopedMockKeychain mock_keychain(kTestKeychainAccessGroup);
+
+  EXPECT_CALL(mock_keychain, ItemCopyMatching)
+      .WillOnce([&mock_keychain](CFDictionaryRef query, CFTypeRef* result) {
+        auto use_data_protection =
+            base::apple::GetValueFromDictionary<CFBooleanRef>(
+                query, kSecUseDataProtectionKeychain);
+        EXPECT_TRUE(CFBooleanGetValue(use_data_protection));
+        return mock_keychain.FakeKeychainV2::ItemCopyMatching(query, result);
+      });
+
+  std::unique_ptr<UnexportableKeyProvider> provider =
+      GetUnexportableKeyProvider({
+          .keychain_access_group = kTestKeychainAccessGroup,
+          .application_tag = kTestApplicationTag,
+      });
+
+  // Trigger a query.
+  CHECK_DEREF(provider).AsStatefulUnexportableKeyProvider()->GetAllKeysSlowly();
+}
+
 }  // namespace
 
 }  // namespace crypto::apple
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential keychain access-group bypass and key confusion in FindUnexportableKeys on macOS

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential logic issue in FindUnexportableKeys on macOS may allow a compromised network process to access and use legacy login keychain EC private keys as a signing oracle. Because the query does not isolate the data-protection keychain or verify the access group of returned items, unauthorized legacy keys can be loaded and bound. If the loaded key does not use the expected curve parameters, this behavior can also trigger a browser-process crash.

Affected files:

  • crypto/apple/unexportable_key_apple.mm

Estimated timestamp from git blame: 2026-01-08

Description

A potential logic and key-confusion vulnerability exists in the macOS implementation of FindUnexportableKeys in crypto/apple/unexportable_key_apple.mm.

When locating unexportable keys via the Apple Keychain API, the search query built by FindUnexportableKeys does not restrict its scope to the modern data-protection keychain. Furthermore, it lacks a manual post-filter to verify the keychain access group of the returned items. Consequently, standard software-backed EC private keys residing in the user’s legacy file-based login keychain can leak past the search filter and be returned to the caller.

Under specific conditions, an attacker who has compromised the sandboxed network process can leverage this behavior to obtain a browser-process ECDSA signing oracle over a victim’s legacy keychain keys (such as imported client certificates), bypassing sandbox and keychain access boundaries.

Note: The following steps are potential/suggested exploitation vectors identified through static analysis. Our tooling currently lacks the ability to execute code or verify these steps dynamically on a running macOS system.

Code Analysis

1. Missing Data-Protection Scoping and Access-Group Post-Filter

In crypto/apple/unexportable_key_apple.mm:150-208, FindUnexportableKeys prepares the SecItemCopyMatching query:

NSMutableDictionary* query = [NSMutableDictionary dictionaryWithDictionary:@{
  CFToNSPtrCast(kSecClass) : CFToNSPtrCast(kSecClassKey),
  CFToNSPtrCast(kSecAttrKeyType) :
      CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
  CFToNSPtrCast(kSecAttrAccessGroup) : access_group,
  CFToNSPtrCast(kSecMatchLimit) : CFToNSPtrCast(kSecMatchLimitAll),
  CFToNSPtrCast(kSecReturnAttributes) : @YES,
  CFToNSPtrCast(kSecReturnRef) : @YES,
}];

Two critical security controls are absent here:

  1. kSecUseDataProtectionKeychain is not set: Without @YES for this attribute, macOS searches both the modern data-protection keychain and the legacy file-based keychain.
  2. No post-filtering on kSecAttrAccessGroup: As documented in sibling code (device/fido/mac/credential_store.mm), the macOS kSecAttrAccessGroup query filter does not reliably exclude legacy keychain items. Sibling code explicitly implements manual validation of the returned kSecAttrAccessGroup attribute, but this validation is missing in FindUnexportableKeys.

2. Reachability and Tag-Splicing

When UnexportableKeyProviderApple::FromWrappedSigningKeySlowly is called via the network-process-exposed Mojo interface, it calls FindUnexportableKeys with an empty application_tag_prefix (matching any returned item).

If a legacy key matches the requested wrapped_key (SHA-1 public key hash), the lookup succeeds. Because the key does not possess the expected DBSC application tag, execution falls into the tag-splicing branch (crypto/apple/unexportable_key_apple.mm:517-534):

NSMutableDictionary* key_attributes = [NSMutableDictionary
    dictionaryWithDictionary:CFToNSPtrCast(key_dicts.front().get())];
key_attributes[CFToNSPtrCast(kSecAttrApplicationTag)] = 
    objc_storage_->application_tag_;
...
if (OSStatus status = crypto::apple::KeychainV2::GetInstance().ItemAdd(
        NSToCFPtrCast(key_attributes), nil);
    status != errSecSuccess) { return nullptr; }
return std::make_unique<UnexportableSigningKeyApple>(
    NSToCFPtrCast(key_attributes));

This clones the attributes (retaining the raw SecKeyRef in kSecValueRef), stamps the expected DBSC application tag, and registers it. Since the tag is stamped on the wrapped key object, the subsequent invariant check CHECK(wrapped_key_and_tag == GetWrappedKeyAndTag(*key)) in unexportable_key_service_impl.cc:462 is successfully bypassed.

Potential Attack Scenario

  1. Preconditions: The victim has an EC P-256 private key in their legacy login keychain that Chrome is permitted to use without prompting (e.g., an mTLS client cert imported via Chrome). The attacker achieves RCE in the sandboxed network process.
  2. Key Targeting: The network process obtains the victim’s public key (e.g., from an active TLS session) and computes its SHA-1 hash to serve as the wrapped_key identifier.
  3. Mojo Request: The network process calls FromWrappedSigningKey(wrapped_key) over the Mojo pipe.
  4. Key Splicing: The browser process executes the lookup, matches the legacy key, stamps the DBSC tag, registers the association, and returns a valid key_id to the network process.
  5. Signing Oracle: The network process calls Sign(key_id, arbitrary_data) to generate arbitrary ECDSA signatures using the legacy private key via the browser process.

Note: If the retrieved legacy key is an EC key but does not use the P-256 curve (e.g., P-384), the SPKI generation inside UnexportableSigningKeyApple’s constructor will dereference an empty std::optional returned by FromEcP256Point, resulting in a browser-process crash (Denial of Service).

Suggested Fix

Update FindUnexportableKeys in crypto/apple/unexportable_key_apple.mm to implement the same mitigations used by its sibling components:

  1. Explicitly restrict the search query to the Data Protection keychain by adding kSecUseDataProtectionKeychain: @YES to the query dictionary.
  2. Add a defensive manual post-filter to verify that the kSecAttrAccessGroup attribute of any returned item matches the requested access_group before allowing it to be used.

Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker