The Enigma of the Safari Push Framework

August 19, 2018

Despite there being numerous tutorials and sources of documentation online, it’s still incredibly difficult to crack the code of Safari push notifications. The documentation by Apple itself leaves much to be desired, and if you’re not familiar with existing API’s, such as that used to send all Apple push notifications, it can be impossible to crack the code. Over three days of blood, sweat, and tears, I managed to figure it out. Check out the code I wrote for this project here.

Push ID and Certification

The first thing you’ll have to do on your journey to setup Safari push notifications is obtain a web push ID for your site. You can do this through the Website Push ID page on the developer site. The description I provided for my website was noahbkim and the push ID I used was web.com.noahbkim. This information is used to identify your web service when you want to send push notifications to anyone subscribed.

The next step is to generate a certificate associated with the web push ID you created. To do this, go to the All Certificates page and create a website push ID certificate. Select the web push ID you created earlier and it will generate the certificate.

Unfortunately, you can’t just download the certificate, as it could potentially be intercepted. Instead, you have to create a certificate signing request, which you have to create from the Keychain Access application on your computer. Save that .certSigningRequest document to your file system and then upload it to the certificate registration page, and you’ll be able to download your web push certificate as a .cer file. Open that in Keychain Access and export it as a PKCS12 or .p12 document.

The Push Package

The push package is what is sent to a potential subscriber’s browser when the subscription process is initiated. It provides icons, information about the websites and the notifications it will send, as well as verification that it’s actually coming from the website’s developer. The actual package is a zip file containing the following items:

package.zip/
  icon.iconset/
    icon_16x16.png
    icon_16x16@2x.png  
    icon_32x32.png
    icon_32x32@2x.png
    icon_128x128.png  
    icon_128x128@2x.png
  website.json
  manifest.json
  signature  

Assembling the Package

While you can assemble a static version of this package manually, I would absolutely not recommend it, as it’s a lot of work. In addition, depending on how complex your website is, you may want to actually track the subscribers to your site separately, as you can send individual notifications through Safari’s push API (as opposed to just batch messages). I ended up writing a Python script to automate this process, and you can take a look at it here. To use this script, you will need two things:

  1. A set of icons sized according to the manifest above. This can be generated from a single icon using the icons.py script.
  2. Your .p12 certificate you generated from the previous steps.

This information should be entered in the pushover.yaml file that will be generated the first time the script is run. In addition to the location of these files, you will also have to fill out details provided in the website.json metadata:

  • The websiteName is intuitive, and the websitePushID should be what you entered into the form in the first section.
  • The allowedDomains option indicates what domains may request permission to send notifications to the user, and as this configuration is YAML, it should be specified in the form of a bulleted list.
  • The urlFormatString specifies a constraint on the format of all URL’s your notifications point to.
  • And finally, the webServiceURL should point to where API requests are made for the push package and registration.

This is reasonably documented by Apple here.

Generating the Manifest

Once the icons and website.json files are in place (this is done automatically by the script), you have to create the manifest.json file. This is essentially a JSON dictionary of the sha512 of all the icons and the website.json file. Note that you have to escape forward slashes in file paths in this file. I don’t know why. Don’t ask. It took me hours to figure this out the hard way.

Signing the Package

After that you have to do some SSL magic and sign the manifest file with your .p12 certificate. I also had absolutely no idea how to do this; while Apple provides a PHP script that does everything for you, it’s poorly documented and doesn’t translate well into other languages. You can take a look at my solution in the same file linked above. It involves abusing several private/not-fully-ported methods from the Python bindings for the OpenSSL package:

p12 = load_certificates(config)

# Create the memory buffer for the signature
with manifest.open("rb") as file:
    buffer_in = crypto._new_mem_buf(file.read())

# Grab the flags from source
PKCS7_DETACHED = 0x40
PKCS7_BINARY = 0x80

# Sign the actual file
pkcs7 = crypto._lib.PKCS7_sign(
    p12.get_certificate()._x509,
    p12.get_privatekey()._pkey,
    crypto._ffi.NULL,
    buffer_in,
    PKCS7_BINARY | PKCS7_DETACHED)

# Write out the result
buffer_out = crypto._new_mem_buf()
crypto._lib.i2d_PKCS7_bio(buffer_out, pkcs7)
der = crypto._bio_to_string(buffer_out)
with signature.open("wb") as file:
    file.write(der)

After managing this, all that’s left is zipping up the package and sending it off.

Using Pushover

Instead of having to deal with all of that (at least 8 hours of staring at examples, documentation, and error messages over the last two days), you can use my nifty pushover.py script. To create a static push package, just run pushover.py with a complete pushover.yml configuration and it will spit out a package.zip in the build folder. The reason I say static is because you might want to have a per-user notification management, but we’ll get to that later.

For now, here’s an example of the workflow. First, install the Python environment, generate your icons, and run pushover.py to generate the empty config file:

$ pipenv install
$ pipenv shell
$ python icons.py original.png files/icon.iconset
$ python pushover.py

Fill out the config file with the location of your certificates, icon set, and website metadata. My actual website configuration is in the pushover repository for reference. Then run pushover again for real.

$ python pushover.py

You’ll find your package.zip in the build/ directory.

Setting up the Pushover Server

To fully setup website push notifications, you’ll need more than just the push package. You also need to fulfill a RESTful web API from the webServiceURL you provided in the website metadata. This should respond to:

  • POST /v2/pushPackages/<push_id> with the package.zip.
  • POST /v1/devices/<device_token>/registrations/<push_id> with logic for user registration. Here you have to track the device ID of your new subscriber so you can send them notifications.
  • DELETE /v1/devices/<device_token>/registrations/<push_id> for unregistering an users who have unsubscribed.
  • POST /v1/log for error messages in JSON format.

I’ve implemented a minimal Flask server that does this in a script called server.py in the repository. Set it up with an SSL certificate and some sort of reverse proxy on your webServiceURL so that it can respond to HTTPS requests, and you’re good to go. I used push.noahbkim.com so I wouldn’t have to mess with my simple Jekyll Nginx configuration.

Sending Push Notifications

The final part of the equation is sending the actual push notifications. Apparently everyone should know how to do this, as Apple’s explanation is basically just “do it” with the following payload format. I also spent a significant amount of trying to decode this system, and came up with the notify script. There are actually several other programs that allow you to send push notifications, but if you want to know how it works, check out the code in the repository.

Closing Remarks

This project was a huge challenge to get through. I had to figure out a whole bunch of sometimes unintuitive and most of the time really complicated stuff, and there wasn’t a lot of clear documentation online. I also had to figure out how to manipulate SSL certificates, which is hopeless without years of expertise or Stackoverflow. Ultimately, I’m glad I did it though. And I hope it helps someone. Make sure to take a look through the code if you didn’t quite understand this writeup! It should be relatively easy to understand.