Wednesday, April 10, 2019

CRX3

Starting with version 73, Chrome has switched the required package format for extensions to crx3. Why new file format?

Date: Fri, 20 Jan 2017 17:10:24 -0800
From: Joshua Pawlicki <waffles@chromium.org>
To: chromium-dev@chromium.org
Subject: Intent to Implement CRX₃
Message-ID: <CAFE=Dz0-aG-w+iA=P6JRL6NmBMVPhAHKeD8diPa72JjELGPzzw@mail.gmail.com>

[...] Chrome extensions are currently packaged for installation/update
as signed zip files called CRX₂ files, using SHA1withRSA for the
signature algorithm. Many of the RSA keys used to sign the files are
insufficiently secure (too short). The CRX₂ format does not allow for
algorithm rotation, key rotation, or multiple proofs. The goal is to
address these issues and leave the door open to future improvements. [...]

Chrome even allows to create a .crx file from the command line (e.g., in Linux: google-chrome --pack-extension=ext-dir --pack-extension-key=file.pem).

What if you'd like to create .crx files on a server that doesn't have Chrome installed?

A brief intro to Crx3

The crx2 file format was very simple: you made an sha1 of a zip file, signed it with an RSA private key & prepended the public key & the signature to the zip archive.

Crx3 prepends a protobuf that can contain an unlimited number of public_key+signature tuples (also called proofs). If you create a .crx file by yourself for a Linux version of Chrome, only 1 proof is required. Extensions from the Chrome Web Store incorporate multiple proofs.

Switching to protobufs also means you need a proper protobuf parser to be able read the new file format.

crx3 file format diagram

To see this in action, let's create a noop extension that consists only from manifest.json file.

$ cat manifest.json
{
"manifest_version": 2,
"name": "foo",
"version": "1.2.3"
}
$ zip foo.zip manifest.json
adding: manifest.json (deflated 25%)

Create an RSA public key in the PEM format:

$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
..................................+++++
.......+++++

Then do npm -g i crx3-utils (a standard disclaimer) & make a .crx:

$ crx3-new private.pem < foo.zip > foo.crx
$ file foo.crx
foo.crx: Google Chrome extension, version 3

You can now drop it into chrome://extensions/ page & Chrome should happily accept it.

To see what's inside the foo.crx, run:

$ crx3-info < foo.crx
id jnedgebbcnmoemphjanchkhfkjjhmael
header 593
payload 231
sha256_with_rsa 1 main_idx=0
sha256_with_ecdsa 0

payload is the size of the original zip archive.

id is the extension id that was calculated during the crx file creation. Here, the public key, from which the calculation was done, is in sha256_with_rsa list (alongside with a signature, both in a tuple under the index 0). If we add another tuple (proof) to the crx file, this time using a different private key, the id won't change, for it's permanently saved in SignedData protobuf structure (see the diagram above). This is very different from crx2, where you had to extract a public key first & then calculate the id from it.

In crx2, the signature was just an sha1 of a zip file. In crx3, each proof signs the following sequence of data:

  1. Magic number
  2. SignedData instance length
  3. SignedData (contains the id)
  4. Zip archive

Web Store

If we download whatever extension from the Web Store (say, Google Dictionary), it'll hold 3 proofs inside:

$ curl -sL 'https://clients2.google.com/service/update2/crx?response=redirect&prodversion=73.0.3683.86&x=id%3Dmgijmajocgfcbeboacabfgobmjgjcoja%26uc&acceptformat=crx3' > google-dictionary.crx

$ /crx3-info < google-dictionary.crx
id mgijmajocgfcbeboacabfgobmjgjcoja
header 1061
payload 44018
sha256_with_rsa 2 main_idx=1
sha256_with_ecdsa 1

sha256_with_rsa has an additional public_key+signature tuple, the public key from which is shared between all the extension from the Web Store. The original proof (from the 'developer' key) is shifted to the end of sha256_with_rsa list.

sha256_with_ecdsa is another tuple to which only Google has the private key.

8 comments:

  1. Hi It is not working solution now..
    crx3-new error: crypto.createPublicKey is not a function

    ReplyDelete
    Replies
    1. it works w/ the current node v12.8.0.

      you probably have node < 11.13.

      Delete
  2. I have followed all given steps but it is not working for me.
    The chrome browser still gives me the error message Package is invalid: 'CRX_REQUIRED_PROOF_MISSING'.

    ReplyDelete
    Replies
    1. just tried creating foo.crx with node-13.3.0; the .crx installs successfully on Chrome 78.0.3904.108 under Fedora 31 & on Chromium 78.0.3904.87 under Windows 10 1909

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. I have also created foo.crx with node-13.3.0 on Mac OS Mojave and tested it on Chrome v78.0.3904.108 under Mac OS Mojave and Windows 10 but observed same error message:
    Package is invalid: 'CRX_REQUIRED_PROOF_MISSING'.

    ReplyDelete
  5. If I install the foo.crx file when developer mode is ON then it gets installed. But when I disabled the developer mode it gives error message.

    ReplyDelete
    Replies
    1. this is not a crx3-utils issue

      if we move `manifest.json` (from the blog post) to an empty directory `foo` and run:

      $ google-chrome --pack-extension=foo --pack-extension-key=private.pem

      Chrome will create `foo.crx` file

      now, if we extract a .zip from `foo.crx` (see crx3-utils repo for a howto) & re-create a .crx with `crx3-new` command, the resulting .crx will be absolutely identical to the .crx produced by Chrome, at least, under Fedora 31

      Delete