Build A Fully Functional, Robust and Secure Passbook Server with (Almost) Zero Cost

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.

1 thought on “Build A Fully Functional, Robust and Secure Passbook Server with (Almost) Zero Cost

  1. Pingback: The service of People’s Card will be stopped in one month | WHEN IT'S DONE

Leave a comment