How to make a Passbook pass in Python

Since iOS 6 arrived this last week, I decided to try making my own Passbook passes using Python.  The example code from Apple is in Ruby, so I figured a port to Python was the thing to do. It’s not really that difficult to do, but there are some details that are kind of fiddly, and you need to use a third-party library to do the cryptographic signing.

You can find the documentation for Passbook in the Passbook Programming Guide at Apple’s developer site.  This guide is available without being a registered developer, although to get the certificates necessary to actually create working passes you do need to be able to use the Provisioning Portal, which means you need to have paid your $99 to become registered.  You’ll need to be an iOS developer, not a Mac developer.  (Is that obvious?  I just wanted to keep someone from paying the $99 for the wrong program.  You can be both, but it’s another $99.)

One of the big pieces I won’t cover here is setting up support for interacting with the passes after you’ve sent them out (updating, etc.) The passes don’t need to have a server to update them–they can be standalone–but if you want to do things like a store card that updates its balance, or a one-time event ticket, you will need to go the extra mile.  I’m working on that next.

The details

OK, so basically, a Passbook pass is a zipfile containing a JSON file, image files, a manifest (a JSON dictionary of SHA1 hashes of all the other files), and a DER-encoded PKCS7 signature of that manifest, signed using the certificate generated by Apple at your request on the Provisioning Portal.  That last bit is the part that makes this whole exercise more than trivial, and what makes Apple the gatekeeper for making the passes.  The whole package is given a file extension of ‘.pkpass’, and served by web servers as the mimetype application/vnd-com.apple.pkpass.

Python's support for this kind of thing seems to be a bit sketchy; I looked around for a while until I found a library that supports the right operations to do the signing the way Apple wants.  This library is called M2Crypto, and it's available as a package in Ubuntu 12.04 (python-m2crypto) or on pypi as m2crypto.

The m2crypto library (hereafter referred to as 'm2') is kind of a pain to install from source, so I suggest you get it prebuilt if you can.  If you do want to do it the hard way, especially on a Mac, you'll really need to use something like Homebrew to get all the pieces in place.  On Ubuntu (if you decide for some mad reason not to use the prebuilt package and use the pypi version instead) you'll need the following packages as build dependencies:  python-dev, libopenssl-dev, swig, and build-essential and python (of course).  I know that this works in Python 2.7; I have not tried it in 3.x.  You are invited to experience your own brand of pain in trying that out.  I am ultimately aiming to use this with Django, which currently doesn't support 3.x, so I'm not fretting about it.

M2 is made up of a few different modules.  The ones that will come into play in this application are the X509, SMIME, and BIO modules.  They handle the certificates and keys, crypto signing, and memory buffering tasks, respectively.

I'm not going to cover all the details of creating the pass.json file (which is the real heart of the pass--you can find all kinds of good info about that in the Apple guide linked above.)  I'm just going to cover the final packaging steps.

Massaging the keys and certificates

Once you have your certificate file from the portal (having followed Apple's instructions), you need to import it back into Keychain Access (by drag and drop or the import menu item).  Then you need to export it back out with its private key, by right-clicking on the private key entry and choosing Export in the popup menu.  You need to save this as p12 format (a PKCS12 container).  It may ask you to enter a password to encrypt the private key.  Keep it short, because you'll be typing it again in a minute to remove it.  I'll assume you saved this package to 'passbook.p12'.

Now we need to use the openssl command-line utility to extract the private key and the certificate into their own files, in PEM (text) format:

$ openssl x509 -in passbook.p12 -out passkey.pem -nocerts

Type that password you just entered in Keychain Access when openssl asks for the 'import password'.  Then you need to type another one (or the same one) because openssl wants to do its own encrypting to protect the key in the new PEM format.  Again, keep this password short, because the next step is to remove it.

$ openssl rsa -in passkey.pem -out passkeyfree.pem

Type that PEM password and now you'll have the private key in an unencrypted PEM format.  I probably don't have to tell you this, but keep this safe.  What that means is up to you.  It is only the key to your Passbook passes (not your whole developer account) but if you don't want someone sending out passes in your name, keep it out of their hands.

$ openssl x509 -in passbook.p12 -out passcert.pem -clcerts -nokeys

Now we need to stuff the PEM-formatted key and certificate back into one file, so that m2 can load them together in one step.

$ cat passkeyfree.pem passcert.pem >passpack.pem

This 'passpack.pem' file should be kept safe too, since it contains the unencrypted key.

You also need the Apple Worldwide Developer Relations CA certificate, since it's the cert that signed your cert, and needs to be included as a secondary certificate in the pass signature.  Import this into Keychain Access (if you don't have it already--the developer signup process very likely has added this already.)  Now export it out again as PEM format, as 'wwdr.pem'.

OK, now we're ready to actually do the deed, so to speak.  In the following Python code, set the KEYPATH variable (line 2) to wherever you have your key and cert files, and the KEY_FILE and WWDR_FILE variables to the names of the respective files as we made above.  (I did it this way so that you could run this on any platform without worrying about pathname separators.)  You will want to run this Python script from inside the pass directory.  That is, you need to be in the directory where all the files for your new pass are: the pass.json and all the image files.  Make sure that's all that's in the directory, because everything in here will get packaged up in your pass.  The code will generate the manifest and sign it with the key and certs you provided.  It will then package up everything into a zipfile and deposit it in the directory one level up; that is, as a sibling of the directory holding the path files.  If you want, you can pass your desired filename as an argument on the command line and the script will use that, tacking on the obligatory '.pkpass' extension.  This is just a short example script, so I imagine I'll be expanding this to use the optparse module and all those kinds of goodies later, as well as creating object classes to manage the actual creation of the pass.json file.  But this is enough to get you past the difficult bit of signing the pass.

Once you have the .pkpass file, you can email it to yourself and it should show up with the right look in iOS Mail, and when tapped on, should show up properly and be able to be added to your Passbook.  Enjoy!  Here's the code:

from glob import glob
import os
import M2Crypto as m2
import json
from hashlib import sha1
from zipfile import ZipFile, ZIP_DEFLATED
import sys

KEYPATH = ".."
KEY_FILE = "passpack.pem"
WWDR_FILE = "wwdr.pem"

if os.path.exists('signature'):
    os.unlink('signature')

if os.path.exists('manifest.json'):
    os.unlink('manifest.json')

flist = glob('*')

mdict = {}

for x in flist:
    mdict[x] = sha1(open(x,'rb').read()).hexdigest()

mjson = json.dumps(mdict, indent=1)
open('manifest.json','wb').write(mjson)

m = m2.SMIME.SMIME()
cs = m2.X509.X509_Stack()
wwdr = open(os.path.join(KEYPATH, WWDR_FILE),'rb').read()
cs.push(m2.X509.load_cert_string(wwdr))

m.load_key(os.path.join(KEYPATH, KEY_FILE))
m.set_x509_stack(cs)

manifest_data = open('manifest.json','rb').read()
mb = m2.BIO.MemoryBuffer(manifest_data)

pkcs7 = m.sign(mb, m2.SMIME.PKCS7_DETACHED | m2.SMIME.PKCS7_BINARY)

mb = m2.BIO.MemoryBuffer()
pkcs7.write_der(mb)
w = open('signature','wb')
w.write(mb.read_all())
w.close()

try:
   fname = sys.argv[1]
except:
   fname = 'test'
a = ZipFile("../{0}.pkpass".format(fname),'w', compression=ZIP_DEFLATED)
nlist = glob('*')
for x in nlist:
    a.write(x)
a.close()
About these ads