plasmo

Chromium Deep Dive: Fixing CRX_REQUIRED_PROOF_MISSING

A deep dive into the Chromium Source Code

Stefan Aleksic
Stefan AleksicApril 7, 2022
cover image for blog

⚠️ UPDATE: We solved this problem and made it into a product called Itero TestBed - the first staging environment for browser extensions. Read on for more details about how to manually overcome the issue, then check out Itero for more details: https://www.plasmo.com/#itero

I wanted to see if I could load Chrome Extensions without using the official Chrome Web Store. When I tried to download an extension from my webserver, I got an error:CRX_REQUIRED_PROOF_MISSING

Why does CRX_REQUIRED_PROOF_MISSING show up?

By default, Google locks down Chrome Extensions so that they can only be installed from the official Chrome Web Store by checking whether Google signed the extension's CRX file.

So when you see the CRX_REQUIRED_PROOF_MISSING error, Chromium says that the Chrome Webstore hasn't signed the CRX file with its private key. Chromium doesn't trust the file as it's not coming from the Chrome Webstore!

Let's dig into this a bit and see if there's a way around this. This article is a deep dive into how Chromium validates and installs extensions, and finding a way around it.

☁️ If you'd just like to make this error go away, skip to the modifying policies section!

Join me by traversing the Chromium source tree online!

Start but Verify

Let's start at components/crx_file/crx_verifier.cc and the function Verify and see where that takes us. The Verify function is what Chromium runs when looking to ensure everything is fine with a given CRX file.

Chrome checks the standard things:

  • File is readable
  • CRX header is present
  • CRX version is the most up-to-date one (at time of writing, 3)

Once it's happy with these, things get a bit spicier! It calls the VerifyCrx3 function.

One error in the VerifyCrx3 function sticks out: VerifierResult::ERROR_REQUIRED_PROOF_MISSING

This is the CRX_REQUIRED_PROOF_MISSING error we're looking for! But what causes it you ask?

1if (public_key_bytes.empty() || !required_key_set.empty())
2
3    return VerifierResult::ERROR_REQUIRED_PROOF_MISSING;
4
5if (require_publisher_key && !found_publisher_key)
6
7    return VerifierResult::ERROR_REQUIRED_PROOF_MISSING;

The second if statement is the one causing the CRX_REQUIRED_PROOF_MISSING error when trying to download extensions from a custom web store.

There are two boolean values here. Let's see what both of them are.

What is require_publisher_key****?

1bool require_publisher_key =
2    format == VerifierFormat::CRX3_WITH_PUBLISHER_PROOF ||
3    format == VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF;

If the CRX format passed into Verify is of a particular type, require_publisher_key will return true.

What is found_publisher_key****?

1constexpr uint8_t kPublisherKeyHash[] = {
2    0x61, 0xf7, 0xf2, 0xa6, 0xbf, 0xcf, 0x74, 0xcd, 0x0b, 0xc1, 0xfe,
3    0x24, 0x97, 0xcc, 0x9b, 0x04, 0x25, 0x4c, 0x65, 0x8f, 0x79, 0xf2,
4    0x14, 0x53, 0x92, 0x86, 0x7e, 0xa8, 0x36, 0x63, 0x67, 0xcf};
5std::vector<uint8_t> publisher_key(std::begin(kPublisherKeyHash),
6                                   std::end(kPublisherKeyHash));
7std::vector<uint8_t> key_hash(crypto::kSHA256Length);
8crypto::SHA256HashString(key, key_hash.data(), key_hash.size());
9
10found_publisher_key =
11  found_publisher_key || key_hash == publisher_key || ...

General idea:

  • Go through each proof within the CRX header
  • Hash the key in the proof
  • Compare it to the Chrome Web Store's publisher key hash
  • If it's the same, the boolean found publisher key value will be true

Logic

If we can get require_publisher_key to be false, we can get Chrome to load extensions that aren't in the Web Store!

What Are the Different CRX Formats Available?

There are three total:

  • CRX3
  • CRX3_WITH_TEST_PUBLISHER_PROOF
  • CRX3_WITH_PUBLISHER_PROOF

