Signing Firefox extensions with Python and M2Crypto · 2011-05-15 00:07 by Wladimir Palant

Sadly, signing Firefox extensions isn’t easy. I’ve seen people give up not having mastered even the very first step (install NSS). And after that you have to use its cryptic command line tools to set up a database, import your certificate into it as well as any intermediate or root certificates required, and then actually use signtool to sign your files. Java’s signtool is easier to handle but incompatible (though, I think the only real difference is that it doesn’t put zigbert.rsa first in the archive). So while rewriting my build scripts in Python I took it as a chance and implemented signing in Python using M2Crypto module. One of the advantages for me was that my build script can now package up the extension entirely in memory, without having to write out intermediate results for signing.

It hasn’t been trivial, finding documentation on both M2Crypto and JAR signatures is surprisingly hard. I owe many thanks to Kevin O’Regan who wrote the only description of how JAR signatures work that I could find. Without his hint, would I have guessed that each section of manifest.mf is hashed separately? Probably not. Anyway, once you have all the pieces together it is surprisingly simple, a Python script with less than 70 lines is sufficient. Here it comes:

#!/usr/bin/env python
import os, sys, re, hashlib, zipfile, base64, M2Crypto
 
def signDir(source_dir, key_file, output_file):
  source_dir = os.path.abspath(source_dir)
 
  # Build file list
  filelist = []
  for dirpath, dirs, files in os.walk(source_dir):
    for file in files:
      abspath = os.path.join(dirpath, file)
      relpath = os.path.relpath(abspath, source_dir).replace('\\', '/')
      handle = open(abspath, 'rb')
      filelist.append((abspath, relpath, handle.read()))
      handle.close()
 
  # Generate manifest.mf and zigbert.sf data
  manifest_sections = []
  signature_sections = []
  def digest(data):
    md5 = hashlib.md5()
    md5.update(data)
    sha1 = hashlib.sha1()
    sha1.update(data)
    return 'Digest-Algorithms: MD5 SHA1\nMD5-Digest: %s\nSHA1-Digest: %s\n' % \
           (base64.b64encode(md5.digest()), base64.b64encode(sha1.digest()))
  def section(manifest, signature):
    manifest_sections.append(manifest)
    signature_sections.append(signature + digest(manifest))
  section('Manifest-Version: 1.0\n', 'Signature-Version: 1.0\n')
  for filepath, relpath, data in filelist:
    section('Name: %s\n%s' % (relpath, digest(data)), 'Name: %s\n' % relpath)
  manifest = '\n'.join(manifest_sections)
  signature = '\n'.join(signature_sections)
 
  # Generate zigbert.rsa (detached zigbert.sf signature)
  handle = open(key_file, 'rb')
  key_data = handle.read()
  handle.close()
  certstack = M2Crypto.X509.X509_Stack()
  first = True
  certificates = re.finditer(r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', key_data, re.S)
  # Ignore first certificate, we will sign with this one. Rest of them needs to
  # be added to the stack manually however.
  certificates.next()
  for match in certificates:
    certstack.push(M2Crypto.X509.load_cert_string(match.group(0)))
 
  mime = M2Crypto.SMIME.SMIME()
  mime.load_key(key_file)
  mime.set_x509_stack(certstack)
  pkcs7 = mime.sign(M2Crypto.BIO.MemoryBuffer(signature),
                    M2Crypto.SMIME.PKCS7_DETACHED | M2Crypto.SMIME.PKCS7_BINARY)
  pkcs7_buffer = M2Crypto.BIO.MemoryBuffer()
  pkcs7.write_der(pkcs7_buffer)
 
  # Write everything into a ZIP file, with zigbert.rsa as first file
  zip = zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED)
  zip.writestr('META-INF/zigbert.rsa', pkcs7_buffer.read())
  zip.writestr('META-INF/zigbert.sf', signature)
  zip.writestr('META-INF/manifest.mf', manifest)
  for filepath, relpath, data in filelist:
    zip.writestr(relpath, data)
 
if __name__ == '__main__':
  if len(sys.argv) < 4:
    print 'Usage: %s source_dir key_file output_file' % sys.argv[0]
    sys.exit(2)
  signDir(sys.argv[1], sys.argv[2], sys.argv[3])

Feel free to use this script or to adapt it for your own build system. Right now it will only pack up a directory and generate a signed XPI file. It uses a “key file” containing your private key followed by your signing certificate as well as any intermediate certificates (all in PEM format). You don’t need the CA root certificate (the browser should know that one already) but the order of intermediate certificates might be important — if you always put a certificate after the one that was signed with it you should be on the safe side.

If you already have your signing certificate in PKCS#12 format you can easily convert it with OpenSSL using this command:

openssl pkcs12 -in certificate.p12 -out certificate.pem

You will need to edit this file to remove the CA root certificate and put the remaining certificates in the correct order. Note also that OpenSSL only exports the private key encrypted so you will have to enter a password. If you later want to use this file in an automated build script where nobody can type in the password you can decrypt a private key with this command:

openssl rsa -in private_encrypted.pem -out private_decrypted.pem

Tags:

Comment [1]

  1. Nils Maier · 2011-05-15 06:18 · #

    Based on your code, I hacked together a module allowing to sign already packed xpis, either from a file or file-like object.
    I released the code to the public domain.

    Having this on github might also make it more discoverable?!

    https://github.com/nmaier/xpisign.py

Commenting is closed for this article.