Files
ipsw/pkg/appstore/profile.go
blacktop c0359c0f69 fix(appstore): add platform flag to provision generate command
- Add `--platform` flag (ios, macos, tvos, catalyst) to select the
  correct certificate and profile types per platform
- Map each platform/type combination to the appropriate ProfileType
- Extract `profileTypeNeedsDevices` helper to cover all dev/adhoc
  profile types across platforms
- Auto-register the host Mac as a development device when no devices
  are found and platform is macos/catalyst
- Fix `DeviceCreateRequest` to omit `deviceClass` and `model` fields
  rather than hard-coding iPhone values
- Initialize profile relationship slices before appending so
  json.Marshal emits `[]` instead of `null`
2026-04-07 17:49:17 -06:00

505 lines
16 KiB
Go

package appstore
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/blacktop/ipsw/internal/download"
)
type ProfileType string
const (
IOS_APP_DEVELOPMENT ProfileType = "IOS_APP_DEVELOPMENT"
IOS_APP_STORE ProfileType = "IOS_APP_STORE"
IOS_APP_ADHOC ProfileType = "IOS_APP_ADHOC"
IOS_APP_INHOUSE ProfileType = "IOS_APP_INHOUSE"
MAC_APP_DEVELOPMENT ProfileType = "MAC_APP_DEVELOPMENT"
MAC_APP_STORE ProfileType = "MAC_APP_STORE"
MAC_APP_DIRECT ProfileType = "MAC_APP_DIRECT"
TVOS_APP_DEVELOPMENT ProfileType = "TVOS_APP_DEVELOPMENT"
TVOS_APP_STORE ProfileType = "TVOS_APP_STORE"
TVOS_APP_ADHOC ProfileType = "TVOS_APP_ADHOC"
TVOS_APP_INHOUSE ProfileType = "TVOS_APP_INHOUSE"
MAC_CATALYST_APP_DEVELOPMENT ProfileType = "MAC_CATALYST_APP_DEVELOPMENT"
MAC_CATALYST_APP_STORE ProfileType = "MAC_CATALYST_APP_STORE"
MAC_CATALYST_APP_DIRECT ProfileType = "MAC_CATALYST_APP_DIRECT"
)
var ProfileTypes = []string{
string(IOS_APP_DEVELOPMENT),
string(IOS_APP_STORE),
string(IOS_APP_ADHOC),
string(IOS_APP_INHOUSE),
string(MAC_APP_DEVELOPMENT),
string(MAC_APP_STORE),
string(MAC_APP_DIRECT),
string(TVOS_APP_DEVELOPMENT),
string(TVOS_APP_STORE),
string(TVOS_APP_ADHOC),
string(TVOS_APP_INHOUSE),
string(MAC_CATALYST_APP_DEVELOPMENT),
string(MAC_CATALYST_APP_STORE),
string(MAC_CATALYST_APP_DIRECT),
}
type ProfileState string
const (
PS_ACTIVE ProfileState = "ACTIVE"
PS_INVALID ProfileState = "INVALID"
)
type Profile struct {
ID string `json:"id"`
Type ProfileType `json:"type"` // profiles
Attributes struct {
Name string `json:"name"`
CreatedDate Date `json:"createdDate"`
ExpirationDate Date `json:"expirationDate"`
ProfileContent []byte `json:"profileContent"`
ProfileState ProfileState `json:"profileState"`
ProfileType ProfileType `json:"profileType"`
UUID string `json:"uuid"`
Platform BundleIdPlatform `json:"platform"`
OfflineProfile bool `json:"isOfflineProfile"`
} `json:"attributes"`
Relationships struct {
BundleID struct {
Data struct {
Type string `json:"type"`
ID string `json:"id"`
} `json:"data"`
Links struct {
Self string `json:"self"`
Related string `json:"related"`
} `json:"links"`
} `json:"bundleId"`
Certificates struct {
Meta struct {
Paging struct {
Total int `json:"total"`
Limit int64 `json:"limit"`
} `json:"paging"`
} `json:"meta"`
Data []struct {
Type string `json:"type"`
ID string `json:"id"`
} `json:"data"`
Links struct {
Self string `json:"self"`
Related string `json:"related"`
} `json:"links"`
} `json:"certificates"`
Devices struct {
Meta struct {
Paging struct {
Total int `json:"total"`
Limit int64 `json:"limit"`
} `json:"paging"`
} `json:"meta"`
Data []struct {
Type string `json:"type"`
ID string `json:"id"`
} `json:"data"`
Links struct {
Self string `json:"self"`
Related string `json:"related"`
} `json:"links"`
} `json:"devices"`
} `json:"relationships"`
Links Links `json:"links"`
}
func (p Profile) IsInvalid() bool {
return p.Attributes.ProfileState == "INVALID"
}
func (p Profile) IsExpired() bool {
return time.Time(p.Attributes.ExpirationDate).Before(time.Now())
}
type Data struct {
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
}
type ProfileCreateRequest struct {
Data struct {
Type string `json:"type"` // profiles
Attributes struct {
Name string `json:"name"`
ProfileType ProfileType `json:"profileType"`
TemplateName string `json:"templateName,omitempty"`
OfflineProfile bool `json:"isOfflineProfile,omitempty"`
} `json:"attributes"`
Relationships struct {
BundleID struct {
Data Data `json:"data"`
} `json:"bundleId"`
Certificates struct {
Data []Data `json:"data"`
} `json:"certificates"`
Devices struct {
Data []Data `json:"data"`
} `json:"devices"`
} `json:"relationships"`
} `json:"data"`
}
type ProfileResponse struct {
Data Profile `json:"data"`
Links DocumentLinks `json:"links"`
}
type ProfilesResponse struct {
Data []Profile `json:"data"`
Links Links `json:"links"`
}
// GetProfiles returns a list provisioning profiles and download their data.
func (as *AppStore) GetProfiles() ([]Profile, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", profilesURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create http GET request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+as.token)
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var profiles ProfilesResponse
if err := json.NewDecoder(resp.Body).Decode(&profiles); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return profiles.Data, nil
}
// GetProfile returns a provisioning profile and download their data.
func (as *AppStore) GetProfile(id string) (*Profile, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", profilesURL+"/"+id, nil)
if err != nil {
return nil, fmt.Errorf("failed to create http GET request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+as.token)
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var profile ProfileResponse
if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &profile.Data, nil
}
// GetProfileBundleID returns the bundle ID information for a specific provisioning profile.
func (as *AppStore) GetProfileBundleID(id string) (*BundleID, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/bundleId", id), nil)
if err != nil {
return nil, fmt.Errorf("failed to create http GET request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+as.token)
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var bundle BundleIdResponse
if err := json.NewDecoder(resp.Body).Decode(&bundle); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &bundle.Data, nil
}
// GetProfileDevices returns a list of all devices for a specific provisioning profile
func (as *AppStore) GetProfileDevices(id string) ([]Device, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/devices", id), nil)
if err != nil {
return nil, fmt.Errorf("failed to create http GET request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+as.token)
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var devices DevicesResponse
if err := json.NewDecoder(resp.Body).Decode(&devices); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return devices.Data, nil
}
// GetProfileCerts returns a list of all certificates and their data for a specific provisioning profile.
func (as *AppStore) GetProfileCerts(id string) ([]Certificate, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/certificates", id), nil)
if err != nil {
return nil, fmt.Errorf("failed to create http GET request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+as.token)
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var certs CertificatesResponse
if err := json.NewDecoder(resp.Body).Decode(&certs); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return certs.Data, nil
}
// CreateProfile creates a new profile
func (as *AppStore) CreateProfile(name string, ptype string, bundleID string, cerIDs, devicesIDs []string, offline bool) (*ProfileResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
var profileCreateRequest ProfileCreateRequest
profileCreateRequest.Data.Type = "profiles"
profileCreateRequest.Data.Attributes.Name = name
profileCreateRequest.Data.Attributes.ProfileType = ProfileType(ptype)
profileCreateRequest.Data.Attributes.OfflineProfile = offline
profileCreateRequest.Data.Relationships.BundleID.Data.Type = "bundleIds"
profileCreateRequest.Data.Relationships.BundleID.Data.ID = bundleID
// Initialize slices so json.Marshal produces [] instead of null when empty
profileCreateRequest.Data.Relationships.Certificates.Data = make([]Data, 0, len(cerIDs))
for _, cerID := range cerIDs {
profileCreateRequest.Data.Relationships.Certificates.Data = append(profileCreateRequest.Data.Relationships.Certificates.Data, Data{
ID: cerID,
Type: "certificates",
})
}
profileCreateRequest.Data.Relationships.Devices.Data = make([]Data, 0, len(devicesIDs))
for _, devicesID := range devicesIDs {
profileCreateRequest.Data.Relationships.Devices.Data = append(profileCreateRequest.Data.Relationships.Devices.Data, Data{
ID: devicesID,
Type: "devices",
})
}
jsonStr, err := json.Marshal(&profileCreateRequest)
if err != nil {
return nil, err
}
// os.WriteFile("req.json", jsonStr, 0644)
req, err := http.NewRequest("POST", profilesURL, bytes.NewBuffer(jsonStr))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+as.token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
var profileResponse ProfileResponse
if err := json.NewDecoder(resp.Body).Decode(&profileResponse); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &profileResponse, nil
}
// DeleteProfile deletes a provisioning profile that is used for app development or distribution.
func (as *AppStore) DeleteProfile(id string) error {
if err := as.createToken(defaultJWTLife); err != nil {
return fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("DELETE", profilesURL+"/"+id, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+as.token)
req.Header.Set("Accept", "application/json")
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(as.Proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
},
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send http request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
var eresp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
return fmt.Errorf("failed to JSON decode http response: %v", err)
}
var errOut strings.Builder
for idx, e := range eresp.Errors {
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
}
return fmt.Errorf("%s: %s", resp.Status, errOut.String())
}
return nil
}