Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ios/imagemounter/personalized_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
}
}

Expand Down
62 changes: 60 additions & 2 deletions ios/imagemounter/tss.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,36 @@ 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
}
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
Expand Down Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions ios/imagemounter/tss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}