15

Recently(more info) I've spotted a strange message in the logs on my quite recent, fresh installation of OS X Mavericks 10.9.2

Apr 27 15:26:47 Ivans-MacBook-Pro.local apsd[194]: Unrecognized leaf certificate

It appears every 15 minutes or so. I've googled it and there are many other users that pasted their logs(many times not even related with that issue) on net that had that message. It seems it is appearing for at least half a year now.

apsd is a constantly running Apple Push Notification service daemon

and it connects to one of the following servers on random:

1-courier.push.apple.com
2-courier.push.apple.com
3-courier.push.apple.com
4-courier.push.apple.com
.
.
.
200-courier.push.apple.com

on port 5223 (custom, but SSL)

every so often(to check for updates I guess)

Trying: https://1-courier.push.apple.com:5223 in the browser however shows that there is an issue related to the server certificate. Namely "This certificate is not valid(host name mismatch)". Which I suppose is because Apple did not create a certificate including a wildcard in the common name. However using Wireshark I see that the communication between apsd and Apple's server continues. I though, that can't possibly be right. Is apsd ignoring the validity check?!

I then tried to perform a man in the middle attack using a self signed certificate. And this is what I got in the logs:

Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: CFNetwork SSLHandshake failed (-9807)
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Failed to evaluate trust: No error. (0), result=5; retrying with revocation checking optional
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: failed to evaluate trust: No error. (0), result=5; retrying with system roots
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Failed to evaluate trust: No error. (0), result=5
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Untrusted peer, closing connection immediately

It's also worth noting that I've spotted(when observing the actual traffic with Wireshark) that the server requires the client to send his certificate. And apsd sends:

Subject Name
Common Name XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (UUID)

Issuer Name
Country US
Organization Apple Inc.
Organization Unit Apple iPhone
Common Name Apple iPhone Device CA
.
.
.

It's strange that Mac OS X is sending what appears to be a public key presenting it self as iPhone? I do have Xcode installed, if it is somehow related to maybe having iPhone simulators installed. But I'm getting the mentioned log message all the times, regardless if Xcode is running or not. And evidently others are too, judging from the logs on the internet.

So what is going on?
How is apsd able to figure out my fake certificate and pass theirs? Is something wrong here?
Note: My MitM implementation is quite simple, with DNS spoofing I give apsd a response that X-courier.push.apple.com is 127.0.0.1. I then listen with a self signed certificate that mimics Apple's server certificate(country, organization, common name...). BUT I did not implement requiring the client certificate from apsd. So I don't yet know if that is somehow related to my failed MitM. Could it be?

Are you guys there on Mac OS X getting the same log message in your Console(It appears in 'All messages' just when you launch it, but is actually in system.log), type 'leaf' in the search box...

UPDATE:
I've implemented requesting the client for its certificate. It's the same(SSLHandshake failure/Failed to evaluate trust). So apsd is checking something, I'm just not too sure it's implemented in the right way. Hopefully someone with far more experience will look into this...

  • The serial number (or any other combination of unique identifier, perhaps thumprint?) of Apple's certificate could perhaps be hard-coded in the apsd application? – Steven Volckaert Apr 27 '14 at 16:01
  • If the developers of apsd did anticipate such behaviour, then I suppose there would be no need to throw that "Unrecognized leaf certificate" log message. So I guess something must be wrong... – Ivan Kovacevic Apr 27 '14 at 16:20
  • If you want to read about Apples security architecture that document is said to be really good: images.apple.com/ipad/business/docs/iOS_Security_Feb14.pdf Judging the public feedback their security architecture is pretty solid. – binaryanomaly Apr 27 '14 at 22:08
  • I blog post I read few months ago you could be interested in : blog.quarkslab.com/imessage-privacy.html – Steeve May 1 '14 at 10:55
  • I've read it. Summarization: Apple can read your messages and view your password. So can your company(by performing a MitM attack) if you allow them to manage your phone beforehand with the "iPhone configuration utility". Anyone else, can't do anything, because they would have to install their certificate on your device, which (as is currently known) is impossible without the user's consent. – Ivan Kovacevic May 1 '14 at 13:14
6
+200

I don't have too much time now and haven't verified this thoroughly, so this is only what I remember from looking into this a few months ago.

With OS X 10.9 and iOS 7, Apple introduced some sort of certificate pinning for the push service, i.e. not allowing any CA the system knows to sign their push server certificate, but only a specific CA. This certificate pinning was a bit strange as it had some sort of expiration date of January 1st 2014, but I didn't look too much into what exactly that means and I don't know whether the latest versions of OS X and iOS still include this.

Unrecognized leaf certificate

So from my current understanding, this message doesn't mean apsd can't verify the certificate at all, but might mean the certificate pinning couldn't verify it (which would mean certificate pinning is currently optional, which in turn might have to do with the expiration date).

You might not have to worry about this directly, as the normal SSL certificate validation still works (as your test confirms). On the other hand I'd love to know why certificate pinning apparently doesn't work and what the expiration date means.

UPDATE: Here's some more information on why the log message is shown.

You can display the server certificate using openssl (same certificate seems to be served on ports 443 and 5223):

openssl s_client -prexit -connect 1-courier.push.apple.com:5223  2>/dev/null | openssl x509 -noout -text |grep -E '(Subject.*CN|Serial)'

As of 2014-05-10, this shows the following information:

Serial Number: 1277288244 (0x4c21df34)
Subject: C=US, ST=California, L=Cupertino, O=Apple Inc., CN=courier.push.apple.com

