Skip to content

Sign the built-in SideStore in automatically - #1

Closed
pwnapplehat wants to merge 4 commits into
mainfrom
feature/livecontainer-auto-signin
Closed

Sign the built-in SideStore in automatically#1
pwnapplehat wants to merge 4 commits into
mainfrom
feature/livecontainer-auto-signin

Conversation

@pwnapplehat

Copy link
Copy Markdown
Owner

What

The +SideStore LiveContainer 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.signIn has a password-less branch, authenticateWithToken(adsid:xcodeToken:), taken whenever its keychain holds appleIDAdsid + appleIDXcodeToken. Those are session.dsid and session.authToken — which iPASide already holds for the signed-in account as adsid and the com.apple.gs.xcode.auth GrandSlam 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 SecItem call and forces it into a per-guest access group TEAMID.com.kdt.livecontainer.shared.N, where N is Int.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_entitlements keychain list), so whichever N SideStore'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.sh links Security.framework.
  • tests/test_signin_seed.py (6): request targets exactly the signed groups, no-dylib writes nothing, failures reported not raised.
  • Docs correction: the earlier certificate bake was overstated as removing sign-in. It doesn't — it makes SideStore reuse iPASide's certificate instead of revoking it. The LiveContainer tab, README and CHANGELOG now state the one-time sign-in, which remains the fallback if the seed is absent.

Verification

  • Full engine suite green (346), including the 6 new tests.
  • Pending: the dylib is iOS arm64 and is built by this PR's macOS CI (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.

pwnapplehat and others added 4 commits July 28, 2026 02:34
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>
@pwnapplehat
pwnapplehat deleted the feature/livecontainer-auto-signin branch July 28, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant