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
2 changes: 1 addition & 1 deletion cmd_device_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
75 changes: 71 additions & 4 deletions ios/imagemounter/personalized_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
87 changes: 87 additions & 0 deletions ios/imagemounter/restore_request_rules_test.go
Original file line number Diff line number Diff line change
@@ -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"])
}
27 changes: 24 additions & 3 deletions ios/imagemounter/tss.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,37 @@ 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
}
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
Expand Down Expand Up @@ -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)
Expand Down
Loading