DI Management Home > Cryptography > How Mozilla signs Add-ons

How Mozilla signs Add-ons


This page analyzes how Mozilla signs .xpi add-on packages. We look at the contents of the three files in the META-INF directory which include text files containing message digest values of the original files plus a Cryptographic Message Syntax (CMS) signed-data object in the file mozilla.rsa that prevents anyone from tampering.

An example add-on

Here is a simple example add-on created using jpm and signed through addons.mozilla.org (AMO). It's the first example given in Mozilla's Add-on SDK > Tutorials >Getting Started (jpm) with a couple of tweaks in the naming so it doesn't clash with other examples.

my_jetpack_addon-0.0.1-an+fx.xpi (use Right-click > Save Link As... to avoid actually installing it)

XPI file structure

An .xpi file is just a zip file (rename it with a ".zip" extension and you can read it with the usual zip tools).

my_jetpack_addon-0.0.1-an+fx.xpi/
    bootstrap.js
    index.js
    install.rdf
    package.json
    README.md
    data/
        icon-16.png
        icon-32.png
        icon-64.png
    META-INF/
        manifest.mf
        mozilla.rsa
        mozilla.sf

The signed version from AMO contains the three new files in the new sub-directory META-INF. It's these three files in META-INF that contain all the signature details.

manifest.mf

The file manifest.mf is a text file with the message digest values of all the relevant files in the package.

The message digest values are in base64 form using both the MD5 and SHA-1 digest algorithms.

Manifest-Version: 1.0

Name: install.rdf
Digest-Algorithms: MD5 SHA1
MD5-Digest: oMwL12WY9PV0qdUTorREPw==
SHA1-Digest: rctzadNfNSsPvwjXpXE5dm6l5gE=

Name: bootstrap.js
Digest-Algorithms: MD5 SHA1
MD5-Digest: 7bXGjs354WXP08z1LJmaUQ==
SHA1-Digest: hBvzrYsRacNpTOCOH+GOhYBn2mE=

Name: index.js
Digest-Algorithms: MD5 SHA1
MD5-Digest: H19QxBWc+PUGGma0SnaEqA==
SHA1-Digest: dZvV83BafZQ7vG0vLyBAWTH/R7o=

[cut]

Name: data/icon-32.png
Digest-Algorithms: MD5 SHA1
MD5-Digest: vhUYwySs1DMMbw48n39qUA==
SHA1-Digest: XAlxiOjnxFpsJ8azT7u55cN8yio=

Name: data/icon-64.png
Digest-Algorithms: MD5 SHA1
MD5-Digest: nVzB1tJGUuWls9gYw5+XMw==
SHA1-Digest: 32CtU+VGgN2h/L8MjWQItrfZwjk=

So if anybody tries to change the contents of any of these files, its message digest value will no longer be valid.

mozilla.sf

The file mozilla.sf is a text file with the message digest value of the file manifest.mf.

Signature-Version: 1.0
MD5-Digest-Manifest: 5vUOacsc98luZtbG+vsr+g==
SHA1-Digest-Manifest: E9jVVbFDGuSLrCQD7MDb9V7fKTc=

This prevents someone re-writing the manifest.mf file and substituting new message digest values for the files they've altered.

But what prevents someone from changing the original files and then rewriting both manifest.mf and mozilla.sf with revised and valid message digest values? This where the file mozilla.rsa comes in.

mozilla.rsa

The file mozilla.rsa is a binary file containing a CMS signed-data object which signs the contents of the file mozilla.sf. CMS files used to be called PKCS7 files and the convention is normally to use the filename extension ".p7s" or ".p7m".

The important thing is that you can't fiddle with this unless you have the AMO signing key, which we don't. So even if you'd changed the message digest values in the other two files to match your new evil add-on files, you wouldn't be able to create a valid signed-data file.

The signed-data file mozilla.rsa is in DER-encoded ASN.1 format with ASN1DUMP output shown here: ASN.1 Dump. The (simplified) overall structure of the file is

ID: signedData
DigestAlg: sha-1
eContent: (empty)
Certificates:
    cert_1:
        SerialNumber: 01 56 EA 2E 2A 48
        Issuer: 'production-signing-ca.addons.mozilla.org'
        Subject: 'Addons'/'@my-addon-y46drv'
        Created: 2016-09-02T09:15:09Z
    cert_2
        SerialNumber: 1048578
        Issuer: 'root-ca-production-amo'
        Subject: 'production-signing-ca.addons.mozilla.org'
        Created: 2015-03-14T23:52:42Z
