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.

Friday, January 11, 2019

Podcasts & Schedules

Sometimes it's immediately obvious whether podcast hosts are professionals or naïve armatures. This is a mapping of BBC's In Our Time show, where each dot represents a single podcast episode:

They had only 1 schedule slippage! The day when the show was uploaded a couple hours earlier was undeniably their producer's last day on the job; because of his unfortunate mismanagement, he became an idle outcast who later on mischievously voted for Brexit. Truly a tragedy without equal, but we digress.

Compare this to Trade Talks podcast:

A complete chaos! It's easy to spot that their initial day of the week was Friday but for some reason they failed to keep the commitment.

For anyone curious, all this nonsense about schedules can be obtained via:

$ curl URL | ./podcast-dow result.png

where podcast-dow is actually a makefile:

#!/usr/bin/make -f

# Requires nokogiri & gnuplot
#
# curl http://feeds.5by5.tv/b2w | ./podcast-dow 1.png [title=hello]

plot = @nokogiri -e 'puts $$_.css("item pubdate").map{|n| Date.parse(n).strftime("%Y-%m-%d %u %a")}' | cat <(echo "$$script") - | gnuplot -e 'set term $1;' - > $@

%.png:; $(call plot,png)
%.svg:; $(call plot,svg)

export define script :=
set grid
set title "$(title)"

set xdata time
set timefmt "%Y-%m-%d"
set format x "%Y-%m"

set xtics rotate by 60 right
set yrange [0:8]

plot "-" using 1:2:ytic(3) with points pointtype 5 title ""
endef

.DELETE_ON_ERROR:
SHELL := /bin/bash

(It works even under Cygwin.)