The apsd binary contains several X.509 certificates used for certificate pinning. I wrote a script to find certificates in a binary. Here's an excerpt when running it on apsd:

+ 469280 Found cert with CN "courier.sandbox.push.apple.com" and serial "1277027356"
+ 470416 Found cert with CN "courier.push.apple.com" and serial "1276925395"
+ 471584 Found cert with CN "Entrust.net Certification Authority (2048)" and serial "946059622"
[skipping some code signing certificates]

As you can see, the pinned certificate for courier.push.apple.com has a different serial number than the one served by Apple's push server. So both are different certificates and apparently this is the reason for the Unrecognized leaf certificate log message you see. As apsd still connects to the server, this means apsd does not require a pinned leaf certificate to match.

Another thing you can see is the third certificate, a CA certificate. Last year, when trying to make apsd connect to pushproxy, I had to replace the CA certificate to make apsd trust pushproxy. This means, while not checking the leaf certificate, apsd might only allow certificates from a certain root CA. I didn't verify this recently.

For checking this thoroughly, besides checking whether CA certificate pinning for the SSL connection is still effective, one would have to check another thing. Before connecting to the push server, apsd downloads a 'configuration bag'. This bag is downloaded via HTTP, but signed. The bag contains the hostname apsd then tries to connect to. This is a way I'm using for pushproxy to redirect apsd to another host. You can find more information about the bag in the pushproxy README and a script to generate such a bag.

  • 1
    Thanks for taking your time to look into this! I awarded the bounty immediately to you, because if anyone will get to the bottom of this I know it's you. And in the scenario you don't catch time after all(but I hope you will), I believe your project: github.com/meeee/pushproxy is a wealthy source of information regarding this subject! – Ivan Kovacevic May 4 '14 at 19:36
  • 1
    Thanks! I digged a bit deeper on why the log message is shown and added the information above. – meeee May 10 '14 at 11:21
4

Since no one responded yet, I thought that maybe I should put my method of how I tested this and then someone can maybe point out if that is OK or not.

I wrote a basic server in python listening on port 5223 like this:

#!/usr/bin/python
import SocketServer
import ssl

class requestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        print self.client_address[0] + ' connected.'
        receivedData = self.request.recv(4096)
        print receivedData

        #self.request.sendall('test')
        return

server = SocketServer.TCPServer(('0.0.0.0', 5223), requestHandler)
server.socket = ssl.wrap_socket(server.socket, server_side=True, keyfile='privateKey.pem', certfile='selfSignedCert.pem')
#server.socket = ssl.wrap_socket(server.socket, server_side=True, keyfile='privateKey.pem', certfile='selfSignedCert.pem', cert_reqs=ssl.CERT_REQUIRED, ca_certs='expectedClient.pem')
print 'Listening on port 5223'
server.serve_forever()

The commented server.socket line at the end is the version with asking the client(apsd) for certificate. I've created the certificates by calling openssl on the console:

openssl req -new -x509 -days 365 -nodes -out selfSignedCert.pem -keyout privateKey.pem

The expectedClient.pem I've extracted using Wireshark from the actual traffic where it is in the DER format. So I've converted it to PEM with this code:

#!/usr/bin/python
import ssl

inFileHandle = open('clientCert.der', 'rb')
outFileHandle = open('clientCert.pem', 'wb')
outFileContent = ssl.DER_cert_to_PEM_cert(inFileHandle.read())
outFileHandle.write(outFileContent)
inFileHandle.close()
outFileHandle.close()

Now I needed to trick apsd that xxx-courier.push.apple.com is actually 127.0.0.1. I wrote a basic DNS bouncer(proxy) that looks for a query with such address and makes a response that the IP is 127.0.0.1.
Here is the code:

#!/usr/bin/python
import SocketServer
import socket

DNS_server = '8.8.8.8' # Your ISP's DNS server(8.8.8.8 is Google Public DNS)
DNS_formatted_address_match = '-courier' + chr(4) + 'push' + chr(5) + 'apple' + chr(3) + 'com' + chr(0)

class requestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        queryData = self.request[0]
        incomingSocket = self.request[1]

        transactionID = queryData[:2] # First two bytes
        DNS_formatted_query_address = queryData[12:].split(chr(0))[0] + chr(0) # First 12 bytes are DNS header

        if DNS_formatted_address_match in queryData:
            # DNS protocol explained: http://technet.microsoft.com/en-us/library/dd197470(v=ws.10).aspx
            forgedResponse = (transactionID + 
                            chr(129) + chr(128) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(0) + 
                            chr(0) + chr(0) + 
                            DNS_formatted_query_address + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(192) + chr(12) + chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(0) + chr(0) + chr(60) + chr(0) + chr(4) + 
                            chr(127) + chr(0) + chr(0) + chr(1))

            incomingSocket.sendto(forgedResponse, self.client_address)
        else:
            outgoingSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            outgoingSocket.sendto(queryData, (DNS_server, 53))
            responseData = outgoingSocket.recv(4096) # DNS response should not be more than 512 bytes so this should be more than enough
            incomingSocket.sendto(responseData, self.client_address)

        return

server = SocketServer.UDPServer(('0.0.0.0', 53), requestHandler)
print 'Listening on port 53'
server.serve_forever()

You need to start it with sudo, because otherwise it will not want to bind on port 53. And the last step was to change the DNS server(s) in my network preferences to 127.0.0.1 so that DNS lookup actually goes through the bouncer and then to the ISP.

That's it!

Now you can test it yourself if you want...

protected by Community May 5 '14 at 5:33

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.