Sign the built-in SideStore in automatically - #1
Closed
pwnapplehat wants to merge 4 commits into
Closed
Conversation
SideStore imports a certificate it finds in its own framework on first launch - the same convention iLoader's isideload writes, confirmed against that source. So when iPASide installs the LiveContainer+SideStore build it now bakes its own certificate in, before signing: ALTCertificate.p12 inside SideStoreApp.framework, encrypted with the certificate's Apple machineId, plus ALTCertificateID and ALTAppGroups in the framework Info.plist. Seeding it with iPASide's own certificate is the point. SideStore then refreshes under the same identity that installed LiveContainer, so the phone renews apps on its own with no separate Apple ID to sign into - which was also the only way to end up with a second, conflicting LiveContainer, so this closes that footgun too. Mechanics: provision.signing_identity() exposes the cached key+cert, and certificate_machine_id() recovers the machineId Apple returns in the cert listing (iPASide submits it with the CSR but does not keep it). run_sideload gained a pre-sign hook, mirroring the post-install one, so a profile can rewrite the bundle while the additions are still covered by the signature; the LiveContainer profile uses it only when the build actually carries SideStore. Not touched: importing the SideStore *account*. It lives in the keychain, even iLoader lists it as unshipped, and baking the certificate makes it unnecessary for refresh. 11 tests, 365 in the engine suite.
… claims
The +SideStore LiveContainer build can now open already signed in as the same
Apple ID iPASide uses, with no prompt. SideStore has a password-less auth path
(authenticateWithToken) taken when its keychain holds an adsid and the Xcode
GrandSlam token; iPASide already has both for the signed-in account, and that
token is dated about a year out, so it outlasts every seven-day app expiry.
Delivery works around LiveContainer's keychain hook, which forces every guest
SecItem call into a per-guest, randomly assigned group
TEAMID.com.kdt.livecontainer.shared.N. The injected helper dylib runs in the
host before any guest - so the hook is not yet installed - and is entitled to
all 128 groups, so it writes the two items into every one of them: whichever N
the SideStore guest is later assigned, its read finds them.
- livecontainer.py: _signin_request + deliver_signin. Skips entirely when the
build ships no helper dylib, so a live token is never left on the device
with nothing to consume it; never raises, like the pairing hand-off.
- sideload.py: delivered from the post-install hook next to the pairing file,
so a rotated token or a re-paired device is re-seeded on every refresh.
- iPASideCertImport.m: writes the two keychain items with SideStore's exact
attributes (service com.SideStore.SideStore, afterFirstUnlock,
synchronizable) into every group; values are never logged and the request
is deleted once stored. build.sh links Security.framework.
- tests/test_signin_seed.py: the request targets exactly the signed keychain
groups, no-dylib writes nothing, failures are reported not raised.
Also corrects docs that overstated the earlier certificate bake as removing the
sign-in: it does not - it makes SideStore reuse iPASide's certificate instead of
revoking it when the user signs in. The LiveContainer tab, README and CHANGELOG
now state the one-time sign-in, which stays the fallback if the seed is absent.
The dylib is iOS arm64 and is rebuilt by the macOS CI (build-lc-helper.yml); the
vendored binary is updated from that artifact in a follow-up commit. On-device
verification that SideStore opens signed in is still pending.
Co-authored-by: Cursor <cursoragent@cursor.com>
… first-launch reset Two reasons the automatic sign-in did not take, both found on the phone: - LiveContainer skips its keychain hook for the bundled SideStore (the isSideStore guard in LCBootstrap), so SideStore is never namespaced into a shared.N group - it reads the process default keychain access group. The dylib was seeding all 128 shared groups but not the default one, so 128 of 128 wrote and SideStore still found nothing. It now also writes with no access group, which lands in that same default group (the dylib is unhooked too), and logs whether that write took. - SideStore's AppDelegate resets the keychain the first time it launches, gated on a firstLaunch default being absent, which erased the seed before it was ever read. deliver_signin now pre-sets that default over house_arrest so the reset is skipped, merging into any existing prefs and leaving a real earlier date alone. Confirmed against the device: the dylib logged "seeded ... 128 of 128 groups" yet SideStore reported "not signed in" with no token-auth attempt, and tracing LCBootstrap showed the skipped hook. Tests cover the first-launch suppression (absent, present, and merge cases). Co-authored-by: Cursor <cursoragent@cursor.com>
The default-group write succeeded (128/128 + default ok) but SideStore still read nil from appleIDAdsid, so its get uses criteria the write did not match. Two changes: - Write the two tokens to the default group in both synchronizable variants, since SideStore's KeychainAccess get(service:account:) may query either and only one needs to match. - After seeding, read the key back with SideStore's exact query - service + account, no access group, returning attributes - across sync any/true/false, and log which access group each match lands in. Run unhooked in the same process, this is the truest picture of what SideStore's own read will see, and pins down group-vs-attribute if it still misses. Token values are never logged. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
+SideStoreLiveContainer build now opens already signed in as the same Apple ID iPASide uses — no Apple ID prompt inside SideStore — so on-device refresh is truly zero-touch.Why it works
SideStore's
AuthenticationOperation.signInhas a password-less branch,authenticateWithToken(adsid:xcodeToken:), taken whenever its keychain holdsappleIDAdsid+appleIDXcodeToken. Those aresession.dsidandsession.authToken— which iPASide already holds for the signed-in account asadsidand thecom.apple.gs.xcode.authGrandSlam token. Apple dates that token ~1 year out, so a single seed outlasts every 7-day app expiry in between.The delivery problem, and the fix
LiveContainer hooks every guest
SecItemcall and forces it into a per-guest access groupTEAMID.com.kdt.livecontainer.shared.N, whereNisInt.random(0..<128)per container. So a keychain write can't target SideStore's group by name.The injected helper dylib runs in the host, before any guest — so that hook isn't installed yet — and the host is entitled to all 128 groups. It writes the two items into every group (exactly the
build_entitlementskeychain list), so whicheverNSideStore's guest is later assigned, its read finds them.Changes
livecontainer.py:_signin_request()+deliver_signin(). Skips entirely if the build ships no helper dylib (a live token is never dropped with nothing to consume it); never raises.sideload.py: delivered from the post-install hook next to the pairing file, so it's re-seeded on every refresh (rotated token / re-paired device).iPASideCertImport.m: seeds the two items with SideStore's exact attributes (com.SideStore.SideStore,afterFirstUnlock,synchronizable), never logs values, deletes the request once stored.build.shlinksSecurity.framework.tests/test_signin_seed.py(6): request targets exactly the signed groups, no-dylib writes nothing, failures reported not raised.Verification
build-lc-helper.yml); its artifact is vendored in a follow-up commit. On-device proof (SideStore opens signed in) is the final gate before release.Security note
The seed places a live ~1-year account token (free dev Apple ID) into a request file on the device until the dylib imports it and deletes it. Never logged.