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') 2To 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 30 June 2018.