最近、iPhoneのiOS6でPassbookという機能が出ました。Passbookはイベントのチケットや、飛行機や船の搭乗券や、クーポンや、ポイントカードを管理出来る地味に便利なアプリ。

僕は興味があって、Pythonでどう作るかを調べてみたので、ここで共有しようと思っている。Passbookはパスの更新の仕組みもありますが、とりあえず、パスを作るとところまで説明しようと。 まずは、Appleの日本語ドキュメントの「Passbook プログラミングガイド」をざっと見たほうがいいかもしれない => https://developer.apple.com/jp/devcenter/ios/library/japanese.html

基礎の仕組み的に、Passbookはサーバーからダウンロードしたzipファイル。パスの内容はpass.jsonというJSONファイルの中に入っている。中身のファイル毎にsha1ハッシュを取って、manifest.jsonというファイルに書いている。そして、manifest.jsonの中身の署名を作成して、signatureというファイルに入れます。

まずは準備

この準備は一番面倒くさい部分なんだけど、結構はまりそうなので、丁寧に説明する。

署名を作るために、Appleのルート証明書が必要。「Passbook プログラミングガイド」の「パスタイプIDを要求する」というところに書いていますが、情報が少ないので、これでやり方が絶対わからないから、わかりやすく説明する。 まずは後で使うキーペア(公開鍵、秘密鍵のペア)を作ります。MacのKeychain Accessでキーペアを作成する。

キーチェーンアクセスメニューから、「証明書アシスタント」の「証明局に証明書を要求...」を選ぶ。情報を入力したら、「ディスクに保存」を選んで、作成する。それで、CSRを保存する。

Pass Type ID を要求する。まずは、 iOS Dev Center にログインする => https://developer.apple.com/devcenter/ios/index.action

そして、右側の「iOS Provisioning Portal」に移動して、左側の「Pass Type IDs」をクリックする。

Pass Type IDs画面で、「New Pass Type ID」ボタンをクリックしてください。

これで適当なDescription と Identifierを入力してください。 Identifier は pass.<ドメイン名>.<パス名> という風に設定するのがおすすめ。「Submit」を押したら、ファイルアップロードの画面が出ます。ここに自分が作った公開鍵をアップします。

アップしたら、Apple側でサインした証明書をダウンロードします。このファイルを保存して、ダブルクリックすることで、キーチェーンアシスタントにインポートします。

次は、Apple のルート証明書をこのURLからダウンロードして、キーチェーンアシスタントにインポートします。 => http://developer.apple.com/certificationauthority/AppleWWDRCA.cer

その手順が終わったら、Keychain Access から鍵を.p12ファイルとして、エクスポートする(以降、cert.p12というファイル名とする)。エクスポートするときに、以前に作った秘密鍵ではなく、「Pass Type ID: ほげほげ」という証明書を選択して、右クリックして、「ほげほげを書き出す」というオプションを選びます。ここにパスワードを指定出来ます。パスワードを後で使いますので、覚えておいてください。

次に、「Apple Developer Relations Certification Authority」の証明書を pem ファイルとして書きだす。(これ以降、AppleWWDRCA.pem のファイル名とする)

書きだした p12 ファイルに対して、下記のコマンドを実行して、証明書(certificate.pem)と公開鍵(key.pem)を書き出す。ここに p12 ファイルのパスワードを使います。pem ファイルのパスワードを指定できます。pem ファイルのパスワードは後で使うので、覚えておいてください。

$ openssl pkcs12 -in cert.p12 -clcerts -nokeys -out certificate.pem
...
$ openssl pkcs12 -in cert.p12 -nocerts -out key.pem
...

この3つのファイル AppleWWDRCA.pem、certificate.pem、key.pem を後で使います。

ライブラリー

Passbook の signature ファイルを作成するために、M2Crypto というライブラリが必要です。virtualenv を作って、インストールします。

$ mkvirtualenv passbook-test
...
(passbook-test)
$ pip install M2Crypto
....

やっと、準備完了。ハァハァ

漸くコーディングできる

まずは、pass.json ファイルのデータを作成する。

passinfo = json.dumps({
     'description': 'Acme Airlines',
     'formatVersion': 1,
     'organizationName': 'Acme Airlines',
     'passTypeIdentifier': 'pass.example.com.examplepass',
     'serialNumber': "123", # パスのユニークなID
     'teamIdentifier': "ABCDE12345", # Apple のチームID
     'backgroundColor': 'rgb(255,255,255)',
     'logoText': 'Acme Airlines',
     'locations': [],
     'barcode': {
         'format': 'PKBarcodeFormatQR',
         'message': "http://example.com/",
         'messageEncoding': 'iso-8859-1',
     },
     'boardingPass': {
         'transitType': 'PKTransitTypeAir',
         "primaryFields": [
             {
                 "key" : "origin",
                 "label" : "Tokyo",
                 "value" : "NRT"
             },
             {
                 "key" : "destination",
                 "label" : "New York",
                 "value" : "NYC"
             }
         ],
     },
 })

次に、画像データを読み込む。僕は PHP-PKPass の example の画像を使いました。

filepaths = [
    ('logo.png', os.path.join('img', 'logo.png')),
    ('icon.png', os.path.join('img', 'icon.png')),
    ('icon@2x.png', os.path.join('img', 'icon@2x.png')),
]

fileinfo = []
for name, path in filepaths:
    with open(path, "rb") as fd:
        fileinfo.append(name, fd.read())

次に、manifest.json を作成します。

manifest = {
    'pass.json': hashlib.sha1(passinfo).hexdigest(),
}
for filename, filedata in fileinfo:
    manifest[filename] = hashlib.sha1(filedata).hexdigest()

manifest = json.dumps(manifest)

次に、signature ファイルを作成する。ここに、 AppleWWDRCA.pem、key.pem、certificate.pemのパスを指定します。そして、証明書のパスワードをここに指定します。

smime = SMIME.SMIME()
#we need to attach wwdr cert as X509
wwdrcert = X509.load_cert('AppleWWDRCA.pem')
stack = X509_Stack()
stack.push(wwdrcert)
smime.set_x509_stack(stack)

# 公開鍵、証明書、パスワードを使います。
smime.load_key('key.pem', 'certificate.pem', callback=lambda p: 'password')
pk7 = smime.sign(SMIME.BIO.MemoryBuffer(manifest), flags=SMIME.PKCS7_DETACHED | SMIME.PKCS7_BINARY)

der = SMIME.BIO.MemoryBuffer()
pk7.write_der(der)

signature = der.getvalue()

漸く最後に、zip ファイルを作成します。

zipfileobj = StringIO()
zf = zipfile.ZipFile(zipfileobj, 'w')
zf.writestr('signature', signature)
zf.writestr('manifest.json', manifest)
zf.writestr('pass.json', passinfo)
for filename, filedata in fileinfo:
    zf.writestr(filename, filedata)
zf.close()

zipfiledata = zipfileobj.getvalue()

iPhone のブラウザに渡すときに、 'application/vnd.apple.pkpass' というコンテントタイプを指定しないといけない。僕は Django をよく使うので、この例を Django で書きますが、どのフレームワークでも、出来るはずです。

response = HttpResponse(
    content=zipfiledata,
    content_type='application/vnd.apple.pkpass',
)
response['Pragma'] = 'no-cache'
response['Content-Disposition'] = 'attachment; filename=pass.pkpass'

これで、zipファイルがダウンロードできて、iPhone で見れるはず。

https://storage.googleapis.com/static.ianlewis.org/prod/img/681/passbook_big.png