Briefing
This post discussed the implementation of a fully workable Passbook server. Instead of a step by step tutorial, it mentioned some technical decisions and considerations in a realistic Passbook server implementation.
This backend implementation was used to support the People’s Card app, which could be used for individuals and small businesses to create their their own passes, and distribute them through Passbook system, in an easy and straight-forward style. Although Apple pulled the app off the App Store for “mystery” reasons at early December, the server kept online for three continuous months, with peak amount of ~9000 sessions, and ~35000 passes generated in a single day.
Note: The majority of the article was written on early October, 2012. Some situation might changed after that time.
References and Resources
Check https://developer.apple.com/passbook/ for the most recent documentations. Besides the references and guides, these two WWDC 2012 presentation are extremely valuable.
Don’t miss the “Passbook Support Materials” package. It includes sample passes, and example code to create a simplest Passbook server.
Selection of Techniques and Web Services
The following techniques and web services were chosen in building the Passbook server:
Sinatra
So far, Sinatra is the simplest solution to build REST style web services, and could be used to easily implement the web service interface that a Passbook server is required.
Beyond that, there is also some important Ruby-based components for creating a fully functional Passbook server. They could be easily integrated with Sinatra to provide the support for APNS (Apple Push Notification Service), JSON, accessing to Parse.com service, etc.
Heroku
It’s possible to deploy the Passbook server to a standalone cloud server, like Amazon’s EC2. But Heroku has its advantage. Firstly, it provides an agile deployment of server apps. The number of running instances could be adjusted quickly according to the server workload. Secondly, the deployment process is based on GIT and works smoothly, especially under OS X console. And the most important is, the app running on Heroku automatically got the SSL support, which could be time consuming when deploying to a custom Linux server by unexperienced people (like me).
To use the Heroku server is technically free for light usage. In this implementation, only the web dyno is needed. In the beginning, we only need to allocate 1 web dyno and 0 worker dyno, which is $0 cost.
The shortcoming of Heroku service is the database cost. Even a basic production database could cost $50 per month, which is quite expensive.
(Update: I just saw there is a starter database of 10M rows with $9 per month, it seems a lot more reasonable for kick-starting)
Amazon S3
Since all the passes generated by the server are binary data. Depending on the size of images that used in the pass, the size of each pass could be 100KB – 1MB. Until now, Amazon’s S3 service is one of the cheapest solution to store this kind of binary datas. It could also provide the correct MIME type that Passbook requires.
Parse.com
The object-based database that parse.com provided is absolutely a flexible solution to support the Passbook web service. To create, maintain, and migrate database structure could be troublesome. That’s where object based database performs much better.
It’s basic plan is free, and have enough API request capacity to execute a small service.
Parse.com also provides Push functionality. However, to setup a customized push service with Sinatra is not difficult. And by the time I checked that, it was impossible to submit the certificate of Passbook service to Parse.com, since it did not recognize that as a valid certificate. I am not sure if the problem has been solved.
Implementation
The Gemfile
Here is the Gemfile to describe the components that would be used in the server
source :rubygems gem 'sinatra' gem 'thin' gem 'haml' gem 'parse_resource' gem 'json' gem 'rack-ssl' gem 'jtv-apns'
Especially, jtv-apns package provides the support of APNS service, and parse_resource is a ActiveRecord-alike framework to use the REST API of Parse.com.
Standard Web Service Interface of Passbook Server
The following API must be provided to communicate with Passbook app, and Apple’s server:
# Registering a Device to Receive Push Notifications for a Pass POST: webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber # Getting the Serial Numbers for Passes Associated with a Device GET: webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier?passesUpdatedSince=tag # Getting the Latest Version of a Pass GET: webServiceURL/version/passes/passTypeIdentifier/serialNumber # Unregistering a Device DELETE: webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber # Logging Errors POST: webServiceURL/version/log
Check Passbook Web Service Reference for the detailed description.
Here is the data declaration, which is very close to the data structure that WWDC presentation described:
class PushToken < ParseResource::Base fields :deviceID, :pushToken validates_presence_of :deviceID, :pushToken end class Registration < ParseResource::Base fields :deviceID, :passTypeID, :serialNumber validates_presence_of :deviceID, :passTypeID, :serialNumber end class PassCard < ParseResource::Base fields :passTypeIdentifier, :serialNumber, :binaryURL, :lastUpdated validates_presence_of :passTypeIdentifier, :serialNumber, :lastUpdated end
And the basic implementation of these API could be found in the server code of Passbook Support Materials. They need to be modified to use the services from Parse.com and Amazon S3. Here is an example:
# Example codes get '/pass-updates/v1/passes/:passTypeIdentifier/:serialNumber' do passTypeIdentifier = params[:passTypeIdentifier] serialNumber = params[:serialNumber] # the authorization code, will be explained later cards = PassCard.where(:passTypeIdentifier => passTypeIdentifier, :serialNumber => serialNumber).all if (cards.count == 0) puts("[Error] No card found") return 400 else if (cards.count > 1) puts("[Error] More than one card returned") end card = cards.first redirect card.binaryURL # the binary URL is the URL for the pass data which stored on S3 service end end
Security
Most of the APIs have a header field for authorization, it could be parsed and tested with the following code:
authToken = env['HTTP_AUTHORIZATION'] puts("HTTP_AUTHORIZATION : #{authToken}") authToken.gsub! "ApplePass ", "" authToCompare = computeAuthToken(passTypeIdentifier, serialNumber) if (authToCompare != authToken) puts("[Error] authToken not match") return [400, "Invalid authToken"] end
The code verify the authorization token in request header to check if that’s a “generic” pass which generated by the server. The server provides a specific API to compute a salted hash from passTypeID and serialNumber as the authorization token. The API will be called in the pass generation process.
def computeAuthToken(passTypeID, serialNumber) salted = $constPrefixString + passTypeID + $constSurfixString + serialNumber; Digest::SHA1.hexdigest salted end # customized API to support the server-side authentication post '/authentication/:passTypeIdentifier/:serialNumber' do if (params[:appKey] != $appKey_v1) puts '[Error] Invalid appKey' return 400 end result = computeAuthToken(params[:passTypeIdentifier], params[:serialNumber]) puts("result %s" % result) result end
Signing Passes
The signing process is important for Passbook system, and should also be made to be part of the service for the maximum security.
$publicKey = OpenSSL::X509::Certificate.new File.read 'certificate.pem' $wwdrIntermediateKey = OpenSSL::X509::Certificate.new File.read 'wwdr_intermediate.pem' $privateKey_pem = File.read 'key.pem' $privateKey = OpenSSL::PKey::RSA.new($privateKey_pem, '') post '/signdata' do # customized security verification data = params['myfile'][:tempfile].read flag = OpenSSL::PKCS7::BINARY|OpenSSL::PKCS7::DETACHED tmp = OpenSSL::PKCS7.sign($publicKey, $privateKey, data, [$wwdrIntermediateKey], flag) response.headers['content_type'] = "application/octet-stream" signature = tmp.to_der attachment('signature') response.write(signature) end
All the pem files are put into the same folder with the server code, and push to heroku server together.
Deployment
The deployment to Heroku is quite straight-forward. After updating the source code, just commit the change locally, and then use the following command to push it to Heroku:
git push heroku master
Check this page for more information.
SSL
There is an option to turn on the HTTP access to the Passbook server on debug devices. However, the SSL must be enabled, since the retail device could only communicate with HTTPS protocol.
It seems the Passbook system won’t accept self-signed SSL certificate. If you are using customized server, you must get a certificate issued by a SSL certificate provider.
Heroku provide the generic SSL for the URL like https://myapp.heroku.com/. And only 2 lines needs to be added in Sinatra app to enable SSL
require 'rack/ssl' use Rack::SSL
Debugging
It’s important to intercept the log API to catch any possible errors. It will be called by Apple server:
post '/pass-updates/v1/log' do puts("[Error] Description: #{params[:description]}}") puts(params) 200 end
The above code simply dump the error message to console. User could use
heroku logs --tail
to monitor the error messages.
That’s just demo code. Real logging service should be used according to the implementation.
Pingback: The service of People’s Card will be stopped in one month | WHEN IT'S DONE