As it can often be, I was bored. I wanted to look into a complex system, and had no idea which. Me and a friend have been sharing our locations to each other’s through Apple’s “Find My”. I asked if he was okay with me piping it into some dumb automations. He said yes, so the plan was to draw a few geofences around places he goes and make Discord announce whenever he arrived or left.
totally accurate and not made up dms (thanks es3n1n for the UI inspiration)
I already had a tiny Steam tracker doing basically the same thing with game activity, so I assumed Find My would be another authenticated request, some JSON, and an evening of work (epic foreshadowing).
I started with the normal iCloud web API and it happily returned my own Apple devices and their locations, but Find My People was nowhere in it. I thought I’d be smart and look into what other people have done. Turns out things aren’t easy as it looks like nobody has done this before (or well, not fully, you’ll see what I mean).
I did not have a Mac to run or instrument FindMy.app, so I started from existing open-source clients and requests against Apple. Later, when guessing field names stopped being funny, I also worked through decompilations of fmfd, findmylocated, and searchpartyd. I’ll link the exact source whenever one of the open-source clients comes up. The loop was mostly: keep the session fixed, change one field or encoding, and see whether Apple’s status code moved.
Warning (Scope)
The client only reads an already accepted share on my Apple Account. It has no methods for sending invitations, changing shares, adding family members, creating geofences on Apple, or performing device actions. The geofencing happens locally after decryption.
Starting with the Friends API
The first thing that looked useful was an old initClient call used by fmfd:
For context, fmfd is the Find My Friends daemon. Even though the app is called Find My now, this API still lives under Apple’s old MobileMe namespace. The dsid in the URL is just the account’s numeric Directory Services ID.
Logging into iCloud gave me a pile of MobileMe tokens for different services. A few of them sounded right, so I tried the obvious ones:
Three tokens, three 401s. Looking at the MobileMe delegate exchange made the reason pretty clear: the login acts as a token broker and gives each iCloud service its own credentials. Apparently, having “Find My” somewhere in the name wasn’t enough.
Even with the correct token, the request still wasn’t complete. initClient wanted the account’s Find My Friends (FMF) host, the courier token for this client’s Apple Push Notification service (APNs) connection, a fairly detailed client context, and a weird bundle of headers called anisette. The courier token identifies this client to APNs, and the topic tokens used to filter pushes are derived from it.
Anisette is basically extra proof that the request came from a provisioned Apple-like client. Some values stay tied to the emulated machine and another looks like a short-lived one-time code. It isn’t the password or a Find My token, but Apple rejects the request without it. FindMy.py generates those headers here and keeps the provisioned identity here. The deviceUDID below is just the Unique Device Identifier (UDID) I assigned to the emulated receiver.
The route still says friends/fmfd, but current clients build that context in findmylocated. currentTime is Unix milliseconds, and subsequent refreshes also echo the model version Apple returned in X-FMF-Model-Version.
Once I had all of that matching what the native daemons send, Apple finally returned the accepted share:
following is basically the list of people sharing their location with me. Great, I could now find my friend and get his opaque fmId, but that was about it: no location and no key. The two flags, secureLocationsCapable: true and fallbackToLegacyAllowed: false, were a pretty good sign that this share had moved to the newer encrypted location path. :sob:
At first I thought getting this response meant the client was set up properly, but clearly it didn’t :(. Those boring context fields only became important later, when I needed Apple to notice this was a new client and send it the existing key.
So I kept initClient for finding the relationship and moved on to pretending to be an actual Apple device, because that is what normal people do.
Becoming an IDS device
Finding the relationship was really the easy part. The encrypted path goes through IDS, Apple’s private device-identity and encrypted-messaging layer. IDS ties an account handle to its registered devices, certificates, push tokens, and message keys; the actual packets travel over APNs. My browser session proved I was logged in, but it didn’t make the Linux client one of those devices.
I found most of the older IDS and APNs code in a pre-rewrite pypush commit. Its login sent a password plist straight to profile.ess.apple.com. Apple accepted the password, asked for 2FA, and then rejected the code:
So the old pypush login was dead. The path that worked was GrandSlam, Apple’s account login protocol, which FindMy.py already implements. After a Secure Remote Password (SRP) exchange and two-factor authentication (2FA), I got an ADSID, another opaque identifier for the authenticated account, and a short-lived password-equivalent token (PET). I used that PET to ask signin/v2 for the com.apple.private.ids delegate instead of trying the password again.
The anisette values from earlier also travelled with that login. These are the interesting ones:
Then I sent the PET to setup.icloud.com/setup/signin/v2/login:
There is one extra bit in there: X-Mme-Nas-Qualify. It contains short-lived native validation data. The open-source implementations call the blob NAC; the name is less useful than the fact that Apple expects it to match the emulated hardware profile and be fresh. The pypush generator builds it, and Apple asks for another one during registration.
Apple returned zero for both the outer request and com.apple.private.ids, plus a profile ID and delegate token. Progress!
Certificate request
Next I had to exchange that delegate token for an IDS authentication certificate through authenticateDS. That meant sending a certificate signing request (CSR), basically a request for Apple to sign this client’s public key. Annoyingly, every attempt returned HTTP 200 as Apple hid the real result inside the response plist.
From there it was a lot of changing one thing, running it again, and still getting 6001:
The error body wasn’t exactly helpful either:
What finally worked is pretty specific: the CSR had to use PKCS#10, the standard certificate-request format, with a 2048-bit RSA key and a SHA-1 signature. Its common name had to be the uppercase SHA-1 of the IDS profile ID. Then I had to put it in an XML plist as Data and gzip the whole thing:
I ended up building the PKCS#10 object manually so I could control the exact bytes, including the empty attributes field and sha1WithRSAEncryption identifier:
My best guess is that the SHA-1 and XML requirements are just old compatibility baggage. authenticateDS is a legacy profile-enrollment endpoint and the accepted request still advertises IDS protocol 1660.
The endpoint itself came from Apple’s signed IDS bag. I hit one more stupid problem here: some of its hosts chained through Apple roots missing from Linux’s certifi bundle. I added fingerprint-pinned copies of Apple Root CA and Apple Root CA G3 from Apple PKI and kept TLS verification enabled.
Getting Find My registration accepted
Having the authentication certificate meant I could finally sign IDS requests. I used it with the APNs identity to ask for the account’s registered handles. Apple returned two usable URIs, although both somehow had status 5051 while the outer response said zero:
I ignored the inner 5051s since Apple had still given me the handles and tried registering the device. Another 6001.
rustpush’s service definition showed what I had wrong. I was registering FMF directly, but Apple registers an alloy multiplexer and puts six Find My topics under it:
The client-data was another rabbit hole. It advertised both the old IDS message identity and NGM v13, Apple’s newer device-to-device message format built around a P-256 device key and signed prekey. NGM is the envelope that will carry the location key later; it is not the encryption used for the location report itself. The registration also included Key Transparency v5 metadata and the Find My capability flags. rustpush’s registration code was the most readable reference I found for this.
Around that, the request needed the APNs token, some native device metadata, and another fresh validation blob. The encoding mattered again too: XML plist, gzip, then IDS signatures over the compressed bytes. The final request carried both the account certificate and the APNs push certificate:
The signature wasn’t over some normal HTTP canonicalization either. Apple wanted a binary concatenation of the nonce, bag key (id-register), query string, compressed body, and push token, with every variable field prefixed by a four-byte length. pypush has the exact routine here.
One signature uses the IDS key and the other uses the APNs push key, so the registration is tied to both identities.
I wish I had a clean “this one field fixed it” answer here, but I changed the layout, service list, and encoding together. I didn’t go back and bisect which one finally made Apple happy.
At that point the Linux client finally had everything it needed to exist as a Find My device: an APNs identity, an IDS authentication certificate, registered handles, message keys, and a Find My service certificate. I saved all of it so it could come back up without another login, which to be honest had started worrying me. It may sound silly, but at that point I logged in (or tried to) a lot and I wasn’t sure if Apple’s security features would kick in and lock my account.
Listening for Find My messages
With registration done, I could move on to actually listening for something. The client connects to private APNs and declares interest in the six Find My subservices, the SearchParty container topic, and com.apple.private.ids. Each push contains an IDS plist with a command, sender handle, sender push token, encryption mode, and payload.
Private APNs isn’t the public API normal app servers use. I had to activate the device certificate, open the binary connection implemented by pypush’s APNSConnection, get a base push token, and tell APNs which topics I cared about. The packets only identify their topic by a SHA-1 hash, so I mapped those hashes back to the service names from registration.
That parent IDS topic caused a particularly stupid failure. I initially subscribed only to the concrete Find My subtopics because those are the topics present in registration. Nothing arrived. Native clients also express interest in com.apple.private.ids: it acts like a courier gate for peer delivery, even though the packet still arrives labelled with its concrete subservice. The moment I added the parent interest, Find My packets started arriving on com.apple.private.alloy.fmd.
Decrypting the payload wasn’t as simple as looking up the sender’s email and grabbing a key. One handle can have several registered devices, each with its own identity. Before touching the payload I query Apple’s current IDS directory and find the identity whose push token matches the packet:
The current IDS envelope is called pair-ec. It comes with the ciphertext, an ephemeral P-256 key, an ECDSA signature, and a short validator tying the sender, receiver, and prekey together. To open it I do ECDH with my registered prekey, verify the sender’s signature, and derive the AES-CTR key and IV using HKDF-SHA-256 and a salt named LastPawn-MessageKeys.
Here’s the actual decryption code, minus the protobuf parsing and shitty error handling:
rustpush uses the same salt and the same 32-byte key / 16-byte IV split. The validator is only six bytes taken from the three public keys, while the ECDSA signature covers the full message.
The directory lookup is not optional decoration here. I only acknowledge and parse the application payload after the token-selected directory identity verifies the pair-ec envelope. Otherwise an arbitrary packet on the connection could hand the location parser a plausible-looking private key.
Asking an existing share to send its key
This was the part I misunderstood for the longest. My first working Friends request discovered the relationship but did not make the sharing device send anything. I assumed the key only travelled when a share was created. That would have meant stopping and recreating the share, which felt wrong for a fairly obvious reason: if I bought another iPhone, Apple would have to give it the keys for my existing relationships somehow. It cannot require every friend to unshare and reshare whenever I add a device.
Looking at the decompilation I ended up figuring what I was missing. The old daemon calls the request context SecureLocationsClientContext, and its coding keys are not guesses:
Current builds add an empty nearbyWatchIdentifiers array as well. The other important type is SPSecureLocationsSubscriptionContext. Its default constructor uses subscription mode 0 and fetch mode 1; following the no-cache branch from that context eventually builds a gateway fetch whose wire strings are proactive and distributeKeys.
The no-cache request is a SubscribeAndFetch with a fetch array, even when it contains one relationship:
There were several wonderfully unhelpful almost-correct variants. An object instead of the fetch array could get an empty HTTP 200 without dispatching anything. Using the FMF-derived topic token instead of the base courier token did the same. Sending the internal integer enum values instead of their custom Codable strings produced HTTP 400. heal was not the missing-key path either: it expects a real location identifier that might have gone stale, while the whole point here was that this client had none.
Before that secure request, the native sequence is:
The two refreshClient calls carry forward Apple’s serverContext, dataContext, and model version. The selected-friend refresh names the existing fmId; it does not edit or recreate the relationship. With that sequence running while the IDS receiver was online, Apple’s service asked the already-sharing device to distribute its current key to the new registered identity.
Then the thing actually arrived:
The application plist was an array containing this object shape:
entityIdentifier is the fmId, and hashedAdvertisement.key.data is the advertised location identifier SearchParty expects. The 85-byte private-key blob was the last surprise. I had assumed P-256 because the NGM identity above uses P-256. It actually doesn’t, probably because its a different layer, the People location key is P-224. Its serialized form is a 57-byte uncompressed public point (04 || X || Y) followed by the 28-byte private scalar. I also derive the public point from that scalar and compare it with the first 57 bytes before accepting the key.
rustpush independently uses SECP224R1 for these shared reports, which would have been a useful detail for me to notice approximately one day earlier.
I only save an envelope after the IDS sender verifies and entityIdentifier matches the fmId selected earlier. Anything else is acknowledged or ignored according to the protocol, but never dumped into the location state. Most importantly, this worked with the original existing share. No stop/restart or resharing was required.
Note (One requirement that remains)
The friend’s sharing device still has to be online long enough to process Apple’s asynchronous distributeKeys command. The relationship does not need to change, but some device holding its current key has to answer.
Fetching and decrypting the location
Once that message gave me the advertised location ID and private key, I could finally talk to SearchParty, the service that stores and returns encrypted Find My reports. It gives me the ciphertext for an ID, but not the key to read it. That’s why the IDS handoff above was important.
The request goes to gateway.icloud.com/findmyservice/fetch with the searchPartyToken from the very first login, fresh anisette headers, the fmId, and the same APNs/client context. Now that I have an advertised identifier, the fetch changes from distributeKeys/proactive to startLocationUpdates/shallow:
Authentication is HTTP Basic with the numeric DSID and scoped SearchParty token. The response keeps the advertised ID outside the ciphertext, which lets me pick the right key without trying every report:
Each locationInfo entry is encrypted and starts with a 57-byte uncompressed P-224 public key. From there I do ECDH with the per-share private key, run X9.63 SHA-256 with that public key as shared info, and split the result into a 16-byte AES key and 16-byte GCM IV:
There are two encryption layers here and they are easy to mix up. NGM protects the key handoff between IDS devices. SearchParty then serves reports encrypted with that per-share key. Once I have the key locally, fetching another report doesn’t need another IDS round trip.
Apple documents a similar end-to-end encrypted setup for the Find My offline-finding network, but that documentation is for devices and items not People. My guess is that the extra per-share key lets Apple revoke one relationship without replacing every Find My key on both accounts.
The plaintext is JSON or a plist containing the coordinate, accuracy, and timestamp. The outer timestamp uses Apple’s 2001 epoch. I added fixtures for the exact P-224 envelope, point/scalar validation, picking the matching advertised ID, stale reports, and selecting the newest location.
And, finally, the live report decrypted. I can’t tell you how satisfied I was to not see another HTTP 200 that really was a “skill issue, try harder”. The existing People share produced a current coordinate, accuracy, and timestamp on Linux.
So that is the whole pipeline exercised end-to-end. GrandSlam authenticates the Apple Account and obtains the IDS delegate. authenticateDS and id-register turn the Linux process into a registered Apple messaging identity. The Friends sequence attaches that identity to the existing accepted relationship. A missing-key SubscribeAndFetch makes the sharing device deliver its current P-224 key over APNs/IDS, inside a sender-verified P-256 NGM envelope. A second SearchParty fetch returns the encrypted report, and the P-224 key opens it locally.
So yeah, in one sentence: authenticate to Apple’s private services, register the Linux machine as an IDS client, receive the existing Find My share key, and use it to fetch and decrypt a consented friend’s latest location.
This was a short (less than a week) but fun project. I’ll probably work on something else like this again in the near future, but I can’t say whether I’ll write a blog for it or not. Anyway, thanks for the read and bye!