First post in a series called Under the Hood of Mobile Development. Each one takes a part of mobile development that almost everyone uses and almost nobody has looked at.
You plug in a cable. The phone buzzes. A dialog says Trust This Computer? You tap Trust, type your passcode, and Xcode shows the device.
Nobody reads that dialog. We tap it the way we accept cookie banners, which is unfortunate, because it's one of the few prompts on the phone that actually hands something over.
Quite a lot happens in those two seconds, and once you know what it is, a bunch of weird things start making sense: why CI machines keep asking to be trusted again, why a phone works on one Mac and not another, why "device not paired" is such a common error.
Here's what's going on.
Something was already running
Before you plugged anything in, your Mac was already running a daemon called usbmuxd. It starts at boot and stays up whether or not a phone is attached.
You can look at its launchd job:
plutil -p /Library/Apple/System/Library/LaunchDaemons/com.apple.usbmuxd.plist
Two things in there are worth noticing. First, the binary is not in /usr/libexec where you'd expect a system daemon. It's a resource file inside a private framework:
/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd
Second, it listens on a Unix socket at /var/run/usbmuxd, and the socket mode is 0777. Any process on your Mac can open it. Read that again if you enjoy being uncomfortable. It's not a bug though, it's the design: usbmuxd is the shared front door to every attached iPhone, and Xcode, Finder and Apple Configurator all go through it. The talking-to-the-phone part is open to everyone. The part that decides what you're allowed to do lives on the phone, which is the right place for it.
So does every third-party iOS tool you've ever used.
Windows gets the same daemon. Install iTunes or Apple Mobile Device Support and you get usbmuxd.exe running under C:\Program Files\Common Files\Apple\Mobile Device Support. Windows has no Unix sockets, so instead of a socket file it listens on TCP 127.0.0.1:27015. Same protocol, same messages, different front door. That's why cross-platform iOS tools carry one extra line of code for Windows and nothing else changes.
usbmuxd is a switchboard, not a driver
The name is short for USB multiplexer daemon. It does two jobs.
The first is noticing devices. Connect to the socket, send a Listen message, and usbmuxd streams events at you: Attached when a phone shows up, Detached when it goes away. Each event carries a small integer DeviceID plus a properties dict. One field in there is called SerialNumber, and it isn't the serial number, it's the UDID. That has been confusing people for about fifteen years now, and it's far too late to rename it. Somewhere out there is the original commit, and we should probably let it rest.
The second job is the interesting one. usbmuxd gives you TCP to the phone. You send a Connect message naming a device and a port, and after that your socket is a pipe to that port on the phone, as if the phone were a server on your network.
That's the whole trick. iOS doesn't expose a USB protocol you talk to directly. It exposes ports, and usbmuxd tunnels them over the cable.
Two details make this fun to actually implement.
One: after Connect succeeds, the socket stops speaking usbmux. It's now a dumb byte pipe. If you try to send another control message on it, clients raise something like "cannot issue control packets". Every extra service you want at the same time needs its own connection to usbmuxd. So port forwarding to an iPhone isn't a kernel tunnel, it's one Unix socket per TCP connection.
Two: the port number in the Connect message is byte-swapped. It goes on the wire in network byte order, so lockdown's port 62078 is written as 32498. Read a plist dump of that traffic and the port just looks wrong. Some clients don't even bother swapping at runtime, they just hardcode 32498 as a constant and add a test asserting it comes back out as 62078. You can feel the lost afternoon behind a test like that.
And the framing changes mid-conversation. usbmux messages use a 16-byte little-endian header. Once you're through to the phone, lockdown frames its messages with a 4-byte big-endian length. Same file descriptor, two endianness conventions, about ten milliseconds apart. If you have ever met someone who writes iOS device tooling and wondered why they seem tired, this is roughly the reason.
Port 62078: the doorman
The port everything connects to is 62078, and the thing listening is lockdownd. It's the entry-point daemon on the phone. It reports device values, manages pairing, and starts every other service.
Every message is an XML property list. The first request a client sends is QueryType, and a healthy phone answers com.apple.mobile.lockdown. If it answers com.apple.mobile.restored instead, the phone is in restore mode, which is a nice cheap way to tell.
Then you can ask for values with GetValue. Here's the part that surprises people: this works before you're trusted. An untrusted host can read the device name, model, iOS version, build number, UDID, activation state, Wi-Fi and Bluetooth MAC addresses, the IMEI, whether a passcode is set, and even NVRAM boot-args.
That's the pre-trust pool. It's smaller than what you get after trusting, but it's not nothing.
Two of those values are worth calling out: HostAttached and TrustedHostAttached. The phone tracks whether the thing on the end of the cable is merely present or actually trusted. You were sizing it up, and it was quietly sizing you up back.
The trust dialog is an error code
This is my favorite part, because it's so much less magical than it looks.
There is no push, no callback, no event. The host sends a Pair request. The phone puts the dialog on screen and answers with an error:
Error: PairingDialogResponsePending
And that's it. The host just sends the identical request again. pymobiledevice3 retries once a second and logs "waiting user pairing dialog...". Other clients don't even loop, they just print "accept the dialog and run this again" and quit.
So the most security-critical moment in the whole relationship is implemented as: ask, get told no, ask again, keep asking until the answer changes. Which is also how most of us learned to talk to our parents.
You only get that specific error if you asked for it, by the way. The pair request includes PairingOptions: {ExtendedPairingErrors: true}. Without that flag the phone gives you a generic failure and you're left guessing.
With it, the error strings map cleanly onto real-world situations, which is genuinely useful when you're debugging someone else's broken setup:
| Error | What's actually happening |
|---|---|
PasswordProtected | Phone is locked. Unlock it. |
PairingDialogResponsePending | Dialog is on screen right now, nobody has tapped. |
UserDeniedPairing | Someone tapped Don't Trust. |
InvalidHostID | You have a pair record, the phone doesn't recognize it. Stale or wiped. |
There's one more failure that isn't an error string at all. If the TLS handshake dies with a zero return right at session start, that usually means you have a pair record but the phone deleted its side. pymobiledevice3 reads that exact signal and throws the record away.
What tapping Trust actually does
Here's the thing that surprised me most when I first read this code.
Your Mac becomes a certificate authority. Not metaphorically. At pair time it generates a brand new self-signed root CA, then uses it to sign two more certificates: one for itself, and one for the phone, built around the phone's own public key which it fetched a moment earlier in plaintext.
There is no Apple PKI anywhere in this. Your laptop invents a tiny throwaway PKI per device and the phone agrees to remember it.
The details are almost comically casual. RSA-2048 keys. Empty subject and issuer names. Serial number 1. Valid for ten years. If you handed a certificate like this to a browser it would refuse to load the page and then tell your users you might be a criminal. It's less a certificate and more a handwritten note reading "he's with me".
The phone accepts it, because it isn't validating a chain. It's just remembering a key. The TLS that follows verifies nothing about the phone either: hostname checking off, certificate validation off. The whole security property is "you hold a key this phone remembers", and everything else is ceremony.
The result gets written into a pair record, which contains roughly this:
HostID your host's identifier
SystemBUID your machine's identifier
HostCertificate PEM
HostPrivateKey PEM
DeviceCertificate PEM
RootCertificate PEM
RootPrivateKey PEM
WiFiMACAddress the phone's Wi-Fi MAC
EscrowBag opaque blob from the phone
A couple of notes on that list.
The private keys are in there, in the clear. On macOS these records live in /var/db/lockdown/, one plist per device. Apple protects the folder now, and Elcomsoft points out they quietly deleted the KB article that used to explain how to change its permissions. But the contents are plain XML. Copy that file to another machine and that machine is trusted, with no dialog.
The escrow bag is the real prize. Apple's own Platform Security guide describes it plainly: the host is given a 256-bit key that unlocks an escrow keybag stored on the device. That's what lets your Mac read protected data from a locked-but-booted phone. It's why backup works when the screen is off.
So "Trust This Computer" is not a handshake. It's a key handover, and the key goes to a file on disk.
One more small thing that's easy to miss: your identity to the phone is thinner than you'd think. In pymobiledevice3 the HostID is a UUIDv3 derived from your computer's hostname. Rename your Mac from "MacBook Pro" to something with a personality and, as far as the phone is concerned, a stranger just walked in.
Why the dialog exists at all
It wasn't always there. Before iOS 7, plugging in meant the data flowed.
In August 2011 Brian Krebs coined "juice jacking" after a free charging kiosk at DEF CON 19, built by Brian Markus and colleagues for the Wall of Sheep. It charged your phone and displayed a warning that it could have done much worse. Over three and a half days, at least 360 people plugged in. Markus at the time: "Most smartphones are configured to just connect and dump off data."
Then in July 2013, three Georgia Tech researchers (Billy Lau, Yeongjin Jang, Chengyu Song) presented Mactans at Black Hat. It was a BeagleBoard, about $45 of open hardware, in a plug-shaped enclosure. Plug an unlocked iPhone 5 into it and it paired over USB, installed a provisioning profile, and side-loaded an app. No prompt, no warning. Their demo app made the phone place a call.
Apple's public comment was one sentence thanking the researchers for their valuable input, which is corporate for "well, that's our summer gone". iOS 7 shipped seven weeks later, on 18 September 2013, with the Trust This Computer prompt.
So every time you tap that dialog, you are personally acknowledging a $45 circuit board from 2013.
There's a great primary artifact from that moment. libimobiledevice issue #20 was opened on 10 September 2013, eight days before iOS 7 shipped, and you can watch open-source developers work the new behavior out from the outside: two new required fields in the pair record, the call order inverted, and a strange error that means "the user hasn't tapped yet, try again". That error is the same PairingDialogResponsePending that tools still poll on today.
The story didn't stop there:
- iOS 11 added the passcode requirement. Before that, an unlocked phone plus a tap was enough, which meant a thief holding your unlocked phone could pair it to their laptop.
- 2018: Roy Iarchy and Adi Sharabani presented trustjacking at RSA. Once a host is trusted it can turn on Wi-Fi sync, and from then on it reaches your phone over the network with no second approval and no notification. That's not a bug in pairing. That's pairing working as designed.
- iOS 11.4.1 added USB Restricted Mode: an hour after the last unlock, the port is charge-only. Elcomsoft defeated it the same week by plugging in any Lightning accessory, even an unpaired one, because that resets the timer. Apple's own $39 camera adapter works fine.
And there's still no way to untrust one computer. Reset Location & Privacy clears every trusted host at once, and takes your app location permissions with it.
After trust: everything is a service
Once you have a session, you ask lockdownd to start things by name:
Request: StartService
Service: com.apple.mobile.installation_proxy
The reply gives you a fresh ephemeral port and a flag saying whether that port wants TLS. You open a new connection through usbmuxd to that port, and now you're talking to the service directly. Lockdown's job is done.
The catalog is long and reads like a tour of everything a Mac can do to a phone: com.apple.afc for files, installation_proxy for installing apps, house_arrest for app containers, syslog_relay for logs, mobile_image_mounter for developer images, pcapd for packet capture, screenshotr for screenshots, debugserver for debugging.
The list isn't queryable. It's a plist compiled into the lockdownd binary, and people extract it by carving it out of the Mach-O with segedit. Mounting the Developer Disk Image adds more entries. That's why so many iOS tooling errors end with "have you mounted the developer image?": the service you asked for genuinely does not exist yet, and the error message is doing its best.
Two nice bits of texture here. Only one service in pymobiledevice3's tree bothers to send the escrow bag along, and it's backup, because backup is the thing that has to work while the phone is locked.
And there's a service called com.apple.mobile.heartbeat whose entire job is keeping your session alive. The phone sends Marco. You answer Polo. That is the actual protocol, in production, on a billion devices. Somebody wrote that, shipped it, and went home.
The same protocol, without the cable
Wi-Fi sync arrived with iOS 5 in 2011, and it's the reason lockdown listens on a real TCP port instead of only over USB.
When it's on, the phone advertises itself over Bonjour as _apple-mobdev2._tcp.local., and a host that already has a pair record connects to port 62078 over the network and runs the identical handshake. That's also why the pair record stores WiFiMACAddress: it's how a Bonjour advertisement gets matched back to the right saved credentials.
But you cannot pair over the network. The trust dialog is a USB-only event. Wi-Fi lockdown always rides on a relationship that was born over a cable.
Which also means every iPhone on your network has TCP 62078 open, permanently, with no user-facing firewall to close it. nmap labels it iphone-sync. The port is the front door. The pair record is the key.
How we know any of this
None of the above is documented by Apple beyond the security guide's high-level summary. Apple's page never even says the word "lockdownd".
We know it because people reverse engineered it, starting almost immediately. The iPhone Dev Team documented the usbmux wire format publicly in 2008, about a year after the original iPhone. libimobiledevice began in 2007 as "libiphone", with the stated goal of letting "penguins talk to fruits".
And in 2009 Hector Martin, who now runs Asahi Linux, wrote an open-source usbmuxd for Linux. The choice that mattered most was small: he put its socket at /var/run/usbmuxd, the same path Apple uses. Same path, same protocol, different implementation. That one decision is why a tool written against Apple's daemon also works on Linux, and why the cross-platform iOS ecosystem exists at all.
If you want to watch the real thing, you don't need any of those projects. Apple's own daemon will narrate. Write DebugLevel = 7 into /Library/Preferences/com.apple.usbmuxd.plist and you can see which process is dialing port 62078 on your phone, live, on a stock Mac.
What to take away
Three things stuck with me.
Pairing is a key handover, not a login. There's no account, no server, no expiry beyond the 30-day unused-record rule. There's a file on your Mac holding private keys and an escrow bag, and possessing that file is what "trusted" means.
The trust dialog is a polling loop. A human tap turns into a different error string on a retry. Once you know that, half the confusing pairing errors in CI logs explain themselves.
Almost all of this is 2008-era design, still load-bearing. XML plists, byte-swapped ports, a switchboard daemon, a doorman on a fixed port, and a keepalive that plays Marco Polo. Your $1,500 phone and your $3,000 laptop greet each other with technology old enough to have a learner's permit, and it works every single time.
We spend a lot of time in this layer, because MobAI drives real iPhones and the difference between a device that "just works" and a device that needs babysitting is usually something in this handshake. When pairing is handled properly, you never think about it. That's the goal.
Next in this series: why iPhones need pairing records, and why your CI machine keeps forgetting.
Sources worth reading
- Apple Platform Security: pairing model security
- libimobiledevice/usbmuxd and libimobiledevice.org
- pymobiledevice3, especially
misc/understanding_idevice_protocol_layers.md - libimobiledevice issue #20, the trust dialog discovered in real time, September 2013
- The Apple Wiki on usbmux, derived from a 2008 writeup
- Enabling usbmuxd's debug logs