app_shim_manager_mac: use the framework's static code signing info

* Move away from using SecCodeCopySelf() to get the signing info base for validating app shims. Instead use SecStaticCodeCreateWithPath() to pull the live framework's signing info.
* This CL also makes app shim validation less brittle.
* Fixes a missing import

(cherry picked from commit b52080a266b4cbb3a0f1477e1f147d987248f950)

Bug: 1263152, 1281111
Change-Id: If0d57f9e3b1d410d618cf62bbd2da78186ce03d5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3453293
Reviewed-by: Mark Mentovai <mark@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Commit-Queue: Tom Burgin <bur@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#971408}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3472503
Cr-Commit-Position: refs/branch-heads/4758@{#1177}
Cr-Branched-From: 4a2cf4baf90326df19c3ee70ff987960d59a386e-refs/heads/main@{#950365}
diff --git a/chrome/browser/apps/app_shim/app_shim_manager_mac.cc b/chrome/browser/apps/app_shim/app_shim_manager_mac.cc
index 45c611d..f5d2187 100644
--- a/chrome/browser/apps/app_shim/app_shim_manager_mac.cc
+++ b/chrome/browser/apps/app_shim/app_shim_manager_mac.cc
@@ -16,12 +16,16 @@
 #include "base/callback.h"
 #include "base/callback_helpers.h"
 #include "base/cxx17_backports.h"
+#include "base/debug/dump_without_crashing.h"
 #include "base/feature_list.h"
 #include "base/files/file_path.h"
 #include "base/hash/sha1.h"
 #include "base/logging.h"
+#include "base/mac/bundle_locations.h"
 #include "base/mac/foundation_util.h"
-#include "base/mac/scoped_cftyperef.h"
+#include "base/mac/mac_logging.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/sys_string_conversions.h"
 #include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
 #include "chrome/browser/apps/app_shim/app_shim_host_mac.h"
 #include "chrome/browser/apps/app_shim/app_shim_listener.h"
@@ -46,11 +50,13 @@
 #include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_shortcut_mac.h"
 #include "chrome/common/chrome_features.h"
+#include "components/crash/core/common/crash_key.h"
 #include "components/crx_file/id_util.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/notification_details.h"
 #include "content/public/browser/notification_service.h"
 #include "content/public/browser/notification_source.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace {
 
@@ -60,87 +66,194 @@
 const base::Feature kAppShimProfileMenuIcons{"AppShimProfileMenuIcons",
                                              base::FEATURE_DISABLED_BY_DEFAULT};
 
-// Create a SHA1 hex digest of a certificate, for use specifically in building
-// a code signing requirement string in IsAcceptablyCodeSigned(), below.
-std::string CertificateSHA1Digest(SecCertificateRef certificate) {
-  base::ScopedCFTypeRef<CFDataRef> certificate_data(
-      SecCertificateCopyData(certificate));
-  char hash[base::kSHA1Length];
-  base::SHA1HashBytes(CFDataGetBytePtr(certificate_data),
-                      CFDataGetLength(certificate_data),
-                      reinterpret_cast<unsigned char*>(hash));
-  return base::HexEncode(hash, base::kSHA1Length);
+// A crash key that is used when dumping because of errors when building and
+// verifying the app shim requirement.
+crash_reporter::CrashKeyString<256> app_shim_requirement_crash_key(
+    "AppShimRequirement");
+
+// This function logs the status and error_details using OSSTATUS_LOG(). It also
+// calls base::debug::DumpWithoutCrashing() using app_shim_requirement_crash_key
+// as a crash key. The status and error_details are appended to the crash key.
+void DumpOSStatusError(OSStatus status, std::string error_details) {
+  OSSTATUS_LOG(ERROR, status) << error_details;
+  crash_reporter::ScopedCrashKeyString crash_key_value(
+      &app_shim_requirement_crash_key,
+      base::StringPrintf("%s: %s (%d)", error_details.c_str(),
+                         logging::DescriptionFromOSStatus(status).c_str(),
+                         status));
+  base::debug::DumpWithoutCrashing();
 }
 
-// Returns whether |pid|'s code signature is trusted:
-// - True if the caller is unsigned (there's nothing to verify).
-// - True if |pid| satisfies the caller's designated requirement.
-// - False otherwise (|pid| does not satisfy caller's designated requirement).
-bool IsAcceptablyCodeSignedInternal(pid_t pid) {
-  base::ScopedCFTypeRef<SecCodeRef> own_code;
-  base::ScopedCFTypeRef<CFDictionaryRef> own_signing_info;
+// This function is similar to DumpOSStatusError(), however it operates without
+// an OSStatus.
+void DumpError(std::string error_details) {
+  LOG(ERROR) << error_details;
+  crash_reporter::ScopedCrashKeyString crash_key_value(
+      &app_shim_requirement_crash_key, error_details);
+  base::debug::DumpWithoutCrashing();
+}
 
-  // Fetch the calling process's designated requirement. The shim can only be
-  // validated if the caller has one (i.e. if the caller is code signed).
+// Creates a requirement for the app shim based on the framework bundle's
+// designated requirement. The caller will own the returned SecRequirementRef.
+// If the returned optional:
+//   * "has_value() == true" app shim validation should occur.
+//   * "has_value() == false" app shim validation should be skipped.
+//   * "has_value() == true && value() == null" validation should always fail.
+absl::optional<SecRequirementRef> CreateAppShimRequirement() {
+  // Note: Don't validate |framework_code|: We don't need to waste time
+  // validating. We are only interested in discovering if the framework bundle
+  // is code-signed, and if so what the designated requirement is.
+  base::ScopedCFTypeRef<CFURLRef> framework_url =
+      base::mac::FilePathToCFURL(base::mac::FrameworkBundlePath());
+  base::ScopedCFTypeRef<SecStaticCodeRef> framework_code;
+  OSStatus status = SecStaticCodeCreateWithPath(
+      framework_url, kSecCSDefaultFlags, framework_code.InitializeInto());
+
+  // If the framework bundle is unsigned there is nothing else to do. We treat
+  // this as success because there’s no identity to protect or even match, so
+  // it’s not dangerous to let the shim connect.
+  if (status == errSecCSUnsigned) {
+    return absl::nullopt;  // has_value() == false
+  }
+
+  // If there was an error obtaining the SecStaticCodeRef something is very
+  // broken or something bad is happening, deny.
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecStaticCodeCreateWithPath");
+    return nullptr;  // has_value() == true
+  }
+
+  // Copy the signing info from the SecStaticCodeRef.
+  base::ScopedCFTypeRef<CFDictionaryRef> framework_signing_info;
+  status = SecCodeCopySigningInformation(
+      framework_code.get(), kSecCSSigningInformation,
+      framework_signing_info.InitializeInto());
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecCodeCopySigningInformation");
+    return nullptr;  // has_value() == true
+  }
+
+  // Look up the code signing flags. If the flags are absent treat this as
+  // unsigned. This decision is consistent with the StaticCode source:
+  // https://github.com/apple-oss-distributions/Security/blob/Security-60157.40.30.0.1/OSX/libsecurity_codesigning/lib/StaticCode.cpp#L2270
+  CFNumberRef framework_signing_info_flags =
+      base::mac::GetValueFromDictionary<CFNumberRef>(framework_signing_info,
+                                                     kSecCodeInfoFlags);
+  if (!framework_signing_info_flags) {
+    return absl::nullopt;  // has_value() == false
+  }
+
+  // If the framework bundle is ad-hoc signed there is nothing else to
+  // do. While the framework bundle is code-signed an ad-hoc signature does not
+  // contain any identities to match against. Treat this as a success.
   //
-  // Note: Don't validate |own_code|: updates modify the browser's bundle and
-  // invalidate its code signature while an update is pending. This can be
-  // revisited after https://crbug.com/496298 is resolved.
-  if (SecCodeCopySelf(kSecCSDefaultFlags, own_code.InitializeInto()) !=
-          errSecSuccess ||
-      SecCodeCopySigningInformation(own_code.get(), kSecCSSigningInformation,
-                                    own_signing_info.InitializeInto()) !=
-          errSecSuccess) {
-    LOG(ERROR) << "Failed to get own code signing information.";
-    return false;
+  // Note: Using a long long to extract the value from the CFNumberRef to be
+  // consistent with how it was packed by Security.framework.
+  // https://github.com/apple-oss-distributions/Security/blob/Security-60157.40.30.0.1/OSX/libsecurity_utilities/lib/cfutilities.h#L262
+  long long flags;
+  if (!CFNumberGetValue(framework_signing_info_flags, kCFNumberLongLongType,
+                        &flags)) {
+    DumpError("CFNumberGetValue");
+    return nullptr;  // has_value() == true
+  }
+  if (static_cast<uint32_t>(flags) & kSecCodeSignatureAdhoc) {
+    return absl::nullopt;  // has_value() == false
   }
 
-  auto* own_certificates = base::mac::GetValueFromDictionary<CFArrayRef>(
-      own_signing_info, kSecCodeInfoCertificates);
-  if (!own_certificates || CFArrayGetCount(own_certificates) < 1) {
-    return true;
+  // Moving on. Time to start building a requirement that we will use to
+  // validate the app shim's code signature. First let's get the framework
+  // bundle requirement. We will build a suitable requirement for the app shim
+  // based off that.
+  base::ScopedCFTypeRef<SecRequirementRef> framework_requirement;
+  status =
+      SecCodeCopyDesignatedRequirement(framework_code, kSecCSDefaultFlags,
+                                       framework_requirement.InitializeInto());
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecCodeCopyDesignatedRequirement");
+    return nullptr;  // has_value() == true
   }
 
-  auto* own_certificate = base::mac::CFCast<SecCertificateRef>(
-      CFArrayGetValueAtIndex(own_certificates, 0));
-  auto own_certificate_hash = CertificateSHA1Digest(own_certificate);
+  base::ScopedCFTypeRef<CFStringRef> framework_requirement_string;
+  status =
+      SecRequirementCopyString(framework_requirement, kSecCSDefaultFlags,
+                               framework_requirement_string.InitializeInto());
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecRequirementCopyString");
+    return nullptr;  // has_value() == true
+  }
 
   base::ScopedCFTypeRef<CFStringRef> shim_requirement_string(
-      CFStringCreateWithFormat(
-          kCFAllocatorDefault, nullptr,
-          CFSTR(
-              "identifier \"app_mode_loader\" and certificate leaf = H\"%s\""),
-          own_certificate_hash.c_str()));
+      apps::AppShimManager::
+          BuildAppShimRequirementStringFromFrameworkRequirementString(
+              framework_requirement_string));
+  if (!shim_requirement_string) {
+    return nullptr;  // has_value() == true
+  }
 
-  base::ScopedCFTypeRef<SecRequirementRef> shim_requirement;
-  if (SecRequirementCreateWithString(
-          shim_requirement_string, kSecCSDefaultFlags,
-          shim_requirement.InitializeInto()) != errSecSuccess) {
-    LOG(ERROR)
-        << "Failed to create a SecRequirementRef from the requirement string \""
-        << shim_requirement_string << "\"";
+  // Parse the requirement.
+  // Using a naked SecRequirementRef here. Since the sole caller of this
+  // function will be storing the returned SecRequirementRef for the lifetime of
+  // the process there is no added benefit to wrapping the return value in a
+  // base::ScopedCFTypeRef.
+  SecRequirementRef shim_requirement;
+  status = SecRequirementCreateWithString(
+      shim_requirement_string, kSecCSDefaultFlags, &shim_requirement);
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status,
+                      std::string("SecRequirementCreateWithString: ") +
+                          base::SysCFStringRefToUTF8(shim_requirement_string));
+    return nullptr;
+  }
+  return shim_requirement;
+}
+
+// Returns whether |app_shim_pid|'s code signature is trusted:
+// - True if the framework bundle is unsigned (there's nothing to verify).
+// - True if |app_shim_pid| satisfies the constructed designated requirement
+// tailored for the app shim based on the framework bundle's requirement.
+// - False otherwise (|app_shim_pid| does not satisfy the constructed designated
+// requirement).
+bool IsAcceptablyCodeSignedInternal(pid_t app_shim_pid) {
+  static absl::optional<SecRequirementRef> app_shim_requirement =
+      CreateAppShimRequirement();
+  if (!app_shim_requirement.has_value()) {
+    // App shim validation is not required because framework bundle is not
+    // code-signed or is ad-hoc code-signed.
+    return true;
+  }
+  if (!app_shim_requirement.value()) {
+    // Framework bundle is code-signed however we were unable to create the app
+    // shim requirement. Deny.
+    // apps::AppShimManager::BuildAppShimRequirementStringFromFrameworkRequirementString
+    // already did the base::debug::DumpWithoutCrashing, possibly on a previous
+    // call. We can return false here without any additional explanation.
     return false;
   }
 
-  base::ScopedCFTypeRef<SecCodeRef> guest_code;
-
-  base::ScopedCFTypeRef<CFNumberRef> pid_cf(
-      CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid));
-  const void* guest_attribute_keys[] = {kSecGuestAttributePid};
-  const void* guest_attribute_values[] = {pid_cf};
-  base::ScopedCFTypeRef<CFDictionaryRef> guest_attributes(CFDictionaryCreate(
-      nullptr, guest_attribute_keys, guest_attribute_values,
-      base::size(guest_attribute_keys), &kCFTypeDictionaryKeyCallBacks,
+  // Verify the app shim.
+  base::ScopedCFTypeRef<CFNumberRef> app_shim_pid_cf(
+      CFNumberCreate(nullptr, kCFNumberIntType, &app_shim_pid));
+  const void* app_shim_attribute_keys[] = {kSecGuestAttributePid};
+  const void* app_shim_attribute_values[] = {app_shim_pid_cf};
+  base::ScopedCFTypeRef<CFDictionaryRef> app_shim_attributes(CFDictionaryCreate(
+      nullptr, app_shim_attribute_keys, app_shim_attribute_values,
+      base::size(app_shim_attribute_keys), &kCFTypeDictionaryKeyCallBacks,
       &kCFTypeDictionaryValueCallBacks));
-  if (SecCodeCopyGuestWithAttributes(nullptr, guest_attributes,
-                                     kSecCSDefaultFlags,
-                                     guest_code.InitializeInto())) {
-    LOG(ERROR) << "Failed to create a SecCodeRef from the app shim's pid.";
+  base::ScopedCFTypeRef<SecCodeRef> app_shim_code;
+  OSStatus status = SecCodeCopyGuestWithAttributes(
+      nullptr, app_shim_attributes, kSecCSDefaultFlags,
+      app_shim_code.InitializeInto());
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecCodeCopyGuestWithAttributes");
     return false;
   }
-
-  return SecCodeCheckValidity(guest_code, kSecCSDefaultFlags,
-                              shim_requirement) == errSecSuccess;
+  status = SecCodeCheckValidity(app_shim_code, kSecCSDefaultFlags,
+                                app_shim_requirement.value());
+  if (status != errSecSuccess) {
+    DumpOSStatusError(status, "SecCodeCheckValidity");
+    return false;
+  }
+  return true;
 }
 
 bool ProfileMenuItemComparator(const chrome::mojom::ProfileMenuItemPtr& a,
@@ -1154,4 +1267,43 @@
   return !files.empty() || !urls.empty() || !override_url.is_empty();
 }
 
+base::ScopedCFTypeRef<CFStringRef>
+AppShimManager::BuildAppShimRequirementStringFromFrameworkRequirementString(
+    CFStringRef framwork_requirement) {
+  // Make sure the framework bundle requirement is in the expected format.
+  // It should start with "identifier" and have exactly 2 quotes.
+  CFIndex len = CFStringGetLength(framwork_requirement);
+  base::ScopedCFTypeRef<CFArrayRef> quote_ranges(
+      CFStringCreateArrayWithFindResults(nullptr, framwork_requirement,
+                                         CFSTR("\""), CFRangeMake(0, len), 0));
+  if (!CFStringHasPrefix(framwork_requirement, CFSTR("identifier \"")) ||
+      !quote_ranges || CFArrayGetCount(quote_ranges) != 2) {
+    DumpError("Framework bundle requirement is malformed.");
+    return base::ScopedCFTypeRef<CFStringRef>(nullptr);
+  }
+
+  // Get the index of the second quote.
+  CFIndex second_quote_index =
+      static_cast<const CFRange*>(CFArrayGetValueAtIndex(quote_ranges, 1))
+          ->location;
+
+  // Make sure there is something to read after the second quote.
+  if (second_quote_index + 1 >= len) {
+    DumpError("Framework bundle requirement is too short");
+    return base::ScopedCFTypeRef<CFStringRef>(nullptr);
+  }
+
+  // Build the app shim requirement. Keep the data from the framework bundle
+  // requirement starting after second quote.
+  base::ScopedCFTypeRef<CFStringRef> right_of_second_quote(
+      CFStringCreateWithSubstring(
+          nullptr, framwork_requirement,
+          CFRangeMake(second_quote_index + 1, len - second_quote_index - 1)));
+  base::ScopedCFTypeRef<CFMutableStringRef> shim_requirement_string(
+      CFStringCreateMutableCopy(nullptr, 0,
+                                CFSTR("identifier \"app_mode_loader\"")));
+  CFStringAppend(shim_requirement_string, right_of_second_quote);
+  return base::ScopedCFTypeRef<CFStringRef>(shim_requirement_string);
+}
+
 }  // namespace apps
