Files
blacktop dbbe994d12 feat(appstore): add P12 bundling and WWDR G3 install for code signing
- Bundle cert + generated key into a password-protected .p12 file
  using go-pkcs12 (legacy 3DES for macOS Keychain compatibility)
- Import the .p12 via `security import` so cert and key are properly
  paired as an identity in the login keychain
- Auto-download and install the Apple WWDR G3 intermediate cert to
  ensure a valid codesigning trust chain
- Change `appstore cert rm` ID from a flag to a positional argument
- Fix missing `Content-Type` header on bundle ID registration request
2026-04-07 16:26:59 -06:00

459 lines
14 KiB
Go

package appstore
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/blacktop/ipsw/internal/download"
)
type bundleIdPlatform string
const (
IOS bundleIdPlatform = "IOS"
MAC_OS bundleIdPlatform = "MAC_OS"
)
type BundleID struct {
ID string `json:"id"`
Type string `json:"type"` // bundleIds
Attributes struct {
ID string `json:"identifier"`
Name string `json:"name"`
Platform bundleIdPlatform `json:"platform"`
SeedID string `json:"seedId"`
} `json:"attributes"`
Relationships struct {
Capabilities BundleIdCapabilitiesResponse `json:"bundleIdCapabilities"`
Profiles struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"` // profiles
} `json:"data"`
Meta Meta `json:"meta"`
Links Links `json:"links"`
} `json:"profiles"`
App struct {
Data struct {
ID string `json:"id"`
Type string `json:"type"` // apps
} `json:"data"`
Links Links `json:"links"`
} `json:"app"`
} `json:"relationships"`
Links Links `json:"links"`
}
type BundleIdResponse struct {
Data BundleID `json:"data"`
Links Links `json:"links"`
Included any `json:"included,omitempty"`
}
type BundleIdsResponse struct {
Data []BundleID `json:"data"`
Links Links `json:"links"`
Meta Meta `json:"meta"`
Included any `json:"included,omitempty"`
}
type BundleIdCreateRequest struct {
Data struct {
Type string `json:"type"` // bundleIds
Attributes struct {
ID string `json:"identifier"`
Name string `json:"name"`
Platform bundleIdPlatform `json:"platform"`
SeedID string `json:"seedId"`
} `json:"attributes"`
} `json:"data"`
}
// GetBundleIDs returns a list bundle IDs that are registered to your team.
func (as *AppStore) GetBundleIDs() ([]BundleID, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", bundleIDsURL, 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 bundles BundleIdsResponse
if err := json.NewDecoder(resp.Body).Decode(&bundles); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return bundles.Data, nil
}
// GetBundleIDByIdentifier looks up a bundle ID resource by its reverse-DNS
// identifier (e.g., "com.example.app"). Returns nil, nil if not found.
func (as *AppStore) GetBundleIDByIdentifier(identifier string) (*BundleID, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
u, _ := url.Parse(bundleIDsURL)
q := u.Query()
q.Set("filter[identifier]", identifier)
u.RawQuery = q.Encode()
req, err := http.NewRequest("GET", u.String(), 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 bundles BundleIdsResponse
if err := json.NewDecoder(resp.Body).Decode(&bundles); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
if len(bundles.Data) == 0 {
return nil, nil
}
return &bundles.Data[0], nil
}
// GetBundleID returns information about a specific bundle ID.
func (as *AppStore) GetBundleID(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", bundleIDsURL+"/"+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
}
// GetBundleIDApp returns the app information for a specific bundle ID.
func (as *AppStore) GetBundleIDApp(id string) (*AppResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", bundleIDsURL+"/"+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 app AppResponse
if err := json.NewDecoder(resp.Body).Decode(&app); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &app, nil
}
// GetBundleIDProfiles returns a list of all provisioning profiles for a specific bundle ID.
func (as *AppStore) GetBundleIDProfiles(id string) (*ProfileResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", bundleIDsURL+"/"+id+"/profiles", 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, nil
}
// GetBundleIDCapabilities returns a list of all capabilities for a specific bundle ID.
func (as *AppStore) GetBundleIDCapabilities(id string) (*BundleIdCapabilitiesResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("GET", bundleIDsURL+"/"+id+"/bundleIdCapabilities", 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 caps BundleIdCapabilitiesResponse
if err := json.NewDecoder(resp.Body).Decode(&caps); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &caps, nil
}
// RegisterBundleID registers a new bundle ID for app development.
func (as *AppStore) RegisterBundleID(name, id string) (*BundleIdResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
var bundleIDCreateRequest BundleIdCreateRequest
bundleIDCreateRequest.Data.Type = "bundleIds"
bundleIDCreateRequest.Data.Attributes.Name = name
bundleIDCreateRequest.Data.Attributes.ID = id
bundleIDCreateRequest.Data.Attributes.Platform = IOS
jsonStr, err := json.Marshal(&bundleIDCreateRequest)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", bundleIDsURL, 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 bidResp BundleIdResponse
if err := json.NewDecoder(resp.Body).Decode(&bidResp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &bidResp, nil
}
// DeleteBundleID deletes a bundle ID that is used for app development.
func (as *AppStore) DeleteBundleID(id string) (*BundleIdResponse, error) {
if err := as.createToken(defaultJWTLife); err != nil {
return nil, fmt.Errorf("failed to create token: %v", err)
}
req, err := http.NewRequest("DELETE", bundleIDsURL+"/"+id, nil)
if err != nil {
return nil, 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 nil, 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 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 bidResp BundleIdResponse
if err := json.NewDecoder(resp.Body).Decode(&bidResp); err != nil {
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
}
return &bidResp, nil
}