diff --git a/cmd_device_debug.go b/cmd_device_debug.go index afaad1dd..8b6bc048 100644 --- a/cmd_device_debug.go +++ b/cmd_device_debug.go @@ -142,7 +142,7 @@ func runImageCommand(ctx commandContext) { err := imagemounter.MountImage(ctx.Device, imagePath) if err != nil { slog.Error("error mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber, "err", err) - return + os.Exit(1) } slog.Info("success mounting image", "image", imagePath, "udid", ctx.Device.Properties.SerialNumber) } diff --git a/ios/imagemounter/personalized_image.go b/ios/imagemounter/personalized_image.go index d35eadd9..e0b19ba8 100644 --- a/ios/imagemounter/personalized_image.go +++ b/ios/imagemounter/personalized_image.go @@ -40,15 +40,82 @@ func (m buildManifest) findIdentity(identifiers personalizationIdentifiers) (bui type manifestEntry struct { Digest []byte - Trusted bool `plist:"Trusted"` - EPRO bool `plist:"EPRO"` - ESEC bool `plist:"ESEC"` + Trusted bool `plist:"Trusted"` + EPRO *bool `plist:"EPRO"` + ESEC *bool `plist:"ESEC"` Name string Info struct { - Path string + Path string + RestoreRequestRules []restoreRequestRule `plist:"RestoreRequestRules"` } } +// restoreRequestRule gates the EPRO/ESEC flags of a manifest component on the +// device's personalization parameters (production mode, security mode, …). +// Modern developer-disk-image build manifests omit literal EPRO/ESEC values and +// express them through these rules instead. +type restoreRequestRule struct { + Actions map[string]interface{} `plist:"Actions"` + Conditions map[string]interface{} `plist:"Conditions"` +} + +// restoreRequestRules returns the rules that determine the EPRO/ESEC flags for +// the TSS personalization request. Apple's libauthinstall (and pymobiledevice3) +// take these from the LoadableTrustCache component and apply them to every +// trusted component of a developer disk image. +func (b buildIdentity) restoreRequestRules() []restoreRequestRule { + if entry, ok := b.Manifest["LoadableTrustCache"]; ok { + return entry.Info.RestoreRequestRules + } + return nil +} + +// applyRestoreRequestRules mutates a TSS manifest entry, applying the actions of +// every rule whose conditions are satisfied by params. This is how EPRO/ESEC end +// up on the request: e.g. a device in production + secure mode gets EPRO=true and +// ESEC=true even though the build manifest carries no literal values. Mirrors the +// rule evaluation in Apple's libauthinstall / pymobiledevice3. +func applyRestoreRequestRules(entry, params map[string]interface{}, rules []restoreRequestRule) { + for _, rule := range rules { + if !restoreRuleConditionsMet(rule.Conditions, params) { + continue + } + for key, value := range rule.Actions { + // 255 is a "leave unchanged" sentinel used by some components. + if v, ok := value.(uint64); ok && v == 255 { + continue + } + entry[key] = value + } + } +} + +func restoreRuleConditionsMet(conditions, params map[string]interface{}) bool { + for key, want := range conditions { + var got interface{} + switch key { + case "ApRawProductionMode", "ApCurrentProductionMode": + got = params["ApProductionMode"] + case "ApRawSecurityMode": + got = params["ApSecurityMode"] + case "ApRequiresImage4": + got = params["ApSupportsImg4"] + case "ApDemotionPolicyOverride": + got = params["DemotionPolicy"] + case "ApInRomDFU": + got = params["ApInRomDFU"] + default: + // Unknown condition: treat the rule as unmatched, matching the + // reference implementations rather than guessing. + return false + } + if got == nil || want != got { + return false + } + } + return true +} + type buildIdentity struct { BoardID string `plist:"ApBoardID"` ChipID string `plist:"ApChipID"` diff --git a/ios/imagemounter/restore_request_rules_test.go b/ios/imagemounter/restore_request_rules_test.go new file mode 100644 index 00000000..258dfeec --- /dev/null +++ b/ios/imagemounter/restore_request_rules_test.go @@ -0,0 +1,87 @@ +package imagemounter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func boolPtr(b bool) *bool { return &b } + +// ddiTrustCacheRules mirrors the RestoreRequestRules that modern developer-disk-image +// build manifests attach to the LoadableTrustCache component. EPRO/ESEC are not literal +// values in the manifest; they are derived from these rules and the device's parameters. +func ddiTrustCacheRules() []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{}{"ESEC": false}, Conditions: map[string]interface{}{"ApRawSecurityMode": false, "ApRequiresImage4": true}}, + {Actions: map[string]interface{}{"ESEC": true}, Conditions: map[string]interface{}{"ApRawSecurityMode": true, "ApRequiresImage4": true}}, + } +} + +// A production, secure device must end up with EPRO=true and ESEC=true, otherwise +// Apple TSS rejects the request with status 94 ("This device isn't eligible for the +// requested build."). +func TestApplyRestoreRequestRulesProductionSecure(t *testing.T) { + params := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{"Digest": []byte{1, 2, 3}, "Trusted": true} + + applyRestoreRequestRules(entry, params, ddiTrustCacheRules()) + + assert.Equal(t, true, entry["EPRO"]) + assert.Equal(t, true, entry["ESEC"]) +} + +// A non-production, non-secure device (e.g. a demoted / development unit) must get +// EPRO=false and ESEC=false from the same rules. +func TestApplyRestoreRequestRulesDevelopment(t *testing.T) { + params := map[string]interface{}{ + "ApProductionMode": false, + "ApSecurityMode": false, + "ApSupportsImg4": true, + } + entry := map[string]interface{}{} + + applyRestoreRequestRules(entry, params, ddiTrustCacheRules()) + + assert.Equal(t, false, entry["EPRO"]) + assert.Equal(t, false, entry["ESEC"]) +} + +// A rule referencing a condition we do not model (or whose parameter is absent) must +// not match, leaving the entry untouched rather than applying its actions. +func TestApplyRestoreRequestRulesUnmatchedConditions(t *testing.T) { + params := map[string]interface{}{"ApProductionMode": true, "ApSupportsImg4": true} + entry := map[string]interface{}{} + + rules := []restoreRequestRule{ + // ApInRomDFU is not set in params -> rule must not apply. + {Actions: map[string]interface{}{"EPRO": false}, Conditions: map[string]interface{}{"ApInRomDFU": true}}, + // Unknown condition -> rule must not apply. + {Actions: map[string]interface{}{"ESEC": true}, Conditions: map[string]interface{}{"SomeUnknownCondition": true}}, + } + + applyRestoreRequestRules(entry, params, rules) + + assert.NotContains(t, entry, "EPRO") + assert.NotContains(t, entry, "ESEC") +} + +// The 255 sentinel means "leave unchanged" and must not overwrite an existing value. +func TestApplyRestoreRequestRulesSentinel(t *testing.T) { + params := map[string]interface{}{"ApProductionMode": true, "ApSupportsImg4": true} + entry := map[string]interface{}{"EPRO": true} + + rules := []restoreRequestRule{ + {Actions: map[string]interface{}{"EPRO": uint64(255)}, Conditions: map[string]interface{}{"ApCurrentProductionMode": true, "ApRequiresImage4": true}}, + } + + applyRestoreRequestRules(entry, params, rules) + + assert.Equal(t, true, entry["EPRO"]) +} diff --git a/ios/imagemounter/tss.go b/ios/imagemounter/tss.go index 488c5376..bfb17d58 100644 --- a/ios/imagemounter/tss.go +++ b/ios/imagemounter/tss.go @@ -48,6 +48,16 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat "UID_MODE": false, } + // Personalization parameters describing this device to the manifest's + // RestoreRequestRules. Developer disk images are always personalized for a + // production, secure, img4-based device. + tssParameters := map[string]interface{}{ + "ApProductionMode": true, + "ApSecurityMode": true, + "ApSupportsImg4": true, + } + rules := identity.restoreRequestRules() + for key, entry := range identity.Manifest { if !entry.Trusted { continue @@ -55,9 +65,20 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat entryParams := map[string]interface{}{ "Digest": entry.Digest, "Trusted": true, - "EPRO": entry.EPRO, - "ESEC": entry.ESEC, } + // Keep any literal EPRO/ESEC the manifest provides, then let its + // RestoreRequestRules override them for this device. Modern DDI manifests + // omit the literals entirely; without applying the rules the request goes + // out with EPRO/ESEC=false and Apple TSS rejects it with status 94 + // ("This device isn't eligible for the requested build."). + if entry.EPRO != nil { + entryParams["EPRO"] = *entry.EPRO + } + if entry.ESEC != nil { + entryParams["ESEC"] = *entry.ESEC + } + applyRestoreRequestRules(entryParams, tssParameters, rules) + if key == "PersonalizedDMG" || key == "PersonalizedDmg" { if entry.Name != "" { entryParams["Name"] = entry.Name @@ -103,7 +124,7 @@ func (t tssClient) getSignature(identity buildIdentity, identifiers personalizat return nil, fmt.Errorf("getSignature: failed to parse response: %w", err) } if resp.status != 0 { - return nil, fmt.Errorf("unexpected status in response %d", resp.status) + return nil, fmt.Errorf("unexpected status in response %d: %q", resp.status, resp.message) } var ticket map[string]interface{} _, err = plist.Unmarshal([]byte(resp.requestString), &ticket)