extensions/common/verifier_formats.cc sheds some light on what each of these means:

1crx_file::VerifierFormat GetWebstoreVerifierFormat(
2    bool test_publisher_enabled) {
3  return test_publisher_enabled
4             ? crx_file::VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF
5             : crx_file::VerifierFormat::CRX3_WITH_PUBLISHER_PROOF;
6}
7
8crx_file::VerifierFormat GetPolicyVerifierFormat() {
9  return crx_file::VerifierFormat::CRX3;
10}
11
12crx_file::VerifierFormat GetExternalVerifierFormat() {
13  return crx_file::VerifierFormat::CRX3;
14}
15
16crx_file::VerifierFormat GetTestVerifierFormat() {
17  return crx_file::VerifierFormat::CRX3;
18}

Chromium enforces that extensions must come from the Web Store through formats with the pattern *_PUBLISHER_PROOF.

If we can figure out a way to get Chromium to call the Verify function with just VerifierFormat::CRX3, require_publisher_key will be false, and it won't error!

Downloading Extensions

We need to figure out how to call Verify with the CRX3 format and determine what calls the Verify function.

When you download a file in Chromium, the ChromeDownloadManagerDelegate::ShouldOpenDownload function runs.

1if (download_crx_util::IsExtensionDownload(*item) &&
2    !extensions::WebstoreInstaller::GetAssociatedApproval(*item)) {
3        scoped_refptr<extensions::CrxInstaller> crx_installer =
4            download_crx_util::OpenChromeExtension(profile_, *item);
5}

So if it was an extension that got downloaded but wasn't associated with the web store, we should call download_crx_util::OpenChromeExtension.

Looking at OpenChromeExtension:

1scoped_refptr<extensions::CrxInstaller> OpenChromeExtension(
2    Profile* profile,
3    const DownloadItem& download_item) {
4  DCHECK_CURRENTLY_ON(BrowserThread::UI);
5
6  scoped_refptr<extensions::CrxInstaller> installer(
7      CreateCrxInstaller(profile, download_item));
8
9  if (OffStoreInstallAllowedByPrefs(profile, download_item)) {
10    installer->set_off_store_install_allow_reason(
11        extensions::CrxInstaller::OffStoreInstallAllowedBecausePref);
12  }
13
14  if (extensions::UserScript::IsURLUserScript(download_item.GetURL(),
15                                              download_item.GetMimeType())) {
16    installer->InstallUserScript(download_item.GetFullPath(),
17                                 download_item.GetURL());
18  } else {
19    DCHECK(!WebstoreInstaller::GetAssociatedApproval(download_item));
20    installer->InstallCrx(download_item.GetFullPath());
21  }
22
23  return installer;
24}

The lines of code that stick out here are:

1if (OffStoreInstallAllowedByPrefs(profile, download_item)) {
2  installer->set_off_store_install_allow_reason(
3      extensions::CrxInstaller::OffStoreInstallAllowedBecausePref);
4}

Some preferences allow what Chromium calls an "off store install".

Let's look at this function's implementation.

chrome/browser/download/download_crx_util.cc:

1bool OffStoreInstallAllowedByPrefs(Profile* profile, const DownloadItem& item) {
2  return g_allow_offstore_install_for_testing ||
3         extensions::ExtensionManagementFactory::GetForBrowserContext(profile)
4             ->IsOffstoreInstallAllowed(item.GetURL(), item.GetReferrerUrl());
5}

The current hypothesis is that if we can get this function to return true, then the format passed into Verify will be of type CRX3, and our extension will load correctly.

I modified the function to always return true, then tested it and confirmed that the hypothesis was valid. Let's dig deeper!

Following the chain, we get to chrome/browser/extensions/extension_management.cc and IsOffStoreInstallAllowed