diff --git a/chrome/browser/apps/app_shim/app_shim_manager_mac.h b/chrome/browser/apps/app_shim/app_shim_manager_mac.h
index 073af17d..c5d0ab3 100644
--- a/chrome/browser/apps/app_shim/app_shim_manager_mac.h
+++ b/chrome/browser/apps/app_shim/app_shim_manager_mac.h
@@ -13,6 +13,7 @@
 
 #include "apps/app_lifetime_monitor.h"
 #include "base/callback_forward.h"
+#include "base/mac/scoped_cftyperef.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
 #include "chrome/browser/apps/app_shim/app_shim_host_mac.h"
@@ -188,6 +189,9 @@
   // AvatarMenuObserver:
   void OnAvatarMenuChanged(AvatarMenu* menu) override;
 
+  static base::ScopedCFTypeRef<CFStringRef>
+      BuildAppShimRequirementStringFromFrameworkRequirementString(CFStringRef);
+
  protected:
   typedef std::set<Browser*> BrowserSet;
 
diff --git a/chrome/browser/apps/app_shim/app_shim_manager_mac_unittest.cc b/chrome/browser/apps/app_shim/app_shim_manager_mac_unittest.cc
index 72fa284..13b61dd 100644
--- a/chrome/browser/apps/app_shim/app_shim_manager_mac_unittest.cc
+++ b/chrome/browser/apps/app_shim/app_shim_manager_mac_unittest.cc
@@ -12,6 +12,7 @@
 #include <vector>
 
 #include "base/bind.h"
