From 2d5597b1b0fa697905380184e26c9f0947e05c5d Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Fri, 14 Nov 2025 14:24:08 -0500 Subject: [PATCH] feat: validate integrity of ASAR Integrity dictionary on macOS (#48587) --- filenames.gni | 2 + shell/common/asar/archive_mac.mm | 4 ++ shell/common/asar/integrity_digest.h | 15 ++++++ shell/common/asar/integrity_digest.mm | 74 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 shell/common/asar/integrity_digest.h create mode 100644 shell/common/asar/integrity_digest.mm diff --git a/filenames.gni b/filenames.gni index 8808b3b907..21b57205fe 100644 --- a/filenames.gni +++ b/filenames.gni @@ -194,6 +194,8 @@ filenames = { "shell/common/api/electron_api_clipboard_mac.mm", "shell/common/api/electron_api_native_image_mac.mm", "shell/common/asar/archive_mac.mm", + "shell/common/asar/integrity_digest.h", + "shell/common/asar/integrity_digest.mm", "shell/common/application_info_mac.mm", "shell/common/language_util_mac.mm", "shell/common/mac/main_application_bundle.h", diff --git a/shell/common/asar/archive_mac.mm b/shell/common/asar/archive_mac.mm index 455c5e03c2..bc6286e7eb 100644 --- a/shell/common/asar/archive_mac.mm +++ b/shell/common/asar/archive_mac.mm @@ -17,6 +17,7 @@ #include "base/files/file_util.h" #include "base/strings/sys_string_conversions.h" #include "shell/common/asar/asar_util.h" +#include "shell/common/asar/integrity_digest.h" namespace asar { @@ -39,6 +40,9 @@ std::optional Archive::HeaderIntegrity() const { NSDictionary* integrity = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ElectronAsarIntegrity"]; + if (!IsIntegrityDictionaryValid(integrity)) + return std::nullopt; + // Integrity not provided if (!integrity) return std::nullopt; diff --git a/shell/common/asar/integrity_digest.h b/shell/common/asar/integrity_digest.h new file mode 100644 index 0000000000..9336baaba5 --- /dev/null +++ b/shell/common/asar/integrity_digest.h @@ -0,0 +1,15 @@ +// Copyright (c) 2025 Noah Gregory +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_COMMON_ASAR_INTEGRITY_DIGEST_H_ +#define ELECTRON_SHELL_COMMON_ASAR_INTEGRITY_DIGEST_H_ + +#include +namespace asar { + +bool IsIntegrityDictionaryValid(NSDictionary* integrity_dict); + +} // namespace asar + +#endif // ELECTRON_SHELL_COMMON_ASAR_INTEGRITY_DIGEST_H_ diff --git a/shell/common/asar/integrity_digest.mm b/shell/common/asar/integrity_digest.mm new file mode 100644 index 0000000000..7f64ae57e2 --- /dev/null +++ b/shell/common/asar/integrity_digest.mm @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Noah Gregory +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/common/asar/integrity_digest.h" + +#include + +#include +#include +#include + +#include "base/strings/sys_string_conversions.h" +#include "crypto/hash.h" + +namespace asar { + +constexpr crypto::hash::HashKind kIntegrityDictionaryHashKind = + crypto::hash::HashKind::kSha256; + +constexpr size_t kIntegrityDictionaryDigestSize = + DigestSizeForHashKind(kIntegrityDictionaryHashKind); +constexpr char kIntegrityDictionaryDigestSentinel[] = + "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A"; + +struct IntegrityDictionaryDigestSlot { + uint8_t sentinel[sizeof(kIntegrityDictionaryDigestSentinel) - 1]; + uint8_t used; + uint8_t version; + uint8_t digest[kIntegrityDictionaryDigestSize]; +}; + +constexpr IntegrityDictionaryDigestSlot MakeDigestSlot( + const char (&sentinel)[33]) { + IntegrityDictionaryDigestSlot slot{}; + std::span slot_sentinel_span(slot.sentinel); + std::copy_n(sentinel, slot_sentinel_span.size(), slot_sentinel_span.begin()); + slot.used = false; // to be set at package-time + slot.version = 0; // to be set at package-time + return slot; +} + +__attribute__((section("__DATA_CONST,__asar_integrity"), used)) +const volatile IntegrityDictionaryDigestSlot kIntegrityDictionaryDigest = + MakeDigestSlot(kIntegrityDictionaryDigestSentinel); + +bool IsIntegrityDictionaryValid(NSDictionary* integrity) { + if (kIntegrityDictionaryDigest.used == false) + return true; // No digest to validate against, fail open + if (kIntegrityDictionaryDigest.version != 1) + return false; // Unknown version, fail closed + crypto::hash::Hasher integrity_hasher(kIntegrityDictionaryHashKind); + for (NSString *relative_path_key in + [[integrity allKeys] sortedArrayUsingComparator:^NSComparisonResult( + NSString* s1, NSString* s2) { + return [s1 compare:s2 options:NSLiteralSearch]; + }]) { + NSDictionary* file_integrity = [integrity objectForKey:relative_path_key]; + NSString* algorithm = [file_integrity objectForKey:@"algorithm"]; + NSString* hash = [file_integrity objectForKey:@"hash"]; + integrity_hasher.Update(base::SysNSStringToUTF8(relative_path_key)); + integrity_hasher.Update(base::SysNSStringToUTF8(algorithm)); + integrity_hasher.Update(base::SysNSStringToUTF8(hash)); + } + std::array digest; + integrity_hasher.Finish(digest); + if (!std::equal(digest.begin(), digest.end(), + kIntegrityDictionaryDigest.digest)) { + return false; + } + return true; +} + +} // namespace asar