1bool ExtensionManagement::IsOffstoreInstallAllowed(
2    const GURL& url,
3    const GURL& referrer_url) const {
4  // No allowed install sites specified, disallow by default.
5  if (!global_settings_->has_restricted_install_sources)
6    return false;
7
8  const URLPatternSet& url_patterns = global_settings_->install_sources;
9
10  if (!url_patterns.MatchesURL(url))
11    return false;
12
13  // The referrer URL must also be allowlisted, unless the URL has the file
14  // scheme (there's no referrer for those URLs).
15  return url.SchemeIsFile() || url_patterns.MatchesURL(referrer_url);
16}

It checks global_settings_ for install_sources that match the CRX file's download URL and referrer.

What is this install_sources thing?

It's a URLPatternSet, but where is it being populated? The same file!

1const char kAllowedInstallSites[] = "extensions.allowed_install_sites";
2const base::ListValue* install_sources_pref =
3    static_cast<const base::ListValue*>(LoadPreference(
4        pref_names::kAllowedInstallSites, true, base::Value::Type::LIST));

It's reading from a config key, extensions.allowed_install_sites, and loading whatever is inside there.

If we can get in there and add our URL, we could get the IsOffStoreInstallAllowed function to return true!

Let's go deeper. What is LoadPreference anyways?

1const base::Value* ExtensionManagement::LoadPreference(
2    const char* pref_name,
3    bool force_managed,
4    base::Value::Type expected_type) const {
5  if (!pref_service_)
6    return nullptr;
7  const PrefService::Preference* pref =
8      pref_service_->FindPreference(pref_name);
9  if (pref && !pref->IsDefaultValue() &&
10      (!force_managed || pref->IsManaged())) {
11    const base::Value* value = pref->GetValue();
12    if (value && value->type() == expected_type)
13      return value;
14  }
15  return nullptr;
16}

The gist of this preference stuff is simple - Chrome has an abstraction for thinking about changes, or "preferences." So instead of the code needing to know that the preference came from some custom policy, or some JSON config change, etc., etc., it has a bunch of code that reads from all those various sources and produces the same preference config no matter what the source is.

That way, code further down the chain can think of things like preferences and doesn't have to worry about the source.

The implementation that we're interested in is in components/policy/core/browser/configuration_policy_pref_store.cc

This file is responsible for abstracting policies into preferences. Let's take a look to see how it does so.

1PrefValueMap* ConfigurationPolicyPrefStore::CreatePreferencesFromPolicies() {
2  std::unique_ptr<PrefValueMap> prefs(new PrefValueMap);
3  PolicyMap filtered_policies =
4      policy_service_
5          ->GetPolicies(PolicyNamespace(POLICY_DOMAIN_CHROME, std::string()))
6          .Clone();
7  filtered_policies.EraseNonmatching(base::BindRepeating(&IsLevel, level_));
8
9  std::unique_ptr<PolicyErrorMap> errors = std::make_unique<PolicyErrorMap>();
10
11  PoliciesSet deprecated_policies;
12  PoliciesSet future_policies;
13  handler_list_->ApplyPolicySettings(filtered_policies, prefs.get(),
14                                     errors.get(), &deprecated_policies,
15                                     &future_policies);
16
17  if (!errors->empty()) {
18    if (errors->IsReady()) {
19      LogErrors(std::move(errors), std::move(deprecated_policies),
20                std::move(future_policies));
21    } else if (policy_connector_) {  // May be null in tests.
22      policy_connector_->NotifyWhenResourceBundleReady(base::BindOnce(
23          &LogErrors, std::move(errors), std::move(deprecated_policies),
24          std::move(future_policies)));
25    }
26  }
27
28  return prefs.release();
29}

So it looks at all of the policies that Chrome knows about, removes any that aren't considered MANDATORY (based on the level), and then populates the preferences using ApplyPolicySettings.

Therefore, the solution to get extensions working off-web store is to use Chrome Enterprise policies.

Mo‎difying Policies for Off Store Install

To see a list of policies you can set, out/Debug/gen/components/policy/policy_constants.h or you can go to the Google Chrome Enterprise Policies site.

Specifically, there are two policies we need to change to allow for off-store installation and avoid the CRX_REQUIRED_PROOF_MISSING error:

ExtensionInstallAllowlist

Setting the policy specifies which extensions are not subject to the blocklist.

[Google Enterprise Policy Documentation](https://chromeenterprise.google/policies/#ExtensionInstallSources)

Chrome enables the extension blocklist by default, which blocks specific extensions from being installed outside the Chrome Web Store. This policy allows you to specify which extensions are not subject to the blocklist. The list of extensions is composed of extension IDs, and you must explicitly allow the extensions you'd like to use in your off-store installs.

💡 The format is extension id(;<update url>) where the part in the parenthesis is optional.

If you don't specify this allowlist value, Chrome will show you the following error message:

This extension is not listed in the Chrome Web Store and may have been added without your knowledge. Learn more

This is different from the CRX_REQUIRED_PROOF_MISSING but it will disable your extension nonetheless.

ExtensionInstallSources

Setting the policy specifies which URLs may install extensions, apps, and themes. Before Google Chrome 21, users could click on a link to a *.crx file, and Google Chrome would offer to install the file after a few warnings. Afterward, such files must be downloaded and dragged to the Google Chrome settings page. This setting allows specific URLs to have the old, easier installation flow.

[Google Enterprise Policy Documentation](https://chromeenterprise.google/policies/#ExtensionInstallSources)

The description here, from my experimentation, is wrong. Even if you download a CRX file and then drag and drop it over to the chrome://extensions page, VerifyCrx3 will still look for the publisher key and give you CRX_REQUIRED_PROOF_MISSING.

💡 This policy file where this value is stored must be of MANDATORY type for you to be able to install extensions off-web store. (See Appendix to learn more about mandatory policies)

Appendix

Locations of Policies

For easy copy/paste:

HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google

HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Chromium

  • \SOFTWARE\Policies\Microsoft\Edge

~/Library/Preferences/com.google.Chrome.plist

~/Library/Preferences/org.chromium.Chromium.plist

~/Library/Preferences/com.microsoft.Edge.plist

/etc/opt/chrome/policies/managed

/etc/chromium/policies/managed

Mandatory

Chrome treats recommended preferences differently from mandatory ones, so it's essential to learn the difference and how you can get Chrome to read your policy as you intend. Otherwise, you will get the CRX_REQUIRED_PROOF_MISSING error.

From my research, Chrome will throw out most policies that aren't considered mandatory.

How can you make a Chrome policy be considered mandatory? The heuristic Chrome tries to use is: "is this policy only writeable by a user with elevated privileges?" The trouble is sometimes, this is ambiguous.

Windows

Setting policies via GPOs, or by modifying registry keys of HKLM (further testing is required to see whether Chrome reads keys from HKCU, etc.) will make them mandatory. Only a user with elevated privileges can modify the Windows Registry HKLM hive.

macOS

Properties written by an MDM tool will be considered mandatory. Chromium considers the rest recommended. Chromium uses the Core Foundation function CFPreferencesAppValueIsForced, which checks whether an MDM solution wrote a property, and thus a user can't change it.

They do not check file privileges as they do on Linux. You can set the com.google.Chrome.plist not to be world writeable, but it's useless.

Linux

Chromium checks file permissions of the policies file to see if it's world writeable. If it isn't world writeable, the policies will be considered mandatory.

Conclusion

Chrome is very shy in explaining what the CRX_REQUIRED_PROOF_MISSING is all about. I hope this article helps answer any questions you had about it, and hope you learned a bit more about the mysterious world of extension validation!


At Plasmo, we're an early-stage team excited about automation, open-source, and especially the browser extension ecosystem.

As you can see in this article on diving deep into Chromium and unraveling CRX_REQUIRED_PROOF, we're building tools to make browser extension development as easy as possible, from end to end. We're going to be building a lot more awesome stuff in this space. If this sounds interesting to you, subscribe to our mailing list!

Back to Blog


Thanks for reading! We're Plasmo, a company on a mission to improve browser extension development for everyone. If you're a company looking to level up your browser extension, reach out, or sign up for Itero to get started.