+#include "base/strings/sys_string_conversions.h"
 #include "base/test/gmock_callback_support.h"
 #include "base/test/mock_callback.h"
 #include "base/test/scoped_feature_list.h"
@@ -1486,4 +1487,54 @@
   ValidateDockMenuItems(menu_items_profile_b, kNumMenuItemsForProfileB);
 }
 
+TEST_F(AppShimManagerTest,
+       BuildAppShimRequirementStringFromFrameworkRequirementStringTest) {
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("identifier \"com.google.Chrome.framework\" and certificate "
+                "leaf = H\"c9a99324ca3fcb23dbcc36bd5fd4f9753305130a\"")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR(
+              "cdhash H\"daa66a31aeb85125bd2459bebf548b2dff5ee83b\" or cdhash "
+              "H\"a8e5300bf9223510fc5b107b23de0d12f419acac\"")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("identifier \"com.google.Chrome.framework\"")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("identifier")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("malformed")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("\"\"\"")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("\"\"")));
+  EXPECT_FALSE(
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          CFSTR("\"")));
+  CFStringRef framework_req = CFSTR(
+      "identifier \"com.google.Chrome.framework\" and anchor "
+      "apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* "
+      "exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] "
+      "/* exists */ and certificate leaf[subject.OU] = EQHXZ8M8AV");
+  base::ScopedCFTypeRef<CFStringRef> got_req =
+      manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
+          framework_req);
+  ASSERT_TRUE(got_req);
+  CFStringRef want_req = CFSTR(
+      "identifier \"app_mode_loader\" and anchor "
+      "apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* "
+      "exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] "
+      "/* exists */ and certificate leaf[subject.OU] = EQHXZ8M8AV");
+  EXPECT_EQ(base::SysCFStringRefToUTF8(got_req),
+            base::SysCFStringRefToUTF8(want_req));
+}
+
 }  // namespace apps