From 88d442eccaf2741a84707335c1033c231fbbc6a8 Mon Sep 17 00:00:00 2001 From: qiuzilin Date: Mon, 22 Jun 2026 15:22:45 +0800 Subject: [PATCH] fix(imagemounter): apply RestoreRequestRules when personalizing DDI Personalized Developer Disk Image mounting (iOS 17+) requests a signature from Apple's TSS server. Newer BuildManifests (e.g. iOS 26) no longer carry static EPRO/ESEC values on the trusted manifest entries; instead they ship `RestoreRequestRules` (on the LoadableTrustCache entry) that derive these fields from the request parameters. Previously getSignature read entry.EPRO/entry.ESEC directly, which decode to their zero value (false) for these manifests. Apple then rejects the request with `STATUS=94 (This device isn't eligible for the requested build)` because a production + secured Img4 device must send EPRO=true/ESEC=true. Parse RestoreRequestRules from the manifest and apply them (matching the algorithm used by idevicerestore / pymobiledevice3) so the correct EPRO/ESEC are sent. Verified end-to-end against a physical iOS 26.5 device, which now mounts successfully. Adds unit tests for applyRestoreRequestRules. --- ios/imagemounter/personalized_image.go | 9 +++ ios/imagemounter/tss.go | 62 ++++++++++++++++++++- ios/imagemounter/tss_test.go | 76 ++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/ios/imagemounter/personalized_image.go b/ios/imagemounter/personalized_image.go index d35eadd96..fead3d586 100644 --- a/ios/imagemounter/personalized_image.go +++ b/ios/imagemounter/personalized_image.go @@ -38,6 +38,11 @@ func (m buildManifest) findIdentity(identifiers personalizationIdentifiers) (bui return buildIdentity{}, fmt.Errorf("findIdentity: failed to find identity for ApBoardId 0x%x and ApChipId 0x%x", identifiers.BoardId, identifiers.ChipID) } +type restoreRequestRule struct { + Actions map[string]interface{} `plist:"Actions"` + Conditions map[string]interface{} `plist:"Conditions"` +} + type manifestEntry struct { Digest []byte Trusted bool `plist:"Trusted"` @@ -46,6 +51,10 @@ type manifestEntry struct { Name string Info struct { Path string + // RestoreRequestRules dynamically determine fields such as EPRO/ESEC that must be + // sent to Apple's TSS. iOS 17+ (notably iOS 26) manifests omit static EPRO/ESEC and + // rely on these rules; without applying them TSS rejects the request with status 94. + RestoreRequestRules []restoreRequestRule `plist:"RestoreRequestRules"` } } diff --git a/ios/imagemounter/tss.go b/ios/imagemounter/tss.go index 488c5376a..36090f04c 100644 --- a/ios/imagemounter/tss.go +++ b/ios/imagemounter/tss.go @@ -48,6 +48,20 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat "UID_MODE": false, } + // Context used to evaluate RestoreRequestRules conditions. Mirrors pymobiledevice3 / + // idevicerestore: production + secure + Img4 device. ApSupportsImg4 maps the + // ApRequiresImage4 condition and is only used for rule evaluation, not sent to Apple. + ruleParams := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + // RestoreRequestRules live on the LoadableTrustCache entry and are applied to every entry. + var rules []restoreRequestRule + if ltc, ok := identity.Manifest["LoadableTrustCache"]; ok { + rules = ltc.Info.RestoreRequestRules + } + for key, entry := range identity.Manifest { if !entry.Trusted { continue @@ -55,9 +69,15 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat entryParams := map[string]interface{}{ "Digest": entry.Digest, "Trusted": true, - "EPRO": entry.EPRO, - "ESEC": entry.ESEC, } + // Apply any statically declared values first, then let the rules override them. + if entry.EPRO { + entryParams["EPRO"] = entry.EPRO + } + if entry.ESEC { + entryParams["ESEC"] = entry.ESEC + } + applyRestoreRequestRules(entryParams, ruleParams, rules) if key == "PersonalizedDMG" || key == "PersonalizedDmg" { if entry.Name != "" { entryParams["Name"] = entry.Name @@ -119,6 +139,44 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat return nil, fmt.Errorf("getSignature: unexpected response status %d", res.StatusCode) } +// restoreRequestRuleConditionMap maps RestoreRequestRules condition keys to the +// request parameter keys they test against (same mapping used by idevicerestore). +var restoreRequestRuleConditionMap = map[string]string{ + "ApRawProductionMode": "ApProductionMode", + "ApCurrentProductionMode": "ApProductionMode", + "ApRawSecurityMode": "ApSecurityMode", + "ApRequiresImage4": "ApSupportsImg4", + "ApDemotionPolicyOverride": "DemotionPolicy", + "ApInRomDFU": "ApInRomDFU", +} + +// applyRestoreRequestRules mutates entry with the Actions of every rule whose +// Conditions are all satisfied by params. This computes fields such as EPRO/ESEC +// that newer (iOS 17+/26) BuildManifests omit but Apple's TSS still requires. +func applyRestoreRequestRules(entry map[string]interface{}, params map[string]interface{}, rules []restoreRequestRule) { + for _, rule := range rules { + fulfilled := true + for condKey, condVal := range rule.Conditions { + paramKey, ok := restoreRequestRuleConditionMap[condKey] + if !ok { + fulfilled = false + break + } + paramVal, ok := params[paramKey] + if !ok || paramVal != condVal { + fulfilled = false + break + } + } + if !fulfilled { + continue + } + for actionKey, actionVal := range rule.Actions { + entry[actionKey] = actionVal + } + } +} + type response struct { status int message string diff --git a/ios/imagemounter/tss_test.go b/ios/imagemounter/tss_test.go index 7a6ab4d74..5f000507d 100644 --- a/ios/imagemounter/tss_test.go +++ b/ios/imagemounter/tss_test.go @@ -55,3 +55,79 @@ func TestParseResponseRequiresRequestStringLast(t *testing.T) { _, err := parseResponse(strings.NewReader("REQUEST_STRING=abc&STATUS=0")) assert.Error(t, err) } + +// iOS 17+ (e.g. iOS 26) BuildManifests omit static EPRO/ESEC and instead provide +// RestoreRequestRules that must be evaluated against the request parameters. These rules +// mirror the LoadableTrustCache.Info.RestoreRequestRules found on a real iOS 26 device. +func ios26RestoreRequestRules() []restoreRequestRule { + return []restoreRequestRule{ + { + Actions: map[string]interface{}{"EPRO": false}, + Conditions: map[string]interface{}{"ApCurrentProductionMode": false, "ApRequiresImage4": true}, + }, + { + Actions: map[string]interface{}{"EPRO": true}, + Conditions: map[string]interface{}{"ApCurrentProductionMode": true, "ApRequiresImage4": true}, + }, + { + Actions: map[string]interface{}{"EPRO": false}, + Conditions: map[string]interface{}{"ApCurrentProductionMode": true, "ApDemotionPolicyOverride": "Demote", "ApInRomDFU": true, "ApRequiresImage4": true}, + }, + { + Actions: map[string]interface{}{"ESEC": false}, + Conditions: map[string]interface{}{"ApRawSecurityMode": false, "ApRequiresImage4": true}, + }, + { + Actions: map[string]interface{}{"ESEC": true}, + Conditions: map[string]interface{}{"ApRawSecurityMode": true, "ApRequiresImage4": true}, + }, + } +} + +func TestApplyRestoreRequestRulesProductionDevice(t *testing.T) { + // Production + secure + Img4 device: EPRO and ESEC must both resolve to true, + // otherwise Apple's TSS rejects the personalization request with status 94. + params := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{"Digest": []byte{0x01}, "Trusted": true} + + applyRestoreRequestRules(entry, params, ios26RestoreRequestRules()) + + assert.Equal(t, true, entry["EPRO"]) + assert.Equal(t, true, entry["ESEC"]) + // The DFU-only EPRO=false rule must not fire (its ApInRomDFU/ApDemotionPolicyOverride + // conditions reference parameters that are absent). +} + +func TestApplyRestoreRequestRulesNonProductionDevice(t *testing.T) { + params := map[string]interface{}{ + "ApProductionMode": false, + "ApSecurityMode": false, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{"Digest": []byte{0x01}, "Trusted": true} + + applyRestoreRequestRules(entry, params, ios26RestoreRequestRules()) + + assert.Equal(t, false, entry["EPRO"]) + assert.Equal(t, false, entry["ESEC"]) +} + +func TestApplyRestoreRequestRulesSkipsUnknownCondition(t *testing.T) { + params := map[string]interface{}{"ApProductionMode": true} + entry := map[string]interface{}{} + rules := []restoreRequestRule{ + { + Actions: map[string]interface{}{"EPRO": true}, + Conditions: map[string]interface{}{"SomeUnmappedCondition": true}, + }, + } + + applyRestoreRequestRules(entry, params, rules) + + _, ok := entry["EPRO"] + assert.False(t, ok, "rule with an unmapped condition key must be skipped") +}