Buffalo Rabor Buffalo Rabor - 5 months ago 113
iOS Question

iOS Push Notifications (APNs) over GAE, SSL Handshake Failure

I am attempting to show proof of concept for iOS Push Notifications from a Google AppEngine application instance using this RPC handler...

PAYLOAD = {'aps': {'alert':'Push!','sound':'default'}}
TOKEN = '[...]'


class APNsTest(BaseRPCHandler):

def get(self, context, name):
self._call_method(context, name)

def send_push(self):

# certificate files
filename = 'VisitorGuidePush'
abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../archive/certificate'))
ca_certs = os.path.abspath(os.path.join(abs_path, '%s.ca'%filename))
certfile = os.path.abspath(os.path.join(abs_path, '%s.crt'%filename))
keyfile = os.path.abspath(os.path.join(abs_path, '%s.key'%filename))

# serialize payload
payload = json.dumps(PAYLOAD)

# APNS server address...
# apns_address = ('api.development.push.apple.com', 443) # Development server
# apns_address = ('api.development.push.apple.com', 2197) # Development server
# apns_address = ('api.push.apple.com', 443) # Production server
apns_address = ('api.push.apple.com', 2197) # Production server

# a socket to connect to APNS over SSL
_sock = socket.socket()
_ssl = ssl.wrap_socket(_sock, keyfile=keyfile,
certfile=certfile,
server_side=False,
cert_reqs=ssl.CERT_REQUIRED,
ssl_version=ssl.PROTOCOL_TLSv1,
ca_certs=ca_certs)
_ssl.connect(apns_address)

# Generate a notification packet
token = binascii.unhexlify(TOKEN)
fmt = '!cH32sH{0:d}s'.format(len(payload))
cmd = '\x00'
message = struct.pack(fmt, cmd, len(token), token, len(payload), payload)

_ssl.write(message)
_ssl.close()

return self.response_result(PAYLOAD)


And need help resolving this error when executing "_ssl.connect(apns_address)"

SSLError: [Errno 1] _ssl.c:507: error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure


My PEM file (derived from a .p12) and device token were generated a week ago by a mobile developer on our team, suggestions for validating these would be helpful. For now I believe there are current and valid.

While the TLSv1 protocol is being specified, I've notice the handshake failure identifies sslv3.

I have attempted many variations and combination of wrap_socket and apns_address, and am consistently stopped by the handshake failure. Which leads me to suspect a problem with the way I am applying the pem certificate.

The primary references I have been using for wrap_socket are Using OpenSSL and TLS/SSL wrapper for socket objects, not to mention more than a few StackOverflow posts.


Please provide advice concerning the appropriate keyfile, certfile, and ca_certs values and any other advice or resources available for GAE based APNs communication. Thanks ~


Updated question...



The original .p12 has been validated using Pusher, and divided via openssl...

openssl pkcs12 -in vgp.p12 -out VisitorGuidePush.key -nodes -nocerts
openssl pkcs12 -in vgp.p12 -out VisitorGuidePush.crt -nodes -nokeys
openssl pkcs12 -in vgp.p12 -out VisitorGuidePush.ca -nodes -cacerts


I'm receiving a new error which appears related to the ca_certs...

SSLError: [Errno 0] _ssl.c:343: error:00000000:lib(0):func(0):reason(0)


Removing the ca_certs requirement or passing in other files like the .p12 or the .crt result in a return to the original handshake failure.

Answer

The appropriate support files start with Creating a Universal Push Notification Client SSL Certificate as a p12 file.

Next, utilizing command line openssl to parse the p12 into the desired certificate and key files...

openssl pkcs12 -in VisitorGuide.p12 -out VisitorGuide.key -nodes -nocerts
openssl pkcs12 -in VisitorGuide.p12 -out VisitorGuide.crt -nodes -nokeys
openssl pkcs12 -in VisitorGuide.p12 -out VisitorGuide.pem -nodes

And finally to obtain a qualified Certificate Authority file (from Troubleshooting Pust Notifications)

In addition to the SSL identity (certificate and associated private key) created by Member Center, you should also install the Entrust CA (2048) root certificate on your provider.

Entrust.net Certificate Authority (2048) download ~ entrust_2048_ca.cer

Note that every GAE instance hosts its own Certificate Authority at /etc/ca-certificates.crt as described here, Using OpenSSL.


With these files added to your project you can make one of two, equally valid, ssl socket objects...

_ssl = ssl.wrap_socket(_sock, keyfile=VisitorGuide.key,
                              certfile=VisitorGuide.crt,
                              server_side=False,
                              cert_reqs=ssl.CERT_REQUIRED,
                              ssl_version=ssl.PROTOCOL_TLSv1,
                              ca_certs=entrust_2048_ca.cer)

...or...

_ssl = ssl.wrap_socket(_sock, certfile=VisitorGuide.pem,
                              server_side=False,
                              cert_reqs=ssl.CERT_REQUIRED,
                              ssl_version=ssl.PROTOCOL_TLSv1,
                              ca_certs=entrust_2048_ca.cer)

TLS/SSL wrapper for socket objects 17.3.4.3. Combined key and certificate explains why both are valid parameter options.


Before I provide the final code block, I have to point out something concerning the APNs address (this proved to be the key point, allowing me to resolve the handshake failure and obtain an SSL connection between GAE and APNs)

According to the iOS Developer Library APNs Provider API

The first step in sending a remote notification is to establish a connection with the appropriate APNs server:

Development server: api.development.push.apple.com:443

Production server: api.push.apple.com:443

Note: You can alternatively use port 2197 when communicating with APNs. You might do this, for example, to allow APNs traffic through your firewall but to block other HTTPS traffic.

But it wasn't until I dug into the Pusher source that I discovered the APNs addresses to which I could connect...

gateway.sandbox.push.apple.com:2195

gateway.push.apple.com:2195


Without further ado...

class APNsTest(BaseRPCHandler):

  def get(self, context, name):
    self._call_method(context, name)

  def send_push(self):

    # certificate files
    abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../cert'))
    pem_file = os.path.abspath(os.path.join(abs_path, 'VisitorGuide.pem'))
    ca_certs = '/etc/ca-certificates.crt'

    # APNS server address...
    apns_address = ('gateway.sandbox.push.apple.com', 2195)
    # apns_address = ('gateway.push.apple.com', 2195)

    # a socket to connect to APNS over SSL
    _sock = socket.socket()
    _ssl = ssl.wrap_socket(_sock, certfile=pem_file,
                                  server_side=False,
                                  cert_reqs=ssl.CERT_REQUIRED,
                                  ssl_version=ssl.PROTOCOL_TLSv1,
                                  ca_certs=ca_certs)
    _ssl.connect(apns_address)

    # a notification packet
    payload = json.dumps(PAYLOAD)
    token = binascii.unhexlify(TOKEN)
    fmt = '!cH32sH{0:d}s'.format(len(payload))
    cmd = '\x00'
    message = struct.pack(fmt, cmd, len(token), token, len(payload), payload)

    _ssl.write(message)
    _ssl.close()

    return self.response_result(PAYLOAD)

...executes without error.