Files
ipsw/pkg/tss/tss.go

539 lines
17 KiB
Go

package tss
import (
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/apex/log"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/internal/download"
"github.com/blacktop/ipsw/pkg/info"
bm "github.com/blacktop/ipsw/pkg/plist"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
)
// NOTES:
// - https://github.com/Neal/savethemblobs
// - https://github.com/tihmstar/tsschecker
// - https://www.theiphonewiki.com/wiki/SHSH
// - https://www.theiphonewiki.com/wiki/SHSH_Protocol
// - https://tsssaver.1conan.com/v2/
// - https://api.ineal.me/tss/docs
const (
tssControllerActionURL = "http://gs.apple.com/TSS/controller?action=2"
// tssControllerActionURL = "http://gsra.apple.com/TSS/controller?action=2" TODO: maybe this is the new URL?
tssClientVersion = "libauthinstall-1049.100.21"
)
var ErrNotSigned = fmt.Errorf("not signed")
// Request is the request sent to the TSS server
type Request struct {
UUID string `plist:"@UUID,omitempty" mapstructure:"@UUID,omitempty"`
ApImg4Ticket bool `plist:"@ApImg4Ticket,omitempty" mapstructure:"@ApImg4Ticket,omitempty"`
BBTicket bool `plist:"@BBTicket,omitempty" mapstructure:"@BBTicket,omitempty"`
HostPlatformInfo string `plist:"@HostPlatformInfo,omitempty" mapstructure:"@HostPlatformInfo,omitempty"`
Locality string `plist:"@Locality,omitempty" mapstructure:"@Locality,omitempty"`
VersionInfo string `plist:"@VersionInfo,omitempty" mapstructure:"@VersionInfo,omitempty"` // = libauthinstall-850.0.1.0.1 (/usr/lib/libauthinstall.dylib)
ApBoardID uint64 `plist:"ApBoardID,omitempty" mapstructure:"ApBoardID,omitempty"`
ApChipID uint64 `plist:"ApChipID,omitempty" mapstructure:"ApChipID,omitempty"`
ApECID uint64 `plist:"ApECID,omitempty" mapstructure:"ApECID,omitempty"`
ApNonce []byte `plist:"ApNonce,omitempty" mapstructure:"ApNonce,omitempty"`
ApProductionMode bool `plist:"ApProductionMode,omitempty" mapstructure:"ApProductionMode,omitempty"`
ApSecurityDomain int `plist:"ApSecurityDomain,omitempty" mapstructure:"ApSecurityDomain,omitempty"` // = 1
ApSecurityMode bool `plist:"ApSecurityMode,omitempty" mapstructure:"ApSecurityMode,omitempty"`
ApSupportsImg4 bool `plist:"ApSupportsImg4,omitempty" mapstructure:"ApSupportsImg4,omitempty"`
PearlCertificationRootPub []byte `plist:"PearlCertificationRootPub,omitempty" mapstructure:"PearlCertificationRootPub,omitempty"`
UniqueBuildID []byte `plist:"UniqueBuildID,omitempty" mapstructure:"UniqueBuildID,omitempty"`
SepNonce []byte `plist:"SepNonce,omitempty" mapstructure:"SepNonce,omitempty"`
UIDMode bool `plist:"UID_MODE" mapstructure:"UID_MODE"`
NeRDEpoch int `plist:"NeRDEpoch,omitempty" mapstructure:"NeRDEpoch,omitempty"`
PermitNeRDPivot []byte `plist:"PermitNeRDPivot,omitempty" mapstructure:"PermitNeRDPivot,omitempty"`
ApSikaFuse int `plist:"Ap,SikaFuse,omitempty" mapstructure:"Ap,SikaFuse,omitempty"`
// Ap,* fields from manifest
ApOSLongVersion string `plist:"Ap,OSLongVersion,omitempty" mapstructure:"Ap,OSLongVersion,omitempty"`
ApProductMarketingVersion string `plist:"Ap,ProductMarketingVersion,omitempty" mapstructure:"Ap,ProductMarketingVersion,omitempty"`
ApProductType string `plist:"Ap,ProductType,omitempty" mapstructure:"Ap,ProductType,omitempty"`
ApSDKPlatform string `plist:"Ap,SDKPlatform,omitempty" mapstructure:"Ap,SDKPlatform,omitempty"`
ApTarget string `plist:"Ap,Target,omitempty" mapstructure:"Ap,Target,omitempty"`
ApTargetType string `plist:"Ap,TargetType,omitempty" mapstructure:"Ap,TargetType,omitempty"`
// Personalize
LoadableTrustCache bm.IdentityManifest `plist:"LoadableTrustCache,omitempty" mapstructure:"LoadableTrustCache,omitempty"`
PersonalizedDMG bm.IdentityManifest `plist:"PersonalizedDMG,omitempty" mapstructure:"PersonalizedDMG,omitempty"`
}
// Response is the response from the TSS server
type Response struct {
Status int
Message string
Plist string
}
// Blob is the TSS response blob with ApImg4Ticket
type Blob struct {
ServerVersion string `plist:"@ServerVersion,omitempty"`
ApImg4Ticket []byte `plist:"ApImg4Ticket,omitempty"`
BBTicket []byte `plist:"BBTicket,omitempty"`
BasebandFirmware struct {
MiscHashTableBodyBlob []byte `plist:"Misc-HashTableBody-Blob,omitempty"`
RestoreSbl1HashTableBodyBlob []byte `plist:"RestoreSBL1-HashTableBody-Blob,omitempty"`
Sbl1HashTableBodyBlob []byte `plist:"SBL1-HashTableBody-Blob,omitempty"`
}
EUICCTicket []byte `plist:"eUICC,Ticket,omitempty"`
}
type RestoreRequestRule struct {
Actions struct {
EPRO bool `plist:"EPRO,omitempty"`
ESEC bool `plist:"ESEC,omitempty"`
} `plist:"Actions,omitempty"`
Conditions struct {
ApRawProductionMode bool `plist:"ApRawProductionMode,omitempty"`
ApRequiresImage4 bool `plist:"ApRequiresImage4,omitempty"`
} `plist:"Conditions,omitempty"`
}
func randomHex(n int) ([]byte, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return nil, err
}
return b, nil
}
func RandomECID() (uint64, error) {
b, err := randomHex(8)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint64(b[:]), nil
}
// applyRestoreRequestRules applies restore request rules to a component
func applyRestoreRequestRules(entry map[string]any, parameters map[string]any, rules any) {
rulesList, ok := rules.([]any)
if !ok {
return
}
for _, rule := range rulesList {
ruleMap, ok := rule.(map[string]any)
if !ok {
continue
}
// Check conditions
conditions, hasConditions := ruleMap["Conditions"].(map[string]any)
conditionsFulfilled := true
if hasConditions {
conditionLoop:
for condKey, condValue := range conditions {
var paramValue any
switch condKey {
case "ApRawProductionMode", "ApCurrentProductionMode":
paramValue = parameters["ApProductionMode"]
case "ApRawSecurityMode":
paramValue = parameters["ApSecurityMode"]
case "ApRequiresImage4":
paramValue = true // We're always using IMG4
case "ApDemotionPolicyOverride":
paramValue = parameters["DemotionPolicy"]
case "ApInRomDFU":
paramValue = parameters["ApInRomDFU"]
default:
// Unknown condition, assume not fulfilled
conditionsFulfilled = false
break conditionLoop
}
if paramValue != condValue {
conditionsFulfilled = false
break
}
}
}
// Apply actions if conditions are fulfilled
if conditionsFulfilled {
if actions, hasActions := ruleMap["Actions"].(map[string]any); hasActions {
for actionKey, actionValue := range actions {
if boolValue, isBool := actionValue.(bool); isBool {
entry[actionKey] = boolValue
}
}
}
}
}
}
func getApImg4Ticket(payload io.Reader, proxy string, insecure bool) (*Blob, error) {
req, err := http.NewRequest("POST", tssControllerActionURL, payload)
if err != nil {
return nil, fmt.Errorf("failed to create https request: %v", err)
}
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Content-type", "text/xml; charset=\"utf-8\"")
req.Header.Add("User-Agent", "InetURL/1.0")
client := &http.Client{
Transport: &http.Transport{
Proxy: download.GetProxy(proxy),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to connect to URL: got status %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
var tr Response
for field := range strings.SplitSeq(string(body), "&") {
key, value, ok := strings.Cut(field, "=")
if !ok {
log.Error("failed to parse response field")
continue
}
switch key {
case "STATUS":
sInt, err := strconv.Atoi(value)
if err != nil {
return nil, err
}
tr.Status = sInt
case "MESSAGE":
tr.Message = value
case "REQUEST_STRING":
tr.Plist = value
}
}
log.WithFields(log.Fields{
"status": tr.Status,
"message": tr.Message,
"request_len": len(tr.Plist),
}).Debug("TSS Response")
if tr.Status == 0 && tr.Message == "SUCCESS" {
var blob Blob
if err := plist.NewDecoder(strings.NewReader(tr.Plist)).Decode(&blob); err != nil {
return nil, fmt.Errorf("failed to decode TSS response REQUEST_STRING: %v", err)
}
return &blob, nil
}
return nil, fmt.Errorf("status: %d, message: %s: %w", tr.Status, tr.Message, ErrNotSigned)
}
// Config represents the configuration for a TSS request.
type Config struct {
Device string
Version string
Build string
ApNonce []byte
SepNonce []byte
ECID uint64
Image4Supported bool
Proxy string
Insecure bool
Output string
Info *info.Info // Optional, if provided will use this info instead of downloading it
}
// GetTSSResponse retrieves a TSS response for the given configuration.
func GetTSSResponse(conf *Config) ([]byte, error) {
var err error
if len(conf.ApNonce) == 0 {
conf.ApNonce, err = randomHex(32)
if err != nil {
return nil, err
}
}
if len(conf.SepNonce) == 0 {
conf.SepNonce, err = randomHex(20)
if err != nil {
return nil, err
}
}
if conf.Build == "" {
conf.Build = conf.Info.Plists.BuildManifest.ProductBuildVersion
}
buildIdentity, err := conf.Info.Plists.GetBuildIdentity(conf.Device)
if err != nil {
return nil, fmt.Errorf("failed to get build identity for %s: %v", conf.Device, err)
}
// Parse board and chip IDs
chipID, err := strconv.ParseUint(strings.TrimPrefix(buildIdentity.ApChipID, "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse chip id: %v", err)
}
boardID, err := strconv.ParseUint(strings.TrimPrefix(buildIdentity.ApBoardID, "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse board id: %v", err)
}
ecid := conf.ECID
if ecid == 0 {
return nil, fmt.Errorf("ECID must be provided to check signing status")
}
tssReq := make(map[string]any)
// Create base request
tssReq["@HostPlatformInfo"] = "mac"
tssReq["@VersionInfo"] = tssClientVersion
tssReq["@UUID"] = strings.ToUpper(uuid.New().String())
tssReq["ApECID"] = ecid
tssReq["UniqueBuildID"] = buildIdentity.UniqueBuildID
tssReq["ApChipID"] = chipID
tssReq["ApBoardID"] = boardID
tssReq["ApSecurityDomain"] = uint64(1)
tssReq["ApSecurityMode"] = true
tssReq["Ap,OSLongVersion"] = buildIdentity.ApOSLongVersion
tssReq["Ap,ProductMarketingVersion"] = buildIdentity.ApProductMarketingVersion
tssReq["Ap,ProductType"] = buildIdentity.ApProductType
tssReq["Ap,SDKPlatform"] = buildIdentity.ApSDKPlatform
tssReq["Ap,Target"] = buildIdentity.ApTarget
tssReq["Ap,TargetType"] = buildIdentity.ApTargetType
tssReq["ApNonce"] = conf.ApNonce
tssReq["SepNonce"] = conf.SepNonce
tssReq["@ApImg4Ticket"] = true
tssReq["ApProductionMode"] = true
tssReq["NeRDEpoch"] = 0
tssReq["PearlCertificationRootPub"] = buildIdentity.PearlCertificationRootPub
tssReq["PermitNeRDPivot"] = buildIdentity.PermitNeRDPivot
tssReq["UID_MODE"] = false
tssReq["Ap,SikaFuse"] = 0
// Parameters for restore request rules
parameters := map[string]any{
"ApProductionMode": true,
"ApSecurityMode": true,
"ApRequiresImage4": true,
}
for k, v := range buildIdentity.Manifest {
m := make(map[string]any)
if _, ok := v.Info["RestoreRequestRules"]; !ok {
continue
}
m["Digest"] = v.Digest
if v.Trusted != nil {
m["Trusted"] = *v.Trusted
}
// Apply restore request rules to set EPRO/ESEC
if RestoreRequestRules, ok := v.Info["RestoreRequestRules"].([]any); ok {
applyRestoreRequestRules(m, parameters, RestoreRequestRules)
}
// Add special fields for specific components
if v.BuildString != nil {
m["BuildString"] = *v.BuildString
}
if v.MemoryMap != nil {
m["MemoryMap"] = *v.MemoryMap
}
if v.ObjectPayloadPropertyDigest != nil {
m["ObjectPayloadPropertyDigest"] = *v.ObjectPayloadPropertyDigest
}
if v.RawDataDigest != nil {
m["RawDataDigest"] = *v.RawDataDigest
}
if v.TBMDigests != nil {
m["TBMDigests"] = *v.TBMDigests
}
tssReq[k] = m
}
trdata, err := plist.MarshalIndent(tssReq, plist.XMLFormat, " ")
if err != nil {
return nil, err
}
// log.Debug("Saving TSS request to /tmp/tss_request.plist")
// if err := os.WriteFile("/tmp/tss_request.plist", trdata, 0644); err != nil {
// return nil, err
// }
blob, err := getApImg4Ticket(bytes.NewReader(trdata), conf.Proxy, conf.Insecure)
if err != nil {
return nil, err
}
plistData, err := plist.MarshalIndent(blob, plist.XMLFormat, " ")
if err != nil {
return nil, err
}
return plistData, nil
}
// PersonalConfig is the config for personalizing a TSS blob
type PersonalConfig struct {
Proxy string
Insecure bool
PersonlID map[string]any
BuildManifest *bm.BuildManifest
}
// Personalize returns a personalized TSS blob
func Personalize(conf *PersonalConfig) ([]byte, error) {
nonce, err := hex.DecodeString(conf.PersonlID["ApNonce"].(string))
if err != nil {
return nil, fmt.Errorf("failed to decode nonce hex-string: %v", err)
}
tssReq := Request{
UUID: uuid.New().String(),
ApImg4Ticket: true,
BBTicket: true,
HostPlatformInfo: "mac",
VersionInfo: tssClientVersion,
ApBoardID: uint64(conf.PersonlID["BoardId"].(float64)),
ApChipID: uint64(conf.PersonlID["ChipID"].(float64)),
ApECID: uint64(conf.PersonlID["UniqueChipID"].(float64)),
ApNonce: nonce,
ApProductionMode: true,
ApSecurityDomain: 1,
ApSecurityMode: true,
UIDMode: false,
SepNonce: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
}
var manifest map[string]bm.IdentityManifest
for _, bid := range conf.BuildManifest.BuildIdentities {
boardID, err := strconv.ParseUint(strings.TrimPrefix(bid.ApBoardID, "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse board id: %v", err)
}
chipID, err := strconv.ParseUint(strings.TrimPrefix(bid.ApChipID, "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse chip id: %v", err)
}
if boardID == uint64(conf.PersonlID["BoardId"].(float64)) && chipID == uint64(conf.PersonlID["ChipID"].(float64)) {
manifest = bid.Manifest
break
}
}
parameters := map[string]any{
"ApProductionMode": true,
"ApSecurityDomain": 1,
"ApSecurityMode": true,
"ApSupportsImg4": true,
}
tssReq.PersonalizedDMG = manifest["PersonalizedDMG"]
tssReq.PersonalizedDMG.Info = nil
tssReq.LoadableTrustCache = manifest["LoadableTrustCache"]
tssReq.LoadableTrustCache.Info = nil
if rules, ok := manifest["LoadableTrustCache"].Info["RestoreRequestRules"]; ok {
for _, rule := range rules.([]any) {
satisfied := true
if conds, ok := rule.(map[string]any)["Conditions"]; ok {
for k, v := range conds.(map[string]any) {
if !satisfied {
break
}
switch k {
case "ApRawProductionMode":
satisfied = parameters["ApProductionMode"] == v
case "ApCurrentProductionMode":
satisfied = parameters["ApProductionMode"] == v
case "ApRawSecurityMode":
satisfied = parameters["ApSecurityMode"] == v
case "ApRequiresImage4":
satisfied = parameters["ApSupportsImg4"] == v
case "ApDemotionPolicyOverride":
satisfied = parameters["DemotionPolicy"] == v
case "ApInRomDFU":
satisfied = parameters["ApInRomDFU"] == v
default:
log.Fatalf("unknown LoadableTrustCache->RestoreRequestRules->Condition: %s", k)
}
}
}
if satisfied {
if actions, ok := rule.(map[string]any)["Actions"]; ok {
for k, v := range actions.(map[string]any) {
switch k {
case "EPRO":
epro := v.(bool)
tssReq.PersonalizedDMG.EPRO = &epro
tssReq.LoadableTrustCache.EPRO = &epro
case "ESEC":
esec := v.(bool)
tssReq.PersonalizedDMG.ESEC = &esec
tssReq.LoadableTrustCache.ESEC = &esec
}
}
}
}
}
}
var tssMap map[string]any
if err := mapstructure.Decode(tssReq, &tssMap); err != nil {
return nil, err
}
// Convert IdentityManifest pointer values to regular values for plist marshalling
tssMap["PersonalizedDMG"] = tssReq.PersonalizedDMG.MarshalMap()
tssMap["LoadableTrustCache"] = tssReq.LoadableTrustCache.MarshalMap()
for k, v := range conf.PersonlID {
if strings.HasPrefix(k, "Ap,") {
switch v := v.(type) {
case float64:
tssMap[k] = uint64(v)
default:
tssMap[k] = v
}
}
}
buf := new(bytes.Buffer)
if err := plist.NewEncoder(buf).Encode(tssMap); err != nil {
return nil, err
}
// os.WriteFile("/tmp/tss.plist", buf.Bytes(), 0644)
blob, err := getApImg4Ticket(buf, conf.Proxy, conf.Insecure)
if err != nil {
return nil, err
}
return blob.ApImg4Ticket, nil
}