SignerInfo:
    SignerIdentifier:
        SerialNumber: 01 56 EA 2E 2A 48
        Issuer: 'production-signing-ca.addons.mozilla.org'
    DigestAlg: sha-1
    SignedAttributes:
        signingTime: 
            2016-09-02T09:15:09Z
        messageDigest:
            3BAA813D5E6B2B9E8E01984CE76E8F0799F1FB6B
    SignatureAlg: rsaEncryption
    SignatureValue:
        12 1B 8F 7C 54 FF 75 ... etc

This is a "detached" or "external" signature. There is no eContent in the file. The content being signed is in the file mozilla.sf. The actual item signed is the message digest value of this file. The SHA-1 message digest of the file mozilla.sf encoded in hexadecimal is 3baa813d5e6b2b9e8e01984ce76e8f0799f1fb6b, which you can see in the SignedAttributes field.

AMO creates a brand new X.509 certificate especially for this add-on, with the subjectName @my-addon-y46drv and serial number 0x0156EA2E2A48 (the decimal number 1472807709256). This is cert_1 in the signed-data file. This new certificate is issued and signed by Mozilla's own certificate with subjectName 'production-signing-ca.addons.mozilla.org', created in March 2015 with serial number 1048578. This is cert_2 in the signed-data file.

The signed-data object is signed by the owner of cert_1, the brand new X.509 certificate. Note that we don't have the private key for this - by design, otherwise an attacker could forge a new version. However, anybody can use the RSA public key in the signer's certificate to verify that the signed-data signature is valid.

Examine the files using Python

A sample Python session using the cryptosyspki package.

Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:19:22) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from cryptosyspki import *
>>> Cnv.tobase64(Hash.file('manifest.mf'))
'E9jVVbFDGuSLrCQD7MDb9V7fKTc='
>>> Cnv.tobase64(Hash.file('manifest.mf',Hash.Alg.MD5))
'5vUOacsc98luZtbG+vsr+g=='
Compare these values with the contents of mozilla.sf
Signature-Version: 1.0
MD5-Digest-Manifest: 5vUOacsc98luZtbG+vsr+g==
SHA1-Digest-Manifest: E9jVVbFDGuSLrCQD7MDb9V7fKTc=
Define a more convenient function to get the SHA-1 digest in base64 form, and use fancy Python tricks to get all the SHA-1 digest values. Compare these values to the SHA1-Digest values in manifest.mf.
>>> def hashfile64(fname):
...     return Cnv.tobase64(Hash.file(fname))
...
>>> hashfile64('install.rdf')
'rctzadNfNSsPvwjXpXE5dm6l5gE='
>>> import os
>>> files = filter(os.path.isfile, os.listdir('.'))
>>> files
['bootstrap.js', 'index.js', 'install.rdf', 'package.json', 'README.md']
>>> [{f:hashfile64(f)} for f in files]
[{'bootstrap.js': 'hBvzrYsRacNpTOCOH+GOhYBn2mE='}, {'index.js': 'dZvV83BafZQ7vG0vLyBAWTH/R7o='}, 
{'install.rdf': 'rctzadNfNSsPvwjXpXE5dm6l5gE='}, {'package.json': 'lTRNuOoN3T770eIvUQk1ZTE89dw='}, 
{'README.md': 'kpO3s49K5zOIS6oqsYwVdVbIm/Q='}]
>>>
Examine mozilla.rsa, which contains ASN.1 data.
>>> os.chdir('META-INF')
>>> Asn1.type('mozilla.rsa')
'PKCS7/CMS SIGNED DATA'
>>> Cms.query_sigdata('mozilla.rsa','messageDigest')
'3baa813d5e6b2b9e8e01984ce76e8f0799f1fb6b'
>>> Cms.query_sigdata('mozilla.rsa','signingTime')
'2016-09-02 09:15:09'
>>> Cms.query_sigdata('mozilla.rsa','CountOfCertificates')
2
To verify the signature, we need the SHA-1 digest of the content of the file that was signed.
>>> h = Hash.hex_from_file('mozilla.sf')
>>> h
'3baa813d5e6b2b9e8e01984ce76e8f0799f1fb6b'
>>> Cms.verify_sigdata('mozilla.rsa', hexdigest=h)
True

Equivalent .NET methods

Equivalent .NET methods in CryptoSys PKI Pro of the Python functions used above.

Python function.NET method
Hash.file Hash.BytesFromFile Method
Hash.hex_from_file Hash.HexFromFile Method
Cnv.tobase64 Cnv.ToBase64 Method (Byte[])
Asn1.type Asn1.Type Method
Cms.query_sigdata Cms.QuerySigData Method
Cms.verify_sigdata Cms.VerifySigData Method

Contact us

To comment on this page or to contact us, please send us a message.

This page first published 6 September 2016. Last updated 25 September 2016.